filepond.esm.js 288 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724
  1. /*!
  2. * FilePond 4.30.4
  3. * Licensed under MIT, https://opensource.org/licenses/MIT/
  4. * Please visit https://pqina.nl/filepond/ for details.
  5. */
  6. /* eslint-disable */
  7. const isNode = value => value instanceof HTMLElement;
  8. const createStore = (initialState, queries = [], actions = []) => {
  9. // internal state
  10. const state = {
  11. ...initialState,
  12. };
  13. // contains all actions for next frame, is clear when actions are requested
  14. const actionQueue = [];
  15. const dispatchQueue = [];
  16. // returns a duplicate of the current state
  17. const getState = () => ({ ...state });
  18. // returns a duplicate of the actions array and clears the actions array
  19. const processActionQueue = () => {
  20. // create copy of actions queue
  21. const queue = [...actionQueue];
  22. // clear actions queue (we don't want no double actions)
  23. actionQueue.length = 0;
  24. return queue;
  25. };
  26. // processes actions that might block the main UI thread
  27. const processDispatchQueue = () => {
  28. // create copy of actions queue
  29. const queue = [...dispatchQueue];
  30. // clear actions queue (we don't want no double actions)
  31. dispatchQueue.length = 0;
  32. // now dispatch these actions
  33. queue.forEach(({ type, data }) => {
  34. dispatch(type, data);
  35. });
  36. };
  37. // adds a new action, calls its handler and
  38. const dispatch = (type, data, isBlocking) => {
  39. // is blocking action (should never block if document is hidden)
  40. if (isBlocking && !document.hidden) {
  41. dispatchQueue.push({ type, data });
  42. return;
  43. }
  44. // if this action has a handler, handle the action
  45. if (actionHandlers[type]) {
  46. actionHandlers[type](data);
  47. }
  48. // now add action
  49. actionQueue.push({
  50. type,
  51. data,
  52. });
  53. };
  54. const query = (str, ...args) => (queryHandles[str] ? queryHandles[str](...args) : null);
  55. const api = {
  56. getState,
  57. processActionQueue,
  58. processDispatchQueue,
  59. dispatch,
  60. query,
  61. };
  62. let queryHandles = {};
  63. queries.forEach(query => {
  64. queryHandles = {
  65. ...query(state),
  66. ...queryHandles,
  67. };
  68. });
  69. let actionHandlers = {};
  70. actions.forEach(action => {
  71. actionHandlers = {
  72. ...action(dispatch, query, state),
  73. ...actionHandlers,
  74. };
  75. });
  76. return api;
  77. };
  78. const defineProperty = (obj, property, definition) => {
  79. if (typeof definition === 'function') {
  80. obj[property] = definition;
  81. return;
  82. }
  83. Object.defineProperty(obj, property, { ...definition });
  84. };
  85. const forin = (obj, cb) => {
  86. for (const key in obj) {
  87. if (!obj.hasOwnProperty(key)) {
  88. continue;
  89. }
  90. cb(key, obj[key]);
  91. }
  92. };
  93. const createObject = definition => {
  94. const obj = {};
  95. forin(definition, property => {
  96. defineProperty(obj, property, definition[property]);
  97. });
  98. return obj;
  99. };
  100. const attr = (node, name, value = null) => {
  101. if (value === null) {
  102. return node.getAttribute(name) || node.hasAttribute(name);
  103. }
  104. node.setAttribute(name, value);
  105. };
  106. const ns = 'http://www.w3.org/2000/svg';
  107. const svgElements = ['svg', 'path']; // only svg elements used
  108. const isSVGElement = tag => svgElements.includes(tag);
  109. const createElement = (tag, className, attributes = {}) => {
  110. if (typeof className === 'object') {
  111. attributes = className;
  112. className = null;
  113. }
  114. const element = isSVGElement(tag)
  115. ? document.createElementNS(ns, tag)
  116. : document.createElement(tag);
  117. if (className) {
  118. if (isSVGElement(tag)) {
  119. attr(element, 'class', className);
  120. } else {
  121. element.className = className;
  122. }
  123. }
  124. forin(attributes, (name, value) => {
  125. attr(element, name, value);
  126. });
  127. return element;
  128. };
  129. const appendChild = parent => (child, index) => {
  130. if (typeof index !== 'undefined' && parent.children[index]) {
  131. parent.insertBefore(child, parent.children[index]);
  132. } else {
  133. parent.appendChild(child);
  134. }
  135. };
  136. const appendChildView = (parent, childViews) => (view, index) => {
  137. if (typeof index !== 'undefined') {
  138. childViews.splice(index, 0, view);
  139. } else {
  140. childViews.push(view);
  141. }
  142. return view;
  143. };
  144. const removeChildView = (parent, childViews) => view => {
  145. // remove from child views
  146. childViews.splice(childViews.indexOf(view), 1);
  147. // remove the element
  148. if (view.element.parentNode) {
  149. parent.removeChild(view.element);
  150. }
  151. return view;
  152. };
  153. const IS_BROWSER = (() =>
  154. typeof window !== 'undefined' && typeof window.document !== 'undefined')();
  155. const isBrowser = () => IS_BROWSER;
  156. const testElement = isBrowser() ? createElement('svg') : {};
  157. const getChildCount =
  158. 'children' in testElement ? el => el.children.length : el => el.childNodes.length;
  159. const getViewRect = (elementRect, childViews, offset, scale) => {
  160. const left = offset[0] || elementRect.left;
  161. const top = offset[1] || elementRect.top;
  162. const right = left + elementRect.width;
  163. const bottom = top + elementRect.height * (scale[1] || 1);
  164. const rect = {
  165. // the rectangle of the element itself
  166. element: {
  167. ...elementRect,
  168. },
  169. // the rectangle of the element expanded to contain its children, does not include any margins
  170. inner: {
  171. left: elementRect.left,
  172. top: elementRect.top,
  173. right: elementRect.right,
  174. bottom: elementRect.bottom,
  175. },
  176. // the rectangle of the element expanded to contain its children including own margin and child margins
  177. // margins will be added after we've recalculated the size
  178. outer: {
  179. left,
  180. top,
  181. right,
  182. bottom,
  183. },
  184. };
  185. // expand rect to fit all child rectangles
  186. childViews
  187. .filter(childView => !childView.isRectIgnored())
  188. .map(childView => childView.rect)
  189. .forEach(childViewRect => {
  190. expandRect(rect.inner, { ...childViewRect.inner });
  191. expandRect(rect.outer, { ...childViewRect.outer });
  192. });
  193. // calculate inner width and height
  194. calculateRectSize(rect.inner);
  195. // append additional margin (top and left margins are included in top and left automatically)
  196. rect.outer.bottom += rect.element.marginBottom;
  197. rect.outer.right += rect.element.marginRight;
  198. // calculate outer width and height
  199. calculateRectSize(rect.outer);
  200. return rect;
  201. };
  202. const expandRect = (parent, child) => {
  203. // adjust for parent offset
  204. child.top += parent.top;
  205. child.right += parent.left;
  206. child.bottom += parent.top;
  207. child.left += parent.left;
  208. if (child.bottom > parent.bottom) {
  209. parent.bottom = child.bottom;
  210. }
  211. if (child.right > parent.right) {
  212. parent.right = child.right;
  213. }
  214. };
  215. const calculateRectSize = rect => {
  216. rect.width = rect.right - rect.left;
  217. rect.height = rect.bottom - rect.top;
  218. };
  219. const isNumber = value => typeof value === 'number';
  220. /**
  221. * Determines if position is at destination
  222. * @param position
  223. * @param destination
  224. * @param velocity
  225. * @param errorMargin
  226. * @returns {boolean}
  227. */
  228. const thereYet = (position, destination, velocity, errorMargin = 0.001) => {
  229. return Math.abs(position - destination) < errorMargin && Math.abs(velocity) < errorMargin;
  230. };
  231. /**
  232. * Spring animation
  233. */
  234. const spring =
  235. // default options
  236. ({ stiffness = 0.5, damping = 0.75, mass = 10 } = {}) =>
  237. // method definition
  238. {
  239. let target = null;
  240. let position = null;
  241. let velocity = 0;
  242. let resting = false;
  243. // updates spring state
  244. const interpolate = (ts, skipToEndState) => {
  245. // in rest, don't animate
  246. if (resting) return;
  247. // need at least a target or position to do springy things
  248. if (!(isNumber(target) && isNumber(position))) {
  249. resting = true;
  250. velocity = 0;
  251. return;
  252. }
  253. // calculate spring force
  254. const f = -(position - target) * stiffness;
  255. // update velocity by adding force based on mass
  256. velocity += f / mass;
  257. // update position by adding velocity
  258. position += velocity;
  259. // slow down based on amount of damping
  260. velocity *= damping;
  261. // we've arrived if we're near target and our velocity is near zero
  262. if (thereYet(position, target, velocity) || skipToEndState) {
  263. position = target;
  264. velocity = 0;
  265. resting = true;
  266. // we done
  267. api.onupdate(position);
  268. api.oncomplete(position);
  269. } else {
  270. // progress update
  271. api.onupdate(position);
  272. }
  273. };
  274. /**
  275. * Set new target value
  276. * @param value
  277. */
  278. const setTarget = value => {
  279. // if currently has no position, set target and position to this value
  280. if (isNumber(value) && !isNumber(position)) {
  281. position = value;
  282. }
  283. // next target value will not be animated to
  284. if (target === null) {
  285. target = value;
  286. position = value;
  287. }
  288. // let start moving to target
  289. target = value;
  290. // already at target
  291. if (position === target || typeof target === 'undefined') {
  292. // now resting as target is current position, stop moving
  293. resting = true;
  294. velocity = 0;
  295. // done!
  296. api.onupdate(position);
  297. api.oncomplete(position);
  298. return;
  299. }
  300. resting = false;
  301. };
  302. // need 'api' to call onupdate callback
  303. const api = createObject({
  304. interpolate,
  305. target: {
  306. set: setTarget,
  307. get: () => target,
  308. },
  309. resting: {
  310. get: () => resting,
  311. },
  312. onupdate: value => {},
  313. oncomplete: value => {},
  314. });
  315. return api;
  316. };
  317. const easeLinear = t => t;
  318. const easeInOutQuad = t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t);
  319. const tween =
  320. // default values
  321. ({ duration = 500, easing = easeInOutQuad, delay = 0 } = {}) =>
  322. // method definition
  323. {
  324. let start = null;
  325. let t;
  326. let p;
  327. let resting = true;
  328. let reverse = false;
  329. let target = null;
  330. const interpolate = (ts, skipToEndState) => {
  331. if (resting || target === null) return;
  332. if (start === null) {
  333. start = ts;
  334. }
  335. if (ts - start < delay) return;
  336. t = ts - start - delay;
  337. if (t >= duration || skipToEndState) {
  338. t = 1;
  339. p = reverse ? 0 : 1;
  340. api.onupdate(p * target);
  341. api.oncomplete(p * target);
  342. resting = true;
  343. } else {
  344. p = t / duration;
  345. api.onupdate((t >= 0 ? easing(reverse ? 1 - p : p) : 0) * target);
  346. }
  347. };
  348. // need 'api' to call onupdate callback
  349. const api = createObject({
  350. interpolate,
  351. target: {
  352. get: () => (reverse ? 0 : target),
  353. set: value => {
  354. // is initial value
  355. if (target === null) {
  356. target = value;
  357. api.onupdate(value);
  358. api.oncomplete(value);
  359. return;
  360. }
  361. // want to tween to a smaller value and have a current value
  362. if (value < target) {
  363. target = 1;
  364. reverse = true;
  365. } else {
  366. // not tweening to a smaller value
  367. reverse = false;
  368. target = value;
  369. }
  370. // let's go!
  371. resting = false;
  372. start = null;
  373. },
  374. },
  375. resting: {
  376. get: () => resting,
  377. },
  378. onupdate: value => {},
  379. oncomplete: value => {},
  380. });
  381. return api;
  382. };
  383. const animator = {
  384. spring,
  385. tween,
  386. };
  387. /*
  388. { type: 'spring', stiffness: .5, damping: .75, mass: 10 };
  389. { translation: { type: 'spring', ... }, ... }
  390. { translation: { x: { type: 'spring', ... } } }
  391. */
  392. const createAnimator = (definition, category, property) => {
  393. // default is single definition
  394. // we check if transform is set, if so, we check if property is set
  395. const def =
  396. definition[category] && typeof definition[category][property] === 'object'
  397. ? definition[category][property]
  398. : definition[category] || definition;
  399. const type = typeof def === 'string' ? def : def.type;
  400. const props = typeof def === 'object' ? { ...def } : {};
  401. return animator[type] ? animator[type](props) : null;
  402. };
  403. const addGetSet = (keys, obj, props, overwrite = false) => {
  404. obj = Array.isArray(obj) ? obj : [obj];
  405. obj.forEach(o => {
  406. keys.forEach(key => {
  407. let name = key;
  408. let getter = () => props[key];
  409. let setter = value => (props[key] = value);
  410. if (typeof key === 'object') {
  411. name = key.key;
  412. getter = key.getter || getter;
  413. setter = key.setter || setter;
  414. }
  415. if (o[name] && !overwrite) {
  416. return;
  417. }
  418. o[name] = {
  419. get: getter,
  420. set: setter,
  421. };
  422. });
  423. });
  424. };
  425. // add to state,
  426. // add getters and setters to internal and external api (if not set)
  427. // setup animators
  428. const animations = ({ mixinConfig, viewProps, viewInternalAPI, viewExternalAPI }) => {
  429. // initial properties
  430. const initialProps = { ...viewProps };
  431. // list of all active animations
  432. const animations = [];
  433. // setup animators
  434. forin(mixinConfig, (property, animation) => {
  435. const animator = createAnimator(animation);
  436. if (!animator) {
  437. return;
  438. }
  439. // when the animator updates, update the view state value
  440. animator.onupdate = value => {
  441. viewProps[property] = value;
  442. };
  443. // set animator target
  444. animator.target = initialProps[property];
  445. // when value is set, set the animator target value
  446. const prop = {
  447. key: property,
  448. setter: value => {
  449. // if already at target, we done!
  450. if (animator.target === value) {
  451. return;
  452. }
  453. animator.target = value;
  454. },
  455. getter: () => viewProps[property],
  456. };
  457. // add getters and setters
  458. addGetSet([prop], [viewInternalAPI, viewExternalAPI], viewProps, true);
  459. // add it to the list for easy updating from the _write method
  460. animations.push(animator);
  461. });
  462. // expose internal write api
  463. return {
  464. write: ts => {
  465. let skipToEndState = document.hidden;
  466. let resting = true;
  467. animations.forEach(animation => {
  468. if (!animation.resting) resting = false;
  469. animation.interpolate(ts, skipToEndState);
  470. });
  471. return resting;
  472. },
  473. destroy: () => {},
  474. };
  475. };
  476. const addEvent = element => (type, fn) => {
  477. element.addEventListener(type, fn);
  478. };
  479. const removeEvent = element => (type, fn) => {
  480. element.removeEventListener(type, fn);
  481. };
  482. // mixin
  483. const listeners = ({
  484. mixinConfig,
  485. viewProps,
  486. viewInternalAPI,
  487. viewExternalAPI,
  488. viewState,
  489. view,
  490. }) => {
  491. const events = [];
  492. const add = addEvent(view.element);
  493. const remove = removeEvent(view.element);
  494. viewExternalAPI.on = (type, fn) => {
  495. events.push({
  496. type,
  497. fn,
  498. });
  499. add(type, fn);
  500. };
  501. viewExternalAPI.off = (type, fn) => {
  502. events.splice(events.findIndex(event => event.type === type && event.fn === fn), 1);
  503. remove(type, fn);
  504. };
  505. return {
  506. write: () => {
  507. // not busy
  508. return true;
  509. },
  510. destroy: () => {
  511. events.forEach(event => {
  512. remove(event.type, event.fn);
  513. });
  514. },
  515. };
  516. };
  517. // add to external api and link to props
  518. const apis = ({ mixinConfig, viewProps, viewExternalAPI }) => {
  519. addGetSet(mixinConfig, viewExternalAPI, viewProps);
  520. };
  521. const isDefined = value => value != null;
  522. // add to state,
  523. // add getters and setters to internal and external api (if not set)
  524. // set initial state based on props in viewProps
  525. // apply as transforms each frame
  526. const defaults = {
  527. opacity: 1,
  528. scaleX: 1,
  529. scaleY: 1,
  530. translateX: 0,
  531. translateY: 0,
  532. rotateX: 0,
  533. rotateY: 0,
  534. rotateZ: 0,
  535. originX: 0,
  536. originY: 0,
  537. };
  538. const styles = ({ mixinConfig, viewProps, viewInternalAPI, viewExternalAPI, view }) => {
  539. // initial props
  540. const initialProps = { ...viewProps };
  541. // current props
  542. const currentProps = {};
  543. // we will add those properties to the external API and link them to the viewState
  544. addGetSet(mixinConfig, [viewInternalAPI, viewExternalAPI], viewProps);
  545. // override rect on internal and external rect getter so it takes in account transforms
  546. const getOffset = () => [viewProps['translateX'] || 0, viewProps['translateY'] || 0];
  547. const getScale = () => [viewProps['scaleX'] || 0, viewProps['scaleY'] || 0];
  548. const getRect = () =>
  549. view.rect ? getViewRect(view.rect, view.childViews, getOffset(), getScale()) : null;
  550. viewInternalAPI.rect = { get: getRect };
  551. viewExternalAPI.rect = { get: getRect };
  552. // apply view props
  553. mixinConfig.forEach(key => {
  554. viewProps[key] =
  555. typeof initialProps[key] === 'undefined' ? defaults[key] : initialProps[key];
  556. });
  557. // expose api
  558. return {
  559. write: () => {
  560. // see if props have changed
  561. if (!propsHaveChanged(currentProps, viewProps)) {
  562. return;
  563. }
  564. // moves element to correct position on screen
  565. applyStyles(view.element, viewProps);
  566. // store new transforms
  567. Object.assign(currentProps, { ...viewProps });
  568. // no longer busy
  569. return true;
  570. },
  571. destroy: () => {},
  572. };
  573. };
  574. const propsHaveChanged = (currentProps, newProps) => {
  575. // different amount of keys
  576. if (Object.keys(currentProps).length !== Object.keys(newProps).length) {
  577. return true;
  578. }
  579. // lets analyze the individual props
  580. for (const prop in newProps) {
  581. if (newProps[prop] !== currentProps[prop]) {
  582. return true;
  583. }
  584. }
  585. return false;
  586. };
  587. const applyStyles = (
  588. element,
  589. {
  590. opacity,
  591. perspective,
  592. translateX,
  593. translateY,
  594. scaleX,
  595. scaleY,
  596. rotateX,
  597. rotateY,
  598. rotateZ,
  599. originX,
  600. originY,
  601. width,
  602. height,
  603. }
  604. ) => {
  605. let transforms = '';
  606. let styles = '';
  607. // handle transform origin
  608. if (isDefined(originX) || isDefined(originY)) {
  609. styles += `transform-origin: ${originX || 0}px ${originY || 0}px;`;
  610. }
  611. // transform order is relevant
  612. // 0. perspective
  613. if (isDefined(perspective)) {
  614. transforms += `perspective(${perspective}px) `;
  615. }
  616. // 1. translate
  617. if (isDefined(translateX) || isDefined(translateY)) {
  618. transforms += `translate3d(${translateX || 0}px, ${translateY || 0}px, 0) `;
  619. }
  620. // 2. scale
  621. if (isDefined(scaleX) || isDefined(scaleY)) {
  622. transforms += `scale3d(${isDefined(scaleX) ? scaleX : 1}, ${
  623. isDefined(scaleY) ? scaleY : 1
  624. }, 1) `;
  625. }
  626. // 3. rotate
  627. if (isDefined(rotateZ)) {
  628. transforms += `rotateZ(${rotateZ}rad) `;
  629. }
  630. if (isDefined(rotateX)) {
  631. transforms += `rotateX(${rotateX}rad) `;
  632. }
  633. if (isDefined(rotateY)) {
  634. transforms += `rotateY(${rotateY}rad) `;
  635. }
  636. // add transforms
  637. if (transforms.length) {
  638. styles += `transform:${transforms};`;
  639. }
  640. // add opacity
  641. if (isDefined(opacity)) {
  642. styles += `opacity:${opacity};`;
  643. // if we reach zero, we make the element inaccessible
  644. if (opacity === 0) {
  645. styles += `visibility:hidden;`;
  646. }
  647. // if we're below 100% opacity this element can't be clicked
  648. if (opacity < 1) {
  649. styles += `pointer-events:none;`;
  650. }
  651. }
  652. // add height
  653. if (isDefined(height)) {
  654. styles += `height:${height}px;`;
  655. }
  656. // add width
  657. if (isDefined(width)) {
  658. styles += `width:${width}px;`;
  659. }
  660. // apply styles
  661. const elementCurrentStyle = element.elementCurrentStyle || '';
  662. // if new styles does not match current styles, lets update!
  663. if (styles.length !== elementCurrentStyle.length || styles !== elementCurrentStyle) {
  664. element.style.cssText = styles;
  665. // store current styles so we can compare them to new styles later on
  666. // _not_ getting the style value is faster
  667. element.elementCurrentStyle = styles;
  668. }
  669. };
  670. const Mixins = {
  671. styles,
  672. listeners,
  673. animations,
  674. apis,
  675. };
  676. const updateRect = (rect = {}, element = {}, style = {}) => {
  677. if (!element.layoutCalculated) {
  678. rect.paddingTop = parseInt(style.paddingTop, 10) || 0;
  679. rect.marginTop = parseInt(style.marginTop, 10) || 0;
  680. rect.marginRight = parseInt(style.marginRight, 10) || 0;
  681. rect.marginBottom = parseInt(style.marginBottom, 10) || 0;
  682. rect.marginLeft = parseInt(style.marginLeft, 10) || 0;
  683. element.layoutCalculated = true;
  684. }
  685. rect.left = element.offsetLeft || 0;
  686. rect.top = element.offsetTop || 0;
  687. rect.width = element.offsetWidth || 0;
  688. rect.height = element.offsetHeight || 0;
  689. rect.right = rect.left + rect.width;
  690. rect.bottom = rect.top + rect.height;
  691. rect.scrollTop = element.scrollTop;
  692. rect.hidden = element.offsetParent === null;
  693. return rect;
  694. };
  695. const createView =
  696. // default view definition
  697. ({
  698. // element definition
  699. tag = 'div',
  700. name = null,
  701. attributes = {},
  702. // view interaction
  703. read = () => {},
  704. write = () => {},
  705. create = () => {},
  706. destroy = () => {},
  707. // hooks
  708. filterFrameActionsForChild = (child, actions) => actions,
  709. didCreateView = () => {},
  710. didWriteView = () => {},
  711. // rect related
  712. ignoreRect = false,
  713. ignoreRectUpdate = false,
  714. // mixins
  715. mixins = [],
  716. } = {}) => (
  717. // each view requires reference to store
  718. store,
  719. // specific properties for this view
  720. props = {}
  721. ) => {
  722. // root element should not be changed
  723. const element = createElement(tag, `filepond--${name}`, attributes);
  724. // style reference should also not be changed
  725. const style = window.getComputedStyle(element, null);
  726. // element rectangle
  727. const rect = updateRect();
  728. let frameRect = null;
  729. // rest state
  730. let isResting = false;
  731. // pretty self explanatory
  732. const childViews = [];
  733. // loaded mixins
  734. const activeMixins = [];
  735. // references to created children
  736. const ref = {};
  737. // state used for each instance
  738. const state = {};
  739. // list of writers that will be called to update this view
  740. const writers = [
  741. write, // default writer
  742. ];
  743. const readers = [
  744. read, // default reader
  745. ];
  746. const destroyers = [
  747. destroy, // default destroy
  748. ];
  749. // core view methods
  750. const getElement = () => element;
  751. const getChildViews = () => childViews.concat();
  752. const getReference = () => ref;
  753. const createChildView = store => (view, props) => view(store, props);
  754. const getRect = () => {
  755. if (frameRect) {
  756. return frameRect;
  757. }
  758. frameRect = getViewRect(rect, childViews, [0, 0], [1, 1]);
  759. return frameRect;
  760. };
  761. const getStyle = () => style;
  762. /**
  763. * Read data from DOM
  764. * @private
  765. */
  766. const _read = () => {
  767. frameRect = null;
  768. // read child views
  769. childViews.forEach(child => child._read());
  770. const shouldUpdate = !(ignoreRectUpdate && rect.width && rect.height);
  771. if (shouldUpdate) {
  772. updateRect(rect, element, style);
  773. }
  774. // readers
  775. const api = { root: internalAPI, props, rect };
  776. readers.forEach(reader => reader(api));
  777. };
  778. /**
  779. * Write data to DOM
  780. * @private
  781. */
  782. const _write = (ts, frameActions, shouldOptimize) => {
  783. // if no actions, we assume that the view is resting
  784. let resting = frameActions.length === 0;
  785. // writers
  786. writers.forEach(writer => {
  787. const writerResting = writer({
  788. props,
  789. root: internalAPI,
  790. actions: frameActions,
  791. timestamp: ts,
  792. shouldOptimize,
  793. });
  794. if (writerResting === false) {
  795. resting = false;
  796. }
  797. });
  798. // run mixins
  799. activeMixins.forEach(mixin => {
  800. // if one of the mixins is still busy after write operation, we are not resting
  801. const mixinResting = mixin.write(ts);
  802. if (mixinResting === false) {
  803. resting = false;
  804. }
  805. });
  806. // updates child views that are currently attached to the DOM
  807. childViews
  808. .filter(child => !!child.element.parentNode)
  809. .forEach(child => {
  810. // if a child view is not resting, we are not resting
  811. const childResting = child._write(
  812. ts,
  813. filterFrameActionsForChild(child, frameActions),
  814. shouldOptimize
  815. );
  816. if (!childResting) {
  817. resting = false;
  818. }
  819. });
  820. // append new elements to DOM and update those
  821. childViews
  822. //.filter(child => !child.element.parentNode)
  823. .forEach((child, index) => {
  824. // skip
  825. if (child.element.parentNode) {
  826. return;
  827. }
  828. // append to DOM
  829. internalAPI.appendChild(child.element, index);
  830. // call read (need to know the size of these elements)
  831. child._read();
  832. // re-call write
  833. child._write(
  834. ts,
  835. filterFrameActionsForChild(child, frameActions),
  836. shouldOptimize
  837. );
  838. // we just added somthing to the dom, no rest
  839. resting = false;
  840. });
  841. // update resting state
  842. isResting = resting;
  843. didWriteView({
  844. props,
  845. root: internalAPI,
  846. actions: frameActions,
  847. timestamp: ts,
  848. });
  849. // let parent know if we are resting
  850. return resting;
  851. };
  852. const _destroy = () => {
  853. activeMixins.forEach(mixin => mixin.destroy());
  854. destroyers.forEach(destroyer => {
  855. destroyer({ root: internalAPI, props });
  856. });
  857. childViews.forEach(child => child._destroy());
  858. };
  859. // sharedAPI
  860. const sharedAPIDefinition = {
  861. element: {
  862. get: getElement,
  863. },
  864. style: {
  865. get: getStyle,
  866. },
  867. childViews: {
  868. get: getChildViews,
  869. },
  870. };
  871. // private API definition
  872. const internalAPIDefinition = {
  873. ...sharedAPIDefinition,
  874. rect: {
  875. get: getRect,
  876. },
  877. // access to custom children references
  878. ref: {
  879. get: getReference,
  880. },
  881. // dom modifiers
  882. is: needle => name === needle,
  883. appendChild: appendChild(element),
  884. createChildView: createChildView(store),
  885. linkView: view => {
  886. childViews.push(view);
  887. return view;
  888. },
  889. unlinkView: view => {
  890. childViews.splice(childViews.indexOf(view), 1);
  891. },
  892. appendChildView: appendChildView(element, childViews),
  893. removeChildView: removeChildView(element, childViews),
  894. registerWriter: writer => writers.push(writer),
  895. registerReader: reader => readers.push(reader),
  896. registerDestroyer: destroyer => destroyers.push(destroyer),
  897. invalidateLayout: () => (element.layoutCalculated = false),
  898. // access to data store
  899. dispatch: store.dispatch,
  900. query: store.query,
  901. };
  902. // public view API methods
  903. const externalAPIDefinition = {
  904. element: {
  905. get: getElement,
  906. },
  907. childViews: {
  908. get: getChildViews,
  909. },
  910. rect: {
  911. get: getRect,
  912. },
  913. resting: {
  914. get: () => isResting,
  915. },
  916. isRectIgnored: () => ignoreRect,
  917. _read,
  918. _write,
  919. _destroy,
  920. };
  921. // mixin API methods
  922. const mixinAPIDefinition = {
  923. ...sharedAPIDefinition,
  924. rect: {
  925. get: () => rect,
  926. },
  927. };
  928. // add mixin functionality
  929. Object.keys(mixins)
  930. .sort((a, b) => {
  931. // move styles to the back of the mixin list (so adjustments of other mixins are applied to the props correctly)
  932. if (a === 'styles') {
  933. return 1;
  934. } else if (b === 'styles') {
  935. return -1;
  936. }
  937. return 0;
  938. })
  939. .forEach(key => {
  940. const mixinAPI = Mixins[key]({
  941. mixinConfig: mixins[key],
  942. viewProps: props,
  943. viewState: state,
  944. viewInternalAPI: internalAPIDefinition,
  945. viewExternalAPI: externalAPIDefinition,
  946. view: createObject(mixinAPIDefinition),
  947. });
  948. if (mixinAPI) {
  949. activeMixins.push(mixinAPI);
  950. }
  951. });
  952. // construct private api
  953. const internalAPI = createObject(internalAPIDefinition);
  954. // create the view
  955. create({
  956. root: internalAPI,
  957. props,
  958. });
  959. // append created child views to root node
  960. const childCount = getChildCount(element); // need to know the current child count so appending happens in correct order
  961. childViews.forEach((child, index) => {
  962. internalAPI.appendChild(child.element, childCount + index);
  963. });
  964. // call did create
  965. didCreateView(internalAPI);
  966. // expose public api
  967. return createObject(externalAPIDefinition);
  968. };
  969. const createPainter = (read, write, fps = 60) => {
  970. const name = '__framePainter';
  971. // set global painter
  972. if (window[name]) {
  973. window[name].readers.push(read);
  974. window[name].writers.push(write);
  975. return;
  976. }
  977. window[name] = {
  978. readers: [read],
  979. writers: [write],
  980. };
  981. const painter = window[name];
  982. const interval = 1000 / fps;
  983. let last = null;
  984. let id = null;
  985. let requestTick = null;
  986. let cancelTick = null;
  987. const setTimerType = () => {
  988. if (document.hidden) {
  989. requestTick = () => window.setTimeout(() => tick(performance.now()), interval);
  990. cancelTick = () => window.clearTimeout(id);
  991. } else {
  992. requestTick = () => window.requestAnimationFrame(tick);
  993. cancelTick = () => window.cancelAnimationFrame(id);
  994. }
  995. };
  996. document.addEventListener('visibilitychange', () => {
  997. if (cancelTick) cancelTick();
  998. setTimerType();
  999. tick(performance.now());
  1000. });
  1001. const tick = ts => {
  1002. // queue next tick
  1003. id = requestTick(tick);
  1004. // limit fps
  1005. if (!last) {
  1006. last = ts;
  1007. }
  1008. const delta = ts - last;
  1009. if (delta <= interval) {
  1010. // skip frame
  1011. return;
  1012. }
  1013. // align next frame
  1014. last = ts - (delta % interval);
  1015. // update view
  1016. painter.readers.forEach(read => read());
  1017. painter.writers.forEach(write => write(ts));
  1018. };
  1019. setTimerType();
  1020. tick(performance.now());
  1021. return {
  1022. pause: () => {
  1023. cancelTick(id);
  1024. },
  1025. };
  1026. };
  1027. const createRoute = (routes, fn) => ({ root, props, actions = [], timestamp, shouldOptimize }) => {
  1028. actions
  1029. .filter(action => routes[action.type])
  1030. .forEach(action =>
  1031. routes[action.type]({ root, props, action: action.data, timestamp, shouldOptimize })
  1032. );
  1033. if (fn) {
  1034. fn({ root, props, actions, timestamp, shouldOptimize });
  1035. }
  1036. };
  1037. const insertBefore = (newNode, referenceNode) =>
  1038. referenceNode.parentNode.insertBefore(newNode, referenceNode);
  1039. const insertAfter = (newNode, referenceNode) => {
  1040. return referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  1041. };
  1042. const isArray = value => Array.isArray(value);
  1043. const isEmpty = value => value == null;
  1044. const trim = str => str.trim();
  1045. const toString = value => '' + value;
  1046. const toArray = (value, splitter = ',') => {
  1047. if (isEmpty(value)) {
  1048. return [];
  1049. }
  1050. if (isArray(value)) {
  1051. return value;
  1052. }
  1053. return toString(value)
  1054. .split(splitter)
  1055. .map(trim)
  1056. .filter(str => str.length);
  1057. };
  1058. const isBoolean = value => typeof value === 'boolean';
  1059. const toBoolean = value => (isBoolean(value) ? value : value === 'true');
  1060. const isString = value => typeof value === 'string';
  1061. const toNumber = value =>
  1062. isNumber(value) ? value : isString(value) ? toString(value).replace(/[a-z]+/gi, '') : 0;
  1063. const toInt = value => parseInt(toNumber(value), 10);
  1064. const toFloat = value => parseFloat(toNumber(value));
  1065. const isInt = value => isNumber(value) && isFinite(value) && Math.floor(value) === value;
  1066. const toBytes = (value, base = 1000) => {
  1067. // is in bytes
  1068. if (isInt(value)) {
  1069. return value;
  1070. }
  1071. // is natural file size
  1072. let naturalFileSize = toString(value).trim();
  1073. // if is value in megabytes
  1074. if (/MB$/i.test(naturalFileSize)) {
  1075. naturalFileSize = naturalFileSize.replace(/MB$i/, '').trim();
  1076. return toInt(naturalFileSize) * base * base;
  1077. }
  1078. // if is value in kilobytes
  1079. if (/KB/i.test(naturalFileSize)) {
  1080. naturalFileSize = naturalFileSize.replace(/KB$i/, '').trim();
  1081. return toInt(naturalFileSize) * base;
  1082. }
  1083. return toInt(naturalFileSize);
  1084. };
  1085. const isFunction = value => typeof value === 'function';
  1086. const toFunctionReference = string => {
  1087. let ref = self;
  1088. let levels = string.split('.');
  1089. let level = null;
  1090. while ((level = levels.shift())) {
  1091. ref = ref[level];
  1092. if (!ref) {
  1093. return null;
  1094. }
  1095. }
  1096. return ref;
  1097. };
  1098. const methods = {
  1099. process: 'POST',
  1100. patch: 'PATCH',
  1101. revert: 'DELETE',
  1102. fetch: 'GET',
  1103. restore: 'GET',
  1104. load: 'GET',
  1105. };
  1106. const createServerAPI = outline => {
  1107. const api = {};
  1108. api.url = isString(outline) ? outline : outline.url || '';
  1109. api.timeout = outline.timeout ? parseInt(outline.timeout, 10) : 0;
  1110. api.headers = outline.headers ? outline.headers : {};
  1111. forin(methods, key => {
  1112. api[key] = createAction(key, outline[key], methods[key], api.timeout, api.headers);
  1113. });
  1114. // remove process if no url or process on outline
  1115. api.process = outline.process || isString(outline) || outline.url ? api.process : null;
  1116. // special treatment for remove
  1117. api.remove = outline.remove || null;
  1118. // remove generic headers from api object
  1119. delete api.headers;
  1120. return api;
  1121. };
  1122. const createAction = (name, outline, method, timeout, headers) => {
  1123. // is explicitely set to null so disable
  1124. if (outline === null) {
  1125. return null;
  1126. }
  1127. // if is custom function, done! Dev handles everything.
  1128. if (typeof outline === 'function') {
  1129. return outline;
  1130. }
  1131. // build action object
  1132. const action = {
  1133. url: method === 'GET' || method === 'PATCH' ? `?${name}=` : '',
  1134. method,
  1135. headers,
  1136. withCredentials: false,
  1137. timeout,
  1138. onload: null,
  1139. ondata: null,
  1140. onerror: null,
  1141. };
  1142. // is a single url
  1143. if (isString(outline)) {
  1144. action.url = outline;
  1145. return action;
  1146. }
  1147. // overwrite
  1148. Object.assign(action, outline);
  1149. // see if should reformat headers;
  1150. if (isString(action.headers)) {
  1151. const parts = action.headers.split(/:(.+)/);
  1152. action.headers = {
  1153. header: parts[0],
  1154. value: parts[1],
  1155. };
  1156. }
  1157. // if is bool withCredentials
  1158. action.withCredentials = toBoolean(action.withCredentials);
  1159. return action;
  1160. };
  1161. const toServerAPI = value => createServerAPI(value);
  1162. const isNull = value => value === null;
  1163. const isObject = value => typeof value === 'object' && value !== null;
  1164. const isAPI = value => {
  1165. return (
  1166. isObject(value) &&
  1167. isString(value.url) &&
  1168. isObject(value.process) &&
  1169. isObject(value.revert) &&
  1170. isObject(value.restore) &&
  1171. isObject(value.fetch)
  1172. );
  1173. };
  1174. const getType = value => {
  1175. if (isArray(value)) {
  1176. return 'array';
  1177. }
  1178. if (isNull(value)) {
  1179. return 'null';
  1180. }
  1181. if (isInt(value)) {
  1182. return 'int';
  1183. }
  1184. if (/^[0-9]+ ?(?:GB|MB|KB)$/gi.test(value)) {
  1185. return 'bytes';
  1186. }
  1187. if (isAPI(value)) {
  1188. return 'api';
  1189. }
  1190. return typeof value;
  1191. };
  1192. const replaceSingleQuotes = str =>
  1193. str
  1194. .replace(/{\s*'/g, '{"')
  1195. .replace(/'\s*}/g, '"}')
  1196. .replace(/'\s*:/g, '":')
  1197. .replace(/:\s*'/g, ':"')
  1198. .replace(/,\s*'/g, ',"')
  1199. .replace(/'\s*,/g, '",');
  1200. const conversionTable = {
  1201. array: toArray,
  1202. boolean: toBoolean,
  1203. int: value => (getType(value) === 'bytes' ? toBytes(value) : toInt(value)),
  1204. number: toFloat,
  1205. float: toFloat,
  1206. bytes: toBytes,
  1207. string: value => (isFunction(value) ? value : toString(value)),
  1208. function: value => toFunctionReference(value),
  1209. serverapi: toServerAPI,
  1210. object: value => {
  1211. try {
  1212. return JSON.parse(replaceSingleQuotes(value));
  1213. } catch (e) {
  1214. return null;
  1215. }
  1216. },
  1217. };
  1218. const convertTo = (value, type) => conversionTable[type](value);
  1219. const getValueByType = (newValue, defaultValue, valueType) => {
  1220. // can always assign default value
  1221. if (newValue === defaultValue) {
  1222. return newValue;
  1223. }
  1224. // get the type of the new value
  1225. let newValueType = getType(newValue);
  1226. // is valid type?
  1227. if (newValueType !== valueType) {
  1228. // is string input, let's attempt to convert
  1229. const convertedValue = convertTo(newValue, valueType);
  1230. // what is the type now
  1231. newValueType = getType(convertedValue);
  1232. // no valid conversions found
  1233. if (convertedValue === null) {
  1234. throw `Trying to assign value with incorrect type to "${option}", allowed type: "${valueType}"`;
  1235. } else {
  1236. newValue = convertedValue;
  1237. }
  1238. }
  1239. // assign new value
  1240. return newValue;
  1241. };
  1242. const createOption = (defaultValue, valueType) => {
  1243. let currentValue = defaultValue;
  1244. return {
  1245. enumerable: true,
  1246. get: () => currentValue,
  1247. set: newValue => {
  1248. currentValue = getValueByType(newValue, defaultValue, valueType);
  1249. },
  1250. };
  1251. };
  1252. const createOptions = options => {
  1253. const obj = {};
  1254. forin(options, prop => {
  1255. const optionDefinition = options[prop];
  1256. obj[prop] = createOption(optionDefinition[0], optionDefinition[1]);
  1257. });
  1258. return createObject(obj);
  1259. };
  1260. const createInitialState = options => ({
  1261. // model
  1262. items: [],
  1263. // timeout used for calling update items
  1264. listUpdateTimeout: null,
  1265. // timeout used for stacking metadata updates
  1266. itemUpdateTimeout: null,
  1267. // queue of items waiting to be processed
  1268. processingQueue: [],
  1269. // options
  1270. options: createOptions(options),
  1271. });
  1272. const fromCamels = (string, separator = '-') =>
  1273. string
  1274. .split(/(?=[A-Z])/)
  1275. .map(part => part.toLowerCase())
  1276. .join(separator);
  1277. const createOptionAPI = (store, options) => {
  1278. const obj = {};
  1279. forin(options, key => {
  1280. obj[key] = {
  1281. get: () => store.getState().options[key],
  1282. set: value => {
  1283. store.dispatch(`SET_${fromCamels(key, '_').toUpperCase()}`, {
  1284. value,
  1285. });
  1286. },
  1287. };
  1288. });
  1289. return obj;
  1290. };
  1291. const createOptionActions = options => (dispatch, query, state) => {
  1292. const obj = {};
  1293. forin(options, key => {
  1294. const name = fromCamels(key, '_').toUpperCase();
  1295. obj[`SET_${name}`] = action => {
  1296. try {
  1297. state.options[key] = action.value;
  1298. } catch (e) {
  1299. // nope, failed
  1300. }
  1301. // we successfully set the value of this option
  1302. dispatch(`DID_SET_${name}`, { value: state.options[key] });
  1303. };
  1304. });
  1305. return obj;
  1306. };
  1307. const createOptionQueries = options => state => {
  1308. const obj = {};
  1309. forin(options, key => {
  1310. obj[`GET_${fromCamels(key, '_').toUpperCase()}`] = action => state.options[key];
  1311. });
  1312. return obj;
  1313. };
  1314. const InteractionMethod = {
  1315. API: 1,
  1316. DROP: 2,
  1317. BROWSE: 3,
  1318. PASTE: 4,
  1319. NONE: 5,
  1320. };
  1321. const getUniqueId = () =>
  1322. Math.random()
  1323. .toString(36)
  1324. .substring(2, 11);
  1325. const arrayRemove = (arr, index) => arr.splice(index, 1);
  1326. const run = (cb, sync) => {
  1327. if (sync) {
  1328. cb();
  1329. } else if (document.hidden) {
  1330. Promise.resolve(1).then(cb);
  1331. } else {
  1332. setTimeout(cb, 0);
  1333. }
  1334. };
  1335. const on = () => {
  1336. const listeners = [];
  1337. const off = (event, cb) => {
  1338. arrayRemove(
  1339. listeners,
  1340. listeners.findIndex(listener => listener.event === event && (listener.cb === cb || !cb))
  1341. );
  1342. };
  1343. const fire = (event, args, sync) => {
  1344. listeners
  1345. .filter(listener => listener.event === event)
  1346. .map(listener => listener.cb)
  1347. .forEach(cb => run(() => cb(...args), sync));
  1348. };
  1349. return {
  1350. fireSync: (event, ...args) => {
  1351. fire(event, args, true);
  1352. },
  1353. fire: (event, ...args) => {
  1354. fire(event, args, false);
  1355. },
  1356. on: (event, cb) => {
  1357. listeners.push({ event, cb });
  1358. },
  1359. onOnce: (event, cb) => {
  1360. listeners.push({
  1361. event,
  1362. cb: (...args) => {
  1363. off(event, cb);
  1364. cb(...args);
  1365. },
  1366. });
  1367. },
  1368. off,
  1369. };
  1370. };
  1371. const copyObjectPropertiesToObject = (src, target, excluded) => {
  1372. Object.getOwnPropertyNames(src)
  1373. .filter(property => !excluded.includes(property))
  1374. .forEach(key =>
  1375. Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(src, key))
  1376. );
  1377. };
  1378. const PRIVATE = [
  1379. 'fire',
  1380. 'process',
  1381. 'revert',
  1382. 'load',
  1383. 'on',
  1384. 'off',
  1385. 'onOnce',
  1386. 'retryLoad',
  1387. 'extend',
  1388. 'archive',
  1389. 'archived',
  1390. 'release',
  1391. 'released',
  1392. 'requestProcessing',
  1393. 'freeze',
  1394. ];
  1395. const createItemAPI = item => {
  1396. const api = {};
  1397. copyObjectPropertiesToObject(item, api, PRIVATE);
  1398. return api;
  1399. };
  1400. const removeReleasedItems = items => {
  1401. items.forEach((item, index) => {
  1402. if (item.released) {
  1403. arrayRemove(items, index);
  1404. }
  1405. });
  1406. };
  1407. const ItemStatus = {
  1408. INIT: 1,
  1409. IDLE: 2,
  1410. PROCESSING_QUEUED: 9,
  1411. PROCESSING: 3,
  1412. PROCESSING_COMPLETE: 5,
  1413. PROCESSING_ERROR: 6,
  1414. PROCESSING_REVERT_ERROR: 10,
  1415. LOADING: 7,
  1416. LOAD_ERROR: 8,
  1417. };
  1418. const FileOrigin = {
  1419. INPUT: 1,
  1420. LIMBO: 2,
  1421. LOCAL: 3,
  1422. };
  1423. const getNonNumeric = str => /[^0-9]+/.exec(str);
  1424. const getDecimalSeparator = () => getNonNumeric((1.1).toLocaleString())[0];
  1425. const getThousandsSeparator = () => {
  1426. // Added for browsers that do not return the thousands separator (happend on native browser Android 4.4.4)
  1427. // We check against the normal toString output and if they're the same return a comma when decimal separator is a dot
  1428. const decimalSeparator = getDecimalSeparator();
  1429. const thousandsStringWithSeparator = (1000.0).toLocaleString();
  1430. const thousandsStringWithoutSeparator = (1000.0).toString();
  1431. if (thousandsStringWithSeparator !== thousandsStringWithoutSeparator) {
  1432. return getNonNumeric(thousandsStringWithSeparator)[0];
  1433. }
  1434. return decimalSeparator === '.' ? ',' : '.';
  1435. };
  1436. const Type = {
  1437. BOOLEAN: 'boolean',
  1438. INT: 'int',
  1439. NUMBER: 'number',
  1440. STRING: 'string',
  1441. ARRAY: 'array',
  1442. OBJECT: 'object',
  1443. FUNCTION: 'function',
  1444. ACTION: 'action',
  1445. SERVER_API: 'serverapi',
  1446. REGEX: 'regex',
  1447. };
  1448. // all registered filters
  1449. const filters = [];
  1450. // loops over matching filters and passes options to each filter, returning the mapped results
  1451. const applyFilterChain = (key, value, utils) =>
  1452. new Promise((resolve, reject) => {
  1453. // find matching filters for this key
  1454. const matchingFilters = filters.filter(f => f.key === key).map(f => f.cb);
  1455. // resolve now
  1456. if (matchingFilters.length === 0) {
  1457. resolve(value);
  1458. return;
  1459. }
  1460. // first filter to kick things of
  1461. const initialFilter = matchingFilters.shift();
  1462. // chain filters
  1463. matchingFilters
  1464. .reduce(
  1465. // loop over promises passing value to next promise
  1466. (current, next) => current.then(value => next(value, utils)),
  1467. // call initial filter, will return a promise
  1468. initialFilter(value, utils)
  1469. // all executed
  1470. )
  1471. .then(value => resolve(value))
  1472. .catch(error => reject(error));
  1473. });
  1474. const applyFilters = (key, value, utils) =>
  1475. filters.filter(f => f.key === key).map(f => f.cb(value, utils));
  1476. // adds a new filter to the list
  1477. const addFilter = (key, cb) => filters.push({ key, cb });
  1478. const extendDefaultOptions = additionalOptions => Object.assign(defaultOptions, additionalOptions);
  1479. const getOptions = () => ({ ...defaultOptions });
  1480. const setOptions = opts => {
  1481. forin(opts, (key, value) => {
  1482. // key does not exist, so this option cannot be set
  1483. if (!defaultOptions[key]) {
  1484. return;
  1485. }
  1486. defaultOptions[key][0] = getValueByType(
  1487. value,
  1488. defaultOptions[key][0],
  1489. defaultOptions[key][1]
  1490. );
  1491. });
  1492. };
  1493. // default options on app
  1494. const defaultOptions = {
  1495. // the id to add to the root element
  1496. id: [null, Type.STRING],
  1497. // input field name to use
  1498. name: ['filepond', Type.STRING],
  1499. // disable the field
  1500. disabled: [false, Type.BOOLEAN],
  1501. // classname to put on wrapper
  1502. className: [null, Type.STRING],
  1503. // is the field required
  1504. required: [false, Type.BOOLEAN],
  1505. // Allow media capture when value is set
  1506. captureMethod: [null, Type.STRING],
  1507. // - "camera", "microphone" or "camcorder",
  1508. // - Does not work with multiple on apple devices
  1509. // - If set, acceptedFileTypes must be made to match with media wildcard "image/*", "audio/*" or "video/*"
  1510. // sync `acceptedFileTypes` property with `accept` attribute
  1511. allowSyncAcceptAttribute: [true, Type.BOOLEAN],
  1512. // Feature toggles
  1513. allowDrop: [true, Type.BOOLEAN], // Allow dropping of files
  1514. allowBrowse: [true, Type.BOOLEAN], // Allow browsing the file system
  1515. allowPaste: [true, Type.BOOLEAN], // Allow pasting files
  1516. allowMultiple: [false, Type.BOOLEAN], // Allow multiple files (disabled by default, as multiple attribute is also required on input to allow multiple)
  1517. allowReplace: [true, Type.BOOLEAN], // Allow dropping a file on other file to replace it (only works when multiple is set to false)
  1518. allowRevert: [true, Type.BOOLEAN], // Allows user to revert file upload
  1519. allowRemove: [true, Type.BOOLEAN], // Allow user to remove a file
  1520. allowProcess: [true, Type.BOOLEAN], // Allows user to process a file, when set to false, this removes the file upload button
  1521. allowReorder: [false, Type.BOOLEAN], // Allow reordering of files
  1522. allowDirectoriesOnly: [false, Type.BOOLEAN], // Allow only selecting directories with browse (no support for filtering dnd at this point)
  1523. // Try store file if `server` not set
  1524. storeAsFile: [false, Type.BOOLEAN],
  1525. // Revert mode
  1526. forceRevert: [false, Type.BOOLEAN], // Set to 'force' to require the file to be reverted before removal
  1527. // Input requirements
  1528. maxFiles: [null, Type.INT], // Max number of files
  1529. checkValidity: [false, Type.BOOLEAN], // Enables custom validity messages
  1530. // Where to put file
  1531. itemInsertLocationFreedom: [true, Type.BOOLEAN], // Set to false to always add items to begin or end of list
  1532. itemInsertLocation: ['before', Type.STRING], // Default index in list to add items that have been dropped at the top of the list
  1533. itemInsertInterval: [75, Type.INT],
  1534. // Drag 'n Drop related
  1535. dropOnPage: [false, Type.BOOLEAN], // Allow dropping of files anywhere on page (prevents browser from opening file if dropped outside of Up)
  1536. dropOnElement: [true, Type.BOOLEAN], // Drop needs to happen on element (set to false to also load drops outside of Up)
  1537. dropValidation: [false, Type.BOOLEAN], // Enable or disable validating files on drop
  1538. ignoredFiles: [['.ds_store', 'thumbs.db', 'desktop.ini'], Type.ARRAY],
  1539. // Upload related
  1540. instantUpload: [true, Type.BOOLEAN], // Should upload files immediately on drop
  1541. maxParallelUploads: [2, Type.INT], // Maximum files to upload in parallel
  1542. 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
  1543. // Chunks
  1544. chunkUploads: [false, Type.BOOLEAN], // Enable chunked uploads
  1545. chunkForce: [false, Type.BOOLEAN], // Force use of chunk uploads even for files smaller than chunk size
  1546. chunkSize: [5000000, Type.INT], // Size of chunks (5MB default)
  1547. chunkRetryDelays: [[500, 1000, 3000], Type.ARRAY], // Amount of times to retry upload of a chunk when it fails
  1548. // The server api end points to use for uploading (see docs)
  1549. server: [null, Type.SERVER_API],
  1550. // File size calculations, can set to 1024, this is only used for display, properties use file size base 1000
  1551. fileSizeBase: [1000, Type.INT],
  1552. // Labels and status messages
  1553. labelFileSizeBytes: ['bytes', Type.STRING],
  1554. labelFileSizeKilobytes: ['KB', Type.STRING],
  1555. labelFileSizeMegabytes: ['MB', Type.STRING],
  1556. labelFileSizeGigabytes: ['GB', Type.STRING],
  1557. labelDecimalSeparator: [getDecimalSeparator(), Type.STRING], // Default is locale separator
  1558. labelThousandsSeparator: [getThousandsSeparator(), Type.STRING], // Default is locale separator
  1559. labelIdle: [
  1560. 'Drag & Drop your files or <span class="filepond--label-action">Browse</span>',
  1561. Type.STRING,
  1562. ],
  1563. labelInvalidField: ['Field contains invalid files', Type.STRING],
  1564. labelFileWaitingForSize: ['Waiting for size', Type.STRING],
  1565. labelFileSizeNotAvailable: ['Size not available', Type.STRING],
  1566. labelFileCountSingular: ['file in list', Type.STRING],
  1567. labelFileCountPlural: ['files in list', Type.STRING],
  1568. labelFileLoading: ['Loading', Type.STRING],
  1569. labelFileAdded: ['Added', Type.STRING], // assistive only
  1570. labelFileLoadError: ['Error during load', Type.STRING],
  1571. labelFileRemoved: ['Removed', Type.STRING], // assistive only
  1572. labelFileRemoveError: ['Error during remove', Type.STRING],
  1573. labelFileProcessing: ['Uploading', Type.STRING],
  1574. labelFileProcessingComplete: ['Upload complete', Type.STRING],
  1575. labelFileProcessingAborted: ['Upload cancelled', Type.STRING],
  1576. labelFileProcessingError: ['Error during upload', Type.STRING],
  1577. labelFileProcessingRevertError: ['Error during revert', Type.STRING],
  1578. labelTapToCancel: ['tap to cancel', Type.STRING],
  1579. labelTapToRetry: ['tap to retry', Type.STRING],
  1580. labelTapToUndo: ['tap to undo', Type.STRING],
  1581. labelButtonRemoveItem: ['Remove', Type.STRING],
  1582. labelButtonAbortItemLoad: ['Abort', Type.STRING],
  1583. labelButtonRetryItemLoad: ['Retry', Type.STRING],
  1584. labelButtonAbortItemProcessing: ['Cancel', Type.STRING],
  1585. labelButtonUndoItemProcessing: ['Undo', Type.STRING],
  1586. labelButtonRetryItemProcessing: ['Retry', Type.STRING],
  1587. labelButtonProcessItem: ['Upload', Type.STRING],
  1588. // make sure width and height plus viewpox are even numbers so icons are nicely centered
  1589. iconRemove: [
  1590. '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M11.586 13l-2.293 2.293a1 1 0 0 0 1.414 1.414L13 14.414l2.293 2.293a1 1 0 0 0 1.414-1.414L14.414 13l2.293-2.293a1 1 0 0 0-1.414-1.414L13 11.586l-2.293-2.293a1 1 0 0 0-1.414 1.414L11.586 13z" fill="currentColor" fill-rule="nonzero"/></svg>',
  1591. Type.STRING,
  1592. ],
  1593. iconProcess: [
  1594. '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M14 10.414v3.585a1 1 0 0 1-2 0v-3.585l-1.293 1.293a1 1 0 0 1-1.414-1.415l3-3a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1-1.414 1.415L14 10.414zM9 18a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2H9z" fill="currentColor" fill-rule="evenodd"/></svg>',
  1595. Type.STRING,
  1596. ],
  1597. iconRetry: [
  1598. '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M10.81 9.185l-.038.02A4.997 4.997 0 0 0 8 13.683a5 5 0 0 0 5 5 5 5 0 0 0 5-5 1 1 0 0 1 2 0A7 7 0 1 1 9.722 7.496l-.842-.21a.999.999 0 1 1 .484-1.94l3.23.806c.535.133.86.675.73 1.21l-.804 3.233a.997.997 0 0 1-1.21.73.997.997 0 0 1-.73-1.21l.23-.928v-.002z" fill="currentColor" fill-rule="nonzero"/></svg>',
  1599. Type.STRING,
  1600. ],
  1601. iconUndo: [
  1602. '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M9.185 10.81l.02-.038A4.997 4.997 0 0 1 13.683 8a5 5 0 0 1 5 5 5 5 0 0 1-5 5 1 1 0 0 0 0 2A7 7 0 1 0 7.496 9.722l-.21-.842a.999.999 0 1 0-1.94.484l.806 3.23c.133.535.675.86 1.21.73l3.233-.803a.997.997 0 0 0 .73-1.21.997.997 0 0 0-1.21-.73l-.928.23-.002-.001z" fill="currentColor" fill-rule="nonzero"/></svg>',
  1603. Type.STRING,
  1604. ],
  1605. iconDone: [
  1606. '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M18.293 9.293a1 1 0 0 1 1.414 1.414l-7.002 7a1 1 0 0 1-1.414 0l-3.998-4a1 1 0 1 1 1.414-1.414L12 15.586l6.294-6.293z" fill="currentColor" fill-rule="nonzero"/></svg>',
  1607. Type.STRING,
  1608. ],
  1609. // event handlers
  1610. oninit: [null, Type.FUNCTION],
  1611. onwarning: [null, Type.FUNCTION],
  1612. onerror: [null, Type.FUNCTION],
  1613. onactivatefile: [null, Type.FUNCTION],
  1614. oninitfile: [null, Type.FUNCTION],
  1615. onaddfilestart: [null, Type.FUNCTION],
  1616. onaddfileprogress: [null, Type.FUNCTION],
  1617. onaddfile: [null, Type.FUNCTION],
  1618. onprocessfilestart: [null, Type.FUNCTION],
  1619. onprocessfileprogress: [null, Type.FUNCTION],
  1620. onprocessfileabort: [null, Type.FUNCTION],
  1621. onprocessfilerevert: [null, Type.FUNCTION],
  1622. onprocessfile: [null, Type.FUNCTION],
  1623. onprocessfiles: [null, Type.FUNCTION],
  1624. onremovefile: [null, Type.FUNCTION],
  1625. onpreparefile: [null, Type.FUNCTION],
  1626. onupdatefiles: [null, Type.FUNCTION],
  1627. onreorderfiles: [null, Type.FUNCTION],
  1628. // hooks
  1629. beforeDropFile: [null, Type.FUNCTION],
  1630. beforeAddFile: [null, Type.FUNCTION],
  1631. beforeRemoveFile: [null, Type.FUNCTION],
  1632. beforePrepareFile: [null, Type.FUNCTION],
  1633. // styles
  1634. stylePanelLayout: [null, Type.STRING], // null 'integrated', 'compact', 'circle'
  1635. stylePanelAspectRatio: [null, Type.STRING], // null or '3:2' or 1
  1636. styleItemPanelAspectRatio: [null, Type.STRING],
  1637. styleButtonRemoveItemPosition: ['left', Type.STRING],
  1638. styleButtonProcessItemPosition: ['right', Type.STRING],
  1639. styleLoadIndicatorPosition: ['right', Type.STRING],
  1640. styleProgressIndicatorPosition: ['right', Type.STRING],
  1641. styleButtonRemoveItemAlign: [false, Type.BOOLEAN],
  1642. // custom initial files array
  1643. files: [[], Type.ARRAY],
  1644. // show support by displaying credits
  1645. credits: [['https://pqina.nl/', 'Powered by PQINA'], Type.ARRAY],
  1646. };
  1647. const getItemByQuery = (items, query) => {
  1648. // just return first index
  1649. if (isEmpty(query)) {
  1650. return items[0] || null;
  1651. }
  1652. // query is index
  1653. if (isInt(query)) {
  1654. return items[query] || null;
  1655. }
  1656. // if query is item, get the id
  1657. if (typeof query === 'object') {
  1658. query = query.id;
  1659. }
  1660. // assume query is a string and return item by id
  1661. return items.find(item => item.id === query) || null;
  1662. };
  1663. const getNumericAspectRatioFromString = aspectRatio => {
  1664. if (isEmpty(aspectRatio)) {
  1665. return aspectRatio;
  1666. }
  1667. if (/:/.test(aspectRatio)) {
  1668. const parts = aspectRatio.split(':');
  1669. return parts[1] / parts[0];
  1670. }
  1671. return parseFloat(aspectRatio);
  1672. };
  1673. const getActiveItems = items => items.filter(item => !item.archived);
  1674. const Status = {
  1675. EMPTY: 0,
  1676. IDLE: 1, // waiting
  1677. ERROR: 2, // a file is in error state
  1678. BUSY: 3, // busy processing or loading
  1679. READY: 4, // all files uploaded
  1680. };
  1681. let res = null;
  1682. const canUpdateFileInput = () => {
  1683. if (res === null) {
  1684. try {
  1685. const dataTransfer = new DataTransfer();
  1686. dataTransfer.items.add(new File(['hello world'], 'This_Works.txt'));
  1687. const el = document.createElement('input');
  1688. el.setAttribute('type', 'file');
  1689. el.files = dataTransfer.files;
  1690. res = el.files.length === 1;
  1691. } catch (err) {
  1692. res = false;
  1693. }
  1694. }
  1695. return res;
  1696. };
  1697. const ITEM_ERROR = [
  1698. ItemStatus.LOAD_ERROR,
  1699. ItemStatus.PROCESSING_ERROR,
  1700. ItemStatus.PROCESSING_REVERT_ERROR,
  1701. ];
  1702. const ITEM_BUSY = [
  1703. ItemStatus.LOADING,
  1704. ItemStatus.PROCESSING,
  1705. ItemStatus.PROCESSING_QUEUED,
  1706. ItemStatus.INIT,
  1707. ];
  1708. const ITEM_READY = [ItemStatus.PROCESSING_COMPLETE];
  1709. const isItemInErrorState = item => ITEM_ERROR.includes(item.status);
  1710. const isItemInBusyState = item => ITEM_BUSY.includes(item.status);
  1711. const isItemInReadyState = item => ITEM_READY.includes(item.status);
  1712. const isAsync = state =>
  1713. isObject(state.options.server) &&
  1714. (isObject(state.options.server.process) || isFunction(state.options.server.process));
  1715. const queries = state => ({
  1716. GET_STATUS: () => {
  1717. const items = getActiveItems(state.items);
  1718. const { EMPTY, ERROR, BUSY, IDLE, READY } = Status;
  1719. if (items.length === 0) return EMPTY;
  1720. if (items.some(isItemInErrorState)) return ERROR;
  1721. if (items.some(isItemInBusyState)) return BUSY;
  1722. if (items.some(isItemInReadyState)) return READY;
  1723. return IDLE;
  1724. },
  1725. GET_ITEM: query => getItemByQuery(state.items, query),
  1726. GET_ACTIVE_ITEM: query => getItemByQuery(getActiveItems(state.items), query),
  1727. GET_ACTIVE_ITEMS: () => getActiveItems(state.items),
  1728. GET_ITEMS: () => state.items,
  1729. GET_ITEM_NAME: query => {
  1730. const item = getItemByQuery(state.items, query);
  1731. return item ? item.filename : null;
  1732. },
  1733. GET_ITEM_SIZE: query => {
  1734. const item = getItemByQuery(state.items, query);
  1735. return item ? item.fileSize : null;
  1736. },
  1737. GET_STYLES: () =>
  1738. Object.keys(state.options)
  1739. .filter(key => /^style/.test(key))
  1740. .map(option => ({
  1741. name: option,
  1742. value: state.options[option],
  1743. })),
  1744. GET_PANEL_ASPECT_RATIO: () => {
  1745. const isShapeCircle = /circle/.test(state.options.stylePanelLayout);
  1746. const aspectRatio = isShapeCircle
  1747. ? 1
  1748. : getNumericAspectRatioFromString(state.options.stylePanelAspectRatio);
  1749. return aspectRatio;
  1750. },
  1751. GET_ITEM_PANEL_ASPECT_RATIO: () => state.options.styleItemPanelAspectRatio,
  1752. GET_ITEMS_BY_STATUS: status =>
  1753. getActiveItems(state.items).filter(item => item.status === status),
  1754. GET_TOTAL_ITEMS: () => getActiveItems(state.items).length,
  1755. SHOULD_UPDATE_FILE_INPUT: () =>
  1756. state.options.storeAsFile && canUpdateFileInput() && !isAsync(state),
  1757. IS_ASYNC: () => isAsync(state),
  1758. GET_FILE_SIZE_LABELS: query => ({
  1759. labelBytes: query('GET_LABEL_FILE_SIZE_BYTES') || undefined,
  1760. labelKilobytes: query('GET_LABEL_FILE_SIZE_KILOBYTES') || undefined,
  1761. labelMegabytes: query('GET_LABEL_FILE_SIZE_MEGABYTES') || undefined,
  1762. labelGigabytes: query('GET_LABEL_FILE_SIZE_GIGABYTES') || undefined,
  1763. }),
  1764. });
  1765. const hasRoomForItem = state => {
  1766. const count = getActiveItems(state.items).length;
  1767. // if cannot have multiple items, to add one item it should currently not contain items
  1768. if (!state.options.allowMultiple) {
  1769. return count === 0;
  1770. }
  1771. // if allows multiple items, we check if a max item count has been set, if not, there's no limit
  1772. const maxFileCount = state.options.maxFiles;
  1773. if (maxFileCount === null) {
  1774. return true;
  1775. }
  1776. // we check if the current count is smaller than the max count, if so, another file can still be added
  1777. if (count < maxFileCount) {
  1778. return true;
  1779. }
  1780. // no more room for another file
  1781. return false;
  1782. };
  1783. const limit = (value, min, max) => Math.max(Math.min(max, value), min);
  1784. const arrayInsert = (arr, index, item) => arr.splice(index, 0, item);
  1785. const insertItem = (items, item, index) => {
  1786. if (isEmpty(item)) {
  1787. return null;
  1788. }
  1789. // if index is undefined, append
  1790. if (typeof index === 'undefined') {
  1791. items.push(item);
  1792. return item;
  1793. }
  1794. // limit the index to the size of the items array
  1795. index = limit(index, 0, items.length);
  1796. // add item to array
  1797. arrayInsert(items, index, item);
  1798. // expose
  1799. return item;
  1800. };
  1801. const isBase64DataURI = str =>
  1802. /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s]*)\s*$/i.test(
  1803. str
  1804. );
  1805. const getFilenameFromURL = url =>
  1806. url
  1807. .split('/')
  1808. .pop()
  1809. .split('?')
  1810. .shift();
  1811. const getExtensionFromFilename = name => name.split('.').pop();
  1812. const guesstimateExtension = type => {
  1813. // if no extension supplied, exit here
  1814. if (typeof type !== 'string') {
  1815. return '';
  1816. }
  1817. // get subtype
  1818. const subtype = type.split('/').pop();
  1819. // is svg subtype
  1820. if (/svg/.test(subtype)) {
  1821. return 'svg';
  1822. }
  1823. if (/zip|compressed/.test(subtype)) {
  1824. return 'zip';
  1825. }
  1826. if (/plain/.test(subtype)) {
  1827. return 'txt';
  1828. }
  1829. if (/msword/.test(subtype)) {
  1830. return 'doc';
  1831. }
  1832. // if is valid subtype
  1833. if (/[a-z]+/.test(subtype)) {
  1834. // always use jpg extension
  1835. if (subtype === 'jpeg') {
  1836. return 'jpg';
  1837. }
  1838. // return subtype
  1839. return subtype;
  1840. }
  1841. return '';
  1842. };
  1843. const leftPad = (value, padding = '') => (padding + value).slice(-padding.length);
  1844. const getDateString = (date = new Date()) =>
  1845. `${date.getFullYear()}-${leftPad(date.getMonth() + 1, '00')}-${leftPad(
  1846. date.getDate(),
  1847. '00'
  1848. )}_${leftPad(date.getHours(), '00')}-${leftPad(date.getMinutes(), '00')}-${leftPad(
  1849. date.getSeconds(),
  1850. '00'
  1851. )}`;
  1852. const getFileFromBlob = (blob, filename, type = null, extension = null) => {
  1853. const file =
  1854. typeof type === 'string'
  1855. ? blob.slice(0, blob.size, type)
  1856. : blob.slice(0, blob.size, blob.type);
  1857. file.lastModifiedDate = new Date();
  1858. // copy relative path
  1859. if (blob._relativePath) file._relativePath = blob._relativePath;
  1860. // if blob has name property, use as filename if no filename supplied
  1861. if (!isString(filename)) {
  1862. filename = getDateString();
  1863. }
  1864. // if filename supplied but no extension and filename has extension
  1865. if (filename && extension === null && getExtensionFromFilename(filename)) {
  1866. file.name = filename;
  1867. } else {
  1868. extension = extension || guesstimateExtension(file.type);
  1869. file.name = filename + (extension ? '.' + extension : '');
  1870. }
  1871. return file;
  1872. };
  1873. const getBlobBuilder = () => {
  1874. return (window.BlobBuilder =
  1875. window.BlobBuilder ||
  1876. window.WebKitBlobBuilder ||
  1877. window.MozBlobBuilder ||
  1878. window.MSBlobBuilder);
  1879. };
  1880. const createBlob = (arrayBuffer, mimeType) => {
  1881. const BB = getBlobBuilder();
  1882. if (BB) {
  1883. const bb = new BB();
  1884. bb.append(arrayBuffer);
  1885. return bb.getBlob(mimeType);
  1886. }
  1887. return new Blob([arrayBuffer], {
  1888. type: mimeType,
  1889. });
  1890. };
  1891. const getBlobFromByteStringWithMimeType = (byteString, mimeType) => {
  1892. const ab = new ArrayBuffer(byteString.length);
  1893. const ia = new Uint8Array(ab);
  1894. for (let i = 0; i < byteString.length; i++) {
  1895. ia[i] = byteString.charCodeAt(i);
  1896. }
  1897. return createBlob(ab, mimeType);
  1898. };
  1899. const getMimeTypeFromBase64DataURI = dataURI => {
  1900. return (/^data:(.+);/.exec(dataURI) || [])[1] || null;
  1901. };
  1902. const getBase64DataFromBase64DataURI = dataURI => {
  1903. // get data part of string (remove data:image/jpeg...,)
  1904. const data = dataURI.split(',')[1];
  1905. // remove any whitespace as that causes InvalidCharacterError in IE
  1906. return data.replace(/\s/g, '');
  1907. };
  1908. const getByteStringFromBase64DataURI = dataURI => {
  1909. return atob(getBase64DataFromBase64DataURI(dataURI));
  1910. };
  1911. const getBlobFromBase64DataURI = dataURI => {
  1912. const mimeType = getMimeTypeFromBase64DataURI(dataURI);
  1913. const byteString = getByteStringFromBase64DataURI(dataURI);
  1914. return getBlobFromByteStringWithMimeType(byteString, mimeType);
  1915. };
  1916. const getFileFromBase64DataURI = (dataURI, filename, extension) => {
  1917. return getFileFromBlob(getBlobFromBase64DataURI(dataURI), filename, null, extension);
  1918. };
  1919. const getFileNameFromHeader = header => {
  1920. // test if is content disposition header, if not exit
  1921. if (!/^content-disposition:/i.test(header)) return null;
  1922. // get filename parts
  1923. const matches = header
  1924. .split(/filename=|filename\*=.+''/)
  1925. .splice(1)
  1926. .map(name => name.trim().replace(/^["']|[;"']{0,2}$/g, ''))
  1927. .filter(name => name.length);
  1928. return matches.length ? decodeURI(matches[matches.length - 1]) : null;
  1929. };
  1930. const getFileSizeFromHeader = header => {
  1931. if (/content-length:/i.test(header)) {
  1932. const size = header.match(/[0-9]+/)[0];
  1933. return size ? parseInt(size, 10) : null;
  1934. }
  1935. return null;
  1936. };
  1937. const getTranfserIdFromHeader = header => {
  1938. if (/x-content-transfer-id:/i.test(header)) {
  1939. const id = (header.split(':')[1] || '').trim();
  1940. return id || null;
  1941. }
  1942. return null;
  1943. };
  1944. const getFileInfoFromHeaders = headers => {
  1945. const info = {
  1946. source: null,
  1947. name: null,
  1948. size: null,
  1949. };
  1950. const rows = headers.split('\n');
  1951. for (let header of rows) {
  1952. const name = getFileNameFromHeader(header);
  1953. if (name) {
  1954. info.name = name;
  1955. continue;
  1956. }
  1957. const size = getFileSizeFromHeader(header);
  1958. if (size) {
  1959. info.size = size;
  1960. continue;
  1961. }
  1962. const source = getTranfserIdFromHeader(header);
  1963. if (source) {
  1964. info.source = source;
  1965. continue;
  1966. }
  1967. }
  1968. return info;
  1969. };
  1970. const createFileLoader = fetchFn => {
  1971. const state = {
  1972. source: null,
  1973. complete: false,
  1974. progress: 0,
  1975. size: null,
  1976. timestamp: null,
  1977. duration: 0,
  1978. request: null,
  1979. };
  1980. const getProgress = () => state.progress;
  1981. const abort = () => {
  1982. if (state.request && state.request.abort) {
  1983. state.request.abort();
  1984. }
  1985. };
  1986. // load source
  1987. const load = () => {
  1988. // get quick reference
  1989. const source = state.source;
  1990. api.fire('init', source);
  1991. // Load Files
  1992. if (source instanceof File) {
  1993. api.fire('load', source);
  1994. } else if (source instanceof Blob) {
  1995. // Load blobs, set default name to current date
  1996. api.fire('load', getFileFromBlob(source, source.name));
  1997. } else if (isBase64DataURI(source)) {
  1998. // Load base 64, set default name to current date
  1999. api.fire('load', getFileFromBase64DataURI(source));
  2000. } else {
  2001. // Deal as if is external URL, let's load it!
  2002. loadURL(source);
  2003. }
  2004. };
  2005. // loads a url
  2006. const loadURL = url => {
  2007. // is remote url and no fetch method supplied
  2008. if (!fetchFn) {
  2009. api.fire('error', {
  2010. type: 'error',
  2011. body: "Can't load URL",
  2012. code: 400,
  2013. });
  2014. return;
  2015. }
  2016. // set request start
  2017. state.timestamp = Date.now();
  2018. // load file
  2019. state.request = fetchFn(
  2020. url,
  2021. response => {
  2022. // update duration
  2023. state.duration = Date.now() - state.timestamp;
  2024. // done!
  2025. state.complete = true;
  2026. // turn blob response into a file
  2027. if (response instanceof Blob) {
  2028. response = getFileFromBlob(response, response.name || getFilenameFromURL(url));
  2029. }
  2030. api.fire(
  2031. 'load',
  2032. // if has received blob, we go with blob, if no response, we return null
  2033. response instanceof Blob ? response : response ? response.body : null
  2034. );
  2035. },
  2036. error => {
  2037. api.fire(
  2038. 'error',
  2039. typeof error === 'string'
  2040. ? {
  2041. type: 'error',
  2042. code: 0,
  2043. body: error,
  2044. }
  2045. : error
  2046. );
  2047. },
  2048. (computable, current, total) => {
  2049. // collected some meta data already
  2050. if (total) {
  2051. state.size = total;
  2052. }
  2053. // update duration
  2054. state.duration = Date.now() - state.timestamp;
  2055. // if we can't compute progress, we're not going to fire progress events
  2056. if (!computable) {
  2057. state.progress = null;
  2058. return;
  2059. }
  2060. // update progress percentage
  2061. state.progress = current / total;
  2062. // expose
  2063. api.fire('progress', state.progress);
  2064. },
  2065. () => {
  2066. api.fire('abort');
  2067. },
  2068. response => {
  2069. const fileinfo = getFileInfoFromHeaders(
  2070. typeof response === 'string' ? response : response.headers
  2071. );
  2072. api.fire('meta', {
  2073. size: state.size || fileinfo.size,
  2074. filename: fileinfo.name,
  2075. source: fileinfo.source,
  2076. });
  2077. }
  2078. );
  2079. };
  2080. const api = {
  2081. ...on(),
  2082. setSource: source => (state.source = source),
  2083. getProgress, // file load progress
  2084. abort, // abort file load
  2085. load, // start load
  2086. };
  2087. return api;
  2088. };
  2089. const isGet = method => /GET|HEAD/.test(method);
  2090. const sendRequest = (data, url, options) => {
  2091. const api = {
  2092. onheaders: () => {},
  2093. onprogress: () => {},
  2094. onload: () => {},
  2095. ontimeout: () => {},
  2096. onerror: () => {},
  2097. onabort: () => {},
  2098. abort: () => {
  2099. aborted = true;
  2100. xhr.abort();
  2101. },
  2102. };
  2103. // timeout identifier, only used when timeout is defined
  2104. let aborted = false;
  2105. let headersReceived = false;
  2106. // set default options
  2107. options = {
  2108. method: 'POST',
  2109. headers: {},
  2110. withCredentials: false,
  2111. ...options,
  2112. };
  2113. // encode url
  2114. url = encodeURI(url);
  2115. // if method is GET, add any received data to url
  2116. if (isGet(options.method) && data) {
  2117. url = `${url}${encodeURIComponent(typeof data === 'string' ? data : JSON.stringify(data))}`;
  2118. }
  2119. // create request
  2120. const xhr = new XMLHttpRequest();
  2121. // progress of load
  2122. const process = isGet(options.method) ? xhr : xhr.upload;
  2123. process.onprogress = e => {
  2124. // no progress event when aborted ( onprogress is called once after abort() )
  2125. if (aborted) {
  2126. return;
  2127. }
  2128. api.onprogress(e.lengthComputable, e.loaded, e.total);
  2129. };
  2130. // tries to get header info to the app as fast as possible
  2131. xhr.onreadystatechange = () => {
  2132. // not interesting in these states ('unsent' and 'openend' as they don't give us any additional info)
  2133. if (xhr.readyState < 2) {
  2134. return;
  2135. }
  2136. // no server response
  2137. if (xhr.readyState === 4 && xhr.status === 0) {
  2138. return;
  2139. }
  2140. if (headersReceived) {
  2141. return;
  2142. }
  2143. headersReceived = true;
  2144. // we've probably received some useful data in response headers
  2145. api.onheaders(xhr);
  2146. };
  2147. // load successful
  2148. xhr.onload = () => {
  2149. // is classified as valid response
  2150. if (xhr.status >= 200 && xhr.status < 300) {
  2151. api.onload(xhr);
  2152. } else {
  2153. api.onerror(xhr);
  2154. }
  2155. };
  2156. // error during load
  2157. xhr.onerror = () => api.onerror(xhr);
  2158. // request aborted
  2159. xhr.onabort = () => {
  2160. aborted = true;
  2161. api.onabort();
  2162. };
  2163. // request timeout
  2164. xhr.ontimeout = () => api.ontimeout(xhr);
  2165. // open up open up!
  2166. xhr.open(options.method, url, true);
  2167. // set timeout if defined (do it after open so IE11 plays ball)
  2168. if (isInt(options.timeout)) {
  2169. xhr.timeout = options.timeout;
  2170. }
  2171. // add headers
  2172. Object.keys(options.headers).forEach(key => {
  2173. const value = unescape(encodeURIComponent(options.headers[key]));
  2174. xhr.setRequestHeader(key, value);
  2175. });
  2176. // set type of response
  2177. if (options.responseType) {
  2178. xhr.responseType = options.responseType;
  2179. }
  2180. // set credentials
  2181. if (options.withCredentials) {
  2182. xhr.withCredentials = true;
  2183. }
  2184. // let's send our data
  2185. xhr.send(data);
  2186. return api;
  2187. };
  2188. const createResponse = (type, code, body, headers) => ({
  2189. type,
  2190. code,
  2191. body,
  2192. headers,
  2193. });
  2194. const createTimeoutResponse = cb => xhr => {
  2195. cb(createResponse('error', 0, 'Timeout', xhr.getAllResponseHeaders()));
  2196. };
  2197. const hasQS = str => /\?/.test(str);
  2198. const buildURL = (...parts) => {
  2199. let url = '';
  2200. parts.forEach(part => {
  2201. url += hasQS(url) && hasQS(part) ? part.replace(/\?/, '&') : part;
  2202. });
  2203. return url;
  2204. };
  2205. const createFetchFunction = (apiUrl = '', action) => {
  2206. // custom handler (should also handle file, load, error, progress and abort)
  2207. if (typeof action === 'function') {
  2208. return action;
  2209. }
  2210. // no action supplied
  2211. if (!action || !isString(action.url)) {
  2212. return null;
  2213. }
  2214. // set onload hanlder
  2215. const onload = action.onload || (res => res);
  2216. const onerror = action.onerror || (res => null);
  2217. // internal handler
  2218. return (url, load, error, progress, abort, headers) => {
  2219. // do local or remote request based on if the url is external
  2220. const request = sendRequest(url, buildURL(apiUrl, action.url), {
  2221. ...action,
  2222. responseType: 'blob',
  2223. });
  2224. request.onload = xhr => {
  2225. // get headers
  2226. const headers = xhr.getAllResponseHeaders();
  2227. // get filename
  2228. const filename = getFileInfoFromHeaders(headers).name || getFilenameFromURL(url);
  2229. // create response
  2230. load(
  2231. createResponse(
  2232. 'load',
  2233. xhr.status,
  2234. action.method === 'HEAD'
  2235. ? null
  2236. : getFileFromBlob(onload(xhr.response), filename),
  2237. headers
  2238. )
  2239. );
  2240. };
  2241. request.onerror = xhr => {
  2242. error(
  2243. createResponse(
  2244. 'error',
  2245. xhr.status,
  2246. onerror(xhr.response) || xhr.statusText,
  2247. xhr.getAllResponseHeaders()
  2248. )
  2249. );
  2250. };
  2251. request.onheaders = xhr => {
  2252. headers(createResponse('headers', xhr.status, null, xhr.getAllResponseHeaders()));
  2253. };
  2254. request.ontimeout = createTimeoutResponse(error);
  2255. request.onprogress = progress;
  2256. request.onabort = abort;
  2257. // should return request
  2258. return request;
  2259. };
  2260. };
  2261. const ChunkStatus = {
  2262. QUEUED: 0,
  2263. COMPLETE: 1,
  2264. PROCESSING: 2,
  2265. ERROR: 3,
  2266. WAITING: 4,
  2267. };
  2268. /*
  2269. function signature:
  2270. (file, metadata, load, error, progress, abort, transfer, options) => {
  2271. return {
  2272. abort:() => {}
  2273. }
  2274. }
  2275. */
  2276. // apiUrl, action, name, file, metadata, load, error, progress, abort, transfer, options
  2277. const processFileChunked = (
  2278. apiUrl,
  2279. action,
  2280. name,
  2281. file,
  2282. metadata,
  2283. load,
  2284. error,
  2285. progress,
  2286. abort,
  2287. transfer,
  2288. options
  2289. ) => {
  2290. // all chunks
  2291. const chunks = [];
  2292. const { chunkTransferId, chunkServer, chunkSize, chunkRetryDelays } = options;
  2293. // default state
  2294. const state = {
  2295. serverId: chunkTransferId,
  2296. aborted: false,
  2297. };
  2298. // set onload handlers
  2299. const ondata = action.ondata || (fd => fd);
  2300. const onload =
  2301. action.onload ||
  2302. ((xhr, method) =>
  2303. method === 'HEAD' ? xhr.getResponseHeader('Upload-Offset') : xhr.response);
  2304. const onerror = action.onerror || (res => null);
  2305. // create server hook
  2306. const requestTransferId = cb => {
  2307. const formData = new FormData();
  2308. // add metadata under same name
  2309. if (isObject(metadata)) formData.append(name, JSON.stringify(metadata));
  2310. const headers =
  2311. typeof action.headers === 'function'
  2312. ? action.headers(file, metadata)
  2313. : {
  2314. ...action.headers,
  2315. 'Upload-Length': file.size,
  2316. };
  2317. const requestParams = {
  2318. ...action,
  2319. headers,
  2320. };
  2321. // send request object
  2322. const request = sendRequest(ondata(formData), buildURL(apiUrl, action.url), requestParams);
  2323. request.onload = xhr => cb(onload(xhr, requestParams.method));
  2324. request.onerror = xhr =>
  2325. error(
  2326. createResponse(
  2327. 'error',
  2328. xhr.status,
  2329. onerror(xhr.response) || xhr.statusText,
  2330. xhr.getAllResponseHeaders()
  2331. )
  2332. );
  2333. request.ontimeout = createTimeoutResponse(error);
  2334. };
  2335. const requestTransferOffset = cb => {
  2336. const requestUrl = buildURL(apiUrl, chunkServer.url, state.serverId);
  2337. const headers =
  2338. typeof action.headers === 'function'
  2339. ? action.headers(state.serverId)
  2340. : {
  2341. ...action.headers,
  2342. };
  2343. const requestParams = {
  2344. headers,
  2345. method: 'HEAD',
  2346. };
  2347. const request = sendRequest(null, requestUrl, requestParams);
  2348. request.onload = xhr => cb(onload(xhr, requestParams.method));
  2349. request.onerror = xhr =>
  2350. error(
  2351. createResponse(
  2352. 'error',
  2353. xhr.status,
  2354. onerror(xhr.response) || xhr.statusText,
  2355. xhr.getAllResponseHeaders()
  2356. )
  2357. );
  2358. request.ontimeout = createTimeoutResponse(error);
  2359. };
  2360. // create chunks
  2361. const lastChunkIndex = Math.floor(file.size / chunkSize);
  2362. for (let i = 0; i <= lastChunkIndex; i++) {
  2363. const offset = i * chunkSize;
  2364. const data = file.slice(offset, offset + chunkSize, 'application/offset+octet-stream');
  2365. chunks[i] = {
  2366. index: i,
  2367. size: data.size,
  2368. offset,
  2369. data,
  2370. file,
  2371. progress: 0,
  2372. retries: [...chunkRetryDelays],
  2373. status: ChunkStatus.QUEUED,
  2374. error: null,
  2375. request: null,
  2376. timeout: null,
  2377. };
  2378. }
  2379. const completeProcessingChunks = () => load(state.serverId);
  2380. const canProcessChunk = chunk =>
  2381. chunk.status === ChunkStatus.QUEUED || chunk.status === ChunkStatus.ERROR;
  2382. const processChunk = chunk => {
  2383. // processing is paused, wait here
  2384. if (state.aborted) return;
  2385. // get next chunk to process
  2386. chunk = chunk || chunks.find(canProcessChunk);
  2387. // no more chunks to process
  2388. if (!chunk) {
  2389. // all done?
  2390. if (chunks.every(chunk => chunk.status === ChunkStatus.COMPLETE)) {
  2391. completeProcessingChunks();
  2392. }
  2393. // no chunk to handle
  2394. return;
  2395. }
  2396. // now processing this chunk
  2397. chunk.status = ChunkStatus.PROCESSING;
  2398. chunk.progress = null;
  2399. // allow parsing of formdata
  2400. const ondata = chunkServer.ondata || (fd => fd);
  2401. const onerror = chunkServer.onerror || (res => null);
  2402. // send request object
  2403. const requestUrl = buildURL(apiUrl, chunkServer.url, state.serverId);
  2404. const headers =
  2405. typeof chunkServer.headers === 'function'
  2406. ? chunkServer.headers(chunk)
  2407. : {
  2408. ...chunkServer.headers,
  2409. 'Content-Type': 'application/offset+octet-stream',
  2410. 'Upload-Offset': chunk.offset,
  2411. 'Upload-Length': file.size,
  2412. 'Upload-Name': file.name,
  2413. };
  2414. const request = (chunk.request = sendRequest(ondata(chunk.data), requestUrl, {
  2415. ...chunkServer,
  2416. headers,
  2417. }));
  2418. request.onload = () => {
  2419. // done!
  2420. chunk.status = ChunkStatus.COMPLETE;
  2421. // remove request reference
  2422. chunk.request = null;
  2423. // start processing more chunks
  2424. processChunks();
  2425. };
  2426. request.onprogress = (lengthComputable, loaded, total) => {
  2427. chunk.progress = lengthComputable ? loaded : null;
  2428. updateTotalProgress();
  2429. };
  2430. request.onerror = xhr => {
  2431. chunk.status = ChunkStatus.ERROR;
  2432. chunk.request = null;
  2433. chunk.error = onerror(xhr.response) || xhr.statusText;
  2434. if (!retryProcessChunk(chunk)) {
  2435. error(
  2436. createResponse(
  2437. 'error',
  2438. xhr.status,
  2439. onerror(xhr.response) || xhr.statusText,
  2440. xhr.getAllResponseHeaders()
  2441. )
  2442. );
  2443. }
  2444. };
  2445. request.ontimeout = xhr => {
  2446. chunk.status = ChunkStatus.ERROR;
  2447. chunk.request = null;
  2448. if (!retryProcessChunk(chunk)) {
  2449. createTimeoutResponse(error)(xhr);
  2450. }
  2451. };
  2452. request.onabort = () => {
  2453. chunk.status = ChunkStatus.QUEUED;
  2454. chunk.request = null;
  2455. abort();
  2456. };
  2457. };
  2458. const retryProcessChunk = chunk => {
  2459. // no more retries left
  2460. if (chunk.retries.length === 0) return false;
  2461. // new retry
  2462. chunk.status = ChunkStatus.WAITING;
  2463. clearTimeout(chunk.timeout);
  2464. chunk.timeout = setTimeout(() => {
  2465. processChunk(chunk);
  2466. }, chunk.retries.shift());
  2467. // we're going to retry
  2468. return true;
  2469. };
  2470. const updateTotalProgress = () => {
  2471. // calculate total progress fraction
  2472. const totalBytesTransfered = chunks.reduce((p, chunk) => {
  2473. if (p === null || chunk.progress === null) return null;
  2474. return p + chunk.progress;
  2475. }, 0);
  2476. // can't compute progress
  2477. if (totalBytesTransfered === null) return progress(false, 0, 0);
  2478. // calculate progress values
  2479. const totalSize = chunks.reduce((total, chunk) => total + chunk.size, 0);
  2480. // can update progress indicator
  2481. progress(true, totalBytesTransfered, totalSize);
  2482. };
  2483. // process new chunks
  2484. const processChunks = () => {
  2485. const totalProcessing = chunks.filter(chunk => chunk.status === ChunkStatus.PROCESSING)
  2486. .length;
  2487. if (totalProcessing >= 1) return;
  2488. processChunk();
  2489. };
  2490. const abortChunks = () => {
  2491. chunks.forEach(chunk => {
  2492. clearTimeout(chunk.timeout);
  2493. if (chunk.request) {
  2494. chunk.request.abort();
  2495. }
  2496. });
  2497. };
  2498. // let's go!
  2499. if (!state.serverId) {
  2500. requestTransferId(serverId => {
  2501. // stop here if aborted, might have happened in between request and callback
  2502. if (state.aborted) return;
  2503. // pass back to item so we can use it if something goes wrong
  2504. transfer(serverId);
  2505. // store internally
  2506. state.serverId = serverId;
  2507. processChunks();
  2508. });
  2509. } else {
  2510. requestTransferOffset(offset => {
  2511. // stop here if aborted, might have happened in between request and callback
  2512. if (state.aborted) return;
  2513. // mark chunks with lower offset as complete
  2514. chunks
  2515. .filter(chunk => chunk.offset < offset)
  2516. .forEach(chunk => {
  2517. chunk.status = ChunkStatus.COMPLETE;
  2518. chunk.progress = chunk.size;
  2519. });
  2520. // continue processing
  2521. processChunks();
  2522. });
  2523. }
  2524. return {
  2525. abort: () => {
  2526. state.aborted = true;
  2527. abortChunks();
  2528. },
  2529. };
  2530. };
  2531. /*
  2532. function signature:
  2533. (file, metadata, load, error, progress, abort) => {
  2534. return {
  2535. abort:() => {}
  2536. }
  2537. }
  2538. */
  2539. const createFileProcessorFunction = (apiUrl, action, name, options) => (
  2540. file,
  2541. metadata,
  2542. load,
  2543. error,
  2544. progress,
  2545. abort,
  2546. transfer
  2547. ) => {
  2548. // no file received
  2549. if (!file) return;
  2550. // if was passed a file, and we can chunk it, exit here
  2551. const canChunkUpload = options.chunkUploads;
  2552. const shouldChunkUpload = canChunkUpload && file.size > options.chunkSize;
  2553. const willChunkUpload = canChunkUpload && (shouldChunkUpload || options.chunkForce);
  2554. if (file instanceof Blob && willChunkUpload)
  2555. return processFileChunked(
  2556. apiUrl,
  2557. action,
  2558. name,
  2559. file,
  2560. metadata,
  2561. load,
  2562. error,
  2563. progress,
  2564. abort,
  2565. transfer,
  2566. options
  2567. );
  2568. // set handlers
  2569. const ondata = action.ondata || (fd => fd);
  2570. const onload = action.onload || (res => res);
  2571. const onerror = action.onerror || (res => null);
  2572. const headers =
  2573. typeof action.headers === 'function'
  2574. ? action.headers(file, metadata) || {}
  2575. : {
  2576. ...action.headers,
  2577. };
  2578. const requestParams = {
  2579. ...action,
  2580. headers,
  2581. };
  2582. // create formdata object
  2583. var formData = new FormData();
  2584. // add metadata under same name
  2585. if (isObject(metadata)) {
  2586. formData.append(name, JSON.stringify(metadata));
  2587. }
  2588. // Turn into an array of objects so no matter what the input, we can handle it the same way
  2589. (file instanceof Blob ? [{ name: null, file }] : file).forEach(item => {
  2590. formData.append(
  2591. name,
  2592. item.file,
  2593. item.name === null ? item.file.name : `${item.name}${item.file.name}`
  2594. );
  2595. });
  2596. // send request object
  2597. const request = sendRequest(ondata(formData), buildURL(apiUrl, action.url), requestParams);
  2598. request.onload = xhr => {
  2599. load(createResponse('load', xhr.status, onload(xhr.response), xhr.getAllResponseHeaders()));
  2600. };
  2601. request.onerror = xhr => {
  2602. error(
  2603. createResponse(
  2604. 'error',
  2605. xhr.status,
  2606. onerror(xhr.response) || xhr.statusText,
  2607. xhr.getAllResponseHeaders()
  2608. )
  2609. );
  2610. };
  2611. request.ontimeout = createTimeoutResponse(error);
  2612. request.onprogress = progress;
  2613. request.onabort = abort;
  2614. // should return request
  2615. return request;
  2616. };
  2617. const createProcessorFunction = (apiUrl = '', action, name, options) => {
  2618. // custom handler (should also handle file, load, error, progress and abort)
  2619. if (typeof action === 'function') return (...params) => action(name, ...params, options);
  2620. // no action supplied
  2621. if (!action || !isString(action.url)) return null;
  2622. // internal handler
  2623. return createFileProcessorFunction(apiUrl, action, name, options);
  2624. };
  2625. /*
  2626. function signature:
  2627. (uniqueFileId, load, error) => { }
  2628. */
  2629. const createRevertFunction = (apiUrl = '', action) => {
  2630. // is custom implementation
  2631. if (typeof action === 'function') {
  2632. return action;
  2633. }
  2634. // no action supplied, return stub function, interface will work, but file won't be removed
  2635. if (!action || !isString(action.url)) {
  2636. return (uniqueFileId, load) => load();
  2637. }
  2638. // set onload hanlder
  2639. const onload = action.onload || (res => res);
  2640. const onerror = action.onerror || (res => null);
  2641. // internal implementation
  2642. return (uniqueFileId, load, error) => {
  2643. const request = sendRequest(
  2644. uniqueFileId,
  2645. apiUrl + action.url,
  2646. action // contains method, headers and withCredentials properties
  2647. );
  2648. request.onload = xhr => {
  2649. load(
  2650. createResponse(
  2651. 'load',
  2652. xhr.status,
  2653. onload(xhr.response),
  2654. xhr.getAllResponseHeaders()
  2655. )
  2656. );
  2657. };
  2658. request.onerror = xhr => {
  2659. error(
  2660. createResponse(
  2661. 'error',
  2662. xhr.status,
  2663. onerror(xhr.response) || xhr.statusText,
  2664. xhr.getAllResponseHeaders()
  2665. )
  2666. );
  2667. };
  2668. request.ontimeout = createTimeoutResponse(error);
  2669. return request;
  2670. };
  2671. };
  2672. const getRandomNumber = (min = 0, max = 1) => min + Math.random() * (max - min);
  2673. const createPerceivedPerformanceUpdater = (
  2674. cb,
  2675. duration = 1000,
  2676. offset = 0,
  2677. tickMin = 25,
  2678. tickMax = 250
  2679. ) => {
  2680. let timeout = null;
  2681. const start = Date.now();
  2682. const tick = () => {
  2683. let runtime = Date.now() - start;
  2684. let delay = getRandomNumber(tickMin, tickMax);
  2685. if (runtime + delay > duration) {
  2686. delay = runtime + delay - duration;
  2687. }
  2688. let progress = runtime / duration;
  2689. if (progress >= 1 || document.hidden) {
  2690. cb(1);
  2691. return;
  2692. }
  2693. cb(progress);
  2694. timeout = setTimeout(tick, delay);
  2695. };
  2696. if (duration > 0) tick();
  2697. return {
  2698. clear: () => {
  2699. clearTimeout(timeout);
  2700. },
  2701. };
  2702. };
  2703. const createFileProcessor = (processFn, options) => {
  2704. const state = {
  2705. complete: false,
  2706. perceivedProgress: 0,
  2707. perceivedPerformanceUpdater: null,
  2708. progress: null,
  2709. timestamp: null,
  2710. perceivedDuration: 0,
  2711. duration: 0,
  2712. request: null,
  2713. response: null,
  2714. };
  2715. const { allowMinimumUploadDuration } = options;
  2716. const process = (file, metadata) => {
  2717. const progressFn = () => {
  2718. // we've not yet started the real download, stop here
  2719. // the request might not go through, for instance, there might be some server trouble
  2720. // if state.progress is null, the server does not allow computing progress and we show the spinner instead
  2721. if (state.duration === 0 || state.progress === null) return;
  2722. // as we're now processing, fire the progress event
  2723. api.fire('progress', api.getProgress());
  2724. };
  2725. const completeFn = () => {
  2726. state.complete = true;
  2727. api.fire('load-perceived', state.response.body);
  2728. };
  2729. // let's start processing
  2730. api.fire('start');
  2731. // set request start
  2732. state.timestamp = Date.now();
  2733. // create perceived performance progress indicator
  2734. state.perceivedPerformanceUpdater = createPerceivedPerformanceUpdater(
  2735. progress => {
  2736. state.perceivedProgress = progress;
  2737. state.perceivedDuration = Date.now() - state.timestamp;
  2738. progressFn();
  2739. // if fake progress is done, and a response has been received,
  2740. // and we've not yet called the complete method
  2741. if (state.response && state.perceivedProgress === 1 && !state.complete) {
  2742. // we done!
  2743. completeFn();
  2744. }
  2745. },
  2746. // random delay as in a list of files you start noticing
  2747. // files uploading at the exact same speed
  2748. allowMinimumUploadDuration ? getRandomNumber(750, 1500) : 0
  2749. );
  2750. // remember request so we can abort it later
  2751. state.request = processFn(
  2752. // the file to process
  2753. file,
  2754. // the metadata to send along
  2755. metadata,
  2756. // callbacks (load, error, progress, abort, transfer)
  2757. // load expects the body to be a server id if
  2758. // you want to make use of revert
  2759. response => {
  2760. // we put the response in state so we can access
  2761. // it outside of this method
  2762. state.response = isObject(response)
  2763. ? response
  2764. : {
  2765. type: 'load',
  2766. code: 200,
  2767. body: `${response}`,
  2768. headers: {},
  2769. };
  2770. // update duration
  2771. state.duration = Date.now() - state.timestamp;
  2772. // force progress to 1 as we're now done
  2773. state.progress = 1;
  2774. // actual load is done let's share results
  2775. api.fire('load', state.response.body);
  2776. // we are really done
  2777. // if perceived progress is 1 ( wait for perceived progress to complete )
  2778. // or if server does not support progress ( null )
  2779. if (
  2780. !allowMinimumUploadDuration ||
  2781. (allowMinimumUploadDuration && state.perceivedProgress === 1)
  2782. ) {
  2783. completeFn();
  2784. }
  2785. },
  2786. // error is expected to be an object with type, code, body
  2787. error => {
  2788. // cancel updater
  2789. state.perceivedPerformanceUpdater.clear();
  2790. // update others about this error
  2791. api.fire(
  2792. 'error',
  2793. isObject(error)
  2794. ? error
  2795. : {
  2796. type: 'error',
  2797. code: 0,
  2798. body: `${error}`,
  2799. }
  2800. );
  2801. },
  2802. // actual processing progress
  2803. (computable, current, total) => {
  2804. // update actual duration
  2805. state.duration = Date.now() - state.timestamp;
  2806. // update actual progress
  2807. state.progress = computable ? current / total : null;
  2808. progressFn();
  2809. },
  2810. // abort does not expect a value
  2811. () => {
  2812. // stop updater
  2813. state.perceivedPerformanceUpdater.clear();
  2814. // fire the abort event so we can switch visuals
  2815. api.fire('abort', state.response ? state.response.body : null);
  2816. },
  2817. // register the id for this transfer
  2818. transferId => {
  2819. api.fire('transfer', transferId);
  2820. }
  2821. );
  2822. };
  2823. const abort = () => {
  2824. // no request running, can't abort
  2825. if (!state.request) return;
  2826. // stop updater
  2827. state.perceivedPerformanceUpdater.clear();
  2828. // abort actual request
  2829. if (state.request.abort) state.request.abort();
  2830. // if has response object, we've completed the request
  2831. state.complete = true;
  2832. };
  2833. const reset = () => {
  2834. abort();
  2835. state.complete = false;
  2836. state.perceivedProgress = 0;
  2837. state.progress = 0;
  2838. state.timestamp = null;
  2839. state.perceivedDuration = 0;
  2840. state.duration = 0;
  2841. state.request = null;
  2842. state.response = null;
  2843. };
  2844. const getProgress = allowMinimumUploadDuration
  2845. ? () => (state.progress ? Math.min(state.progress, state.perceivedProgress) : null)
  2846. : () => state.progress || null;
  2847. const getDuration = allowMinimumUploadDuration
  2848. ? () => Math.min(state.duration, state.perceivedDuration)
  2849. : () => state.duration;
  2850. const api = {
  2851. ...on(),
  2852. process, // start processing file
  2853. abort, // abort active process request
  2854. getProgress,
  2855. getDuration,
  2856. reset,
  2857. };
  2858. return api;
  2859. };
  2860. const getFilenameWithoutExtension = name => name.substring(0, name.lastIndexOf('.')) || name;
  2861. const createFileStub = source => {
  2862. let data = [source.name, source.size, source.type];
  2863. // is blob or base64, then we need to set the name
  2864. if (source instanceof Blob || isBase64DataURI(source)) {
  2865. data[0] = source.name || getDateString();
  2866. } else if (isBase64DataURI(source)) {
  2867. // if is base64 data uri we need to determine the average size and type
  2868. data[1] = source.length;
  2869. data[2] = getMimeTypeFromBase64DataURI(source);
  2870. } else if (isString(source)) {
  2871. // url
  2872. data[0] = getFilenameFromURL(source);
  2873. data[1] = 0;
  2874. data[2] = 'application/octet-stream';
  2875. }
  2876. return {
  2877. name: data[0],
  2878. size: data[1],
  2879. type: data[2],
  2880. };
  2881. };
  2882. const isFile = value => !!(value instanceof File || (value instanceof Blob && value.name));
  2883. const deepCloneObject = src => {
  2884. if (!isObject(src)) return src;
  2885. const target = isArray(src) ? [] : {};
  2886. for (const key in src) {
  2887. if (!src.hasOwnProperty(key)) continue;
  2888. const v = src[key];
  2889. target[key] = v && isObject(v) ? deepCloneObject(v) : v;
  2890. }
  2891. return target;
  2892. };
  2893. const createItem = (origin = null, serverFileReference = null, file = null) => {
  2894. // unique id for this item, is used to identify the item across views
  2895. const id = getUniqueId();
  2896. /**
  2897. * Internal item state
  2898. */
  2899. const state = {
  2900. // is archived
  2901. archived: false,
  2902. // if is frozen, no longer fires events
  2903. frozen: false,
  2904. // removed from view
  2905. released: false,
  2906. // original source
  2907. source: null,
  2908. // file model reference
  2909. file,
  2910. // id of file on server
  2911. serverFileReference,
  2912. // id of file transfer on server
  2913. transferId: null,
  2914. // is aborted
  2915. processingAborted: false,
  2916. // current item status
  2917. status: serverFileReference ? ItemStatus.PROCESSING_COMPLETE : ItemStatus.INIT,
  2918. // active processes
  2919. activeLoader: null,
  2920. activeProcessor: null,
  2921. };
  2922. // callback used when abort processing is called to link back to the resolve method
  2923. let abortProcessingRequestComplete = null;
  2924. /**
  2925. * Externally added item metadata
  2926. */
  2927. const metadata = {};
  2928. // item data
  2929. const setStatus = status => (state.status = status);
  2930. // fire event unless the item has been archived
  2931. const fire = (event, ...params) => {
  2932. if (state.released || state.frozen) return;
  2933. api.fire(event, ...params);
  2934. };
  2935. // file data
  2936. const getFileExtension = () => getExtensionFromFilename(state.file.name);
  2937. const getFileType = () => state.file.type;
  2938. const getFileSize = () => state.file.size;
  2939. const getFile = () => state.file;
  2940. //
  2941. // logic to load a file
  2942. //
  2943. const load = (source, loader, onload) => {
  2944. // remember the original item source
  2945. state.source = source;
  2946. // source is known
  2947. api.fireSync('init');
  2948. // file stub is already there
  2949. if (state.file) {
  2950. api.fireSync('load-skip');
  2951. return;
  2952. }
  2953. // set a stub file object while loading the actual data
  2954. state.file = createFileStub(source);
  2955. // starts loading
  2956. loader.on('init', () => {
  2957. fire('load-init');
  2958. });
  2959. // we'eve received a size indication, let's update the stub
  2960. loader.on('meta', meta => {
  2961. // set size of file stub
  2962. state.file.size = meta.size;
  2963. // set name of file stub
  2964. state.file.filename = meta.filename;
  2965. // if has received source, we done
  2966. if (meta.source) {
  2967. origin = FileOrigin.LIMBO;
  2968. state.serverFileReference = meta.source;
  2969. state.status = ItemStatus.PROCESSING_COMPLETE;
  2970. }
  2971. // size has been updated
  2972. fire('load-meta');
  2973. });
  2974. // the file is now loading we need to update the progress indicators
  2975. loader.on('progress', progress => {
  2976. setStatus(ItemStatus.LOADING);
  2977. fire('load-progress', progress);
  2978. });
  2979. // an error was thrown while loading the file, we need to switch to error state
  2980. loader.on('error', error => {
  2981. setStatus(ItemStatus.LOAD_ERROR);
  2982. fire('load-request-error', error);
  2983. });
  2984. // user or another process aborted the file load (cannot retry)
  2985. loader.on('abort', () => {
  2986. setStatus(ItemStatus.INIT);
  2987. fire('load-abort');
  2988. });
  2989. // done loading
  2990. loader.on('load', file => {
  2991. // as we've now loaded the file the loader is no longer required
  2992. state.activeLoader = null;
  2993. // called when file has loaded succesfully
  2994. const success = result => {
  2995. // set (possibly) transformed file
  2996. state.file = isFile(result) ? result : state.file;
  2997. // file received
  2998. if (origin === FileOrigin.LIMBO && state.serverFileReference) {
  2999. setStatus(ItemStatus.PROCESSING_COMPLETE);
  3000. } else {
  3001. setStatus(ItemStatus.IDLE);
  3002. }
  3003. fire('load');
  3004. };
  3005. const error = result => {
  3006. // set original file
  3007. state.file = file;
  3008. fire('load-meta');
  3009. setStatus(ItemStatus.LOAD_ERROR);
  3010. fire('load-file-error', result);
  3011. };
  3012. // if we already have a server file reference, we don't need to call the onload method
  3013. if (state.serverFileReference) {
  3014. success(file);
  3015. return;
  3016. }
  3017. // no server id, let's give this file the full treatment
  3018. onload(file, success, error);
  3019. });
  3020. // set loader source data
  3021. loader.setSource(source);
  3022. // set as active loader
  3023. state.activeLoader = loader;
  3024. // load the source data
  3025. loader.load();
  3026. };
  3027. const retryLoad = () => {
  3028. if (!state.activeLoader) {
  3029. return;
  3030. }
  3031. state.activeLoader.load();
  3032. };
  3033. const abortLoad = () => {
  3034. if (state.activeLoader) {
  3035. state.activeLoader.abort();
  3036. return;
  3037. }
  3038. setStatus(ItemStatus.INIT);
  3039. fire('load-abort');
  3040. };
  3041. //
  3042. // logic to process a file
  3043. //
  3044. const process = (processor, onprocess) => {
  3045. // processing was aborted
  3046. if (state.processingAborted) {
  3047. state.processingAborted = false;
  3048. return;
  3049. }
  3050. // now processing
  3051. setStatus(ItemStatus.PROCESSING);
  3052. // reset abort callback
  3053. abortProcessingRequestComplete = null;
  3054. // if no file loaded we'll wait for the load event
  3055. if (!(state.file instanceof Blob)) {
  3056. api.on('load', () => {
  3057. process(processor, onprocess);
  3058. });
  3059. return;
  3060. }
  3061. // setup processor
  3062. processor.on('load', serverFileReference => {
  3063. // need this id to be able to revert the upload
  3064. state.transferId = null;
  3065. state.serverFileReference = serverFileReference;
  3066. });
  3067. // register transfer id
  3068. processor.on('transfer', transferId => {
  3069. // need this id to be able to revert the upload
  3070. state.transferId = transferId;
  3071. });
  3072. processor.on('load-perceived', serverFileReference => {
  3073. // no longer required
  3074. state.activeProcessor = null;
  3075. // need this id to be able to rever the upload
  3076. state.transferId = null;
  3077. state.serverFileReference = serverFileReference;
  3078. setStatus(ItemStatus.PROCESSING_COMPLETE);
  3079. fire('process-complete', serverFileReference);
  3080. });
  3081. processor.on('start', () => {
  3082. fire('process-start');
  3083. });
  3084. processor.on('error', error => {
  3085. state.activeProcessor = null;
  3086. setStatus(ItemStatus.PROCESSING_ERROR);
  3087. fire('process-error', error);
  3088. });
  3089. processor.on('abort', serverFileReference => {
  3090. state.activeProcessor = null;
  3091. // if file was uploaded but processing was cancelled during perceived processor time store file reference
  3092. state.serverFileReference = serverFileReference;
  3093. setStatus(ItemStatus.IDLE);
  3094. fire('process-abort');
  3095. // has timeout so doesn't interfere with remove action
  3096. if (abortProcessingRequestComplete) {
  3097. abortProcessingRequestComplete();
  3098. }
  3099. });
  3100. processor.on('progress', progress => {
  3101. fire('process-progress', progress);
  3102. });
  3103. // when successfully transformed
  3104. const success = file => {
  3105. // if was archived in the mean time, don't process
  3106. if (state.archived) return;
  3107. // process file!
  3108. processor.process(file, { ...metadata });
  3109. };
  3110. // something went wrong during transform phase
  3111. const error = console.error;
  3112. // start processing the file
  3113. onprocess(state.file, success, error);
  3114. // set as active processor
  3115. state.activeProcessor = processor;
  3116. };
  3117. const requestProcessing = () => {
  3118. state.processingAborted = false;
  3119. setStatus(ItemStatus.PROCESSING_QUEUED);
  3120. };
  3121. const abortProcessing = () =>
  3122. new Promise(resolve => {
  3123. if (!state.activeProcessor) {
  3124. state.processingAborted = true;
  3125. setStatus(ItemStatus.IDLE);
  3126. fire('process-abort');
  3127. resolve();
  3128. return;
  3129. }
  3130. abortProcessingRequestComplete = () => {
  3131. resolve();
  3132. };
  3133. state.activeProcessor.abort();
  3134. });
  3135. //
  3136. // logic to revert a processed file
  3137. //
  3138. const revert = (revertFileUpload, forceRevert) =>
  3139. new Promise((resolve, reject) => {
  3140. // a completed upload will have a serverFileReference, a failed chunked upload where
  3141. // getting a serverId succeeded but >=0 chunks have been uploaded will have transferId set
  3142. const serverTransferId =
  3143. state.serverFileReference !== null ? state.serverFileReference : state.transferId;
  3144. // cannot revert without a server id for this process
  3145. if (serverTransferId === null) {
  3146. resolve();
  3147. return;
  3148. }
  3149. // revert the upload (fire and forget)
  3150. revertFileUpload(
  3151. serverTransferId,
  3152. () => {
  3153. // reset file server id and transfer id as now it's not available on the server
  3154. state.serverFileReference = null;
  3155. state.transferId = null;
  3156. resolve();
  3157. },
  3158. error => {
  3159. // don't set error state when reverting is optional, it will always resolve
  3160. if (!forceRevert) {
  3161. resolve();
  3162. return;
  3163. }
  3164. // oh no errors
  3165. setStatus(ItemStatus.PROCESSING_REVERT_ERROR);
  3166. fire('process-revert-error');
  3167. reject(error);
  3168. }
  3169. );
  3170. // fire event
  3171. setStatus(ItemStatus.IDLE);
  3172. fire('process-revert');
  3173. });
  3174. // exposed methods
  3175. const setMetadata = (key, value, silent) => {
  3176. const keys = key.split('.');
  3177. const root = keys[0];
  3178. const last = keys.pop();
  3179. let data = metadata;
  3180. keys.forEach(key => (data = data[key]));
  3181. // compare old value against new value, if they're the same, we're not updating
  3182. if (JSON.stringify(data[last]) === JSON.stringify(value)) return;
  3183. // update value
  3184. data[last] = value;
  3185. // fire update
  3186. fire('metadata-update', {
  3187. key: root,
  3188. value: metadata[root],
  3189. silent,
  3190. });
  3191. };
  3192. const getMetadata = key => deepCloneObject(key ? metadata[key] : metadata);
  3193. const api = {
  3194. id: { get: () => id },
  3195. origin: { get: () => origin, set: value => (origin = value) },
  3196. serverId: { get: () => state.serverFileReference },
  3197. transferId: { get: () => state.transferId },
  3198. status: { get: () => state.status },
  3199. filename: { get: () => state.file.name },
  3200. filenameWithoutExtension: { get: () => getFilenameWithoutExtension(state.file.name) },
  3201. fileExtension: { get: getFileExtension },
  3202. fileType: { get: getFileType },
  3203. fileSize: { get: getFileSize },
  3204. file: { get: getFile },
  3205. relativePath: { get: () => state.file._relativePath },
  3206. source: { get: () => state.source },
  3207. getMetadata,
  3208. setMetadata: (key, value, silent) => {
  3209. if (isObject(key)) {
  3210. const data = key;
  3211. Object.keys(data).forEach(key => {
  3212. setMetadata(key, data[key], value);
  3213. });
  3214. return key;
  3215. }
  3216. setMetadata(key, value, silent);
  3217. return value;
  3218. },
  3219. extend: (name, handler) => (itemAPI[name] = handler),
  3220. abortLoad,
  3221. retryLoad,
  3222. requestProcessing,
  3223. abortProcessing,
  3224. load,
  3225. process,
  3226. revert,
  3227. ...on(),
  3228. freeze: () => (state.frozen = true),
  3229. release: () => (state.released = true),
  3230. released: { get: () => state.released },
  3231. archive: () => (state.archived = true),
  3232. archived: { get: () => state.archived },
  3233. };
  3234. // create it here instead of returning it instantly so we can extend it later
  3235. const itemAPI = createObject(api);
  3236. return itemAPI;
  3237. };
  3238. const getItemIndexByQuery = (items, query) => {
  3239. // just return first index
  3240. if (isEmpty(query)) {
  3241. return 0;
  3242. }
  3243. // invalid queries
  3244. if (!isString(query)) {
  3245. return -1;
  3246. }
  3247. // return item by id (or -1 if not found)
  3248. return items.findIndex(item => item.id === query);
  3249. };
  3250. const getItemById = (items, itemId) => {
  3251. const index = getItemIndexByQuery(items, itemId);
  3252. if (index < 0) {
  3253. return;
  3254. }
  3255. return items[index] || null;
  3256. };
  3257. const fetchBlob = (url, load, error, progress, abort, headers) => {
  3258. const request = sendRequest(null, url, {
  3259. method: 'GET',
  3260. responseType: 'blob',
  3261. });
  3262. request.onload = xhr => {
  3263. // get headers
  3264. const headers = xhr.getAllResponseHeaders();
  3265. // get filename
  3266. const filename = getFileInfoFromHeaders(headers).name || getFilenameFromURL(url);
  3267. // create response
  3268. load(createResponse('load', xhr.status, getFileFromBlob(xhr.response, filename), headers));
  3269. };
  3270. request.onerror = xhr => {
  3271. error(createResponse('error', xhr.status, xhr.statusText, xhr.getAllResponseHeaders()));
  3272. };
  3273. request.onheaders = xhr => {
  3274. headers(createResponse('headers', xhr.status, null, xhr.getAllResponseHeaders()));
  3275. };
  3276. request.ontimeout = createTimeoutResponse(error);
  3277. request.onprogress = progress;
  3278. request.onabort = abort;
  3279. // should return request
  3280. return request;
  3281. };
  3282. const getDomainFromURL = url => {
  3283. if (url.indexOf('//') === 0) {
  3284. url = location.protocol + url;
  3285. }
  3286. return url
  3287. .toLowerCase()
  3288. .replace('blob:', '')
  3289. .replace(/([a-z])?:\/\//, '$1')
  3290. .split('/')[0];
  3291. };
  3292. const isExternalURL = url =>
  3293. (url.indexOf(':') > -1 || url.indexOf('//') > -1) &&
  3294. getDomainFromURL(location.href) !== getDomainFromURL(url);
  3295. const dynamicLabel = label => (...params) => (isFunction(label) ? label(...params) : label);
  3296. const isMockItem = item => !isFile(item.file);
  3297. const listUpdated = (dispatch, state) => {
  3298. clearTimeout(state.listUpdateTimeout);
  3299. state.listUpdateTimeout = setTimeout(() => {
  3300. dispatch('DID_UPDATE_ITEMS', { items: getActiveItems(state.items) });
  3301. }, 0);
  3302. };
  3303. const optionalPromise = (fn, ...params) =>
  3304. new Promise(resolve => {
  3305. if (!fn) {
  3306. return resolve(true);
  3307. }
  3308. const result = fn(...params);
  3309. if (result == null) {
  3310. return resolve(true);
  3311. }
  3312. if (typeof result === 'boolean') {
  3313. return resolve(result);
  3314. }
  3315. if (typeof result.then === 'function') {
  3316. result.then(resolve);
  3317. }
  3318. });
  3319. const sortItems = (state, compare) => {
  3320. state.items.sort((a, b) => compare(createItemAPI(a), createItemAPI(b)));
  3321. };
  3322. // returns item based on state
  3323. const getItemByQueryFromState = (state, itemHandler) => ({
  3324. query,
  3325. success = () => {},
  3326. failure = () => {},
  3327. ...options
  3328. } = {}) => {
  3329. const item = getItemByQuery(state.items, query);
  3330. if (!item) {
  3331. failure({
  3332. error: createResponse('error', 0, 'Item not found'),
  3333. file: null,
  3334. });
  3335. return;
  3336. }
  3337. itemHandler(item, success, failure, options || {});
  3338. };
  3339. const actions = (dispatch, query, state) => ({
  3340. /**
  3341. * Aborts all ongoing processes
  3342. */
  3343. ABORT_ALL: () => {
  3344. getActiveItems(state.items).forEach(item => {
  3345. item.freeze();
  3346. item.abortLoad();
  3347. item.abortProcessing();
  3348. });
  3349. },
  3350. /**
  3351. * Sets initial files
  3352. */
  3353. DID_SET_FILES: ({ value = [] }) => {
  3354. // map values to file objects
  3355. const files = value.map(file => ({
  3356. source: file.source ? file.source : file,
  3357. options: file.options,
  3358. }));
  3359. // loop over files, if file is in list, leave it be, if not, remove
  3360. // test if items should be moved
  3361. let activeItems = getActiveItems(state.items);
  3362. activeItems.forEach(item => {
  3363. // if item not is in new value, remove
  3364. if (!files.find(file => file.source === item.source || file.source === item.file)) {
  3365. dispatch('REMOVE_ITEM', { query: item, remove: false });
  3366. }
  3367. });
  3368. // add new files
  3369. activeItems = getActiveItems(state.items);
  3370. files.forEach((file, index) => {
  3371. // if file is already in list
  3372. if (activeItems.find(item => item.source === file.source || item.file === file.source))
  3373. return;
  3374. // not in list, add
  3375. dispatch('ADD_ITEM', {
  3376. ...file,
  3377. interactionMethod: InteractionMethod.NONE,
  3378. index,
  3379. });
  3380. });
  3381. },
  3382. DID_UPDATE_ITEM_METADATA: ({ id, action, change }) => {
  3383. // don't do anything
  3384. if (change.silent) return;
  3385. // if is called multiple times in close succession we combined all calls together to save resources
  3386. clearTimeout(state.itemUpdateTimeout);
  3387. state.itemUpdateTimeout = setTimeout(() => {
  3388. const item = getItemById(state.items, id);
  3389. // only revert and attempt to upload when we're uploading to a server
  3390. if (!query('IS_ASYNC')) {
  3391. // should we update the output data
  3392. applyFilterChain('SHOULD_PREPARE_OUTPUT', false, {
  3393. item,
  3394. query,
  3395. action,
  3396. change,
  3397. }).then(shouldPrepareOutput => {
  3398. // plugins determined the output data should be prepared (or not), can be adjusted with beforePrepareOutput hook
  3399. const beforePrepareFile = query('GET_BEFORE_PREPARE_FILE');
  3400. if (beforePrepareFile)
  3401. shouldPrepareOutput = beforePrepareFile(item, shouldPrepareOutput);
  3402. if (!shouldPrepareOutput) return;
  3403. dispatch(
  3404. 'REQUEST_PREPARE_OUTPUT',
  3405. {
  3406. query: id,
  3407. item,
  3408. success: file => {
  3409. dispatch('DID_PREPARE_OUTPUT', { id, file });
  3410. },
  3411. },
  3412. true
  3413. );
  3414. });
  3415. return;
  3416. }
  3417. // if is local item we need to enable upload button so change can be propagated to server
  3418. if (item.origin === FileOrigin.LOCAL) {
  3419. dispatch('DID_LOAD_ITEM', {
  3420. id: item.id,
  3421. error: null,
  3422. serverFileReference: item.source,
  3423. });
  3424. }
  3425. // for async scenarios
  3426. const upload = () => {
  3427. // we push this forward a bit so the interface is updated correctly
  3428. setTimeout(() => {
  3429. dispatch('REQUEST_ITEM_PROCESSING', { query: id });
  3430. }, 32);
  3431. };
  3432. const revert = doUpload => {
  3433. item.revert(
  3434. createRevertFunction(state.options.server.url, state.options.server.revert),
  3435. query('GET_FORCE_REVERT')
  3436. )
  3437. .then(doUpload ? upload : () => {})
  3438. .catch(() => {});
  3439. };
  3440. const abort = doUpload => {
  3441. item.abortProcessing().then(doUpload ? upload : () => {});
  3442. };
  3443. // if we should re-upload the file immediately
  3444. if (item.status === ItemStatus.PROCESSING_COMPLETE) {
  3445. return revert(state.options.instantUpload);
  3446. }
  3447. // if currently uploading, cancel upload
  3448. if (item.status === ItemStatus.PROCESSING) {
  3449. return abort(state.options.instantUpload);
  3450. }
  3451. if (state.options.instantUpload) {
  3452. upload();
  3453. }
  3454. }, 0);
  3455. },
  3456. MOVE_ITEM: ({ query, index }) => {
  3457. const item = getItemByQuery(state.items, query);
  3458. if (!item) return;
  3459. const currentIndex = state.items.indexOf(item);
  3460. index = limit(index, 0, state.items.length - 1);
  3461. if (currentIndex === index) return;
  3462. state.items.splice(index, 0, state.items.splice(currentIndex, 1)[0]);
  3463. },
  3464. SORT: ({ compare }) => {
  3465. sortItems(state, compare);
  3466. dispatch('DID_SORT_ITEMS', {
  3467. items: query('GET_ACTIVE_ITEMS'),
  3468. });
  3469. },
  3470. ADD_ITEMS: ({ items, index, interactionMethod, success = () => {}, failure = () => {} }) => {
  3471. let currentIndex = index;
  3472. if (index === -1 || typeof index === 'undefined') {
  3473. const insertLocation = query('GET_ITEM_INSERT_LOCATION');
  3474. const totalItems = query('GET_TOTAL_ITEMS');
  3475. currentIndex = insertLocation === 'before' ? 0 : totalItems;
  3476. }
  3477. const ignoredFiles = query('GET_IGNORED_FILES');
  3478. const isValidFile = source =>
  3479. isFile(source) ? !ignoredFiles.includes(source.name.toLowerCase()) : !isEmpty(source);
  3480. const validItems = items.filter(isValidFile);
  3481. const promises = validItems.map(
  3482. source =>
  3483. new Promise((resolve, reject) => {
  3484. dispatch('ADD_ITEM', {
  3485. interactionMethod,
  3486. source: source.source || source,
  3487. success: resolve,
  3488. failure: reject,
  3489. index: currentIndex++,
  3490. options: source.options || {},
  3491. });
  3492. })
  3493. );
  3494. Promise.all(promises)
  3495. .then(success)
  3496. .catch(failure);
  3497. },
  3498. /**
  3499. * @param source
  3500. * @param index
  3501. * @param interactionMethod
  3502. */
  3503. ADD_ITEM: ({
  3504. source,
  3505. index = -1,
  3506. interactionMethod,
  3507. success = () => {},
  3508. failure = () => {},
  3509. options = {},
  3510. }) => {
  3511. // if no source supplied
  3512. if (isEmpty(source)) {
  3513. failure({
  3514. error: createResponse('error', 0, 'No source'),
  3515. file: null,
  3516. });
  3517. return;
  3518. }
  3519. // filter out invalid file items, used to filter dropped directory contents
  3520. if (isFile(source) && state.options.ignoredFiles.includes(source.name.toLowerCase())) {
  3521. // fail silently
  3522. return;
  3523. }
  3524. // test if there's still room in the list of files
  3525. if (!hasRoomForItem(state)) {
  3526. // if multiple allowed, we can't replace
  3527. // or if only a single item is allowed but we're not allowed to replace it we exit
  3528. if (
  3529. state.options.allowMultiple ||
  3530. (!state.options.allowMultiple && !state.options.allowReplace)
  3531. ) {
  3532. const error = createResponse('warning', 0, 'Max files');
  3533. dispatch('DID_THROW_MAX_FILES', {
  3534. source,
  3535. error,
  3536. });
  3537. failure({ error, file: null });
  3538. return;
  3539. }
  3540. // let's replace the item
  3541. // id of first item we're about to remove
  3542. const item = getActiveItems(state.items)[0];
  3543. // if has been processed remove it from the server as well
  3544. if (
  3545. item.status === ItemStatus.PROCESSING_COMPLETE ||
  3546. item.status === ItemStatus.PROCESSING_REVERT_ERROR
  3547. ) {
  3548. const forceRevert = query('GET_FORCE_REVERT');
  3549. item.revert(
  3550. createRevertFunction(state.options.server.url, state.options.server.revert),
  3551. forceRevert
  3552. )
  3553. .then(() => {
  3554. if (!forceRevert) return;
  3555. // try to add now
  3556. dispatch('ADD_ITEM', {
  3557. source,
  3558. index,
  3559. interactionMethod,
  3560. success,
  3561. failure,
  3562. options,
  3563. });
  3564. })
  3565. .catch(() => {}); // no need to handle this catch state for now
  3566. if (forceRevert) return;
  3567. }
  3568. // remove first item as it will be replaced by this item
  3569. dispatch('REMOVE_ITEM', { query: item.id });
  3570. }
  3571. // where did the file originate
  3572. const origin =
  3573. options.type === 'local'
  3574. ? FileOrigin.LOCAL
  3575. : options.type === 'limbo'
  3576. ? FileOrigin.LIMBO
  3577. : FileOrigin.INPUT;
  3578. // create a new blank item
  3579. const item = createItem(
  3580. // where did this file come from
  3581. origin,
  3582. // an input file never has a server file reference
  3583. origin === FileOrigin.INPUT ? null : source,
  3584. // file mock data, if defined
  3585. options.file
  3586. );
  3587. // set initial meta data
  3588. Object.keys(options.metadata || {}).forEach(key => {
  3589. item.setMetadata(key, options.metadata[key]);
  3590. });
  3591. // created the item, let plugins add methods
  3592. applyFilters('DID_CREATE_ITEM', item, { query, dispatch });
  3593. // where to insert new items
  3594. const itemInsertLocation = query('GET_ITEM_INSERT_LOCATION');
  3595. // adjust index if is not allowed to pick location
  3596. if (!state.options.itemInsertLocationFreedom) {
  3597. index = itemInsertLocation === 'before' ? -1 : state.items.length;
  3598. }
  3599. // add item to list
  3600. insertItem(state.items, item, index);
  3601. // sort items in list
  3602. if (isFunction(itemInsertLocation) && source) {
  3603. sortItems(state, itemInsertLocation);
  3604. }
  3605. // get a quick reference to the item id
  3606. const id = item.id;
  3607. // observe item events
  3608. item.on('init', () => {
  3609. dispatch('DID_INIT_ITEM', { id });
  3610. });
  3611. item.on('load-init', () => {
  3612. dispatch('DID_START_ITEM_LOAD', { id });
  3613. });
  3614. item.on('load-meta', () => {
  3615. dispatch('DID_UPDATE_ITEM_META', { id });
  3616. });
  3617. item.on('load-progress', progress => {
  3618. dispatch('DID_UPDATE_ITEM_LOAD_PROGRESS', { id, progress });
  3619. });
  3620. item.on('load-request-error', error => {
  3621. const mainStatus = dynamicLabel(state.options.labelFileLoadError)(error);
  3622. // is client error, no way to recover
  3623. if (error.code >= 400 && error.code < 500) {
  3624. dispatch('DID_THROW_ITEM_INVALID', {
  3625. id,
  3626. error,
  3627. status: {
  3628. main: mainStatus,
  3629. sub: `${error.code} (${error.body})`,
  3630. },
  3631. });
  3632. // reject the file so can be dealt with through API
  3633. failure({ error, file: createItemAPI(item) });
  3634. return;
  3635. }
  3636. // is possible server error, so might be possible to retry
  3637. dispatch('DID_THROW_ITEM_LOAD_ERROR', {
  3638. id,
  3639. error,
  3640. status: {
  3641. main: mainStatus,
  3642. sub: state.options.labelTapToRetry,
  3643. },
  3644. });
  3645. });
  3646. item.on('load-file-error', error => {
  3647. dispatch('DID_THROW_ITEM_INVALID', {
  3648. id,
  3649. error: error.status,
  3650. status: error.status,
  3651. });
  3652. failure({ error: error.status, file: createItemAPI(item) });
  3653. });
  3654. item.on('load-abort', () => {
  3655. dispatch('REMOVE_ITEM', { query: id });
  3656. });
  3657. item.on('load-skip', () => {
  3658. dispatch('COMPLETE_LOAD_ITEM', {
  3659. query: id,
  3660. item,
  3661. data: {
  3662. source,
  3663. success,
  3664. },
  3665. });
  3666. });
  3667. item.on('load', () => {
  3668. const handleAdd = shouldAdd => {
  3669. // no should not add this file
  3670. if (!shouldAdd) {
  3671. dispatch('REMOVE_ITEM', {
  3672. query: id,
  3673. });
  3674. return;
  3675. }
  3676. // now interested in metadata updates
  3677. item.on('metadata-update', change => {
  3678. dispatch('DID_UPDATE_ITEM_METADATA', { id, change });
  3679. });
  3680. // let plugins decide if the output data should be prepared at this point
  3681. // means we'll do this and wait for idle state
  3682. applyFilterChain('SHOULD_PREPARE_OUTPUT', false, { item, query }).then(
  3683. shouldPrepareOutput => {
  3684. // plugins determined the output data should be prepared (or not), can be adjusted with beforePrepareOutput hook
  3685. const beforePrepareFile = query('GET_BEFORE_PREPARE_FILE');
  3686. if (beforePrepareFile)
  3687. shouldPrepareOutput = beforePrepareFile(item, shouldPrepareOutput);
  3688. const loadComplete = () => {
  3689. dispatch('COMPLETE_LOAD_ITEM', {
  3690. query: id,
  3691. item,
  3692. data: {
  3693. source,
  3694. success,
  3695. },
  3696. });
  3697. listUpdated(dispatch, state);
  3698. };
  3699. // exit
  3700. if (shouldPrepareOutput) {
  3701. // wait for idle state and then run PREPARE_OUTPUT
  3702. dispatch(
  3703. 'REQUEST_PREPARE_OUTPUT',
  3704. {
  3705. query: id,
  3706. item,
  3707. success: file => {
  3708. dispatch('DID_PREPARE_OUTPUT', { id, file });
  3709. loadComplete();
  3710. },
  3711. },
  3712. true
  3713. );
  3714. return;
  3715. }
  3716. loadComplete();
  3717. }
  3718. );
  3719. };
  3720. // item loaded, allow plugins to
  3721. // - read data (quickly)
  3722. // - add metadata
  3723. applyFilterChain('DID_LOAD_ITEM', item, { query, dispatch })
  3724. .then(() => {
  3725. optionalPromise(query('GET_BEFORE_ADD_FILE'), createItemAPI(item)).then(
  3726. handleAdd
  3727. );
  3728. })
  3729. .catch(e => {
  3730. if (!e || !e.error || !e.status) return handleAdd(false);
  3731. dispatch('DID_THROW_ITEM_INVALID', {
  3732. id,
  3733. error: e.error,
  3734. status: e.status,
  3735. });
  3736. });
  3737. });
  3738. item.on('process-start', () => {
  3739. dispatch('DID_START_ITEM_PROCESSING', { id });
  3740. });
  3741. item.on('process-progress', progress => {
  3742. dispatch('DID_UPDATE_ITEM_PROCESS_PROGRESS', { id, progress });
  3743. });
  3744. item.on('process-error', error => {
  3745. dispatch('DID_THROW_ITEM_PROCESSING_ERROR', {
  3746. id,
  3747. error,
  3748. status: {
  3749. main: dynamicLabel(state.options.labelFileProcessingError)(error),
  3750. sub: state.options.labelTapToRetry,
  3751. },
  3752. });
  3753. });
  3754. item.on('process-revert-error', error => {
  3755. dispatch('DID_THROW_ITEM_PROCESSING_REVERT_ERROR', {
  3756. id,
  3757. error,
  3758. status: {
  3759. main: dynamicLabel(state.options.labelFileProcessingRevertError)(error),
  3760. sub: state.options.labelTapToRetry,
  3761. },
  3762. });
  3763. });
  3764. item.on('process-complete', serverFileReference => {
  3765. dispatch('DID_COMPLETE_ITEM_PROCESSING', {
  3766. id,
  3767. error: null,
  3768. serverFileReference,
  3769. });
  3770. dispatch('DID_DEFINE_VALUE', { id, value: serverFileReference });
  3771. });
  3772. item.on('process-abort', () => {
  3773. dispatch('DID_ABORT_ITEM_PROCESSING', { id });
  3774. });
  3775. item.on('process-revert', () => {
  3776. dispatch('DID_REVERT_ITEM_PROCESSING', { id });
  3777. dispatch('DID_DEFINE_VALUE', { id, value: null });
  3778. });
  3779. // let view know the item has been inserted
  3780. dispatch('DID_ADD_ITEM', { id, index, interactionMethod });
  3781. listUpdated(dispatch, state);
  3782. // start loading the source
  3783. const { url, load, restore, fetch } = state.options.server || {};
  3784. item.load(
  3785. source,
  3786. // 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)
  3787. createFileLoader(
  3788. origin === FileOrigin.INPUT
  3789. ? // input, if is remote, see if should use custom fetch, else use default fetchBlob
  3790. isString(source) && isExternalURL(source)
  3791. ? fetch
  3792. ? createFetchFunction(url, fetch)
  3793. : fetchBlob // remote url
  3794. : fetchBlob // try to fetch url
  3795. : // limbo or local
  3796. origin === FileOrigin.LIMBO
  3797. ? createFetchFunction(url, restore) // limbo
  3798. : createFetchFunction(url, load) // local
  3799. ),
  3800. // called when the file is loaded so it can be piped through the filters
  3801. (file, success, error) => {
  3802. // let's process the file
  3803. applyFilterChain('LOAD_FILE', file, { query })
  3804. .then(success)
  3805. .catch(error);
  3806. }
  3807. );
  3808. },
  3809. REQUEST_PREPARE_OUTPUT: ({ item, success, failure = () => {} }) => {
  3810. // error response if item archived
  3811. const err = {
  3812. error: createResponse('error', 0, 'Item not found'),
  3813. file: null,
  3814. };
  3815. // don't handle archived items, an item could have been archived (load aborted) while waiting to be prepared
  3816. if (item.archived) return failure(err);
  3817. // allow plugins to alter the file data
  3818. applyFilterChain('PREPARE_OUTPUT', item.file, { query, item }).then(result => {
  3819. applyFilterChain('COMPLETE_PREPARE_OUTPUT', result, { query, item }).then(result => {
  3820. // don't handle archived items, an item could have been archived (load aborted) while being prepared
  3821. if (item.archived) return failure(err);
  3822. // we done!
  3823. success(result);
  3824. });
  3825. });
  3826. },
  3827. COMPLETE_LOAD_ITEM: ({ item, data }) => {
  3828. const { success, source } = data;
  3829. // sort items in list
  3830. const itemInsertLocation = query('GET_ITEM_INSERT_LOCATION');
  3831. if (isFunction(itemInsertLocation) && source) {
  3832. sortItems(state, itemInsertLocation);
  3833. }
  3834. // let interface know the item has loaded
  3835. dispatch('DID_LOAD_ITEM', {
  3836. id: item.id,
  3837. error: null,
  3838. serverFileReference: item.origin === FileOrigin.INPUT ? null : source,
  3839. });
  3840. // item has been successfully loaded and added to the
  3841. // list of items so can now be safely returned for use
  3842. success(createItemAPI(item));
  3843. // if this is a local server file we need to show a different state
  3844. if (item.origin === FileOrigin.LOCAL) {
  3845. dispatch('DID_LOAD_LOCAL_ITEM', { id: item.id });
  3846. return;
  3847. }
  3848. // if is a temp server file we prevent async upload call here (as the file is already on the server)
  3849. if (item.origin === FileOrigin.LIMBO) {
  3850. dispatch('DID_COMPLETE_ITEM_PROCESSING', {
  3851. id: item.id,
  3852. error: null,
  3853. serverFileReference: source,
  3854. });
  3855. dispatch('DID_DEFINE_VALUE', {
  3856. id: item.id,
  3857. value: item.serverId || source,
  3858. });
  3859. return;
  3860. }
  3861. // id we are allowed to upload the file immediately, lets do it
  3862. if (query('IS_ASYNC') && state.options.instantUpload) {
  3863. dispatch('REQUEST_ITEM_PROCESSING', { query: item.id });
  3864. }
  3865. },
  3866. RETRY_ITEM_LOAD: getItemByQueryFromState(state, item => {
  3867. // try loading the source one more time
  3868. item.retryLoad();
  3869. }),
  3870. REQUEST_ITEM_PREPARE: getItemByQueryFromState(state, (item, success, failure) => {
  3871. dispatch(
  3872. 'REQUEST_PREPARE_OUTPUT',
  3873. {
  3874. query: item.id,
  3875. item,
  3876. success: file => {
  3877. dispatch('DID_PREPARE_OUTPUT', { id: item.id, file });
  3878. success({
  3879. file: item,
  3880. output: file,
  3881. });
  3882. },
  3883. failure,
  3884. },
  3885. true
  3886. );
  3887. }),
  3888. REQUEST_ITEM_PROCESSING: getItemByQueryFromState(state, (item, success, failure) => {
  3889. // cannot be queued (or is already queued)
  3890. const itemCanBeQueuedForProcessing =
  3891. // waiting for something
  3892. item.status === ItemStatus.IDLE ||
  3893. // processing went wrong earlier
  3894. item.status === ItemStatus.PROCESSING_ERROR;
  3895. // not ready to be processed
  3896. if (!itemCanBeQueuedForProcessing) {
  3897. const processNow = () =>
  3898. dispatch('REQUEST_ITEM_PROCESSING', { query: item, success, failure });
  3899. const process = () => (document.hidden ? processNow() : setTimeout(processNow, 32));
  3900. // if already done processing or tried to revert but didn't work, try again
  3901. if (
  3902. item.status === ItemStatus.PROCESSING_COMPLETE ||
  3903. item.status === ItemStatus.PROCESSING_REVERT_ERROR
  3904. ) {
  3905. item.revert(
  3906. createRevertFunction(state.options.server.url, state.options.server.revert),
  3907. query('GET_FORCE_REVERT')
  3908. )
  3909. .then(process)
  3910. .catch(() => {}); // don't continue with processing if something went wrong
  3911. } else if (item.status === ItemStatus.PROCESSING) {
  3912. item.abortProcessing().then(process);
  3913. }
  3914. return;
  3915. }
  3916. // already queued for processing
  3917. if (item.status === ItemStatus.PROCESSING_QUEUED) return;
  3918. item.requestProcessing();
  3919. dispatch('DID_REQUEST_ITEM_PROCESSING', { id: item.id });
  3920. dispatch('PROCESS_ITEM', { query: item, success, failure }, true);
  3921. }),
  3922. PROCESS_ITEM: getItemByQueryFromState(state, (item, success, failure) => {
  3923. const maxParallelUploads = query('GET_MAX_PARALLEL_UPLOADS');
  3924. const totalCurrentUploads = query('GET_ITEMS_BY_STATUS', ItemStatus.PROCESSING).length;
  3925. // queue and wait till queue is freed up
  3926. if (totalCurrentUploads === maxParallelUploads) {
  3927. // queue for later processing
  3928. state.processingQueue.push({
  3929. id: item.id,
  3930. success,
  3931. failure,
  3932. });
  3933. // stop it!
  3934. return;
  3935. }
  3936. // if was not queued or is already processing exit here
  3937. if (item.status === ItemStatus.PROCESSING) return;
  3938. const processNext = () => {
  3939. // process queueud items
  3940. const queueEntry = state.processingQueue.shift();
  3941. // no items left
  3942. if (!queueEntry) return;
  3943. // get item reference
  3944. const { id, success, failure } = queueEntry;
  3945. const itemReference = getItemByQuery(state.items, id);
  3946. // if item was archived while in queue, jump to next
  3947. if (!itemReference || itemReference.archived) {
  3948. processNext();
  3949. return;
  3950. }
  3951. // process queued item
  3952. dispatch('PROCESS_ITEM', { query: id, success, failure }, true);
  3953. };
  3954. // we done function
  3955. item.onOnce('process-complete', () => {
  3956. success(createItemAPI(item));
  3957. processNext();
  3958. // if origin is local, and we're instant uploading, trigger remove of original
  3959. // as revert will remove file from list
  3960. const server = state.options.server;
  3961. const instantUpload = state.options.instantUpload;
  3962. if (instantUpload && item.origin === FileOrigin.LOCAL && isFunction(server.remove)) {
  3963. const noop = () => {};
  3964. item.origin = FileOrigin.LIMBO;
  3965. state.options.server.remove(item.source, noop, noop);
  3966. }
  3967. // All items processed? No errors?
  3968. const allItemsProcessed =
  3969. query('GET_ITEMS_BY_STATUS', ItemStatus.PROCESSING_COMPLETE).length ===
  3970. state.items.length;
  3971. if (allItemsProcessed) {
  3972. dispatch('DID_COMPLETE_ITEM_PROCESSING_ALL');
  3973. }
  3974. });
  3975. // we error function
  3976. item.onOnce('process-error', error => {
  3977. failure({ error, file: createItemAPI(item) });
  3978. processNext();
  3979. });
  3980. // start file processing
  3981. const options = state.options;
  3982. item.process(
  3983. createFileProcessor(
  3984. createProcessorFunction(options.server.url, options.server.process, options.name, {
  3985. chunkTransferId: item.transferId,
  3986. chunkServer: options.server.patch,
  3987. chunkUploads: options.chunkUploads,
  3988. chunkForce: options.chunkForce,
  3989. chunkSize: options.chunkSize,
  3990. chunkRetryDelays: options.chunkRetryDelays,
  3991. }),
  3992. {
  3993. allowMinimumUploadDuration: query('GET_ALLOW_MINIMUM_UPLOAD_DURATION'),
  3994. }
  3995. ),
  3996. // called when the file is about to be processed so it can be piped through the transform filters
  3997. (file, success, error) => {
  3998. // allow plugins to alter the file data
  3999. applyFilterChain('PREPARE_OUTPUT', file, { query, item })
  4000. .then(file => {
  4001. dispatch('DID_PREPARE_OUTPUT', { id: item.id, file });
  4002. success(file);
  4003. })
  4004. .catch(error);
  4005. }
  4006. );
  4007. }),
  4008. RETRY_ITEM_PROCESSING: getItemByQueryFromState(state, item => {
  4009. dispatch('REQUEST_ITEM_PROCESSING', { query: item });
  4010. }),
  4011. REQUEST_REMOVE_ITEM: getItemByQueryFromState(state, item => {
  4012. optionalPromise(query('GET_BEFORE_REMOVE_FILE'), createItemAPI(item)).then(shouldRemove => {
  4013. if (!shouldRemove) {
  4014. return;
  4015. }
  4016. dispatch('REMOVE_ITEM', { query: item });
  4017. });
  4018. }),
  4019. RELEASE_ITEM: getItemByQueryFromState(state, item => {
  4020. item.release();
  4021. }),
  4022. REMOVE_ITEM: getItemByQueryFromState(state, (item, success, failure, options) => {
  4023. const removeFromView = () => {
  4024. // get id reference
  4025. const id = item.id;
  4026. // archive the item, this does not remove it from the list
  4027. getItemById(state.items, id).archive();
  4028. // tell the view the item has been removed
  4029. dispatch('DID_REMOVE_ITEM', { error: null, id, item });
  4030. // now the list has been modified
  4031. listUpdated(dispatch, state);
  4032. // correctly removed
  4033. success(createItemAPI(item));
  4034. };
  4035. // if this is a local file and the `server.remove` function has been configured,
  4036. // send source there so dev can remove file from server
  4037. const server = state.options.server;
  4038. if (
  4039. item.origin === FileOrigin.LOCAL &&
  4040. server &&
  4041. isFunction(server.remove) &&
  4042. options.remove !== false
  4043. ) {
  4044. dispatch('DID_START_ITEM_REMOVE', { id: item.id });
  4045. server.remove(
  4046. item.source,
  4047. () => removeFromView(),
  4048. status => {
  4049. dispatch('DID_THROW_ITEM_REMOVE_ERROR', {
  4050. id: item.id,
  4051. error: createResponse('error', 0, status, null),
  4052. status: {
  4053. main: dynamicLabel(state.options.labelFileRemoveError)(status),
  4054. sub: state.options.labelTapToRetry,
  4055. },
  4056. });
  4057. }
  4058. );
  4059. } else {
  4060. // if is requesting revert and can revert need to call revert handler (not calling request_ because that would also trigger beforeRemoveHook)
  4061. if (
  4062. (options.revert && item.origin !== FileOrigin.LOCAL && item.serverId !== null) ||
  4063. // if chunked uploads are enabled and we're uploading in chunks for this specific file
  4064. // or if the file isn't big enough for chunked uploads but chunkForce is set then call
  4065. // revert before removing from the view...
  4066. (state.options.chunkUploads && item.file.size > state.options.chunkSize) ||
  4067. (state.options.chunkUploads && state.options.chunkForce)
  4068. ) {
  4069. item.revert(
  4070. createRevertFunction(state.options.server.url, state.options.server.revert),
  4071. query('GET_FORCE_REVERT')
  4072. );
  4073. }
  4074. // can now safely remove from view
  4075. removeFromView();
  4076. }
  4077. }),
  4078. ABORT_ITEM_LOAD: getItemByQueryFromState(state, item => {
  4079. item.abortLoad();
  4080. }),
  4081. ABORT_ITEM_PROCESSING: getItemByQueryFromState(state, item => {
  4082. // test if is already processed
  4083. if (item.serverId) {
  4084. dispatch('REVERT_ITEM_PROCESSING', { id: item.id });
  4085. return;
  4086. }
  4087. // abort
  4088. item.abortProcessing().then(() => {
  4089. const shouldRemove = state.options.instantUpload;
  4090. if (shouldRemove) {
  4091. dispatch('REMOVE_ITEM', { query: item.id });
  4092. }
  4093. });
  4094. }),
  4095. REQUEST_REVERT_ITEM_PROCESSING: getItemByQueryFromState(state, item => {
  4096. // not instant uploading, revert immediately
  4097. if (!state.options.instantUpload) {
  4098. dispatch('REVERT_ITEM_PROCESSING', { query: item });
  4099. return;
  4100. }
  4101. // if we're instant uploading the file will also be removed if we revert,
  4102. // so if a before remove file hook is defined we need to run it now
  4103. const handleRevert = shouldRevert => {
  4104. if (!shouldRevert) return;
  4105. dispatch('REVERT_ITEM_PROCESSING', { query: item });
  4106. };
  4107. const fn = query('GET_BEFORE_REMOVE_FILE');
  4108. if (!fn) {
  4109. return handleRevert(true);
  4110. }
  4111. const requestRemoveResult = fn(createItemAPI(item));
  4112. if (requestRemoveResult == null) {
  4113. // undefined or null
  4114. return handleRevert(true);
  4115. }
  4116. if (typeof requestRemoveResult === 'boolean') {
  4117. return handleRevert(requestRemoveResult);
  4118. }
  4119. if (typeof requestRemoveResult.then === 'function') {
  4120. requestRemoveResult.then(handleRevert);
  4121. }
  4122. }),
  4123. REVERT_ITEM_PROCESSING: getItemByQueryFromState(state, item => {
  4124. item.revert(
  4125. createRevertFunction(state.options.server.url, state.options.server.revert),
  4126. query('GET_FORCE_REVERT')
  4127. )
  4128. .then(() => {
  4129. const shouldRemove = state.options.instantUpload || isMockItem(item);
  4130. if (shouldRemove) {
  4131. dispatch('REMOVE_ITEM', { query: item.id });
  4132. }
  4133. })
  4134. .catch(() => {});
  4135. }),
  4136. SET_OPTIONS: ({ options }) => {
  4137. // get all keys passed
  4138. const optionKeys = Object.keys(options);
  4139. // get prioritized keyed to include (remove once not in options object)
  4140. const prioritizedOptionKeys = PrioritizedOptions.filter(key => optionKeys.includes(key));
  4141. // order the keys, prioritized first, then rest
  4142. const orderedOptionKeys = [
  4143. // add prioritized first if passed to options, else remove
  4144. ...prioritizedOptionKeys,
  4145. // prevent duplicate keys
  4146. ...Object.keys(options).filter(key => !prioritizedOptionKeys.includes(key)),
  4147. ];
  4148. // dispatch set event for each option
  4149. orderedOptionKeys.forEach(key => {
  4150. dispatch(`SET_${fromCamels(key, '_').toUpperCase()}`, {
  4151. value: options[key],
  4152. });
  4153. });
  4154. },
  4155. });
  4156. const PrioritizedOptions = [
  4157. 'server', // must be processed before "files"
  4158. ];
  4159. const formatFilename = name => name;
  4160. const createElement$1 = tagName => {
  4161. return document.createElement(tagName);
  4162. };
  4163. const text = (node, value) => {
  4164. let textNode = node.childNodes[0];
  4165. if (!textNode) {
  4166. textNode = document.createTextNode(value);
  4167. node.appendChild(textNode);
  4168. } else if (value !== textNode.nodeValue) {
  4169. textNode.nodeValue = value;
  4170. }
  4171. };
  4172. const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => {
  4173. const angleInRadians = (((angleInDegrees % 360) - 90) * Math.PI) / 180.0;
  4174. return {
  4175. x: centerX + radius * Math.cos(angleInRadians),
  4176. y: centerY + radius * Math.sin(angleInRadians),
  4177. };
  4178. };
  4179. const describeArc = (x, y, radius, startAngle, endAngle, arcSweep) => {
  4180. const start = polarToCartesian(x, y, radius, endAngle);
  4181. const end = polarToCartesian(x, y, radius, startAngle);
  4182. return ['M', start.x, start.y, 'A', radius, radius, 0, arcSweep, 0, end.x, end.y].join(' ');
  4183. };
  4184. const percentageArc = (x, y, radius, from, to) => {
  4185. let arcSweep = 1;
  4186. if (to > from && to - from <= 0.5) {
  4187. arcSweep = 0;
  4188. }
  4189. if (from > to && from - to >= 0.5) {
  4190. arcSweep = 0;
  4191. }
  4192. return describeArc(
  4193. x,
  4194. y,
  4195. radius,
  4196. Math.min(0.9999, from) * 360,
  4197. Math.min(0.9999, to) * 360,
  4198. arcSweep
  4199. );
  4200. };
  4201. const create = ({ root, props }) => {
  4202. // start at 0
  4203. props.spin = false;
  4204. props.progress = 0;
  4205. props.opacity = 0;
  4206. // svg
  4207. const svg = createElement('svg');
  4208. root.ref.path = createElement('path', {
  4209. 'stroke-width': 2,
  4210. 'stroke-linecap': 'round',
  4211. });
  4212. svg.appendChild(root.ref.path);
  4213. root.ref.svg = svg;
  4214. root.appendChild(svg);
  4215. };
  4216. const write = ({ root, props }) => {
  4217. if (props.opacity === 0) {
  4218. return;
  4219. }
  4220. if (props.align) {
  4221. root.element.dataset.align = props.align;
  4222. }
  4223. // get width of stroke
  4224. const ringStrokeWidth = parseInt(attr(root.ref.path, 'stroke-width'), 10);
  4225. // calculate size of ring
  4226. const size = root.rect.element.width * 0.5;
  4227. // ring state
  4228. let ringFrom = 0;
  4229. let ringTo = 0;
  4230. // now in busy mode
  4231. if (props.spin) {
  4232. ringFrom = 0;
  4233. ringTo = 0.5;
  4234. } else {
  4235. ringFrom = 0;
  4236. ringTo = props.progress;
  4237. }
  4238. // get arc path
  4239. const coordinates = percentageArc(size, size, size - ringStrokeWidth, ringFrom, ringTo);
  4240. // update progress bar
  4241. attr(root.ref.path, 'd', coordinates);
  4242. // hide while contains 0 value
  4243. attr(root.ref.path, 'stroke-opacity', props.spin || props.progress > 0 ? 1 : 0);
  4244. };
  4245. const progressIndicator = createView({
  4246. tag: 'div',
  4247. name: 'progress-indicator',
  4248. ignoreRectUpdate: true,
  4249. ignoreRect: true,
  4250. create,
  4251. write,
  4252. mixins: {
  4253. apis: ['progress', 'spin', 'align'],
  4254. styles: ['opacity'],
  4255. animations: {
  4256. opacity: { type: 'tween', duration: 500 },
  4257. progress: {
  4258. type: 'spring',
  4259. stiffness: 0.95,
  4260. damping: 0.65,
  4261. mass: 10,
  4262. },
  4263. },
  4264. },
  4265. });
  4266. const create$1 = ({ root, props }) => {
  4267. root.element.innerHTML = (props.icon || '') + `<span>${props.label}</span>`;
  4268. props.isDisabled = false;
  4269. };
  4270. const write$1 = ({ root, props }) => {
  4271. const { isDisabled } = props;
  4272. const shouldDisable = root.query('GET_DISABLED') || props.opacity === 0;
  4273. if (shouldDisable && !isDisabled) {
  4274. props.isDisabled = true;
  4275. attr(root.element, 'disabled', 'disabled');
  4276. } else if (!shouldDisable && isDisabled) {
  4277. props.isDisabled = false;
  4278. root.element.removeAttribute('disabled');
  4279. }
  4280. };
  4281. const fileActionButton = createView({
  4282. tag: 'button',
  4283. attributes: {
  4284. type: 'button',
  4285. },
  4286. ignoreRect: true,
  4287. ignoreRectUpdate: true,
  4288. name: 'file-action-button',
  4289. mixins: {
  4290. apis: ['label'],
  4291. styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity'],
  4292. animations: {
  4293. scaleX: 'spring',
  4294. scaleY: 'spring',
  4295. translateX: 'spring',
  4296. translateY: 'spring',
  4297. opacity: { type: 'tween', duration: 250 },
  4298. },
  4299. listeners: true,
  4300. },
  4301. create: create$1,
  4302. write: write$1,
  4303. });
  4304. const toNaturalFileSize = (bytes, decimalSeparator = '.', base = 1000, options = {}) => {
  4305. const {
  4306. labelBytes = 'bytes',
  4307. labelKilobytes = 'KB',
  4308. labelMegabytes = 'MB',
  4309. labelGigabytes = 'GB',
  4310. } = options;
  4311. // no negative byte sizes
  4312. bytes = Math.round(Math.abs(bytes));
  4313. const KB = base;
  4314. const MB = base * base;
  4315. const GB = base * base * base;
  4316. // just bytes
  4317. if (bytes < KB) {
  4318. return `${bytes} ${labelBytes}`;
  4319. }
  4320. // kilobytes
  4321. if (bytes < MB) {
  4322. return `${Math.floor(bytes / KB)} ${labelKilobytes}`;
  4323. }
  4324. // megabytes
  4325. if (bytes < GB) {
  4326. return `${removeDecimalsWhenZero(bytes / MB, 1, decimalSeparator)} ${labelMegabytes}`;
  4327. }
  4328. // gigabytes
  4329. return `${removeDecimalsWhenZero(bytes / GB, 2, decimalSeparator)} ${labelGigabytes}`;
  4330. };
  4331. const removeDecimalsWhenZero = (value, decimalCount, separator) => {
  4332. return value
  4333. .toFixed(decimalCount)
  4334. .split('.')
  4335. .filter(part => part !== '0')
  4336. .join(separator);
  4337. };
  4338. const create$2 = ({ root, props }) => {
  4339. // filename
  4340. const fileName = createElement$1('span');
  4341. fileName.className = 'filepond--file-info-main';
  4342. // hide for screenreaders
  4343. // the file is contained in a fieldset with legend that contains the filename
  4344. // no need to read it twice
  4345. attr(fileName, 'aria-hidden', 'true');
  4346. root.appendChild(fileName);
  4347. root.ref.fileName = fileName;
  4348. // filesize
  4349. const fileSize = createElement$1('span');
  4350. fileSize.className = 'filepond--file-info-sub';
  4351. root.appendChild(fileSize);
  4352. root.ref.fileSize = fileSize;
  4353. // set initial values
  4354. text(fileSize, root.query('GET_LABEL_FILE_WAITING_FOR_SIZE'));
  4355. text(fileName, formatFilename(root.query('GET_ITEM_NAME', props.id)));
  4356. };
  4357. const updateFile = ({ root, props }) => {
  4358. text(
  4359. root.ref.fileSize,
  4360. toNaturalFileSize(
  4361. root.query('GET_ITEM_SIZE', props.id),
  4362. '.',
  4363. root.query('GET_FILE_SIZE_BASE'),
  4364. root.query('GET_FILE_SIZE_LABELS', root.query)
  4365. )
  4366. );
  4367. text(root.ref.fileName, formatFilename(root.query('GET_ITEM_NAME', props.id)));
  4368. };
  4369. const updateFileSizeOnError = ({ root, props }) => {
  4370. // if size is available don't fallback to unknown size message
  4371. if (isInt(root.query('GET_ITEM_SIZE', props.id))) {
  4372. updateFile({ root, props });
  4373. return;
  4374. }
  4375. text(root.ref.fileSize, root.query('GET_LABEL_FILE_SIZE_NOT_AVAILABLE'));
  4376. };
  4377. const fileInfo = createView({
  4378. name: 'file-info',
  4379. ignoreRect: true,
  4380. ignoreRectUpdate: true,
  4381. write: createRoute({
  4382. DID_LOAD_ITEM: updateFile,
  4383. DID_UPDATE_ITEM_META: updateFile,
  4384. DID_THROW_ITEM_LOAD_ERROR: updateFileSizeOnError,
  4385. DID_THROW_ITEM_INVALID: updateFileSizeOnError,
  4386. }),
  4387. didCreateView: root => {
  4388. applyFilters('CREATE_VIEW', { ...root, view: root });
  4389. },
  4390. create: create$2,
  4391. mixins: {
  4392. styles: ['translateX', 'translateY'],
  4393. animations: {
  4394. translateX: 'spring',
  4395. translateY: 'spring',
  4396. },
  4397. },
  4398. });
  4399. const toPercentage = value => Math.round(value * 100);
  4400. const create$3 = ({ root }) => {
  4401. // main status
  4402. const main = createElement$1('span');
  4403. main.className = 'filepond--file-status-main';
  4404. root.appendChild(main);
  4405. root.ref.main = main;
  4406. // sub status
  4407. const sub = createElement$1('span');
  4408. sub.className = 'filepond--file-status-sub';
  4409. root.appendChild(sub);
  4410. root.ref.sub = sub;
  4411. didSetItemLoadProgress({ root, action: { progress: null } });
  4412. };
  4413. const didSetItemLoadProgress = ({ root, action }) => {
  4414. const title =
  4415. action.progress === null
  4416. ? root.query('GET_LABEL_FILE_LOADING')
  4417. : `${root.query('GET_LABEL_FILE_LOADING')} ${toPercentage(action.progress)}%`;
  4418. text(root.ref.main, title);
  4419. text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL'));
  4420. };
  4421. const didSetItemProcessProgress = ({ root, action }) => {
  4422. const title =
  4423. action.progress === null
  4424. ? root.query('GET_LABEL_FILE_PROCESSING')
  4425. : `${root.query('GET_LABEL_FILE_PROCESSING')} ${toPercentage(action.progress)}%`;
  4426. text(root.ref.main, title);
  4427. text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL'));
  4428. };
  4429. const didRequestItemProcessing = ({ root }) => {
  4430. text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING'));
  4431. text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL'));
  4432. };
  4433. const didAbortItemProcessing = ({ root }) => {
  4434. text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING_ABORTED'));
  4435. text(root.ref.sub, root.query('GET_LABEL_TAP_TO_RETRY'));
  4436. };
  4437. const didCompleteItemProcessing = ({ root }) => {
  4438. text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING_COMPLETE'));
  4439. text(root.ref.sub, root.query('GET_LABEL_TAP_TO_UNDO'));
  4440. };
  4441. const clear = ({ root }) => {
  4442. text(root.ref.main, '');
  4443. text(root.ref.sub, '');
  4444. };
  4445. const error = ({ root, action }) => {
  4446. text(root.ref.main, action.status.main);
  4447. text(root.ref.sub, action.status.sub);
  4448. };
  4449. const fileStatus = createView({
  4450. name: 'file-status',
  4451. ignoreRect: true,
  4452. ignoreRectUpdate: true,
  4453. write: createRoute({
  4454. DID_LOAD_ITEM: clear,
  4455. DID_REVERT_ITEM_PROCESSING: clear,
  4456. DID_REQUEST_ITEM_PROCESSING: didRequestItemProcessing,
  4457. DID_ABORT_ITEM_PROCESSING: didAbortItemProcessing,
  4458. DID_COMPLETE_ITEM_PROCESSING: didCompleteItemProcessing,
  4459. DID_UPDATE_ITEM_PROCESS_PROGRESS: didSetItemProcessProgress,
  4460. DID_UPDATE_ITEM_LOAD_PROGRESS: didSetItemLoadProgress,
  4461. DID_THROW_ITEM_LOAD_ERROR: error,
  4462. DID_THROW_ITEM_INVALID: error,
  4463. DID_THROW_ITEM_PROCESSING_ERROR: error,
  4464. DID_THROW_ITEM_PROCESSING_REVERT_ERROR: error,
  4465. DID_THROW_ITEM_REMOVE_ERROR: error,
  4466. }),
  4467. didCreateView: root => {
  4468. applyFilters('CREATE_VIEW', { ...root, view: root });
  4469. },
  4470. create: create$3,
  4471. mixins: {
  4472. styles: ['translateX', 'translateY', 'opacity'],
  4473. animations: {
  4474. opacity: { type: 'tween', duration: 250 },
  4475. translateX: 'spring',
  4476. translateY: 'spring',
  4477. },
  4478. },
  4479. });
  4480. /**
  4481. * Button definitions for the file view
  4482. */
  4483. const Buttons = {
  4484. AbortItemLoad: {
  4485. label: 'GET_LABEL_BUTTON_ABORT_ITEM_LOAD',
  4486. action: 'ABORT_ITEM_LOAD',
  4487. className: 'filepond--action-abort-item-load',
  4488. align: 'LOAD_INDICATOR_POSITION', // right
  4489. },
  4490. RetryItemLoad: {
  4491. label: 'GET_LABEL_BUTTON_RETRY_ITEM_LOAD',
  4492. action: 'RETRY_ITEM_LOAD',
  4493. icon: 'GET_ICON_RETRY',
  4494. className: 'filepond--action-retry-item-load',
  4495. align: 'BUTTON_PROCESS_ITEM_POSITION', // right
  4496. },
  4497. RemoveItem: {
  4498. label: 'GET_LABEL_BUTTON_REMOVE_ITEM',
  4499. action: 'REQUEST_REMOVE_ITEM',
  4500. icon: 'GET_ICON_REMOVE',
  4501. className: 'filepond--action-remove-item',
  4502. align: 'BUTTON_REMOVE_ITEM_POSITION', // left
  4503. },
  4504. ProcessItem: {
  4505. label: 'GET_LABEL_BUTTON_PROCESS_ITEM',
  4506. action: 'REQUEST_ITEM_PROCESSING',
  4507. icon: 'GET_ICON_PROCESS',
  4508. className: 'filepond--action-process-item',
  4509. align: 'BUTTON_PROCESS_ITEM_POSITION', // right
  4510. },
  4511. AbortItemProcessing: {
  4512. label: 'GET_LABEL_BUTTON_ABORT_ITEM_PROCESSING',
  4513. action: 'ABORT_ITEM_PROCESSING',
  4514. className: 'filepond--action-abort-item-processing',
  4515. align: 'BUTTON_PROCESS_ITEM_POSITION', // right
  4516. },
  4517. RetryItemProcessing: {
  4518. label: 'GET_LABEL_BUTTON_RETRY_ITEM_PROCESSING',
  4519. action: 'RETRY_ITEM_PROCESSING',
  4520. icon: 'GET_ICON_RETRY',
  4521. className: 'filepond--action-retry-item-processing',
  4522. align: 'BUTTON_PROCESS_ITEM_POSITION', // right
  4523. },
  4524. RevertItemProcessing: {
  4525. label: 'GET_LABEL_BUTTON_UNDO_ITEM_PROCESSING',
  4526. action: 'REQUEST_REVERT_ITEM_PROCESSING',
  4527. icon: 'GET_ICON_UNDO',
  4528. className: 'filepond--action-revert-item-processing',
  4529. align: 'BUTTON_PROCESS_ITEM_POSITION', // right
  4530. },
  4531. };
  4532. // make a list of buttons, we can then remove buttons from this list if they're disabled
  4533. const ButtonKeys = [];
  4534. forin(Buttons, key => {
  4535. ButtonKeys.push(key);
  4536. });
  4537. const calculateFileInfoOffset = root => {
  4538. if (getRemoveIndicatorAligment(root) === 'right') return 0;
  4539. const buttonRect = root.ref.buttonRemoveItem.rect.element;
  4540. return buttonRect.hidden ? null : buttonRect.width + buttonRect.left;
  4541. };
  4542. const calculateButtonWidth = root => {
  4543. const buttonRect = root.ref.buttonAbortItemLoad.rect.element;
  4544. return buttonRect.width;
  4545. };
  4546. // Force on full pixels so text stays crips
  4547. const calculateFileVerticalCenterOffset = root =>
  4548. Math.floor(root.ref.buttonRemoveItem.rect.element.height / 4);
  4549. const calculateFileHorizontalCenterOffset = root =>
  4550. Math.floor(root.ref.buttonRemoveItem.rect.element.left / 2);
  4551. const getLoadIndicatorAlignment = root => root.query('GET_STYLE_LOAD_INDICATOR_POSITION');
  4552. const getProcessIndicatorAlignment = root => root.query('GET_STYLE_PROGRESS_INDICATOR_POSITION');
  4553. const getRemoveIndicatorAligment = root => root.query('GET_STYLE_BUTTON_REMOVE_ITEM_POSITION');
  4554. const DefaultStyle = {
  4555. buttonAbortItemLoad: { opacity: 0 },
  4556. buttonRetryItemLoad: { opacity: 0 },
  4557. buttonRemoveItem: { opacity: 0 },
  4558. buttonProcessItem: { opacity: 0 },
  4559. buttonAbortItemProcessing: { opacity: 0 },
  4560. buttonRetryItemProcessing: { opacity: 0 },
  4561. buttonRevertItemProcessing: { opacity: 0 },
  4562. loadProgressIndicator: { opacity: 0, align: getLoadIndicatorAlignment },
  4563. processProgressIndicator: { opacity: 0, align: getProcessIndicatorAlignment },
  4564. processingCompleteIndicator: { opacity: 0, scaleX: 0.75, scaleY: 0.75 },
  4565. info: { translateX: 0, translateY: 0, opacity: 0 },
  4566. status: { translateX: 0, translateY: 0, opacity: 0 },
  4567. };
  4568. const IdleStyle = {
  4569. buttonRemoveItem: { opacity: 1 },
  4570. buttonProcessItem: { opacity: 1 },
  4571. info: { translateX: calculateFileInfoOffset },
  4572. status: { translateX: calculateFileInfoOffset },
  4573. };
  4574. const ProcessingStyle = {
  4575. buttonAbortItemProcessing: { opacity: 1 },
  4576. processProgressIndicator: { opacity: 1 },
  4577. status: { opacity: 1 },
  4578. };
  4579. const StyleMap = {
  4580. DID_THROW_ITEM_INVALID: {
  4581. buttonRemoveItem: { opacity: 1 },
  4582. info: { translateX: calculateFileInfoOffset },
  4583. status: { translateX: calculateFileInfoOffset, opacity: 1 },
  4584. },
  4585. DID_START_ITEM_LOAD: {
  4586. buttonAbortItemLoad: { opacity: 1 },
  4587. loadProgressIndicator: { opacity: 1 },
  4588. status: { opacity: 1 },
  4589. },
  4590. DID_THROW_ITEM_LOAD_ERROR: {
  4591. buttonRetryItemLoad: { opacity: 1 },
  4592. buttonRemoveItem: { opacity: 1 },
  4593. info: { translateX: calculateFileInfoOffset },
  4594. status: { opacity: 1 },
  4595. },
  4596. DID_START_ITEM_REMOVE: {
  4597. processProgressIndicator: { opacity: 1, align: getRemoveIndicatorAligment },
  4598. info: { translateX: calculateFileInfoOffset },
  4599. status: { opacity: 0 },
  4600. },
  4601. DID_THROW_ITEM_REMOVE_ERROR: {
  4602. processProgressIndicator: { opacity: 0, align: getRemoveIndicatorAligment },
  4603. buttonRemoveItem: { opacity: 1 },
  4604. info: { translateX: calculateFileInfoOffset },
  4605. status: { opacity: 1, translateX: calculateFileInfoOffset },
  4606. },
  4607. DID_LOAD_ITEM: IdleStyle,
  4608. DID_LOAD_LOCAL_ITEM: {
  4609. buttonRemoveItem: { opacity: 1 },
  4610. info: { translateX: calculateFileInfoOffset },
  4611. status: { translateX: calculateFileInfoOffset },
  4612. },
  4613. DID_START_ITEM_PROCESSING: ProcessingStyle,
  4614. DID_REQUEST_ITEM_PROCESSING: ProcessingStyle,
  4615. DID_UPDATE_ITEM_PROCESS_PROGRESS: ProcessingStyle,
  4616. DID_COMPLETE_ITEM_PROCESSING: {
  4617. buttonRevertItemProcessing: { opacity: 1 },
  4618. info: { opacity: 1 },
  4619. status: { opacity: 1 },
  4620. },
  4621. DID_THROW_ITEM_PROCESSING_ERROR: {
  4622. buttonRemoveItem: { opacity: 1 },
  4623. buttonRetryItemProcessing: { opacity: 1 },
  4624. status: { opacity: 1 },
  4625. info: { translateX: calculateFileInfoOffset },
  4626. },
  4627. DID_THROW_ITEM_PROCESSING_REVERT_ERROR: {
  4628. buttonRevertItemProcessing: { opacity: 1 },
  4629. status: { opacity: 1 },
  4630. info: { opacity: 1 },
  4631. },
  4632. DID_ABORT_ITEM_PROCESSING: {
  4633. buttonRemoveItem: { opacity: 1 },
  4634. buttonProcessItem: { opacity: 1 },
  4635. info: { translateX: calculateFileInfoOffset },
  4636. status: { opacity: 1 },
  4637. },
  4638. DID_REVERT_ITEM_PROCESSING: IdleStyle,
  4639. };
  4640. // complete indicator view
  4641. const processingCompleteIndicatorView = createView({
  4642. create: ({ root }) => {
  4643. root.element.innerHTML = root.query('GET_ICON_DONE');
  4644. },
  4645. name: 'processing-complete-indicator',
  4646. ignoreRect: true,
  4647. mixins: {
  4648. styles: ['scaleX', 'scaleY', 'opacity'],
  4649. animations: {
  4650. scaleX: 'spring',
  4651. scaleY: 'spring',
  4652. opacity: { type: 'tween', duration: 250 },
  4653. },
  4654. },
  4655. });
  4656. /**
  4657. * Creates the file view
  4658. */
  4659. const create$4 = ({ root, props }) => {
  4660. // copy Buttons object
  4661. const LocalButtons = Object.keys(Buttons).reduce((prev, curr) => {
  4662. prev[curr] = { ...Buttons[curr] };
  4663. return prev;
  4664. }, {});
  4665. const { id } = props;
  4666. // allow reverting upload
  4667. const allowRevert = root.query('GET_ALLOW_REVERT');
  4668. // allow remove file
  4669. const allowRemove = root.query('GET_ALLOW_REMOVE');
  4670. // allow processing upload
  4671. const allowProcess = root.query('GET_ALLOW_PROCESS');
  4672. // is instant uploading, need this to determine the icon of the undo button
  4673. const instantUpload = root.query('GET_INSTANT_UPLOAD');
  4674. // is async set up
  4675. const isAsync = root.query('IS_ASYNC');
  4676. // should align remove item buttons
  4677. const alignRemoveItemButton = root.query('GET_STYLE_BUTTON_REMOVE_ITEM_ALIGN');
  4678. // enabled buttons array
  4679. let buttonFilter;
  4680. if (isAsync) {
  4681. if (allowProcess && !allowRevert) {
  4682. // only remove revert button
  4683. buttonFilter = key => !/RevertItemProcessing/.test(key);
  4684. } else if (!allowProcess && allowRevert) {
  4685. // only remove process button
  4686. buttonFilter = key => !/ProcessItem|RetryItemProcessing|AbortItemProcessing/.test(key);
  4687. } else if (!allowProcess && !allowRevert) {
  4688. // remove all process buttons
  4689. buttonFilter = key => !/Process/.test(key);
  4690. }
  4691. } else {
  4692. // no process controls available
  4693. buttonFilter = key => !/Process/.test(key);
  4694. }
  4695. const enabledButtons = buttonFilter ? ButtonKeys.filter(buttonFilter) : ButtonKeys.concat();
  4696. // update icon and label for revert button when instant uploading
  4697. if (instantUpload && allowRevert) {
  4698. LocalButtons['RevertItemProcessing'].label = 'GET_LABEL_BUTTON_REMOVE_ITEM';
  4699. LocalButtons['RevertItemProcessing'].icon = 'GET_ICON_REMOVE';
  4700. }
  4701. // remove last button (revert) if not allowed
  4702. if (isAsync && !allowRevert) {
  4703. const map = StyleMap['DID_COMPLETE_ITEM_PROCESSING'];
  4704. map.info.translateX = calculateFileHorizontalCenterOffset;
  4705. map.info.translateY = calculateFileVerticalCenterOffset;
  4706. map.status.translateY = calculateFileVerticalCenterOffset;
  4707. map.processingCompleteIndicator = { opacity: 1, scaleX: 1, scaleY: 1 };
  4708. }
  4709. // should align center
  4710. if (isAsync && !allowProcess) {
  4711. [
  4712. 'DID_START_ITEM_PROCESSING',
  4713. 'DID_REQUEST_ITEM_PROCESSING',
  4714. 'DID_UPDATE_ITEM_PROCESS_PROGRESS',
  4715. 'DID_THROW_ITEM_PROCESSING_ERROR',
  4716. ].forEach(key => {
  4717. StyleMap[key].status.translateY = calculateFileVerticalCenterOffset;
  4718. });
  4719. StyleMap['DID_THROW_ITEM_PROCESSING_ERROR'].status.translateX = calculateButtonWidth;
  4720. }
  4721. // move remove button to right
  4722. if (alignRemoveItemButton && allowRevert) {
  4723. LocalButtons['RevertItemProcessing'].align = 'BUTTON_REMOVE_ITEM_POSITION';
  4724. const map = StyleMap['DID_COMPLETE_ITEM_PROCESSING'];
  4725. map.info.translateX = calculateFileInfoOffset;
  4726. map.status.translateY = calculateFileVerticalCenterOffset;
  4727. map.processingCompleteIndicator = { opacity: 1, scaleX: 1, scaleY: 1 };
  4728. }
  4729. // show/hide RemoveItem button
  4730. if (!allowRemove) {
  4731. LocalButtons['RemoveItem'].disabled = true;
  4732. }
  4733. // create the button views
  4734. forin(LocalButtons, (key, definition) => {
  4735. // create button
  4736. const buttonView = root.createChildView(fileActionButton, {
  4737. label: root.query(definition.label),
  4738. icon: root.query(definition.icon),
  4739. opacity: 0,
  4740. });
  4741. // should be appended?
  4742. if (enabledButtons.includes(key)) {
  4743. root.appendChildView(buttonView);
  4744. }
  4745. // toggle
  4746. if (definition.disabled) {
  4747. buttonView.element.setAttribute('disabled', 'disabled');
  4748. buttonView.element.setAttribute('hidden', 'hidden');
  4749. }
  4750. // add position attribute
  4751. buttonView.element.dataset.align = root.query(`GET_STYLE_${definition.align}`);
  4752. // add class
  4753. buttonView.element.classList.add(definition.className);
  4754. // handle interactions
  4755. buttonView.on('click', e => {
  4756. e.stopPropagation();
  4757. if (definition.disabled) return;
  4758. root.dispatch(definition.action, { query: id });
  4759. });
  4760. // set reference
  4761. root.ref[`button${key}`] = buttonView;
  4762. });
  4763. // checkmark
  4764. root.ref.processingCompleteIndicator = root.appendChildView(
  4765. root.createChildView(processingCompleteIndicatorView)
  4766. );
  4767. root.ref.processingCompleteIndicator.element.dataset.align = root.query(
  4768. `GET_STYLE_BUTTON_PROCESS_ITEM_POSITION`
  4769. );
  4770. // create file info view
  4771. root.ref.info = root.appendChildView(root.createChildView(fileInfo, { id }));
  4772. // create file status view
  4773. root.ref.status = root.appendChildView(root.createChildView(fileStatus, { id }));
  4774. // add progress indicators
  4775. const loadIndicatorView = root.appendChildView(
  4776. root.createChildView(progressIndicator, {
  4777. opacity: 0,
  4778. align: root.query(`GET_STYLE_LOAD_INDICATOR_POSITION`),
  4779. })
  4780. );
  4781. loadIndicatorView.element.classList.add('filepond--load-indicator');
  4782. root.ref.loadProgressIndicator = loadIndicatorView;
  4783. const progressIndicatorView = root.appendChildView(
  4784. root.createChildView(progressIndicator, {
  4785. opacity: 0,
  4786. align: root.query(`GET_STYLE_PROGRESS_INDICATOR_POSITION`),
  4787. })
  4788. );
  4789. progressIndicatorView.element.classList.add('filepond--process-indicator');
  4790. root.ref.processProgressIndicator = progressIndicatorView;
  4791. // current active styles
  4792. root.ref.activeStyles = [];
  4793. };
  4794. const write$2 = ({ root, actions, props }) => {
  4795. // route actions
  4796. route({ root, actions, props });
  4797. // select last state change action
  4798. let action = actions
  4799. .concat()
  4800. .filter(action => /^DID_/.test(action.type))
  4801. .reverse()
  4802. .find(action => StyleMap[action.type]);
  4803. // a new action happened, let's get the matching styles
  4804. if (action) {
  4805. // define new active styles
  4806. root.ref.activeStyles = [];
  4807. const stylesToApply = StyleMap[action.type];
  4808. forin(DefaultStyle, (name, defaultStyles) => {
  4809. // get reference to control
  4810. const control = root.ref[name];
  4811. // loop over all styles for this control
  4812. forin(defaultStyles, (key, defaultValue) => {
  4813. const value =
  4814. stylesToApply[name] && typeof stylesToApply[name][key] !== 'undefined'
  4815. ? stylesToApply[name][key]
  4816. : defaultValue;
  4817. root.ref.activeStyles.push({ control, key, value });
  4818. });
  4819. });
  4820. }
  4821. // apply active styles to element
  4822. root.ref.activeStyles.forEach(({ control, key, value }) => {
  4823. control[key] = typeof value === 'function' ? value(root) : value;
  4824. });
  4825. };
  4826. const route = createRoute({
  4827. DID_SET_LABEL_BUTTON_ABORT_ITEM_PROCESSING: ({ root, action }) => {
  4828. root.ref.buttonAbortItemProcessing.label = action.value;
  4829. },
  4830. DID_SET_LABEL_BUTTON_ABORT_ITEM_LOAD: ({ root, action }) => {
  4831. root.ref.buttonAbortItemLoad.label = action.value;
  4832. },
  4833. DID_SET_LABEL_BUTTON_ABORT_ITEM_REMOVAL: ({ root, action }) => {
  4834. root.ref.buttonAbortItemRemoval.label = action.value;
  4835. },
  4836. DID_REQUEST_ITEM_PROCESSING: ({ root }) => {
  4837. root.ref.processProgressIndicator.spin = true;
  4838. root.ref.processProgressIndicator.progress = 0;
  4839. },
  4840. DID_START_ITEM_LOAD: ({ root }) => {
  4841. root.ref.loadProgressIndicator.spin = true;
  4842. root.ref.loadProgressIndicator.progress = 0;
  4843. },
  4844. DID_START_ITEM_REMOVE: ({ root }) => {
  4845. root.ref.processProgressIndicator.spin = true;
  4846. root.ref.processProgressIndicator.progress = 0;
  4847. },
  4848. DID_UPDATE_ITEM_LOAD_PROGRESS: ({ root, action }) => {
  4849. root.ref.loadProgressIndicator.spin = false;
  4850. root.ref.loadProgressIndicator.progress = action.progress;
  4851. },
  4852. DID_UPDATE_ITEM_PROCESS_PROGRESS: ({ root, action }) => {
  4853. root.ref.processProgressIndicator.spin = false;
  4854. root.ref.processProgressIndicator.progress = action.progress;
  4855. },
  4856. });
  4857. const file = createView({
  4858. create: create$4,
  4859. write: write$2,
  4860. didCreateView: root => {
  4861. applyFilters('CREATE_VIEW', { ...root, view: root });
  4862. },
  4863. name: 'file',
  4864. });
  4865. /**
  4866. * Creates the file view
  4867. */
  4868. const create$5 = ({ root, props }) => {
  4869. // filename
  4870. root.ref.fileName = createElement$1('legend');
  4871. root.appendChild(root.ref.fileName);
  4872. // file appended
  4873. root.ref.file = root.appendChildView(root.createChildView(file, { id: props.id }));
  4874. // data has moved to data.js
  4875. root.ref.data = false;
  4876. };
  4877. /**
  4878. * Data storage
  4879. */
  4880. const didLoadItem = ({ root, props }) => {
  4881. // updates the legend of the fieldset so screenreaders can better group buttons
  4882. text(root.ref.fileName, formatFilename(root.query('GET_ITEM_NAME', props.id)));
  4883. };
  4884. const fileWrapper = createView({
  4885. create: create$5,
  4886. ignoreRect: true,
  4887. write: createRoute({
  4888. DID_LOAD_ITEM: didLoadItem,
  4889. }),
  4890. didCreateView: root => {
  4891. applyFilters('CREATE_VIEW', { ...root, view: root });
  4892. },
  4893. tag: 'fieldset',
  4894. name: 'file-wrapper',
  4895. });
  4896. const PANEL_SPRING_PROPS = { type: 'spring', damping: 0.6, mass: 7 };
  4897. const create$6 = ({ root, props }) => {
  4898. [
  4899. {
  4900. name: 'top',
  4901. },
  4902. {
  4903. name: 'center',
  4904. props: {
  4905. translateY: null,
  4906. scaleY: null,
  4907. },
  4908. mixins: {
  4909. animations: {
  4910. scaleY: PANEL_SPRING_PROPS,
  4911. },
  4912. styles: ['translateY', 'scaleY'],
  4913. },
  4914. },
  4915. {
  4916. name: 'bottom',
  4917. props: {
  4918. translateY: null,
  4919. },
  4920. mixins: {
  4921. animations: {
  4922. translateY: PANEL_SPRING_PROPS,
  4923. },
  4924. styles: ['translateY'],
  4925. },
  4926. },
  4927. ].forEach(section => {
  4928. createSection(root, section, props.name);
  4929. });
  4930. root.element.classList.add(`filepond--${props.name}`);
  4931. root.ref.scalable = null;
  4932. };
  4933. const createSection = (root, section, className) => {
  4934. const viewConstructor = createView({
  4935. name: `panel-${section.name} filepond--${className}`,
  4936. mixins: section.mixins,
  4937. ignoreRectUpdate: true,
  4938. });
  4939. const view = root.createChildView(viewConstructor, section.props);
  4940. root.ref[section.name] = root.appendChildView(view);
  4941. };
  4942. const write$3 = ({ root, props }) => {
  4943. // update scalable state
  4944. if (root.ref.scalable === null || props.scalable !== root.ref.scalable) {
  4945. root.ref.scalable = isBoolean(props.scalable) ? props.scalable : true;
  4946. root.element.dataset.scalable = root.ref.scalable;
  4947. }
  4948. // no height, can't set
  4949. if (!props.height) return;
  4950. // get child rects
  4951. const topRect = root.ref.top.rect.element;
  4952. const bottomRect = root.ref.bottom.rect.element;
  4953. // make sure height never is smaller than bottom and top seciton heights combined (will probably never happen, but who knows)
  4954. const height = Math.max(topRect.height + bottomRect.height, props.height);
  4955. // offset center part
  4956. root.ref.center.translateY = topRect.height;
  4957. // scale center part
  4958. // use math ceil to prevent transparent lines because of rounding errors
  4959. root.ref.center.scaleY = (height - topRect.height - bottomRect.height) / 100;
  4960. // offset bottom part
  4961. root.ref.bottom.translateY = height - bottomRect.height;
  4962. };
  4963. const panel = createView({
  4964. name: 'panel',
  4965. read: ({ root, props }) => (props.heightCurrent = root.ref.bottom.translateY),
  4966. write: write$3,
  4967. create: create$6,
  4968. ignoreRect: true,
  4969. mixins: {
  4970. apis: ['height', 'heightCurrent', 'scalable'],
  4971. },
  4972. });
  4973. const createDragHelper = items => {
  4974. const itemIds = items.map(item => item.id);
  4975. let prevIndex = undefined;
  4976. return {
  4977. setIndex: index => {
  4978. prevIndex = index;
  4979. },
  4980. getIndex: () => prevIndex,
  4981. getItemIndex: item => itemIds.indexOf(item.id),
  4982. };
  4983. };
  4984. const ITEM_TRANSLATE_SPRING = {
  4985. type: 'spring',
  4986. stiffness: 0.75,
  4987. damping: 0.45,
  4988. mass: 10,
  4989. };
  4990. const ITEM_SCALE_SPRING = 'spring';
  4991. const StateMap = {
  4992. DID_START_ITEM_LOAD: 'busy',
  4993. DID_UPDATE_ITEM_LOAD_PROGRESS: 'loading',
  4994. DID_THROW_ITEM_INVALID: 'load-invalid',
  4995. DID_THROW_ITEM_LOAD_ERROR: 'load-error',
  4996. DID_LOAD_ITEM: 'idle',
  4997. DID_THROW_ITEM_REMOVE_ERROR: 'remove-error',
  4998. DID_START_ITEM_REMOVE: 'busy',
  4999. DID_START_ITEM_PROCESSING: 'busy processing',
  5000. DID_REQUEST_ITEM_PROCESSING: 'busy processing',
  5001. DID_UPDATE_ITEM_PROCESS_PROGRESS: 'processing',
  5002. DID_COMPLETE_ITEM_PROCESSING: 'processing-complete',
  5003. DID_THROW_ITEM_PROCESSING_ERROR: 'processing-error',
  5004. DID_THROW_ITEM_PROCESSING_REVERT_ERROR: 'processing-revert-error',
  5005. DID_ABORT_ITEM_PROCESSING: 'cancelled',
  5006. DID_REVERT_ITEM_PROCESSING: 'idle',
  5007. };
  5008. /**
  5009. * Creates the file view
  5010. */
  5011. const create$7 = ({ root, props }) => {
  5012. // select
  5013. root.ref.handleClick = e => root.dispatch('DID_ACTIVATE_ITEM', { id: props.id });
  5014. // set id
  5015. root.element.id = `filepond--item-${props.id}`;
  5016. root.element.addEventListener('click', root.ref.handleClick);
  5017. // file view
  5018. root.ref.container = root.appendChildView(root.createChildView(fileWrapper, { id: props.id }));
  5019. // file panel
  5020. root.ref.panel = root.appendChildView(root.createChildView(panel, { name: 'item-panel' }));
  5021. // default start height
  5022. root.ref.panel.height = null;
  5023. // by default not marked for removal
  5024. props.markedForRemoval = false;
  5025. // if not allowed to reorder file items, exit here
  5026. if (!root.query('GET_ALLOW_REORDER')) return;
  5027. // set to idle so shows grab cursor
  5028. root.element.dataset.dragState = 'idle';
  5029. const grab = e => {
  5030. if (!e.isPrimary) return;
  5031. let removedActivateListener = false;
  5032. const origin = {
  5033. x: e.pageX,
  5034. y: e.pageY,
  5035. };
  5036. props.dragOrigin = {
  5037. x: root.translateX,
  5038. y: root.translateY,
  5039. };
  5040. props.dragCenter = {
  5041. x: e.offsetX,
  5042. y: e.offsetY,
  5043. };
  5044. const dragState = createDragHelper(root.query('GET_ACTIVE_ITEMS'));
  5045. root.dispatch('DID_GRAB_ITEM', { id: props.id, dragState });
  5046. const drag = e => {
  5047. if (!e.isPrimary) return;
  5048. e.stopPropagation();
  5049. e.preventDefault();
  5050. props.dragOffset = {
  5051. x: e.pageX - origin.x,
  5052. y: e.pageY - origin.y,
  5053. };
  5054. // if dragged stop listening to clicks, will re-add when done dragging
  5055. const dist =
  5056. props.dragOffset.x * props.dragOffset.x + props.dragOffset.y * props.dragOffset.y;
  5057. if (dist > 16 && !removedActivateListener) {
  5058. removedActivateListener = true;
  5059. root.element.removeEventListener('click', root.ref.handleClick);
  5060. }
  5061. root.dispatch('DID_DRAG_ITEM', { id: props.id, dragState });
  5062. };
  5063. const drop = e => {
  5064. if (!e.isPrimary) return;
  5065. document.removeEventListener('pointermove', drag);
  5066. document.removeEventListener('pointerup', drop);
  5067. props.dragOffset = {
  5068. x: e.pageX - origin.x,
  5069. y: e.pageY - origin.y,
  5070. };
  5071. root.dispatch('DID_DROP_ITEM', { id: props.id, dragState });
  5072. // start listening to clicks again
  5073. if (removedActivateListener) {
  5074. setTimeout(() => root.element.addEventListener('click', root.ref.handleClick), 0);
  5075. }
  5076. };
  5077. document.addEventListener('pointermove', drag);
  5078. document.addEventListener('pointerup', drop);
  5079. };
  5080. root.element.addEventListener('pointerdown', grab);
  5081. };
  5082. const route$1 = createRoute({
  5083. DID_UPDATE_PANEL_HEIGHT: ({ root, action }) => {
  5084. root.height = action.height;
  5085. },
  5086. });
  5087. const write$4 = createRoute(
  5088. {
  5089. DID_GRAB_ITEM: ({ root, props }) => {
  5090. props.dragOrigin = {
  5091. x: root.translateX,
  5092. y: root.translateY,
  5093. };
  5094. },
  5095. DID_DRAG_ITEM: ({ root }) => {
  5096. root.element.dataset.dragState = 'drag';
  5097. },
  5098. DID_DROP_ITEM: ({ root, props }) => {
  5099. props.dragOffset = null;
  5100. props.dragOrigin = null;
  5101. root.element.dataset.dragState = 'drop';
  5102. },
  5103. },
  5104. ({ root, actions, props, shouldOptimize }) => {
  5105. if (root.element.dataset.dragState === 'drop') {
  5106. if (root.scaleX <= 1) {
  5107. root.element.dataset.dragState = 'idle';
  5108. }
  5109. }
  5110. // select last state change action
  5111. let action = actions
  5112. .concat()
  5113. .filter(action => /^DID_/.test(action.type))
  5114. .reverse()
  5115. .find(action => StateMap[action.type]);
  5116. // no need to set same state twice
  5117. if (action && action.type !== props.currentState) {
  5118. // set current state
  5119. props.currentState = action.type;
  5120. // set state
  5121. root.element.dataset.filepondItemState = StateMap[props.currentState] || '';
  5122. }
  5123. // route actions
  5124. const aspectRatio =
  5125. root.query('GET_ITEM_PANEL_ASPECT_RATIO') || root.query('GET_PANEL_ASPECT_RATIO');
  5126. if (!aspectRatio) {
  5127. route$1({ root, actions, props });
  5128. if (!root.height && root.ref.container.rect.element.height > 0) {
  5129. root.height = root.ref.container.rect.element.height;
  5130. }
  5131. } else if (!shouldOptimize) {
  5132. root.height = root.rect.element.width * aspectRatio;
  5133. }
  5134. // sync panel height with item height
  5135. if (shouldOptimize) {
  5136. root.ref.panel.height = null;
  5137. }
  5138. root.ref.panel.height = root.height;
  5139. }
  5140. );
  5141. const item = createView({
  5142. create: create$7,
  5143. write: write$4,
  5144. destroy: ({ root, props }) => {
  5145. root.element.removeEventListener('click', root.ref.handleClick);
  5146. root.dispatch('RELEASE_ITEM', { query: props.id });
  5147. },
  5148. tag: 'li',
  5149. name: 'item',
  5150. mixins: {
  5151. apis: [
  5152. 'id',
  5153. 'interactionMethod',
  5154. 'markedForRemoval',
  5155. 'spawnDate',
  5156. 'dragCenter',
  5157. 'dragOrigin',
  5158. 'dragOffset',
  5159. ],
  5160. styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity', 'height'],
  5161. animations: {
  5162. scaleX: ITEM_SCALE_SPRING,
  5163. scaleY: ITEM_SCALE_SPRING,
  5164. translateX: ITEM_TRANSLATE_SPRING,
  5165. translateY: ITEM_TRANSLATE_SPRING,
  5166. opacity: { type: 'tween', duration: 150 },
  5167. },
  5168. },
  5169. });
  5170. var getItemsPerRow = (horizontalSpace, itemWidth) => {
  5171. // add one pixel leeway, when using percentages for item width total items can be 1.99 per row
  5172. return Math.max(1, Math.floor((horizontalSpace + 1) / itemWidth));
  5173. };
  5174. const getItemIndexByPosition = (view, children, positionInView) => {
  5175. if (!positionInView) return;
  5176. const horizontalSpace = view.rect.element.width;
  5177. // const children = view.childViews;
  5178. const l = children.length;
  5179. let last = null;
  5180. // -1, don't move items to accomodate (either add to top or bottom)
  5181. if (l === 0 || positionInView.top < children[0].rect.element.top) return -1;
  5182. // let's get the item width
  5183. const item = children[0];
  5184. const itemRect = item.rect.element;
  5185. const itemHorizontalMargin = itemRect.marginLeft + itemRect.marginRight;
  5186. const itemWidth = itemRect.width + itemHorizontalMargin;
  5187. const itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth);
  5188. // stack
  5189. if (itemsPerRow === 1) {
  5190. for (let index = 0; index < l; index++) {
  5191. const child = children[index];
  5192. const childMid = child.rect.outer.top + child.rect.element.height * 0.5;
  5193. if (positionInView.top < childMid) {
  5194. return index;
  5195. }
  5196. }
  5197. return l;
  5198. }
  5199. // grid
  5200. const itemVerticalMargin = itemRect.marginTop + itemRect.marginBottom;
  5201. const itemHeight = itemRect.height + itemVerticalMargin;
  5202. for (let index = 0; index < l; index++) {
  5203. const indexX = index % itemsPerRow;
  5204. const indexY = Math.floor(index / itemsPerRow);
  5205. const offsetX = indexX * itemWidth;
  5206. const offsetY = indexY * itemHeight;
  5207. const itemTop = offsetY - itemRect.marginTop;
  5208. const itemRight = offsetX + itemWidth;
  5209. const itemBottom = offsetY + itemHeight + itemRect.marginBottom;
  5210. if (positionInView.top < itemBottom && positionInView.top > itemTop) {
  5211. if (positionInView.left < itemRight) {
  5212. return index;
  5213. } else if (index !== l - 1) {
  5214. last = index;
  5215. } else {
  5216. last = null;
  5217. }
  5218. }
  5219. }
  5220. if (last !== null) {
  5221. return last;
  5222. }
  5223. return l;
  5224. };
  5225. const dropAreaDimensions = {
  5226. height: 0,
  5227. width: 0,
  5228. get getHeight() {
  5229. return this.height;
  5230. },
  5231. set setHeight(val) {
  5232. if (this.height === 0 || val === 0) this.height = val;
  5233. },
  5234. get getWidth() {
  5235. return this.width;
  5236. },
  5237. set setWidth(val) {
  5238. if (this.width === 0 || val === 0) this.width = val;
  5239. },
  5240. setDimensions: function(height, width) {
  5241. if (this.height === 0 || height === 0) this.height = height;
  5242. if (this.width === 0 || width === 0) this.width = width;
  5243. },
  5244. };
  5245. const create$8 = ({ root }) => {
  5246. // need to set role to list as otherwise it won't be read as a list by VoiceOver
  5247. attr(root.element, 'role', 'list');
  5248. root.ref.lastItemSpanwDate = Date.now();
  5249. };
  5250. /**
  5251. * Inserts a new item
  5252. * @param root
  5253. * @param action
  5254. */
  5255. const addItemView = ({ root, action }) => {
  5256. const { id, index, interactionMethod } = action;
  5257. root.ref.addIndex = index;
  5258. const now = Date.now();
  5259. let spawnDate = now;
  5260. let opacity = 1;
  5261. if (interactionMethod !== InteractionMethod.NONE) {
  5262. opacity = 0;
  5263. const cooldown = root.query('GET_ITEM_INSERT_INTERVAL');
  5264. const dist = now - root.ref.lastItemSpanwDate;
  5265. spawnDate = dist < cooldown ? now + (cooldown - dist) : now;
  5266. }
  5267. root.ref.lastItemSpanwDate = spawnDate;
  5268. root.appendChildView(
  5269. root.createChildView(
  5270. // view type
  5271. item,
  5272. // props
  5273. {
  5274. spawnDate,
  5275. id,
  5276. opacity,
  5277. interactionMethod,
  5278. }
  5279. ),
  5280. index
  5281. );
  5282. };
  5283. const moveItem = (item, x, y, vx = 0, vy = 1) => {
  5284. // set to null to remove animation while dragging
  5285. if (item.dragOffset) {
  5286. item.translateX = null;
  5287. item.translateY = null;
  5288. item.translateX = item.dragOrigin.x + item.dragOffset.x;
  5289. item.translateY = item.dragOrigin.y + item.dragOffset.y;
  5290. item.scaleX = 1.025;
  5291. item.scaleY = 1.025;
  5292. } else {
  5293. item.translateX = x;
  5294. item.translateY = y;
  5295. if (Date.now() > item.spawnDate) {
  5296. // reveal element
  5297. if (item.opacity === 0) {
  5298. introItemView(item, x, y, vx, vy);
  5299. }
  5300. // make sure is default scale every frame
  5301. item.scaleX = 1;
  5302. item.scaleY = 1;
  5303. item.opacity = 1;
  5304. }
  5305. }
  5306. };
  5307. const introItemView = (item, x, y, vx, vy) => {
  5308. if (item.interactionMethod === InteractionMethod.NONE) {
  5309. item.translateX = null;
  5310. item.translateX = x;
  5311. item.translateY = null;
  5312. item.translateY = y;
  5313. } else if (item.interactionMethod === InteractionMethod.DROP) {
  5314. item.translateX = null;
  5315. item.translateX = x - vx * 20;
  5316. item.translateY = null;
  5317. item.translateY = y - vy * 10;
  5318. item.scaleX = 0.8;
  5319. item.scaleY = 0.8;
  5320. } else if (item.interactionMethod === InteractionMethod.BROWSE) {
  5321. item.translateY = null;
  5322. item.translateY = y - 30;
  5323. } else if (item.interactionMethod === InteractionMethod.API) {
  5324. item.translateX = null;
  5325. item.translateX = x - 30;
  5326. item.translateY = null;
  5327. }
  5328. };
  5329. /**
  5330. * Removes an existing item
  5331. * @param root
  5332. * @param action
  5333. */
  5334. const removeItemView = ({ root, action }) => {
  5335. const { id } = action;
  5336. // get the view matching the given id
  5337. const view = root.childViews.find(child => child.id === id);
  5338. // if no view found, exit
  5339. if (!view) {
  5340. return;
  5341. }
  5342. // animate view out of view
  5343. view.scaleX = 0.9;
  5344. view.scaleY = 0.9;
  5345. view.opacity = 0;
  5346. // mark for removal
  5347. view.markedForRemoval = true;
  5348. };
  5349. const getItemHeight = child =>
  5350. child.rect.element.height +
  5351. child.rect.element.marginBottom * 0.5 +
  5352. child.rect.element.marginTop * 0.5;
  5353. const getItemWidth = child =>
  5354. child.rect.element.width +
  5355. child.rect.element.marginLeft * 0.5 +
  5356. child.rect.element.marginRight * 0.5;
  5357. const dragItem = ({ root, action }) => {
  5358. const { id, dragState } = action;
  5359. // reference to item
  5360. const item = root.query('GET_ITEM', { id });
  5361. // get the view matching the given id
  5362. const view = root.childViews.find(child => child.id === id);
  5363. const numItems = root.childViews.length;
  5364. const oldIndex = dragState.getItemIndex(item);
  5365. // if no view found, exit
  5366. if (!view) return;
  5367. const dragPosition = {
  5368. x: view.dragOrigin.x + view.dragOffset.x + view.dragCenter.x,
  5369. y: view.dragOrigin.y + view.dragOffset.y + view.dragCenter.y,
  5370. };
  5371. // get drag area dimensions
  5372. const dragHeight = getItemHeight(view);
  5373. const dragWidth = getItemWidth(view);
  5374. // get rows and columns (There will always be at least one row and one column if a file is present)
  5375. let cols = Math.floor(root.rect.outer.width / dragWidth);
  5376. if (cols > numItems) cols = numItems;
  5377. // rows are used to find when we have left the preview area bounding box
  5378. const rows = Math.floor(numItems / cols + 1);
  5379. dropAreaDimensions.setHeight = dragHeight * rows;
  5380. dropAreaDimensions.setWidth = dragWidth * cols;
  5381. // get new index of dragged item
  5382. var location = {
  5383. y: Math.floor(dragPosition.y / dragHeight),
  5384. x: Math.floor(dragPosition.x / dragWidth),
  5385. getGridIndex: function getGridIndex() {
  5386. if (
  5387. dragPosition.y > dropAreaDimensions.getHeight ||
  5388. dragPosition.y < 0 ||
  5389. dragPosition.x > dropAreaDimensions.getWidth ||
  5390. dragPosition.x < 0
  5391. )
  5392. return oldIndex;
  5393. return this.y * cols + this.x;
  5394. },
  5395. getColIndex: function getColIndex() {
  5396. const items = root.query('GET_ACTIVE_ITEMS');
  5397. const visibleChildren = root.childViews.filter(child => child.rect.element.height);
  5398. const children = items.map(item =>
  5399. visibleChildren.find(childView => childView.id === item.id)
  5400. );
  5401. const currentIndex = children.findIndex(child => child === view);
  5402. const dragHeight = getItemHeight(view);
  5403. const l = children.length;
  5404. let idx = l;
  5405. let childHeight = 0;
  5406. let childBottom = 0;
  5407. let childTop = 0;
  5408. for (let i = 0; i < l; i++) {
  5409. childHeight = getItemHeight(children[i]);
  5410. childTop = childBottom;
  5411. childBottom = childTop + childHeight;
  5412. if (dragPosition.y < childBottom) {
  5413. if (currentIndex > i) {
  5414. if (dragPosition.y < childTop + dragHeight) {
  5415. idx = i;
  5416. break;
  5417. }
  5418. continue;
  5419. }
  5420. idx = i;
  5421. break;
  5422. }
  5423. }
  5424. return idx;
  5425. },
  5426. };
  5427. // get new index
  5428. const index = cols > 1 ? location.getGridIndex() : location.getColIndex();
  5429. root.dispatch('MOVE_ITEM', { query: view, index });
  5430. // if the index of the item changed, dispatch reorder action
  5431. const currentIndex = dragState.getIndex();
  5432. if (currentIndex === undefined || currentIndex !== index) {
  5433. dragState.setIndex(index);
  5434. if (currentIndex === undefined) return;
  5435. root.dispatch('DID_REORDER_ITEMS', {
  5436. items: root.query('GET_ACTIVE_ITEMS'),
  5437. origin: oldIndex,
  5438. target: index,
  5439. });
  5440. }
  5441. };
  5442. /**
  5443. * Setup action routes
  5444. */
  5445. const route$2 = createRoute({
  5446. DID_ADD_ITEM: addItemView,
  5447. DID_REMOVE_ITEM: removeItemView,
  5448. DID_DRAG_ITEM: dragItem,
  5449. });
  5450. /**
  5451. * Write to view
  5452. * @param root
  5453. * @param actions
  5454. * @param props
  5455. */
  5456. const write$5 = ({ root, props, actions, shouldOptimize }) => {
  5457. // route actions
  5458. route$2({ root, props, actions });
  5459. const { dragCoordinates } = props;
  5460. // available space on horizontal axis
  5461. const horizontalSpace = root.rect.element.width;
  5462. // only draw children that have dimensions
  5463. const visibleChildren = root.childViews.filter(child => child.rect.element.height);
  5464. // sort based on current active items
  5465. const children = root
  5466. .query('GET_ACTIVE_ITEMS')
  5467. .map(item => visibleChildren.find(child => child.id === item.id))
  5468. .filter(item => item);
  5469. // get index
  5470. const dragIndex = dragCoordinates
  5471. ? getItemIndexByPosition(root, children, dragCoordinates)
  5472. : null;
  5473. // add index is used to reserve the dropped/added item index till the actual item is rendered
  5474. const addIndex = root.ref.addIndex || null;
  5475. // add index no longer needed till possibly next draw
  5476. root.ref.addIndex = null;
  5477. let dragIndexOffset = 0;
  5478. let removeIndexOffset = 0;
  5479. let addIndexOffset = 0;
  5480. if (children.length === 0) return;
  5481. const childRect = children[0].rect.element;
  5482. const itemVerticalMargin = childRect.marginTop + childRect.marginBottom;
  5483. const itemHorizontalMargin = childRect.marginLeft + childRect.marginRight;
  5484. const itemWidth = childRect.width + itemHorizontalMargin;
  5485. const itemHeight = childRect.height + itemVerticalMargin;
  5486. const itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth);
  5487. // stack
  5488. if (itemsPerRow === 1) {
  5489. let offsetY = 0;
  5490. let dragOffset = 0;
  5491. children.forEach((child, index) => {
  5492. if (dragIndex) {
  5493. let dist = index - dragIndex;
  5494. if (dist === -2) {
  5495. dragOffset = -itemVerticalMargin * 0.25;
  5496. } else if (dist === -1) {
  5497. dragOffset = -itemVerticalMargin * 0.75;
  5498. } else if (dist === 0) {
  5499. dragOffset = itemVerticalMargin * 0.75;
  5500. } else if (dist === 1) {
  5501. dragOffset = itemVerticalMargin * 0.25;
  5502. } else {
  5503. dragOffset = 0;
  5504. }
  5505. }
  5506. if (shouldOptimize) {
  5507. child.translateX = null;
  5508. child.translateY = null;
  5509. }
  5510. if (!child.markedForRemoval) {
  5511. moveItem(child, 0, offsetY + dragOffset);
  5512. }
  5513. let itemHeight = child.rect.element.height + itemVerticalMargin;
  5514. let visualHeight = itemHeight * (child.markedForRemoval ? child.opacity : 1);
  5515. offsetY += visualHeight;
  5516. });
  5517. }
  5518. // grid
  5519. else {
  5520. let prevX = 0;
  5521. let prevY = 0;
  5522. children.forEach((child, index) => {
  5523. if (index === dragIndex) {
  5524. dragIndexOffset = 1;
  5525. }
  5526. if (index === addIndex) {
  5527. addIndexOffset += 1;
  5528. }
  5529. if (child.markedForRemoval && child.opacity < 0.5) {
  5530. removeIndexOffset -= 1;
  5531. }
  5532. const visualIndex = index + addIndexOffset + dragIndexOffset + removeIndexOffset;
  5533. const indexX = visualIndex % itemsPerRow;
  5534. const indexY = Math.floor(visualIndex / itemsPerRow);
  5535. const offsetX = indexX * itemWidth;
  5536. const offsetY = indexY * itemHeight;
  5537. const vectorX = Math.sign(offsetX - prevX);
  5538. const vectorY = Math.sign(offsetY - prevY);
  5539. prevX = offsetX;
  5540. prevY = offsetY;
  5541. if (child.markedForRemoval) return;
  5542. if (shouldOptimize) {
  5543. child.translateX = null;
  5544. child.translateY = null;
  5545. }
  5546. moveItem(child, offsetX, offsetY, vectorX, vectorY);
  5547. });
  5548. }
  5549. };
  5550. /**
  5551. * Filters actions that are meant specifically for a certain child of the list
  5552. * @param child
  5553. * @param actions
  5554. */
  5555. const filterSetItemActions = (child, actions) =>
  5556. actions.filter(action => {
  5557. // if action has an id, filter out actions that don't have this child id
  5558. if (action.data && action.data.id) {
  5559. return child.id === action.data.id;
  5560. }
  5561. // allow all other actions
  5562. return true;
  5563. });
  5564. const list = createView({
  5565. create: create$8,
  5566. write: write$5,
  5567. tag: 'ul',
  5568. name: 'list',
  5569. didWriteView: ({ root }) => {
  5570. root.childViews
  5571. .filter(view => view.markedForRemoval && view.opacity === 0 && view.resting)
  5572. .forEach(view => {
  5573. view._destroy();
  5574. root.removeChildView(view);
  5575. });
  5576. },
  5577. filterFrameActionsForChild: filterSetItemActions,
  5578. mixins: {
  5579. apis: ['dragCoordinates'],
  5580. },
  5581. });
  5582. const create$9 = ({ root, props }) => {
  5583. root.ref.list = root.appendChildView(root.createChildView(list));
  5584. props.dragCoordinates = null;
  5585. props.overflowing = false;
  5586. };
  5587. const storeDragCoordinates = ({ root, props, action }) => {
  5588. if (!root.query('GET_ITEM_INSERT_LOCATION_FREEDOM')) return;
  5589. props.dragCoordinates = {
  5590. left: action.position.scopeLeft - root.ref.list.rect.element.left,
  5591. top:
  5592. action.position.scopeTop -
  5593. (root.rect.outer.top + root.rect.element.marginTop + root.rect.element.scrollTop),
  5594. };
  5595. };
  5596. const clearDragCoordinates = ({ props }) => {
  5597. props.dragCoordinates = null;
  5598. };
  5599. const route$3 = createRoute({
  5600. DID_DRAG: storeDragCoordinates,
  5601. DID_END_DRAG: clearDragCoordinates,
  5602. });
  5603. const write$6 = ({ root, props, actions }) => {
  5604. // route actions
  5605. route$3({ root, props, actions });
  5606. // current drag position
  5607. root.ref.list.dragCoordinates = props.dragCoordinates;
  5608. // if currently overflowing but no longer received overflow
  5609. if (props.overflowing && !props.overflow) {
  5610. props.overflowing = false;
  5611. // reset overflow state
  5612. root.element.dataset.state = '';
  5613. root.height = null;
  5614. }
  5615. // if is not overflowing currently but does receive overflow value
  5616. if (props.overflow) {
  5617. const newHeight = Math.round(props.overflow);
  5618. if (newHeight !== root.height) {
  5619. props.overflowing = true;
  5620. root.element.dataset.state = 'overflow';
  5621. root.height = newHeight;
  5622. }
  5623. }
  5624. };
  5625. const listScroller = createView({
  5626. create: create$9,
  5627. write: write$6,
  5628. name: 'list-scroller',
  5629. mixins: {
  5630. apis: ['overflow', 'dragCoordinates'],
  5631. styles: ['height', 'translateY'],
  5632. animations: {
  5633. translateY: 'spring',
  5634. },
  5635. },
  5636. });
  5637. const attrToggle = (element, name, state, enabledValue = '') => {
  5638. if (state) {
  5639. attr(element, name, enabledValue);
  5640. } else {
  5641. element.removeAttribute(name);
  5642. }
  5643. };
  5644. const resetFileInput = input => {
  5645. // no value, no need to reset
  5646. if (!input || input.value === '') {
  5647. return;
  5648. }
  5649. try {
  5650. // for modern browsers
  5651. input.value = '';
  5652. } catch (err) {}
  5653. // for IE10
  5654. if (input.value) {
  5655. // quickly append input to temp form and reset form
  5656. const form = createElement$1('form');
  5657. const parentNode = input.parentNode;
  5658. const ref = input.nextSibling;
  5659. form.appendChild(input);
  5660. form.reset();
  5661. // re-inject input where it originally was
  5662. if (ref) {
  5663. parentNode.insertBefore(input, ref);
  5664. } else {
  5665. parentNode.appendChild(input);
  5666. }
  5667. }
  5668. };
  5669. const create$a = ({ root, props }) => {
  5670. // set id so can be referenced from outside labels
  5671. root.element.id = `filepond--browser-${props.id}`;
  5672. // set name of element (is removed when a value is set)
  5673. attr(root.element, 'name', root.query('GET_NAME'));
  5674. // we have to link this element to the status element
  5675. attr(root.element, 'aria-controls', `filepond--assistant-${props.id}`);
  5676. // set label, we use labelled by as otherwise the screenreader does not read the "browse" text in the label (as it has tabindex: 0)
  5677. attr(root.element, 'aria-labelledby', `filepond--drop-label-${props.id}`);
  5678. // set configurable props
  5679. setAcceptedFileTypes({ root, action: { value: root.query('GET_ACCEPTED_FILE_TYPES') } });
  5680. toggleAllowMultiple({ root, action: { value: root.query('GET_ALLOW_MULTIPLE') } });
  5681. toggleDirectoryFilter({ root, action: { value: root.query('GET_ALLOW_DIRECTORIES_ONLY') } });
  5682. toggleDisabled({ root });
  5683. toggleRequired({ root, action: { value: root.query('GET_REQUIRED') } });
  5684. setCaptureMethod({ root, action: { value: root.query('GET_CAPTURE_METHOD') } });
  5685. // handle changes to the input field
  5686. root.ref.handleChange = e => {
  5687. if (!root.element.value) {
  5688. return;
  5689. }
  5690. // extract files and move value of webkitRelativePath path to _relativePath
  5691. const files = Array.from(root.element.files).map(file => {
  5692. file._relativePath = file.webkitRelativePath;
  5693. return file;
  5694. });
  5695. // we add a little delay so the OS file select window can move out of the way before we add our file
  5696. setTimeout(() => {
  5697. // load files
  5698. props.onload(files);
  5699. // reset input, it's just for exposing a method to drop files, should not retain any state
  5700. resetFileInput(root.element);
  5701. }, 250);
  5702. };
  5703. root.element.addEventListener('change', root.ref.handleChange);
  5704. };
  5705. const setAcceptedFileTypes = ({ root, action }) => {
  5706. if (!root.query('GET_ALLOW_SYNC_ACCEPT_ATTRIBUTE')) return;
  5707. attrToggle(root.element, 'accept', !!action.value, action.value ? action.value.join(',') : '');
  5708. };
  5709. const toggleAllowMultiple = ({ root, action }) => {
  5710. attrToggle(root.element, 'multiple', action.value);
  5711. };
  5712. const toggleDirectoryFilter = ({ root, action }) => {
  5713. attrToggle(root.element, 'webkitdirectory', action.value);
  5714. };
  5715. const toggleDisabled = ({ root }) => {
  5716. const isDisabled = root.query('GET_DISABLED');
  5717. const doesAllowBrowse = root.query('GET_ALLOW_BROWSE');
  5718. const disableField = isDisabled || !doesAllowBrowse;
  5719. attrToggle(root.element, 'disabled', disableField);
  5720. };
  5721. const toggleRequired = ({ root, action }) => {
  5722. // want to remove required, always possible
  5723. if (!action.value) {
  5724. attrToggle(root.element, 'required', false);
  5725. }
  5726. // if want to make required, only possible when zero items
  5727. else if (root.query('GET_TOTAL_ITEMS') === 0) {
  5728. attrToggle(root.element, 'required', true);
  5729. }
  5730. };
  5731. const setCaptureMethod = ({ root, action }) => {
  5732. attrToggle(root.element, 'capture', !!action.value, action.value === true ? '' : action.value);
  5733. };
  5734. const updateRequiredStatus = ({ root }) => {
  5735. const { element } = root;
  5736. // always remove the required attribute when more than zero items
  5737. if (root.query('GET_TOTAL_ITEMS') > 0) {
  5738. attrToggle(element, 'required', false);
  5739. attrToggle(element, 'name', false);
  5740. } else {
  5741. // add name attribute
  5742. attrToggle(element, 'name', true, root.query('GET_NAME'));
  5743. // remove any validation messages
  5744. const shouldCheckValidity = root.query('GET_CHECK_VALIDITY');
  5745. if (shouldCheckValidity) {
  5746. element.setCustomValidity('');
  5747. }
  5748. // we only add required if the field has been deemed required
  5749. if (root.query('GET_REQUIRED')) {
  5750. attrToggle(element, 'required', true);
  5751. }
  5752. }
  5753. };
  5754. const updateFieldValidityStatus = ({ root }) => {
  5755. const shouldCheckValidity = root.query('GET_CHECK_VALIDITY');
  5756. if (!shouldCheckValidity) return;
  5757. root.element.setCustomValidity(root.query('GET_LABEL_INVALID_FIELD'));
  5758. };
  5759. const browser = createView({
  5760. tag: 'input',
  5761. name: 'browser',
  5762. ignoreRect: true,
  5763. ignoreRectUpdate: true,
  5764. attributes: {
  5765. type: 'file',
  5766. },
  5767. create: create$a,
  5768. destroy: ({ root }) => {
  5769. root.element.removeEventListener('change', root.ref.handleChange);
  5770. },
  5771. write: createRoute({
  5772. DID_LOAD_ITEM: updateRequiredStatus,
  5773. DID_REMOVE_ITEM: updateRequiredStatus,
  5774. DID_THROW_ITEM_INVALID: updateFieldValidityStatus,
  5775. DID_SET_DISABLED: toggleDisabled,
  5776. DID_SET_ALLOW_BROWSE: toggleDisabled,
  5777. DID_SET_ALLOW_DIRECTORIES_ONLY: toggleDirectoryFilter,
  5778. DID_SET_ALLOW_MULTIPLE: toggleAllowMultiple,
  5779. DID_SET_ACCEPTED_FILE_TYPES: setAcceptedFileTypes,
  5780. DID_SET_CAPTURE_METHOD: setCaptureMethod,
  5781. DID_SET_REQUIRED: toggleRequired,
  5782. }),
  5783. });
  5784. const Key = {
  5785. ENTER: 13,
  5786. SPACE: 32,
  5787. };
  5788. const create$b = ({ root, props }) => {
  5789. // create the label and link it to the file browser
  5790. const label = createElement$1('label');
  5791. attr(label, 'for', `filepond--browser-${props.id}`);
  5792. // use for labeling file input (aria-labelledby on file input)
  5793. attr(label, 'id', `filepond--drop-label-${props.id}`);
  5794. // hide the label for screenreaders, the input element will read the contents of the label when it's focussed. If we don't set aria-hidden the screenreader will also navigate the contents of the label separately from the input.
  5795. attr(label, 'aria-hidden', 'true');
  5796. // handle keys
  5797. root.ref.handleKeyDown = e => {
  5798. const isActivationKey = e.keyCode === Key.ENTER || e.keyCode === Key.SPACE;
  5799. if (!isActivationKey) return;
  5800. // stops from triggering the element a second time
  5801. e.preventDefault();
  5802. // click link (will then in turn activate file input)
  5803. root.ref.label.click();
  5804. };
  5805. root.ref.handleClick = e => {
  5806. const isLabelClick = e.target === label || label.contains(e.target);
  5807. // don't want to click twice
  5808. if (isLabelClick) return;
  5809. // click link (will then in turn activate file input)
  5810. root.ref.label.click();
  5811. };
  5812. // attach events
  5813. label.addEventListener('keydown', root.ref.handleKeyDown);
  5814. root.element.addEventListener('click', root.ref.handleClick);
  5815. // update
  5816. updateLabelValue(label, props.caption);
  5817. // add!
  5818. root.appendChild(label);
  5819. root.ref.label = label;
  5820. };
  5821. const updateLabelValue = (label, value) => {
  5822. label.innerHTML = value;
  5823. const clickable = label.querySelector('.filepond--label-action');
  5824. if (clickable) {
  5825. attr(clickable, 'tabindex', '0');
  5826. }
  5827. return value;
  5828. };
  5829. const dropLabel = createView({
  5830. name: 'drop-label',
  5831. ignoreRect: true,
  5832. create: create$b,
  5833. destroy: ({ root }) => {
  5834. root.ref.label.addEventListener('keydown', root.ref.handleKeyDown);
  5835. root.element.removeEventListener('click', root.ref.handleClick);
  5836. },
  5837. write: createRoute({
  5838. DID_SET_LABEL_IDLE: ({ root, action }) => {
  5839. updateLabelValue(root.ref.label, action.value);
  5840. },
  5841. }),
  5842. mixins: {
  5843. styles: ['opacity', 'translateX', 'translateY'],
  5844. animations: {
  5845. opacity: { type: 'tween', duration: 150 },
  5846. translateX: 'spring',
  5847. translateY: 'spring',
  5848. },
  5849. },
  5850. });
  5851. const blob = createView({
  5852. name: 'drip-blob',
  5853. ignoreRect: true,
  5854. mixins: {
  5855. styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity'],
  5856. animations: {
  5857. scaleX: 'spring',
  5858. scaleY: 'spring',
  5859. translateX: 'spring',
  5860. translateY: 'spring',
  5861. opacity: { type: 'tween', duration: 250 },
  5862. },
  5863. },
  5864. });
  5865. const addBlob = ({ root }) => {
  5866. const centerX = root.rect.element.width * 0.5;
  5867. const centerY = root.rect.element.height * 0.5;
  5868. root.ref.blob = root.appendChildView(
  5869. root.createChildView(blob, {
  5870. opacity: 0,
  5871. scaleX: 2.5,
  5872. scaleY: 2.5,
  5873. translateX: centerX,
  5874. translateY: centerY,
  5875. })
  5876. );
  5877. };
  5878. const moveBlob = ({ root, action }) => {
  5879. if (!root.ref.blob) {
  5880. addBlob({ root });
  5881. return;
  5882. }
  5883. root.ref.blob.translateX = action.position.scopeLeft;
  5884. root.ref.blob.translateY = action.position.scopeTop;
  5885. root.ref.blob.scaleX = 1;
  5886. root.ref.blob.scaleY = 1;
  5887. root.ref.blob.opacity = 1;
  5888. };
  5889. const hideBlob = ({ root }) => {
  5890. if (!root.ref.blob) {
  5891. return;
  5892. }
  5893. root.ref.blob.opacity = 0;
  5894. };
  5895. const explodeBlob = ({ root }) => {
  5896. if (!root.ref.blob) {
  5897. return;
  5898. }
  5899. root.ref.blob.scaleX = 2.5;
  5900. root.ref.blob.scaleY = 2.5;
  5901. root.ref.blob.opacity = 0;
  5902. };
  5903. const write$7 = ({ root, props, actions }) => {
  5904. route$4({ root, props, actions });
  5905. const { blob } = root.ref;
  5906. if (actions.length === 0 && blob && blob.opacity === 0) {
  5907. root.removeChildView(blob);
  5908. root.ref.blob = null;
  5909. }
  5910. };
  5911. const route$4 = createRoute({
  5912. DID_DRAG: moveBlob,
  5913. DID_DROP: explodeBlob,
  5914. DID_END_DRAG: hideBlob,
  5915. });
  5916. const drip = createView({
  5917. ignoreRect: true,
  5918. ignoreRectUpdate: true,
  5919. name: 'drip',
  5920. write: write$7,
  5921. });
  5922. const setInputFiles = (element, files) => {
  5923. try {
  5924. // Create a DataTransfer instance and add a newly created file
  5925. const dataTransfer = new DataTransfer();
  5926. files.forEach(file => {
  5927. if (file instanceof File) {
  5928. dataTransfer.items.add(file);
  5929. } else {
  5930. dataTransfer.items.add(
  5931. new File([file], file.name, {
  5932. type: file.type,
  5933. })
  5934. );
  5935. }
  5936. });
  5937. // Assign the DataTransfer files list to the file input
  5938. element.files = dataTransfer.files;
  5939. } catch (err) {
  5940. return false;
  5941. }
  5942. return true;
  5943. };
  5944. const create$c = ({ root }) => (root.ref.fields = {});
  5945. const getField = (root, id) => root.ref.fields[id];
  5946. const syncFieldPositionsWithItems = root => {
  5947. root.query('GET_ACTIVE_ITEMS').forEach(item => {
  5948. if (!root.ref.fields[item.id]) return;
  5949. root.element.appendChild(root.ref.fields[item.id]);
  5950. });
  5951. };
  5952. const didReorderItems = ({ root }) => syncFieldPositionsWithItems(root);
  5953. const didAddItem = ({ root, action }) => {
  5954. const fileItem = root.query('GET_ITEM', action.id);
  5955. const isLocalFile = fileItem.origin === FileOrigin.LOCAL;
  5956. const shouldUseFileInput = !isLocalFile && root.query('SHOULD_UPDATE_FILE_INPUT');
  5957. const dataContainer = createElement$1('input');
  5958. dataContainer.type = shouldUseFileInput ? 'file' : 'hidden';
  5959. dataContainer.name = root.query('GET_NAME');
  5960. dataContainer.disabled = root.query('GET_DISABLED');
  5961. root.ref.fields[action.id] = dataContainer;
  5962. syncFieldPositionsWithItems(root);
  5963. };
  5964. const didLoadItem$1 = ({ root, action }) => {
  5965. const field = getField(root, action.id);
  5966. if (!field) return;
  5967. // store server ref in hidden input
  5968. if (action.serverFileReference !== null) field.value = action.serverFileReference;
  5969. // store file item in file input
  5970. if (!root.query('SHOULD_UPDATE_FILE_INPUT')) return;
  5971. const fileItem = root.query('GET_ITEM', action.id);
  5972. setInputFiles(field, [fileItem.file]);
  5973. };
  5974. const didPrepareOutput = ({ root, action }) => {
  5975. // this timeout pushes the handler after 'load'
  5976. if (!root.query('SHOULD_UPDATE_FILE_INPUT')) return;
  5977. setTimeout(() => {
  5978. const field = getField(root, action.id);
  5979. if (!field) return;
  5980. setInputFiles(field, [action.file]);
  5981. }, 0);
  5982. };
  5983. const didSetDisabled = ({ root }) => {
  5984. root.element.disabled = root.query('GET_DISABLED');
  5985. };
  5986. const didRemoveItem = ({ root, action }) => {
  5987. const field = getField(root, action.id);
  5988. if (!field) return;
  5989. if (field.parentNode) field.parentNode.removeChild(field);
  5990. delete root.ref.fields[action.id];
  5991. };
  5992. // only runs for server files (so doesn't deal with file input)
  5993. const didDefineValue = ({ root, action }) => {
  5994. const field = getField(root, action.id);
  5995. if (!field) return;
  5996. if (action.value === null) {
  5997. // clear field value
  5998. field.removeAttribute('value');
  5999. } else {
  6000. // set field value
  6001. field.value = action.value;
  6002. }
  6003. syncFieldPositionsWithItems(root);
  6004. };
  6005. const write$8 = createRoute({
  6006. DID_SET_DISABLED: didSetDisabled,
  6007. DID_ADD_ITEM: didAddItem,
  6008. DID_LOAD_ITEM: didLoadItem$1,
  6009. DID_REMOVE_ITEM: didRemoveItem,
  6010. DID_DEFINE_VALUE: didDefineValue,
  6011. DID_PREPARE_OUTPUT: didPrepareOutput,
  6012. DID_REORDER_ITEMS: didReorderItems,
  6013. DID_SORT_ITEMS: didReorderItems,
  6014. });
  6015. const data = createView({
  6016. tag: 'fieldset',
  6017. name: 'data',
  6018. create: create$c,
  6019. write: write$8,
  6020. ignoreRect: true,
  6021. });
  6022. const getRootNode = element => ('getRootNode' in element ? element.getRootNode() : document);
  6023. const images = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'tiff'];
  6024. const text$1 = ['css', 'csv', 'html', 'txt'];
  6025. const map = {
  6026. zip: 'zip|compressed',
  6027. epub: 'application/epub+zip',
  6028. };
  6029. const guesstimateMimeType = (extension = '') => {
  6030. extension = extension.toLowerCase();
  6031. if (images.includes(extension)) {
  6032. return (
  6033. 'image/' + (extension === 'jpg' ? 'jpeg' : extension === 'svg' ? 'svg+xml' : extension)
  6034. );
  6035. }
  6036. if (text$1.includes(extension)) {
  6037. return 'text/' + extension;
  6038. }
  6039. return map[extension] || '';
  6040. };
  6041. const requestDataTransferItems = dataTransfer =>
  6042. new Promise((resolve, reject) => {
  6043. // 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)
  6044. const links = getLinks(dataTransfer);
  6045. if (links.length && !hasFiles(dataTransfer)) {
  6046. return resolve(links);
  6047. }
  6048. // try to get files from the transfer
  6049. getFiles(dataTransfer).then(resolve);
  6050. });
  6051. /**
  6052. * Test if datatransfer has files
  6053. */
  6054. const hasFiles = dataTransfer => {
  6055. if (dataTransfer.files) return dataTransfer.files.length > 0;
  6056. return false;
  6057. };
  6058. /**
  6059. * Extracts files from a DataTransfer object
  6060. */
  6061. const getFiles = dataTransfer =>
  6062. new Promise((resolve, reject) => {
  6063. // get the transfer items as promises
  6064. const promisedFiles = (dataTransfer.items ? Array.from(dataTransfer.items) : [])
  6065. // only keep file system items (files and directories)
  6066. .filter(item => isFileSystemItem(item))
  6067. // map each item to promise
  6068. .map(item => getFilesFromItem(item));
  6069. // if is empty, see if we can extract some info from the files property as a fallback
  6070. if (!promisedFiles.length) {
  6071. // TODO: test for directories (should not be allowed)
  6072. // Use FileReader, problem is that the files property gets lost in the process
  6073. resolve(dataTransfer.files ? Array.from(dataTransfer.files) : []);
  6074. return;
  6075. }
  6076. // done!
  6077. Promise.all(promisedFiles)
  6078. .then(returnedFileGroups => {
  6079. // flatten groups
  6080. const files = [];
  6081. returnedFileGroups.forEach(group => {
  6082. files.push.apply(files, group);
  6083. });
  6084. // done (filter out empty files)!
  6085. resolve(
  6086. files
  6087. .filter(file => file)
  6088. .map(file => {
  6089. if (!file._relativePath) file._relativePath = file.webkitRelativePath;
  6090. return file;
  6091. })
  6092. );
  6093. })
  6094. .catch(console.error);
  6095. });
  6096. const isFileSystemItem = item => {
  6097. if (isEntry(item)) {
  6098. const entry = getAsEntry(item);
  6099. if (entry) {
  6100. return entry.isFile || entry.isDirectory;
  6101. }
  6102. }
  6103. return item.kind === 'file';
  6104. };
  6105. const getFilesFromItem = item =>
  6106. new Promise((resolve, reject) => {
  6107. if (isDirectoryEntry(item)) {
  6108. getFilesInDirectory(getAsEntry(item))
  6109. .then(resolve)
  6110. .catch(reject);
  6111. return;
  6112. }
  6113. resolve([item.getAsFile()]);
  6114. });
  6115. const getFilesInDirectory = entry =>
  6116. new Promise((resolve, reject) => {
  6117. const files = [];
  6118. // the total entries to read
  6119. let dirCounter = 0;
  6120. let fileCounter = 0;
  6121. const resolveIfDone = () => {
  6122. if (fileCounter === 0 && dirCounter === 0) {
  6123. resolve(files);
  6124. }
  6125. };
  6126. // the recursive function
  6127. const readEntries = dirEntry => {
  6128. dirCounter++;
  6129. const directoryReader = dirEntry.createReader();
  6130. // directories are returned in batches, we need to process all batches before we're done
  6131. const readBatch = () => {
  6132. directoryReader.readEntries(entries => {
  6133. if (entries.length === 0) {
  6134. dirCounter--;
  6135. resolveIfDone();
  6136. return;
  6137. }
  6138. entries.forEach(entry => {
  6139. // recursively read more directories
  6140. if (entry.isDirectory) {
  6141. readEntries(entry);
  6142. } else {
  6143. // read as file
  6144. fileCounter++;
  6145. entry.file(file => {
  6146. const correctedFile = correctMissingFileType(file);
  6147. if (entry.fullPath) correctedFile._relativePath = entry.fullPath;
  6148. files.push(correctedFile);
  6149. fileCounter--;
  6150. resolveIfDone();
  6151. });
  6152. }
  6153. });
  6154. // try to get next batch of files
  6155. readBatch();
  6156. }, reject);
  6157. };
  6158. // read first batch of files
  6159. readBatch();
  6160. };
  6161. // go!
  6162. readEntries(entry);
  6163. });
  6164. const correctMissingFileType = file => {
  6165. if (file.type.length) return file;
  6166. const date = file.lastModifiedDate;
  6167. const name = file.name;
  6168. const type = guesstimateMimeType(getExtensionFromFilename(file.name));
  6169. if (!type.length) return file;
  6170. file = file.slice(0, file.size, type);
  6171. file.name = name;
  6172. file.lastModifiedDate = date;
  6173. return file;
  6174. };
  6175. const isDirectoryEntry = item => isEntry(item) && (getAsEntry(item) || {}).isDirectory;
  6176. const isEntry = item => 'webkitGetAsEntry' in item;
  6177. const getAsEntry = item => item.webkitGetAsEntry();
  6178. /**
  6179. * Extracts links from a DataTransfer object
  6180. */
  6181. const getLinks = dataTransfer => {
  6182. let links = [];
  6183. try {
  6184. // look in meta data property
  6185. links = getLinksFromTransferMetaData(dataTransfer);
  6186. if (links.length) {
  6187. return links;
  6188. }
  6189. links = getLinksFromTransferURLData(dataTransfer);
  6190. } catch (e) {
  6191. // nope nope nope (probably IE trouble)
  6192. }
  6193. return links;
  6194. };
  6195. const getLinksFromTransferURLData = dataTransfer => {
  6196. let data = dataTransfer.getData('url');
  6197. if (typeof data === 'string' && data.length) {
  6198. return [data];
  6199. }
  6200. return [];
  6201. };
  6202. const getLinksFromTransferMetaData = dataTransfer => {
  6203. let data = dataTransfer.getData('text/html');
  6204. if (typeof data === 'string' && data.length) {
  6205. const matches = data.match(/src\s*=\s*"(.+?)"/);
  6206. if (matches) {
  6207. return [matches[1]];
  6208. }
  6209. }
  6210. return [];
  6211. };
  6212. const dragNDropObservers = [];
  6213. const eventPosition = e => ({
  6214. pageLeft: e.pageX,
  6215. pageTop: e.pageY,
  6216. scopeLeft: e.offsetX || e.layerX,
  6217. scopeTop: e.offsetY || e.layerY,
  6218. });
  6219. const createDragNDropClient = (element, scopeToObserve, filterElement) => {
  6220. const observer = getDragNDropObserver(scopeToObserve);
  6221. const client = {
  6222. element,
  6223. filterElement,
  6224. state: null,
  6225. ondrop: () => {},
  6226. onenter: () => {},
  6227. ondrag: () => {},
  6228. onexit: () => {},
  6229. onload: () => {},
  6230. allowdrop: () => {},
  6231. };
  6232. client.destroy = observer.addListener(client);
  6233. return client;
  6234. };
  6235. const getDragNDropObserver = element => {
  6236. // see if already exists, if so, return
  6237. const observer = dragNDropObservers.find(item => item.element === element);
  6238. if (observer) {
  6239. return observer;
  6240. }
  6241. // create new observer, does not yet exist for this element
  6242. const newObserver = createDragNDropObserver(element);
  6243. dragNDropObservers.push(newObserver);
  6244. return newObserver;
  6245. };
  6246. const createDragNDropObserver = element => {
  6247. const clients = [];
  6248. const routes = {
  6249. dragenter,
  6250. dragover,
  6251. dragleave,
  6252. drop,
  6253. };
  6254. const handlers = {};
  6255. forin(routes, (event, createHandler) => {
  6256. handlers[event] = createHandler(element, clients);
  6257. element.addEventListener(event, handlers[event], false);
  6258. });
  6259. const observer = {
  6260. element,
  6261. addListener: client => {
  6262. // add as client
  6263. clients.push(client);
  6264. // return removeListener function
  6265. return () => {
  6266. // remove client
  6267. clients.splice(clients.indexOf(client), 1);
  6268. // if no more clients, clean up observer
  6269. if (clients.length === 0) {
  6270. dragNDropObservers.splice(dragNDropObservers.indexOf(observer), 1);
  6271. forin(routes, event => {
  6272. element.removeEventListener(event, handlers[event], false);
  6273. });
  6274. }
  6275. };
  6276. },
  6277. };
  6278. return observer;
  6279. };
  6280. const elementFromPoint = (root, point) => {
  6281. if (!('elementFromPoint' in root)) {
  6282. root = document;
  6283. }
  6284. return root.elementFromPoint(point.x, point.y);
  6285. };
  6286. const isEventTarget = (e, target) => {
  6287. // get root
  6288. const root = getRootNode(target);
  6289. // get element at position
  6290. // if root is not actual shadow DOM and does not have elementFromPoint method, use the one on document
  6291. const elementAtPosition = elementFromPoint(root, {
  6292. x: e.pageX - window.pageXOffset,
  6293. y: e.pageY - window.pageYOffset,
  6294. });
  6295. // test if target is the element or if one of its children is
  6296. return elementAtPosition === target || target.contains(elementAtPosition);
  6297. };
  6298. let initialTarget = null;
  6299. const setDropEffect = (dataTransfer, effect) => {
  6300. // is in try catch as IE11 will throw error if not
  6301. try {
  6302. dataTransfer.dropEffect = effect;
  6303. } catch (e) {}
  6304. };
  6305. const dragenter = (root, clients) => e => {
  6306. e.preventDefault();
  6307. initialTarget = e.target;
  6308. clients.forEach(client => {
  6309. const { element, onenter } = client;
  6310. if (isEventTarget(e, element)) {
  6311. client.state = 'enter';
  6312. // fire enter event
  6313. onenter(eventPosition(e));
  6314. }
  6315. });
  6316. };
  6317. const dragover = (root, clients) => e => {
  6318. e.preventDefault();
  6319. const dataTransfer = e.dataTransfer;
  6320. requestDataTransferItems(dataTransfer).then(items => {
  6321. let overDropTarget = false;
  6322. clients.some(client => {
  6323. const { filterElement, element, onenter, onexit, ondrag, allowdrop } = client;
  6324. // by default we can drop
  6325. setDropEffect(dataTransfer, 'copy');
  6326. // allow transfer of these items
  6327. const allowsTransfer = allowdrop(items);
  6328. // only used when can be dropped on page
  6329. if (!allowsTransfer) {
  6330. setDropEffect(dataTransfer, 'none');
  6331. return;
  6332. }
  6333. // targetting this client
  6334. if (isEventTarget(e, element)) {
  6335. overDropTarget = true;
  6336. // had no previous state, means we are entering this client
  6337. if (client.state === null) {
  6338. client.state = 'enter';
  6339. onenter(eventPosition(e));
  6340. return;
  6341. }
  6342. // now over element (no matter if it allows the drop or not)
  6343. client.state = 'over';
  6344. // needs to allow transfer
  6345. if (filterElement && !allowsTransfer) {
  6346. setDropEffect(dataTransfer, 'none');
  6347. return;
  6348. }
  6349. // dragging
  6350. ondrag(eventPosition(e));
  6351. } else {
  6352. // should be over an element to drop
  6353. if (filterElement && !overDropTarget) {
  6354. setDropEffect(dataTransfer, 'none');
  6355. }
  6356. // might have just left this client?
  6357. if (client.state) {
  6358. client.state = null;
  6359. onexit(eventPosition(e));
  6360. }
  6361. }
  6362. });
  6363. });
  6364. };
  6365. const drop = (root, clients) => e => {
  6366. e.preventDefault();
  6367. const dataTransfer = e.dataTransfer;
  6368. requestDataTransferItems(dataTransfer).then(items => {
  6369. clients.forEach(client => {
  6370. const { filterElement, element, ondrop, onexit, allowdrop } = client;
  6371. client.state = null;
  6372. // if we're filtering on element we need to be over the element to drop
  6373. if (filterElement && !isEventTarget(e, element)) return;
  6374. // no transfer for this client
  6375. if (!allowdrop(items)) return onexit(eventPosition(e));
  6376. // we can drop these items on this client
  6377. ondrop(eventPosition(e), items);
  6378. });
  6379. });
  6380. };
  6381. const dragleave = (root, clients) => e => {
  6382. if (initialTarget !== e.target) {
  6383. return;
  6384. }
  6385. clients.forEach(client => {
  6386. const { onexit } = client;
  6387. client.state = null;
  6388. onexit(eventPosition(e));
  6389. });
  6390. };
  6391. const createHopper = (scope, validateItems, options) => {
  6392. // is now hopper scope
  6393. scope.classList.add('filepond--hopper');
  6394. // shortcuts
  6395. const { catchesDropsOnPage, requiresDropOnElement, filterItems = items => items } = options;
  6396. // create a dnd client
  6397. const client = createDragNDropClient(
  6398. scope,
  6399. catchesDropsOnPage ? document.documentElement : scope,
  6400. requiresDropOnElement
  6401. );
  6402. // current client state
  6403. let lastState = '';
  6404. let currentState = '';
  6405. // determines if a file may be dropped
  6406. client.allowdrop = items => {
  6407. // TODO: if we can, throw error to indicate the items cannot by dropped
  6408. return validateItems(filterItems(items));
  6409. };
  6410. client.ondrop = (position, items) => {
  6411. const filteredItems = filterItems(items);
  6412. if (!validateItems(filteredItems)) {
  6413. api.ondragend(position);
  6414. return;
  6415. }
  6416. currentState = 'drag-drop';
  6417. api.onload(filteredItems, position);
  6418. };
  6419. client.ondrag = position => {
  6420. api.ondrag(position);
  6421. };
  6422. client.onenter = position => {
  6423. currentState = 'drag-over';
  6424. api.ondragstart(position);
  6425. };
  6426. client.onexit = position => {
  6427. currentState = 'drag-exit';
  6428. api.ondragend(position);
  6429. };
  6430. const api = {
  6431. updateHopperState: () => {
  6432. if (lastState !== currentState) {
  6433. scope.dataset.hopperState = currentState;
  6434. lastState = currentState;
  6435. }
  6436. },
  6437. onload: () => {},
  6438. ondragstart: () => {},
  6439. ondrag: () => {},
  6440. ondragend: () => {},
  6441. destroy: () => {
  6442. // destroy client
  6443. client.destroy();
  6444. },
  6445. };
  6446. return api;
  6447. };
  6448. let listening = false;
  6449. const listeners$1 = [];
  6450. const handlePaste = e => {
  6451. // if is pasting in input or textarea and the target is outside of a filepond scope, ignore
  6452. const activeEl = document.activeElement;
  6453. if (activeEl && /textarea|input/i.test(activeEl.nodeName)) {
  6454. // test textarea or input is contained in filepond root
  6455. let inScope = false;
  6456. let element = activeEl;
  6457. while (element !== document.body) {
  6458. if (element.classList.contains('filepond--root')) {
  6459. inScope = true;
  6460. break;
  6461. }
  6462. element = element.parentNode;
  6463. }
  6464. if (!inScope) return;
  6465. }
  6466. requestDataTransferItems(e.clipboardData).then(files => {
  6467. // no files received
  6468. if (!files.length) {
  6469. return;
  6470. }
  6471. // notify listeners of received files
  6472. listeners$1.forEach(listener => listener(files));
  6473. });
  6474. };
  6475. const listen = cb => {
  6476. // can't add twice
  6477. if (listeners$1.includes(cb)) {
  6478. return;
  6479. }
  6480. // add initial listener
  6481. listeners$1.push(cb);
  6482. // setup paste listener for entire page
  6483. if (listening) {
  6484. return;
  6485. }
  6486. listening = true;
  6487. document.addEventListener('paste', handlePaste);
  6488. };
  6489. const unlisten = listener => {
  6490. arrayRemove(listeners$1, listeners$1.indexOf(listener));
  6491. // clean up
  6492. if (listeners$1.length === 0) {
  6493. document.removeEventListener('paste', handlePaste);
  6494. listening = false;
  6495. }
  6496. };
  6497. const createPaster = () => {
  6498. const cb = files => {
  6499. api.onload(files);
  6500. };
  6501. const api = {
  6502. destroy: () => {
  6503. unlisten(cb);
  6504. },
  6505. onload: () => {},
  6506. };
  6507. listen(cb);
  6508. return api;
  6509. };
  6510. /**
  6511. * Creates the file view
  6512. */
  6513. const create$d = ({ root, props }) => {
  6514. root.element.id = `filepond--assistant-${props.id}`;
  6515. attr(root.element, 'role', 'status');
  6516. attr(root.element, 'aria-live', 'polite');
  6517. attr(root.element, 'aria-relevant', 'additions');
  6518. };
  6519. let addFilesNotificationTimeout = null;
  6520. let notificationClearTimeout = null;
  6521. const filenames = [];
  6522. const assist = (root, message) => {
  6523. root.element.textContent = message;
  6524. };
  6525. const clear$1 = root => {
  6526. root.element.textContent = '';
  6527. };
  6528. const listModified = (root, filename, label) => {
  6529. const total = root.query('GET_TOTAL_ITEMS');
  6530. assist(
  6531. root,
  6532. `${label} ${filename}, ${total} ${
  6533. total === 1
  6534. ? root.query('GET_LABEL_FILE_COUNT_SINGULAR')
  6535. : root.query('GET_LABEL_FILE_COUNT_PLURAL')
  6536. }`
  6537. );
  6538. // clear group after set amount of time so the status is not read twice
  6539. clearTimeout(notificationClearTimeout);
  6540. notificationClearTimeout = setTimeout(() => {
  6541. clear$1(root);
  6542. }, 1500);
  6543. };
  6544. const isUsingFilePond = root => root.element.parentNode.contains(document.activeElement);
  6545. const itemAdded = ({ root, action }) => {
  6546. if (!isUsingFilePond(root)) {
  6547. return;
  6548. }
  6549. root.element.textContent = '';
  6550. const item = root.query('GET_ITEM', action.id);
  6551. filenames.push(item.filename);
  6552. clearTimeout(addFilesNotificationTimeout);
  6553. addFilesNotificationTimeout = setTimeout(() => {
  6554. listModified(root, filenames.join(', '), root.query('GET_LABEL_FILE_ADDED'));
  6555. filenames.length = 0;
  6556. }, 750);
  6557. };
  6558. const itemRemoved = ({ root, action }) => {
  6559. if (!isUsingFilePond(root)) {
  6560. return;
  6561. }
  6562. const item = action.item;
  6563. listModified(root, item.filename, root.query('GET_LABEL_FILE_REMOVED'));
  6564. };
  6565. const itemProcessed = ({ root, action }) => {
  6566. // will also notify the user when FilePond is not being used, as the user might be occupied with other activities while uploading a file
  6567. const item = root.query('GET_ITEM', action.id);
  6568. const filename = item.filename;
  6569. const label = root.query('GET_LABEL_FILE_PROCESSING_COMPLETE');
  6570. assist(root, `${filename} ${label}`);
  6571. };
  6572. const itemProcessedUndo = ({ root, action }) => {
  6573. const item = root.query('GET_ITEM', action.id);
  6574. const filename = item.filename;
  6575. const label = root.query('GET_LABEL_FILE_PROCESSING_ABORTED');
  6576. assist(root, `${filename} ${label}`);
  6577. };
  6578. const itemError = ({ root, action }) => {
  6579. const item = root.query('GET_ITEM', action.id);
  6580. const filename = item.filename;
  6581. // will also notify the user when FilePond is not being used, as the user might be occupied with other activities while uploading a file
  6582. assist(root, `${action.status.main} ${filename} ${action.status.sub}`);
  6583. };
  6584. const assistant = createView({
  6585. create: create$d,
  6586. ignoreRect: true,
  6587. ignoreRectUpdate: true,
  6588. write: createRoute({
  6589. DID_LOAD_ITEM: itemAdded,
  6590. DID_REMOVE_ITEM: itemRemoved,
  6591. DID_COMPLETE_ITEM_PROCESSING: itemProcessed,
  6592. DID_ABORT_ITEM_PROCESSING: itemProcessedUndo,
  6593. DID_REVERT_ITEM_PROCESSING: itemProcessedUndo,
  6594. DID_THROW_ITEM_REMOVE_ERROR: itemError,
  6595. DID_THROW_ITEM_LOAD_ERROR: itemError,
  6596. DID_THROW_ITEM_INVALID: itemError,
  6597. DID_THROW_ITEM_PROCESSING_ERROR: itemError,
  6598. }),
  6599. tag: 'span',
  6600. name: 'assistant',
  6601. });
  6602. const toCamels = (string, separator = '-') =>
  6603. string.replace(new RegExp(`${separator}.`, 'g'), sub => sub.charAt(1).toUpperCase());
  6604. const debounce = (func, interval = 16, immidiateOnly = true) => {
  6605. let last = Date.now();
  6606. let timeout = null;
  6607. return (...args) => {
  6608. clearTimeout(timeout);
  6609. const dist = Date.now() - last;
  6610. const fn = () => {
  6611. last = Date.now();
  6612. func(...args);
  6613. };
  6614. if (dist < interval) {
  6615. // we need to delay by the difference between interval and dist
  6616. // for example: if distance is 10 ms and interval is 16 ms,
  6617. // we need to wait an additional 6ms before calling the function)
  6618. if (!immidiateOnly) {
  6619. timeout = setTimeout(fn, interval - dist);
  6620. }
  6621. } else {
  6622. // go!
  6623. fn();
  6624. }
  6625. };
  6626. };
  6627. const MAX_FILES_LIMIT = 1000000;
  6628. const prevent = e => e.preventDefault();
  6629. const create$e = ({ root, props }) => {
  6630. // Add id
  6631. const id = root.query('GET_ID');
  6632. if (id) {
  6633. root.element.id = id;
  6634. }
  6635. // Add className
  6636. const className = root.query('GET_CLASS_NAME');
  6637. if (className) {
  6638. className
  6639. .split(' ')
  6640. .filter(name => name.length)
  6641. .forEach(name => {
  6642. root.element.classList.add(name);
  6643. });
  6644. }
  6645. // Field label
  6646. root.ref.label = root.appendChildView(
  6647. root.createChildView(dropLabel, {
  6648. ...props,
  6649. translateY: null,
  6650. caption: root.query('GET_LABEL_IDLE'),
  6651. })
  6652. );
  6653. // List of items
  6654. root.ref.list = root.appendChildView(root.createChildView(listScroller, { translateY: null }));
  6655. // Background panel
  6656. root.ref.panel = root.appendChildView(root.createChildView(panel, { name: 'panel-root' }));
  6657. // Assistant notifies assistive tech when content changes
  6658. root.ref.assistant = root.appendChildView(root.createChildView(assistant, { ...props }));
  6659. // Data
  6660. root.ref.data = root.appendChildView(root.createChildView(data, { ...props }));
  6661. // Measure (tests if fixed height was set)
  6662. // DOCTYPE needs to be set for this to work
  6663. root.ref.measure = createElement$1('div');
  6664. root.ref.measure.style.height = '100%';
  6665. root.element.appendChild(root.ref.measure);
  6666. // information on the root height or fixed height status
  6667. root.ref.bounds = null;
  6668. // apply initial style properties
  6669. root.query('GET_STYLES')
  6670. .filter(style => !isEmpty(style.value))
  6671. .map(({ name, value }) => {
  6672. root.element.dataset[name] = value;
  6673. });
  6674. // determine if width changed
  6675. root.ref.widthPrevious = null;
  6676. root.ref.widthUpdated = debounce(() => {
  6677. root.ref.updateHistory = [];
  6678. root.dispatch('DID_RESIZE_ROOT');
  6679. }, 250);
  6680. // history of updates
  6681. root.ref.previousAspectRatio = null;
  6682. root.ref.updateHistory = [];
  6683. // prevent scrolling and zooming on iOS (only if supports pointer events, for then we can enable reorder)
  6684. const canHover = window.matchMedia('(pointer: fine) and (hover: hover)').matches;
  6685. const hasPointerEvents = 'PointerEvent' in window;
  6686. if (root.query('GET_ALLOW_REORDER') && hasPointerEvents && !canHover) {
  6687. root.element.addEventListener('touchmove', prevent, { passive: false });
  6688. root.element.addEventListener('gesturestart', prevent);
  6689. }
  6690. // add credits
  6691. const credits = root.query('GET_CREDITS');
  6692. const hasCredits = credits.length === 2;
  6693. if (hasCredits) {
  6694. const frag = document.createElement('a');
  6695. frag.className = 'filepond--credits';
  6696. frag.setAttribute('aria-hidden', 'true');
  6697. frag.href = credits[0];
  6698. frag.tabindex = -1;
  6699. frag.target = '_blank';
  6700. frag.rel = 'noopener noreferrer';
  6701. frag.textContent = credits[1];
  6702. root.element.appendChild(frag);
  6703. root.ref.credits = frag;
  6704. }
  6705. };
  6706. const write$9 = ({ root, props, actions }) => {
  6707. // route actions
  6708. route$5({ root, props, actions });
  6709. // apply style properties
  6710. actions
  6711. .filter(action => /^DID_SET_STYLE_/.test(action.type))
  6712. .filter(action => !isEmpty(action.data.value))
  6713. .map(({ type, data }) => {
  6714. const name = toCamels(type.substring(8).toLowerCase(), '_');
  6715. root.element.dataset[name] = data.value;
  6716. root.invalidateLayout();
  6717. });
  6718. if (root.rect.element.hidden) return;
  6719. if (root.rect.element.width !== root.ref.widthPrevious) {
  6720. root.ref.widthPrevious = root.rect.element.width;
  6721. root.ref.widthUpdated();
  6722. }
  6723. // get box bounds, we do this only once
  6724. let bounds = root.ref.bounds;
  6725. if (!bounds) {
  6726. bounds = root.ref.bounds = calculateRootBoundingBoxHeight(root);
  6727. // destroy measure element
  6728. root.element.removeChild(root.ref.measure);
  6729. root.ref.measure = null;
  6730. }
  6731. // get quick references to various high level parts of the upload tool
  6732. const { hopper, label, list, panel } = root.ref;
  6733. // sets correct state to hopper scope
  6734. if (hopper) {
  6735. hopper.updateHopperState();
  6736. }
  6737. // bool to indicate if we're full or not
  6738. const aspectRatio = root.query('GET_PANEL_ASPECT_RATIO');
  6739. const isMultiItem = root.query('GET_ALLOW_MULTIPLE');
  6740. const totalItems = root.query('GET_TOTAL_ITEMS');
  6741. const maxItems = isMultiItem ? root.query('GET_MAX_FILES') || MAX_FILES_LIMIT : 1;
  6742. const atMaxCapacity = totalItems === maxItems;
  6743. // action used to add item
  6744. const addAction = actions.find(action => action.type === 'DID_ADD_ITEM');
  6745. // if reached max capacity and we've just reached it
  6746. if (atMaxCapacity && addAction) {
  6747. // get interaction type
  6748. const interactionMethod = addAction.data.interactionMethod;
  6749. // hide label
  6750. label.opacity = 0;
  6751. if (isMultiItem) {
  6752. label.translateY = -40;
  6753. } else {
  6754. if (interactionMethod === InteractionMethod.API) {
  6755. label.translateX = 40;
  6756. } else if (interactionMethod === InteractionMethod.BROWSE) {
  6757. label.translateY = 40;
  6758. } else {
  6759. label.translateY = 30;
  6760. }
  6761. }
  6762. } else if (!atMaxCapacity) {
  6763. label.opacity = 1;
  6764. label.translateX = 0;
  6765. label.translateY = 0;
  6766. }
  6767. const listItemMargin = calculateListItemMargin(root);
  6768. const listHeight = calculateListHeight(root);
  6769. const labelHeight = label.rect.element.height;
  6770. const currentLabelHeight = !isMultiItem || atMaxCapacity ? 0 : labelHeight;
  6771. const listMarginTop = atMaxCapacity ? list.rect.element.marginTop : 0;
  6772. const listMarginBottom = totalItems === 0 ? 0 : list.rect.element.marginBottom;
  6773. const visualHeight = currentLabelHeight + listMarginTop + listHeight.visual + listMarginBottom;
  6774. const boundsHeight = currentLabelHeight + listMarginTop + listHeight.bounds + listMarginBottom;
  6775. // link list to label bottom position
  6776. list.translateY =
  6777. Math.max(0, currentLabelHeight - list.rect.element.marginTop) - listItemMargin.top;
  6778. if (aspectRatio) {
  6779. // fixed aspect ratio
  6780. // calculate height based on width
  6781. const width = root.rect.element.width;
  6782. const height = width * aspectRatio;
  6783. // clear history if aspect ratio has changed
  6784. if (aspectRatio !== root.ref.previousAspectRatio) {
  6785. root.ref.previousAspectRatio = aspectRatio;
  6786. root.ref.updateHistory = [];
  6787. }
  6788. // remember this width
  6789. const history = root.ref.updateHistory;
  6790. history.push(width);
  6791. const MAX_BOUNCES = 2;
  6792. if (history.length > MAX_BOUNCES * 2) {
  6793. const l = history.length;
  6794. const bottom = l - 10;
  6795. let bounces = 0;
  6796. for (let i = l; i >= bottom; i--) {
  6797. if (history[i] === history[i - 2]) {
  6798. bounces++;
  6799. }
  6800. if (bounces >= MAX_BOUNCES) {
  6801. // dont adjust height
  6802. return;
  6803. }
  6804. }
  6805. }
  6806. // fix height of panel so it adheres to aspect ratio
  6807. panel.scalable = false;
  6808. panel.height = height;
  6809. // available height for list
  6810. const listAvailableHeight =
  6811. // the height of the panel minus the label height
  6812. height -
  6813. currentLabelHeight -
  6814. // the room we leave open between the end of the list and the panel bottom
  6815. (listMarginBottom - listItemMargin.bottom) -
  6816. // if we're full we need to leave some room between the top of the panel and the list
  6817. (atMaxCapacity ? listMarginTop : 0);
  6818. if (listHeight.visual > listAvailableHeight) {
  6819. list.overflow = listAvailableHeight;
  6820. } else {
  6821. list.overflow = null;
  6822. }
  6823. // set container bounds (so pushes siblings downwards)
  6824. root.height = height;
  6825. } else if (bounds.fixedHeight) {
  6826. // fixed height
  6827. // fix height of panel
  6828. panel.scalable = false;
  6829. // available height for list
  6830. const listAvailableHeight =
  6831. // the height of the panel minus the label height
  6832. bounds.fixedHeight -
  6833. currentLabelHeight -
  6834. // the room we leave open between the end of the list and the panel bottom
  6835. (listMarginBottom - listItemMargin.bottom) -
  6836. // if we're full we need to leave some room between the top of the panel and the list
  6837. (atMaxCapacity ? listMarginTop : 0);
  6838. // set list height
  6839. if (listHeight.visual > listAvailableHeight) {
  6840. list.overflow = listAvailableHeight;
  6841. } else {
  6842. list.overflow = null;
  6843. }
  6844. // no need to set container bounds as these are handles by CSS fixed height
  6845. } else if (bounds.cappedHeight) {
  6846. // max-height
  6847. // not a fixed height panel
  6848. const isCappedHeight = visualHeight >= bounds.cappedHeight;
  6849. const panelHeight = Math.min(bounds.cappedHeight, visualHeight);
  6850. panel.scalable = true;
  6851. panel.height = isCappedHeight
  6852. ? panelHeight
  6853. : panelHeight - listItemMargin.top - listItemMargin.bottom;
  6854. // available height for list
  6855. const listAvailableHeight =
  6856. // the height of the panel minus the label height
  6857. panelHeight -
  6858. currentLabelHeight -
  6859. // the room we leave open between the end of the list and the panel bottom
  6860. (listMarginBottom - listItemMargin.bottom) -
  6861. // if we're full we need to leave some room between the top of the panel and the list
  6862. (atMaxCapacity ? listMarginTop : 0);
  6863. // set list height (if is overflowing)
  6864. if (visualHeight > bounds.cappedHeight && listHeight.visual > listAvailableHeight) {
  6865. list.overflow = listAvailableHeight;
  6866. } else {
  6867. list.overflow = null;
  6868. }
  6869. // set container bounds (so pushes siblings downwards)
  6870. root.height = Math.min(
  6871. bounds.cappedHeight,
  6872. boundsHeight - listItemMargin.top - listItemMargin.bottom
  6873. );
  6874. } else {
  6875. // flexible height
  6876. // not a fixed height panel
  6877. const itemMargin = totalItems > 0 ? listItemMargin.top + listItemMargin.bottom : 0;
  6878. panel.scalable = true;
  6879. panel.height = Math.max(labelHeight, visualHeight - itemMargin);
  6880. // set container bounds (so pushes siblings downwards)
  6881. root.height = Math.max(labelHeight, boundsHeight - itemMargin);
  6882. }
  6883. // move credits to bottom
  6884. if (root.ref.credits && panel.heightCurrent)
  6885. root.ref.credits.style.transform = `translateY(${panel.heightCurrent}px)`;
  6886. };
  6887. const calculateListItemMargin = root => {
  6888. const item = root.ref.list.childViews[0].childViews[0];
  6889. return item
  6890. ? {
  6891. top: item.rect.element.marginTop,
  6892. bottom: item.rect.element.marginBottom,
  6893. }
  6894. : {
  6895. top: 0,
  6896. bottom: 0,
  6897. };
  6898. };
  6899. const calculateListHeight = root => {
  6900. let visual = 0;
  6901. let bounds = 0;
  6902. // get file list reference
  6903. const scrollList = root.ref.list;
  6904. const itemList = scrollList.childViews[0];
  6905. const visibleChildren = itemList.childViews.filter(child => child.rect.element.height);
  6906. const children = root
  6907. .query('GET_ACTIVE_ITEMS')
  6908. .map(item => visibleChildren.find(child => child.id === item.id))
  6909. .filter(item => item);
  6910. // no children, done!
  6911. if (children.length === 0) return { visual, bounds };
  6912. const horizontalSpace = itemList.rect.element.width;
  6913. const dragIndex = getItemIndexByPosition(itemList, children, scrollList.dragCoordinates);
  6914. const childRect = children[0].rect.element;
  6915. const itemVerticalMargin = childRect.marginTop + childRect.marginBottom;
  6916. const itemHorizontalMargin = childRect.marginLeft + childRect.marginRight;
  6917. const itemWidth = childRect.width + itemHorizontalMargin;
  6918. const itemHeight = childRect.height + itemVerticalMargin;
  6919. const newItem = typeof dragIndex !== 'undefined' && dragIndex >= 0 ? 1 : 0;
  6920. const removedItem = children.find(child => child.markedForRemoval && child.opacity < 0.45)
  6921. ? -1
  6922. : 0;
  6923. const verticalItemCount = children.length + newItem + removedItem;
  6924. const itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth);
  6925. // stack
  6926. if (itemsPerRow === 1) {
  6927. children.forEach(item => {
  6928. const height = item.rect.element.height + itemVerticalMargin;
  6929. bounds += height;
  6930. visual += height * item.opacity;
  6931. });
  6932. }
  6933. // grid
  6934. else {
  6935. bounds = Math.ceil(verticalItemCount / itemsPerRow) * itemHeight;
  6936. visual = bounds;
  6937. }
  6938. return { visual, bounds };
  6939. };
  6940. const calculateRootBoundingBoxHeight = root => {
  6941. const height = root.ref.measureHeight || null;
  6942. const cappedHeight = parseInt(root.style.maxHeight, 10) || null;
  6943. const fixedHeight = height === 0 ? null : height;
  6944. return {
  6945. cappedHeight,
  6946. fixedHeight,
  6947. };
  6948. };
  6949. const exceedsMaxFiles = (root, items) => {
  6950. const allowReplace = root.query('GET_ALLOW_REPLACE');
  6951. const allowMultiple = root.query('GET_ALLOW_MULTIPLE');
  6952. const totalItems = root.query('GET_TOTAL_ITEMS');
  6953. let maxItems = root.query('GET_MAX_FILES');
  6954. // total amount of items being dragged
  6955. const totalBrowseItems = items.length;
  6956. // if does not allow multiple items and dragging more than one item
  6957. if (!allowMultiple && totalBrowseItems > 1) {
  6958. root.dispatch('DID_THROW_MAX_FILES', {
  6959. source: items,
  6960. error: createResponse('warning', 0, 'Max files'),
  6961. });
  6962. return true;
  6963. }
  6964. // limit max items to one if not allowed to drop multiple items
  6965. maxItems = allowMultiple ? maxItems : 1;
  6966. if (!allowMultiple && allowReplace) {
  6967. // There is only one item, so there is room to replace or add an item
  6968. return false;
  6969. }
  6970. // no more room?
  6971. const hasMaxItems = isInt(maxItems);
  6972. if (hasMaxItems && totalItems + totalBrowseItems > maxItems) {
  6973. root.dispatch('DID_THROW_MAX_FILES', {
  6974. source: items,
  6975. error: createResponse('warning', 0, 'Max files'),
  6976. });
  6977. return true;
  6978. }
  6979. return false;
  6980. };
  6981. const getDragIndex = (list, children, position) => {
  6982. const itemList = list.childViews[0];
  6983. return getItemIndexByPosition(itemList, children, {
  6984. left: position.scopeLeft - itemList.rect.element.left,
  6985. top:
  6986. position.scopeTop -
  6987. (list.rect.outer.top + list.rect.element.marginTop + list.rect.element.scrollTop),
  6988. });
  6989. };
  6990. /**
  6991. * Enable or disable file drop functionality
  6992. */
  6993. const toggleDrop = root => {
  6994. const isAllowed = root.query('GET_ALLOW_DROP');
  6995. const isDisabled = root.query('GET_DISABLED');
  6996. const enabled = isAllowed && !isDisabled;
  6997. if (enabled && !root.ref.hopper) {
  6998. const hopper = createHopper(
  6999. root.element,
  7000. items => {
  7001. // allow quick validation of dropped items
  7002. const beforeDropFile = root.query('GET_BEFORE_DROP_FILE') || (() => true);
  7003. // all items should be validated by all filters as valid
  7004. const dropValidation = root.query('GET_DROP_VALIDATION');
  7005. return dropValidation
  7006. ? items.every(
  7007. item =>
  7008. applyFilters('ALLOW_HOPPER_ITEM', item, {
  7009. query: root.query,
  7010. }).every(result => result === true) && beforeDropFile(item)
  7011. )
  7012. : true;
  7013. },
  7014. {
  7015. filterItems: items => {
  7016. const ignoredFiles = root.query('GET_IGNORED_FILES');
  7017. return items.filter(item => {
  7018. if (isFile(item)) {
  7019. return !ignoredFiles.includes(item.name.toLowerCase());
  7020. }
  7021. return true;
  7022. });
  7023. },
  7024. catchesDropsOnPage: root.query('GET_DROP_ON_PAGE'),
  7025. requiresDropOnElement: root.query('GET_DROP_ON_ELEMENT'),
  7026. }
  7027. );
  7028. hopper.onload = (items, position) => {
  7029. // get item children elements and sort based on list sort
  7030. const list = root.ref.list.childViews[0];
  7031. const visibleChildren = list.childViews.filter(child => child.rect.element.height);
  7032. const children = root
  7033. .query('GET_ACTIVE_ITEMS')
  7034. .map(item => visibleChildren.find(child => child.id === item.id))
  7035. .filter(item => item);
  7036. applyFilterChain('ADD_ITEMS', items, { dispatch: root.dispatch }).then(queue => {
  7037. // these files don't fit so stop here
  7038. if (exceedsMaxFiles(root, queue)) return false;
  7039. // go
  7040. root.dispatch('ADD_ITEMS', {
  7041. items: queue,
  7042. index: getDragIndex(root.ref.list, children, position),
  7043. interactionMethod: InteractionMethod.DROP,
  7044. });
  7045. });
  7046. root.dispatch('DID_DROP', { position });
  7047. root.dispatch('DID_END_DRAG', { position });
  7048. };
  7049. hopper.ondragstart = position => {
  7050. root.dispatch('DID_START_DRAG', { position });
  7051. };
  7052. hopper.ondrag = debounce(position => {
  7053. root.dispatch('DID_DRAG', { position });
  7054. });
  7055. hopper.ondragend = position => {
  7056. root.dispatch('DID_END_DRAG', { position });
  7057. };
  7058. root.ref.hopper = hopper;
  7059. root.ref.drip = root.appendChildView(root.createChildView(drip));
  7060. } else if (!enabled && root.ref.hopper) {
  7061. root.ref.hopper.destroy();
  7062. root.ref.hopper = null;
  7063. root.removeChildView(root.ref.drip);
  7064. }
  7065. };
  7066. /**
  7067. * Enable or disable browse functionality
  7068. */
  7069. const toggleBrowse = (root, props) => {
  7070. const isAllowed = root.query('GET_ALLOW_BROWSE');
  7071. const isDisabled = root.query('GET_DISABLED');
  7072. const enabled = isAllowed && !isDisabled;
  7073. if (enabled && !root.ref.browser) {
  7074. root.ref.browser = root.appendChildView(
  7075. root.createChildView(browser, {
  7076. ...props,
  7077. onload: items => {
  7078. applyFilterChain('ADD_ITEMS', items, {
  7079. dispatch: root.dispatch,
  7080. }).then(queue => {
  7081. // these files don't fit so stop here
  7082. if (exceedsMaxFiles(root, queue)) return false;
  7083. // add items!
  7084. root.dispatch('ADD_ITEMS', {
  7085. items: queue,
  7086. index: -1,
  7087. interactionMethod: InteractionMethod.BROWSE,
  7088. });
  7089. });
  7090. },
  7091. }),
  7092. 0
  7093. );
  7094. } else if (!enabled && root.ref.browser) {
  7095. root.removeChildView(root.ref.browser);
  7096. root.ref.browser = null;
  7097. }
  7098. };
  7099. /**
  7100. * Enable or disable paste functionality
  7101. */
  7102. const togglePaste = root => {
  7103. const isAllowed = root.query('GET_ALLOW_PASTE');
  7104. const isDisabled = root.query('GET_DISABLED');
  7105. const enabled = isAllowed && !isDisabled;
  7106. if (enabled && !root.ref.paster) {
  7107. root.ref.paster = createPaster();
  7108. root.ref.paster.onload = items => {
  7109. applyFilterChain('ADD_ITEMS', items, { dispatch: root.dispatch }).then(queue => {
  7110. // these files don't fit so stop here
  7111. if (exceedsMaxFiles(root, queue)) return false;
  7112. // add items!
  7113. root.dispatch('ADD_ITEMS', {
  7114. items: queue,
  7115. index: -1,
  7116. interactionMethod: InteractionMethod.PASTE,
  7117. });
  7118. });
  7119. };
  7120. } else if (!enabled && root.ref.paster) {
  7121. root.ref.paster.destroy();
  7122. root.ref.paster = null;
  7123. }
  7124. };
  7125. /**
  7126. * Route actions
  7127. */
  7128. const route$5 = createRoute({
  7129. DID_SET_ALLOW_BROWSE: ({ root, props }) => {
  7130. toggleBrowse(root, props);
  7131. },
  7132. DID_SET_ALLOW_DROP: ({ root }) => {
  7133. toggleDrop(root);
  7134. },
  7135. DID_SET_ALLOW_PASTE: ({ root }) => {
  7136. togglePaste(root);
  7137. },
  7138. DID_SET_DISABLED: ({ root, props }) => {
  7139. toggleDrop(root);
  7140. togglePaste(root);
  7141. toggleBrowse(root, props);
  7142. const isDisabled = root.query('GET_DISABLED');
  7143. if (isDisabled) {
  7144. root.element.dataset.disabled = 'disabled';
  7145. } else {
  7146. // delete root.element.dataset.disabled; <= this does not work on iOS 10
  7147. root.element.removeAttribute('data-disabled');
  7148. }
  7149. },
  7150. });
  7151. const root = createView({
  7152. name: 'root',
  7153. read: ({ root }) => {
  7154. if (root.ref.measure) {
  7155. root.ref.measureHeight = root.ref.measure.offsetHeight;
  7156. }
  7157. },
  7158. create: create$e,
  7159. write: write$9,
  7160. destroy: ({ root }) => {
  7161. if (root.ref.paster) {
  7162. root.ref.paster.destroy();
  7163. }
  7164. if (root.ref.hopper) {
  7165. root.ref.hopper.destroy();
  7166. }
  7167. root.element.removeEventListener('touchmove', prevent);
  7168. root.element.removeEventListener('gesturestart', prevent);
  7169. },
  7170. mixins: {
  7171. styles: ['height'],
  7172. },
  7173. });
  7174. // creates the app
  7175. const createApp = (initialOptions = {}) => {
  7176. // let element
  7177. let originalElement = null;
  7178. // get default options
  7179. const defaultOptions = getOptions();
  7180. // create the data store, this will contain all our app info
  7181. const store = createStore(
  7182. // initial state (should be serializable)
  7183. createInitialState(defaultOptions),
  7184. // queries
  7185. [queries, createOptionQueries(defaultOptions)],
  7186. // action handlers
  7187. [actions, createOptionActions(defaultOptions)]
  7188. );
  7189. // set initial options
  7190. store.dispatch('SET_OPTIONS', { options: initialOptions });
  7191. // kick thread if visibility changes
  7192. const visibilityHandler = () => {
  7193. if (document.hidden) return;
  7194. store.dispatch('KICK');
  7195. };
  7196. document.addEventListener('visibilitychange', visibilityHandler);
  7197. // re-render on window resize start and finish
  7198. let resizeDoneTimer = null;
  7199. let isResizing = false;
  7200. let isResizingHorizontally = false;
  7201. let initialWindowWidth = null;
  7202. let currentWindowWidth = null;
  7203. const resizeHandler = () => {
  7204. if (!isResizing) {
  7205. isResizing = true;
  7206. }
  7207. clearTimeout(resizeDoneTimer);
  7208. resizeDoneTimer = setTimeout(() => {
  7209. isResizing = false;
  7210. initialWindowWidth = null;
  7211. currentWindowWidth = null;
  7212. if (isResizingHorizontally) {
  7213. isResizingHorizontally = false;
  7214. store.dispatch('DID_STOP_RESIZE');
  7215. }
  7216. }, 500);
  7217. };
  7218. window.addEventListener('resize', resizeHandler);
  7219. // render initial view
  7220. const view = root(store, { id: getUniqueId() });
  7221. //
  7222. // PRIVATE API -------------------------------------------------------------------------------------
  7223. //
  7224. let isResting = false;
  7225. let isHidden = false;
  7226. const readWriteApi = {
  7227. // necessary for update loop
  7228. /**
  7229. * Reads from dom (never call manually)
  7230. * @private
  7231. */
  7232. _read: () => {
  7233. // test if we're resizing horizontally
  7234. // TODO: see if we can optimize this by measuring root rect
  7235. if (isResizing) {
  7236. currentWindowWidth = window.innerWidth;
  7237. if (!initialWindowWidth) {
  7238. initialWindowWidth = currentWindowWidth;
  7239. }
  7240. if (!isResizingHorizontally && currentWindowWidth !== initialWindowWidth) {
  7241. store.dispatch('DID_START_RESIZE');
  7242. isResizingHorizontally = true;
  7243. }
  7244. }
  7245. if (isHidden && isResting) {
  7246. // test if is no longer hidden
  7247. isResting = view.element.offsetParent === null;
  7248. }
  7249. // if resting, no need to read as numbers will still all be correct
  7250. if (isResting) return;
  7251. // read view data
  7252. view._read();
  7253. // if is hidden we need to know so we exit rest mode when revealed
  7254. isHidden = view.rect.element.hidden;
  7255. },
  7256. /**
  7257. * Writes to dom (never call manually)
  7258. * @private
  7259. */
  7260. _write: ts => {
  7261. // get all actions from store
  7262. const actions = store
  7263. .processActionQueue()
  7264. // filter out set actions (these will automatically trigger DID_SET)
  7265. .filter(action => !/^SET_/.test(action.type));
  7266. // if was idling and no actions stop here
  7267. if (isResting && !actions.length) return;
  7268. // some actions might trigger events
  7269. routeActionsToEvents(actions);
  7270. // update the view
  7271. isResting = view._write(ts, actions, isResizingHorizontally);
  7272. // will clean up all archived items
  7273. removeReleasedItems(store.query('GET_ITEMS'));
  7274. // now idling
  7275. if (isResting) {
  7276. store.processDispatchQueue();
  7277. }
  7278. },
  7279. };
  7280. //
  7281. // EXPOSE EVENTS -------------------------------------------------------------------------------------
  7282. //
  7283. const createEvent = name => data => {
  7284. // create default event
  7285. const event = {
  7286. type: name,
  7287. };
  7288. // no data to add
  7289. if (!data) {
  7290. return event;
  7291. }
  7292. // copy relevant props
  7293. if (data.hasOwnProperty('error')) {
  7294. event.error = data.error ? { ...data.error } : null;
  7295. }
  7296. if (data.status) {
  7297. event.status = { ...data.status };
  7298. }
  7299. if (data.file) {
  7300. event.output = data.file;
  7301. }
  7302. // only source is available, else add item if possible
  7303. if (data.source) {
  7304. event.file = data.source;
  7305. } else if (data.item || data.id) {
  7306. const item = data.item ? data.item : store.query('GET_ITEM', data.id);
  7307. event.file = item ? createItemAPI(item) : null;
  7308. }
  7309. // map all items in a possible items array
  7310. if (data.items) {
  7311. event.items = data.items.map(createItemAPI);
  7312. }
  7313. // if this is a progress event add the progress amount
  7314. if (/progress/.test(name)) {
  7315. event.progress = data.progress;
  7316. }
  7317. // copy relevant props
  7318. if (data.hasOwnProperty('origin') && data.hasOwnProperty('target')) {
  7319. event.origin = data.origin;
  7320. event.target = data.target;
  7321. }
  7322. return event;
  7323. };
  7324. const eventRoutes = {
  7325. DID_DESTROY: createEvent('destroy'),
  7326. DID_INIT: createEvent('init'),
  7327. DID_THROW_MAX_FILES: createEvent('warning'),
  7328. DID_INIT_ITEM: createEvent('initfile'),
  7329. DID_START_ITEM_LOAD: createEvent('addfilestart'),
  7330. DID_UPDATE_ITEM_LOAD_PROGRESS: createEvent('addfileprogress'),
  7331. DID_LOAD_ITEM: createEvent('addfile'),
  7332. DID_THROW_ITEM_INVALID: [createEvent('error'), createEvent('addfile')],
  7333. DID_THROW_ITEM_LOAD_ERROR: [createEvent('error'), createEvent('addfile')],
  7334. DID_THROW_ITEM_REMOVE_ERROR: [createEvent('error'), createEvent('removefile')],
  7335. DID_PREPARE_OUTPUT: createEvent('preparefile'),
  7336. DID_START_ITEM_PROCESSING: createEvent('processfilestart'),
  7337. DID_UPDATE_ITEM_PROCESS_PROGRESS: createEvent('processfileprogress'),
  7338. DID_ABORT_ITEM_PROCESSING: createEvent('processfileabort'),
  7339. DID_COMPLETE_ITEM_PROCESSING: createEvent('processfile'),
  7340. DID_COMPLETE_ITEM_PROCESSING_ALL: createEvent('processfiles'),
  7341. DID_REVERT_ITEM_PROCESSING: createEvent('processfilerevert'),
  7342. DID_THROW_ITEM_PROCESSING_ERROR: [createEvent('error'), createEvent('processfile')],
  7343. DID_REMOVE_ITEM: createEvent('removefile'),
  7344. DID_UPDATE_ITEMS: createEvent('updatefiles'),
  7345. DID_ACTIVATE_ITEM: createEvent('activatefile'),
  7346. DID_REORDER_ITEMS: createEvent('reorderfiles'),
  7347. };
  7348. const exposeEvent = event => {
  7349. // create event object to be dispatched
  7350. const detail = { pond: exports, ...event };
  7351. delete detail.type;
  7352. view.element.dispatchEvent(
  7353. new CustomEvent(`FilePond:${event.type}`, {
  7354. // event info
  7355. detail,
  7356. // event behaviour
  7357. bubbles: true,
  7358. cancelable: true,
  7359. composed: true, // triggers listeners outside of shadow root
  7360. })
  7361. );
  7362. // event object to params used for `on()` event handlers and callbacks `oninit()`
  7363. const params = [];
  7364. // if is possible error event, make it the first param
  7365. if (event.hasOwnProperty('error')) {
  7366. params.push(event.error);
  7367. }
  7368. // file is always section
  7369. if (event.hasOwnProperty('file')) {
  7370. params.push(event.file);
  7371. }
  7372. // append other props
  7373. const filtered = ['type', 'error', 'file'];
  7374. Object.keys(event)
  7375. .filter(key => !filtered.includes(key))
  7376. .forEach(key => params.push(event[key]));
  7377. // on(type, () => { })
  7378. exports.fire(event.type, ...params);
  7379. // oninit = () => {}
  7380. const handler = store.query(`GET_ON${event.type.toUpperCase()}`);
  7381. if (handler) {
  7382. handler(...params);
  7383. }
  7384. };
  7385. const routeActionsToEvents = actions => {
  7386. if (!actions.length) return;
  7387. actions
  7388. .filter(action => eventRoutes[action.type])
  7389. .forEach(action => {
  7390. const routes = eventRoutes[action.type];
  7391. (Array.isArray(routes) ? routes : [routes]).forEach(route => {
  7392. // this isn't fantastic, but because of the stacking of settimeouts plugins can handle the did_load before the did_init
  7393. if (action.type === 'DID_INIT_ITEM') {
  7394. exposeEvent(route(action.data));
  7395. } else {
  7396. setTimeout(() => {
  7397. exposeEvent(route(action.data));
  7398. }, 0);
  7399. }
  7400. });
  7401. });
  7402. };
  7403. //
  7404. // PUBLIC API -------------------------------------------------------------------------------------
  7405. //
  7406. const setOptions = options => store.dispatch('SET_OPTIONS', { options });
  7407. const getFile = query => store.query('GET_ACTIVE_ITEM', query);
  7408. const prepareFile = query =>
  7409. new Promise((resolve, reject) => {
  7410. store.dispatch('REQUEST_ITEM_PREPARE', {
  7411. query,
  7412. success: item => {
  7413. resolve(item);
  7414. },
  7415. failure: error => {
  7416. reject(error);
  7417. },
  7418. });
  7419. });
  7420. const addFile = (source, options = {}) =>
  7421. new Promise((resolve, reject) => {
  7422. addFiles([{ source, options }], { index: options.index })
  7423. .then(items => resolve(items && items[0]))
  7424. .catch(reject);
  7425. });
  7426. const isFilePondFile = obj => obj.file && obj.id;
  7427. const removeFile = (query, options) => {
  7428. // if only passed options
  7429. if (typeof query === 'object' && !isFilePondFile(query) && !options) {
  7430. options = query;
  7431. query = undefined;
  7432. }
  7433. // request item removal
  7434. store.dispatch('REMOVE_ITEM', { ...options, query });
  7435. // see if item has been removed
  7436. return store.query('GET_ACTIVE_ITEM', query) === null;
  7437. };
  7438. const addFiles = (...args) =>
  7439. new Promise((resolve, reject) => {
  7440. const sources = [];
  7441. const options = {};
  7442. // user passed a sources array
  7443. if (isArray(args[0])) {
  7444. sources.push.apply(sources, args[0]);
  7445. Object.assign(options, args[1] || {});
  7446. } else {
  7447. // user passed sources as arguments, last one might be options object
  7448. const lastArgument = args[args.length - 1];
  7449. if (typeof lastArgument === 'object' && !(lastArgument instanceof Blob)) {
  7450. Object.assign(options, args.pop());
  7451. }
  7452. // add rest to sources
  7453. sources.push(...args);
  7454. }
  7455. store.dispatch('ADD_ITEMS', {
  7456. items: sources,
  7457. index: options.index,
  7458. interactionMethod: InteractionMethod.API,
  7459. success: resolve,
  7460. failure: reject,
  7461. });
  7462. });
  7463. const getFiles = () => store.query('GET_ACTIVE_ITEMS');
  7464. const processFile = query =>
  7465. new Promise((resolve, reject) => {
  7466. store.dispatch('REQUEST_ITEM_PROCESSING', {
  7467. query,
  7468. success: item => {
  7469. resolve(item);
  7470. },
  7471. failure: error => {
  7472. reject(error);
  7473. },
  7474. });
  7475. });
  7476. const prepareFiles = (...args) => {
  7477. const queries = Array.isArray(args[0]) ? args[0] : args;
  7478. const items = queries.length ? queries : getFiles();
  7479. return Promise.all(items.map(prepareFile));
  7480. };
  7481. const processFiles = (...args) => {
  7482. const queries = Array.isArray(args[0]) ? args[0] : args;
  7483. if (!queries.length) {
  7484. const files = getFiles().filter(
  7485. item =>
  7486. !(item.status === ItemStatus.IDLE && item.origin === FileOrigin.LOCAL) &&
  7487. item.status !== ItemStatus.PROCESSING &&
  7488. item.status !== ItemStatus.PROCESSING_COMPLETE &&
  7489. item.status !== ItemStatus.PROCESSING_REVERT_ERROR
  7490. );
  7491. return Promise.all(files.map(processFile));
  7492. }
  7493. return Promise.all(queries.map(processFile));
  7494. };
  7495. const removeFiles = (...args) => {
  7496. const queries = Array.isArray(args[0]) ? args[0] : args;
  7497. let options;
  7498. if (typeof queries[queries.length - 1] === 'object') {
  7499. options = queries.pop();
  7500. } else if (Array.isArray(args[0])) {
  7501. options = args[1];
  7502. }
  7503. const files = getFiles();
  7504. if (!queries.length) return Promise.all(files.map(file => removeFile(file, options)));
  7505. // when removing by index the indexes shift after each file removal so we need to convert indexes to ids
  7506. const mappedQueries = queries
  7507. .map(query => (isNumber(query) ? (files[query] ? files[query].id : null) : query))
  7508. .filter(query => query);
  7509. return mappedQueries.map(q => removeFile(q, options));
  7510. };
  7511. const exports = {
  7512. // supports events
  7513. ...on(),
  7514. // inject private api methods
  7515. ...readWriteApi,
  7516. // inject all getters and setters
  7517. ...createOptionAPI(store, defaultOptions),
  7518. /**
  7519. * Override options defined in options object
  7520. * @param options
  7521. */
  7522. setOptions,
  7523. /**
  7524. * Load the given file
  7525. * @param source - the source of the file (either a File, base64 data uri or url)
  7526. * @param options - object, { index: 0 }
  7527. */
  7528. addFile,
  7529. /**
  7530. * Load the given files
  7531. * @param sources - the sources of the files to load
  7532. * @param options - object, { index: 0 }
  7533. */
  7534. addFiles,
  7535. /**
  7536. * Returns the file objects matching the given query
  7537. * @param query { string, number, null }
  7538. */
  7539. getFile,
  7540. /**
  7541. * Upload file with given name
  7542. * @param query { string, number, null }
  7543. */
  7544. processFile,
  7545. /**
  7546. * Request prepare output for file with given name
  7547. * @param query { string, number, null }
  7548. */
  7549. prepareFile,
  7550. /**
  7551. * Removes a file by its name
  7552. * @param query { string, number, null }
  7553. */
  7554. removeFile,
  7555. /**
  7556. * Moves a file to a new location in the files list
  7557. */
  7558. moveFile: (query, index) => store.dispatch('MOVE_ITEM', { query, index }),
  7559. /**
  7560. * Returns all files (wrapped in public api)
  7561. */
  7562. getFiles,
  7563. /**
  7564. * Starts uploading all files
  7565. */
  7566. processFiles,
  7567. /**
  7568. * Clears all files from the files list
  7569. */
  7570. removeFiles,
  7571. /**
  7572. * Starts preparing output of all files
  7573. */
  7574. prepareFiles,
  7575. /**
  7576. * Sort list of files
  7577. */
  7578. sort: compare => store.dispatch('SORT', { compare }),
  7579. /**
  7580. * Browse the file system for a file
  7581. */
  7582. browse: () => {
  7583. // needs to be trigger directly as user action needs to be traceable (is not traceable in requestAnimationFrame)
  7584. var input = view.element.querySelector('input[type=file]');
  7585. if (input) {
  7586. input.click();
  7587. }
  7588. },
  7589. /**
  7590. * Destroys the app
  7591. */
  7592. destroy: () => {
  7593. // request destruction
  7594. exports.fire('destroy', view.element);
  7595. // stop active processes (file uploads, fetches, stuff like that)
  7596. // loop over items and depending on states call abort for ongoing processes
  7597. store.dispatch('ABORT_ALL');
  7598. // destroy view
  7599. view._destroy();
  7600. // stop listening to resize
  7601. window.removeEventListener('resize', resizeHandler);
  7602. // stop listening to the visiblitychange event
  7603. document.removeEventListener('visibilitychange', visibilityHandler);
  7604. // dispatch destroy
  7605. store.dispatch('DID_DESTROY');
  7606. },
  7607. /**
  7608. * Inserts the plugin before the target element
  7609. */
  7610. insertBefore: element => insertBefore(view.element, element),
  7611. /**
  7612. * Inserts the plugin after the target element
  7613. */
  7614. insertAfter: element => insertAfter(view.element, element),
  7615. /**
  7616. * Appends the plugin to the target element
  7617. */
  7618. appendTo: element => element.appendChild(view.element),
  7619. /**
  7620. * Replaces an element with the app
  7621. */
  7622. replaceElement: element => {
  7623. // insert the app before the element
  7624. insertBefore(view.element, element);
  7625. // remove the original element
  7626. element.parentNode.removeChild(element);
  7627. // remember original element
  7628. originalElement = element;
  7629. },
  7630. /**
  7631. * Restores the original element
  7632. */
  7633. restoreElement: () => {
  7634. if (!originalElement) {
  7635. return; // no element to restore
  7636. }
  7637. // restore original element
  7638. insertAfter(originalElement, view.element);
  7639. // remove our element
  7640. view.element.parentNode.removeChild(view.element);
  7641. // remove reference
  7642. originalElement = null;
  7643. },
  7644. /**
  7645. * Returns true if the app root is attached to given element
  7646. * @param element
  7647. */
  7648. isAttachedTo: element => view.element === element || originalElement === element,
  7649. /**
  7650. * Returns the root element
  7651. */
  7652. element: {
  7653. get: () => view.element,
  7654. },
  7655. /**
  7656. * Returns the current pond status
  7657. */
  7658. status: {
  7659. get: () => store.query('GET_STATUS'),
  7660. },
  7661. };
  7662. // Done!
  7663. store.dispatch('DID_INIT');
  7664. // create actual api object
  7665. return createObject(exports);
  7666. };
  7667. const createAppObject = (customOptions = {}) => {
  7668. // default options
  7669. const defaultOptions = {};
  7670. forin(getOptions(), (key, value) => {
  7671. defaultOptions[key] = value[0];
  7672. });
  7673. // set app options
  7674. const app = createApp({
  7675. // default options
  7676. ...defaultOptions,
  7677. // custom options
  7678. ...customOptions,
  7679. });
  7680. // return the plugin instance
  7681. return app;
  7682. };
  7683. const lowerCaseFirstLetter = string => string.charAt(0).toLowerCase() + string.slice(1);
  7684. const attributeNameToPropertyName = attributeName => toCamels(attributeName.replace(/^data-/, ''));
  7685. const mapObject = (object, propertyMap) => {
  7686. // remove unwanted
  7687. forin(propertyMap, (selector, mapping) => {
  7688. forin(object, (property, value) => {
  7689. // create regexp shortcut
  7690. const selectorRegExp = new RegExp(selector);
  7691. // tests if
  7692. const matches = selectorRegExp.test(property);
  7693. // no match, skip
  7694. if (!matches) {
  7695. return;
  7696. }
  7697. // if there's a mapping, the original property is always removed
  7698. delete object[property];
  7699. // should only remove, we done!
  7700. if (mapping === false) {
  7701. return;
  7702. }
  7703. // move value to new property
  7704. if (isString(mapping)) {
  7705. object[mapping] = value;
  7706. return;
  7707. }
  7708. // move to group
  7709. const group = mapping.group;
  7710. if (isObject(mapping) && !object[group]) {
  7711. object[group] = {};
  7712. }
  7713. object[group][lowerCaseFirstLetter(property.replace(selectorRegExp, ''))] = value;
  7714. });
  7715. // do submapping
  7716. if (mapping.mapping) {
  7717. mapObject(object[mapping.group], mapping.mapping);
  7718. }
  7719. });
  7720. };
  7721. const getAttributesAsObject = (node, attributeMapping = {}) => {
  7722. // turn attributes into object
  7723. const attributes = [];
  7724. forin(node.attributes, index => {
  7725. attributes.push(node.attributes[index]);
  7726. });
  7727. const output = attributes
  7728. .filter(attribute => attribute.name)
  7729. .reduce((obj, attribute) => {
  7730. const value = attr(node, attribute.name);
  7731. obj[attributeNameToPropertyName(attribute.name)] =
  7732. value === attribute.name ? true : value;
  7733. return obj;
  7734. }, {});
  7735. // do mapping of object properties
  7736. mapObject(output, attributeMapping);
  7737. return output;
  7738. };
  7739. const createAppAtElement = (element, options = {}) => {
  7740. // how attributes of the input element are mapped to the options for the plugin
  7741. const attributeMapping = {
  7742. // translate to other name
  7743. '^class$': 'className',
  7744. '^multiple$': 'allowMultiple',
  7745. '^capture$': 'captureMethod',
  7746. '^webkitdirectory$': 'allowDirectoriesOnly',
  7747. // group under single property
  7748. '^server': {
  7749. group: 'server',
  7750. mapping: {
  7751. '^process': {
  7752. group: 'process',
  7753. },
  7754. '^revert': {
  7755. group: 'revert',
  7756. },
  7757. '^fetch': {
  7758. group: 'fetch',
  7759. },
  7760. '^restore': {
  7761. group: 'restore',
  7762. },
  7763. '^load': {
  7764. group: 'load',
  7765. },
  7766. },
  7767. },
  7768. // don't include in object
  7769. '^type$': false,
  7770. '^files$': false,
  7771. };
  7772. // add additional option translators
  7773. applyFilters('SET_ATTRIBUTE_TO_OPTION_MAP', attributeMapping);
  7774. // create final options object by setting options object and then overriding options supplied on element
  7775. const mergedOptions = {
  7776. ...options,
  7777. };
  7778. const attributeOptions = getAttributesAsObject(
  7779. element.nodeName === 'FIELDSET' ? element.querySelector('input[type=file]') : element,
  7780. attributeMapping
  7781. );
  7782. // merge with options object
  7783. Object.keys(attributeOptions).forEach(key => {
  7784. if (isObject(attributeOptions[key])) {
  7785. if (!isObject(mergedOptions[key])) {
  7786. mergedOptions[key] = {};
  7787. }
  7788. Object.assign(mergedOptions[key], attributeOptions[key]);
  7789. } else {
  7790. mergedOptions[key] = attributeOptions[key];
  7791. }
  7792. });
  7793. // if parent is a fieldset, get files from parent by selecting all input fields that are not file upload fields
  7794. // these will then be automatically set to the initial files
  7795. mergedOptions.files = (options.files || []).concat(
  7796. Array.from(element.querySelectorAll('input:not([type=file])')).map(input => ({
  7797. source: input.value,
  7798. options: {
  7799. type: input.dataset.type,
  7800. },
  7801. }))
  7802. );
  7803. // build plugin
  7804. const app = createAppObject(mergedOptions);
  7805. // add already selected files
  7806. if (element.files) {
  7807. Array.from(element.files).forEach(file => {
  7808. app.addFile(file);
  7809. });
  7810. }
  7811. // replace the target element
  7812. app.replaceElement(element);
  7813. // expose
  7814. return app;
  7815. };
  7816. // if an element is passed, we create the instance at that element, if not, we just create an up object
  7817. const createApp$1 = (...args) =>
  7818. isNode(args[0]) ? createAppAtElement(...args) : createAppObject(...args);
  7819. const PRIVATE_METHODS = ['fire', '_read', '_write'];
  7820. const createAppAPI = app => {
  7821. const api = {};
  7822. copyObjectPropertiesToObject(app, api, PRIVATE_METHODS);
  7823. return api;
  7824. };
  7825. /**
  7826. * Replaces placeholders in given string with replacements
  7827. * @param string - "Foo {bar}""
  7828. * @param replacements - { "bar": 10 }
  7829. */
  7830. const replaceInString = (string, replacements) =>
  7831. string.replace(/(?:{([a-zA-Z]+)})/g, (match, group) => replacements[group]);
  7832. const createWorker = fn => {
  7833. const workerBlob = new Blob(['(', fn.toString(), ')()'], {
  7834. type: 'application/javascript',
  7835. });
  7836. const workerURL = URL.createObjectURL(workerBlob);
  7837. const worker = new Worker(workerURL);
  7838. return {
  7839. transfer: (message, cb) => {},
  7840. post: (message, cb, transferList) => {
  7841. const id = getUniqueId();
  7842. worker.onmessage = e => {
  7843. if (e.data.id === id) {
  7844. cb(e.data.message);
  7845. }
  7846. };
  7847. worker.postMessage(
  7848. {
  7849. id,
  7850. message,
  7851. },
  7852. transferList
  7853. );
  7854. },
  7855. terminate: () => {
  7856. worker.terminate();
  7857. URL.revokeObjectURL(workerURL);
  7858. },
  7859. };
  7860. };
  7861. const loadImage = url =>
  7862. new Promise((resolve, reject) => {
  7863. const img = new Image();
  7864. img.onload = () => {
  7865. resolve(img);
  7866. };
  7867. img.onerror = e => {
  7868. reject(e);
  7869. };
  7870. img.src = url;
  7871. });
  7872. const renameFile = (file, name) => {
  7873. const renamedFile = file.slice(0, file.size, file.type);
  7874. renamedFile.lastModifiedDate = file.lastModifiedDate;
  7875. renamedFile.name = name;
  7876. return renamedFile;
  7877. };
  7878. const copyFile = file => renameFile(file, file.name);
  7879. // already registered plugins (can't register twice)
  7880. const registeredPlugins = [];
  7881. // pass utils to plugin
  7882. const createAppPlugin = plugin => {
  7883. // already registered
  7884. if (registeredPlugins.includes(plugin)) {
  7885. return;
  7886. }
  7887. // remember this plugin
  7888. registeredPlugins.push(plugin);
  7889. // setup!
  7890. const pluginOutline = plugin({
  7891. addFilter,
  7892. utils: {
  7893. Type,
  7894. forin,
  7895. isString,
  7896. isFile,
  7897. toNaturalFileSize,
  7898. replaceInString,
  7899. getExtensionFromFilename,
  7900. getFilenameWithoutExtension,
  7901. guesstimateMimeType,
  7902. getFileFromBlob,
  7903. getFilenameFromURL,
  7904. createRoute,
  7905. createWorker,
  7906. createView,
  7907. createItemAPI,
  7908. loadImage,
  7909. copyFile,
  7910. renameFile,
  7911. createBlob,
  7912. applyFilterChain,
  7913. text,
  7914. getNumericAspectRatioFromString,
  7915. },
  7916. views: {
  7917. fileActionButton,
  7918. },
  7919. });
  7920. // add plugin options to default options
  7921. extendDefaultOptions(pluginOutline.options);
  7922. };
  7923. // feature detection used by supported() method
  7924. const isOperaMini = () => Object.prototype.toString.call(window.operamini) === '[object OperaMini]';
  7925. const hasPromises = () => 'Promise' in window;
  7926. const hasBlobSlice = () => 'slice' in Blob.prototype;
  7927. const hasCreateObjectURL = () => 'URL' in window && 'createObjectURL' in window.URL;
  7928. const hasVisibility = () => 'visibilityState' in document;
  7929. const hasTiming = () => 'performance' in window; // iOS 8.x
  7930. const hasCSSSupports = () => 'supports' in (window.CSS || {}); // use to detect Safari 9+
  7931. const isIE11 = () => /MSIE|Trident/.test(window.navigator.userAgent);
  7932. const supported = (() => {
  7933. // Runs immediately and then remembers result for subsequent calls
  7934. const isSupported =
  7935. // Has to be a browser
  7936. isBrowser() &&
  7937. // Can't run on Opera Mini due to lack of everything
  7938. !isOperaMini() &&
  7939. // Require these APIs to feature detect a modern browser
  7940. hasVisibility() &&
  7941. hasPromises() &&
  7942. hasBlobSlice() &&
  7943. hasCreateObjectURL() &&
  7944. hasTiming() &&
  7945. // doesn't need CSSSupports but is a good way to detect Safari 9+ (we do want to support IE11 though)
  7946. (hasCSSSupports() || isIE11());
  7947. return () => isSupported;
  7948. })();
  7949. /**
  7950. * Plugin internal state (over all instances)
  7951. */
  7952. const state = {
  7953. // active app instances, used to redraw the apps and to find the later
  7954. apps: [],
  7955. };
  7956. // plugin name
  7957. const name = 'filepond';
  7958. /**
  7959. * Public Plugin methods
  7960. */
  7961. const fn = () => {};
  7962. let Status$1 = {};
  7963. let FileStatus = {};
  7964. let FileOrigin$1 = {};
  7965. let OptionTypes = {};
  7966. let create$f = fn;
  7967. let destroy = fn;
  7968. let parse = fn;
  7969. let find = fn;
  7970. let registerPlugin = fn;
  7971. let getOptions$1 = fn;
  7972. let setOptions$1 = fn;
  7973. // if not supported, no API
  7974. if (supported()) {
  7975. // start painter and fire load event
  7976. createPainter(
  7977. () => {
  7978. state.apps.forEach(app => app._read());
  7979. },
  7980. ts => {
  7981. state.apps.forEach(app => app._write(ts));
  7982. }
  7983. );
  7984. // fire loaded event so we know when FilePond is available
  7985. const dispatch = () => {
  7986. // let others know we have area ready
  7987. document.dispatchEvent(
  7988. new CustomEvent('FilePond:loaded', {
  7989. detail: {
  7990. supported,
  7991. create: create$f,
  7992. destroy,
  7993. parse,
  7994. find,
  7995. registerPlugin,
  7996. setOptions: setOptions$1,
  7997. },
  7998. })
  7999. );
  8000. // clean up event
  8001. document.removeEventListener('DOMContentLoaded', dispatch);
  8002. };
  8003. if (document.readyState !== 'loading') {
  8004. // move to back of execution queue, FilePond should have been exported by then
  8005. setTimeout(() => dispatch(), 0);
  8006. } else {
  8007. document.addEventListener('DOMContentLoaded', dispatch);
  8008. }
  8009. // updates the OptionTypes object based on the current options
  8010. const updateOptionTypes = () =>
  8011. forin(getOptions(), (key, value) => {
  8012. OptionTypes[key] = value[1];
  8013. });
  8014. Status$1 = { ...Status };
  8015. FileOrigin$1 = { ...FileOrigin };
  8016. FileStatus = { ...ItemStatus };
  8017. OptionTypes = {};
  8018. updateOptionTypes();
  8019. // create method, creates apps and adds them to the app array
  8020. create$f = (...args) => {
  8021. const app = createApp$1(...args);
  8022. app.on('destroy', destroy);
  8023. state.apps.push(app);
  8024. return createAppAPI(app);
  8025. };
  8026. // destroys apps and removes them from the app array
  8027. destroy = hook => {
  8028. // returns true if the app was destroyed successfully
  8029. const indexToRemove = state.apps.findIndex(app => app.isAttachedTo(hook));
  8030. if (indexToRemove >= 0) {
  8031. // remove from apps
  8032. const app = state.apps.splice(indexToRemove, 1)[0];
  8033. // restore original dom element
  8034. app.restoreElement();
  8035. return true;
  8036. }
  8037. return false;
  8038. };
  8039. // parses the given context for plugins (does not include the context element itself)
  8040. parse = context => {
  8041. // get all possible hooks
  8042. const matchedHooks = Array.from(context.querySelectorAll(`.${name}`));
  8043. // filter out already active hooks
  8044. const newHooks = matchedHooks.filter(
  8045. newHook => !state.apps.find(app => app.isAttachedTo(newHook))
  8046. );
  8047. // create new instance for each hook
  8048. return newHooks.map(hook => create$f(hook));
  8049. };
  8050. // returns an app based on the given element hook
  8051. find = hook => {
  8052. const app = state.apps.find(app => app.isAttachedTo(hook));
  8053. if (!app) {
  8054. return null;
  8055. }
  8056. return createAppAPI(app);
  8057. };
  8058. // adds a plugin extension
  8059. registerPlugin = (...plugins) => {
  8060. // register plugins
  8061. plugins.forEach(createAppPlugin);
  8062. // update OptionTypes, each plugin might have extended the default options
  8063. updateOptionTypes();
  8064. };
  8065. getOptions$1 = () => {
  8066. const opts = {};
  8067. forin(getOptions(), (key, value) => {
  8068. opts[key] = value[0];
  8069. });
  8070. return opts;
  8071. };
  8072. setOptions$1 = opts => {
  8073. if (isObject(opts)) {
  8074. // update existing plugins
  8075. state.apps.forEach(app => {
  8076. app.setOptions(opts);
  8077. });
  8078. // override defaults
  8079. setOptions(opts);
  8080. }
  8081. // return new options
  8082. return getOptions$1();
  8083. };
  8084. }
  8085. export {
  8086. FileOrigin$1 as FileOrigin,
  8087. FileStatus,
  8088. OptionTypes,
  8089. Status$1 as Status,
  8090. create$f as create,
  8091. destroy,
  8092. find,
  8093. getOptions$1 as getOptions,
  8094. parse,
  8095. registerPlugin,
  8096. setOptions$1 as setOptions,
  8097. supported,
  8098. };