1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724 |
- /*!
- * FilePond 4.30.4
- * Licensed under MIT, https://opensource.org/licenses/MIT/
- * Please visit https://pqina.nl/filepond/ for details.
- */
- /* eslint-disable */
- const isNode = value => value instanceof HTMLElement;
- const createStore = (initialState, queries = [], actions = []) => {
- // internal state
- const state = {
- ...initialState,
- };
- // contains all actions for next frame, is clear when actions are requested
- const actionQueue = [];
- const dispatchQueue = [];
- // returns a duplicate of the current state
- const getState = () => ({ ...state });
- // returns a duplicate of the actions array and clears the actions array
- const processActionQueue = () => {
- // create copy of actions queue
- const queue = [...actionQueue];
- // clear actions queue (we don't want no double actions)
- actionQueue.length = 0;
- return queue;
- };
- // processes actions that might block the main UI thread
- const processDispatchQueue = () => {
- // create copy of actions queue
- const queue = [...dispatchQueue];
- // clear actions queue (we don't want no double actions)
- dispatchQueue.length = 0;
- // now dispatch these actions
- queue.forEach(({ type, data }) => {
- dispatch(type, data);
- });
- };
- // adds a new action, calls its handler and
- const dispatch = (type, data, isBlocking) => {
- // is blocking action (should never block if document is hidden)
- if (isBlocking && !document.hidden) {
- dispatchQueue.push({ type, data });
- return;
- }
- // if this action has a handler, handle the action
- if (actionHandlers[type]) {
- actionHandlers[type](data);
- }
- // now add action
- actionQueue.push({
- type,
- data,
- });
- };
- const query = (str, ...args) => (queryHandles[str] ? queryHandles[str](...args) : null);
- const api = {
- getState,
- processActionQueue,
- processDispatchQueue,
- dispatch,
- query,
- };
- let queryHandles = {};
- queries.forEach(query => {
- queryHandles = {
- ...query(state),
- ...queryHandles,
- };
- });
- let actionHandlers = {};
- actions.forEach(action => {
- actionHandlers = {
- ...action(dispatch, query, state),
- ...actionHandlers,
- };
- });
- return api;
- };
- const defineProperty = (obj, property, definition) => {
- if (typeof definition === 'function') {
- obj[property] = definition;
- return;
- }
- Object.defineProperty(obj, property, { ...definition });
- };
- const forin = (obj, cb) => {
- for (const key in obj) {
- if (!obj.hasOwnProperty(key)) {
- continue;
- }
- cb(key, obj[key]);
- }
- };
- const createObject = definition => {
- const obj = {};
- forin(definition, property => {
- defineProperty(obj, property, definition[property]);
- });
- return obj;
- };
- const attr = (node, name, value = null) => {
- if (value === null) {
- return node.getAttribute(name) || node.hasAttribute(name);
- }
- node.setAttribute(name, value);
- };
- const ns = 'http://www.w3.org/2000/svg';
- const svgElements = ['svg', 'path']; // only svg elements used
- const isSVGElement = tag => svgElements.includes(tag);
- const createElement = (tag, className, attributes = {}) => {
- if (typeof className === 'object') {
- attributes = className;
- className = null;
- }
- const element = isSVGElement(tag)
- ? document.createElementNS(ns, tag)
- : document.createElement(tag);
- if (className) {
- if (isSVGElement(tag)) {
- attr(element, 'class', className);
- } else {
- element.className = className;
- }
- }
- forin(attributes, (name, value) => {
- attr(element, name, value);
- });
- return element;
- };
- const appendChild = parent => (child, index) => {
- if (typeof index !== 'undefined' && parent.children[index]) {
- parent.insertBefore(child, parent.children[index]);
- } else {
- parent.appendChild(child);
- }
- };
- const appendChildView = (parent, childViews) => (view, index) => {
- if (typeof index !== 'undefined') {
- childViews.splice(index, 0, view);
- } else {
- childViews.push(view);
- }
- return view;
- };
- const removeChildView = (parent, childViews) => view => {
- // remove from child views
- childViews.splice(childViews.indexOf(view), 1);
- // remove the element
- if (view.element.parentNode) {
- parent.removeChild(view.element);
- }
- return view;
- };
- const IS_BROWSER = (() =>
- typeof window !== 'undefined' && typeof window.document !== 'undefined')();
- const isBrowser = () => IS_BROWSER;
- const testElement = isBrowser() ? createElement('svg') : {};
- const getChildCount =
- 'children' in testElement ? el => el.children.length : el => el.childNodes.length;
- const getViewRect = (elementRect, childViews, offset, scale) => {
- const left = offset[0] || elementRect.left;
- const top = offset[1] || elementRect.top;
- const right = left + elementRect.width;
- const bottom = top + elementRect.height * (scale[1] || 1);
- const rect = {
- // the rectangle of the element itself
- element: {
- ...elementRect,
- },
- // the rectangle of the element expanded to contain its children, does not include any margins
- inner: {
- left: elementRect.left,
- top: elementRect.top,
- right: elementRect.right,
- bottom: elementRect.bottom,
- },
- // the rectangle of the element expanded to contain its children including own margin and child margins
- // margins will be added after we've recalculated the size
- outer: {
- left,
- top,
- right,
- bottom,
- },
- };
- // expand rect to fit all child rectangles
- childViews
- .filter(childView => !childView.isRectIgnored())
- .map(childView => childView.rect)
- .forEach(childViewRect => {
- expandRect(rect.inner, { ...childViewRect.inner });
- expandRect(rect.outer, { ...childViewRect.outer });
- });
- // calculate inner width and height
- calculateRectSize(rect.inner);
- // append additional margin (top and left margins are included in top and left automatically)
- rect.outer.bottom += rect.element.marginBottom;
- rect.outer.right += rect.element.marginRight;
- // calculate outer width and height
- calculateRectSize(rect.outer);
- return rect;
- };
- const expandRect = (parent, child) => {
- // adjust for parent offset
- child.top += parent.top;
- child.right += parent.left;
- child.bottom += parent.top;
- child.left += parent.left;
- if (child.bottom > parent.bottom) {
- parent.bottom = child.bottom;
- }
- if (child.right > parent.right) {
- parent.right = child.right;
- }
- };
- const calculateRectSize = rect => {
- rect.width = rect.right - rect.left;
- rect.height = rect.bottom - rect.top;
- };
- const isNumber = value => typeof value === 'number';
- /**
- * Determines if position is at destination
- * @param position
- * @param destination
- * @param velocity
- * @param errorMargin
- * @returns {boolean}
- */
- const thereYet = (position, destination, velocity, errorMargin = 0.001) => {
- return Math.abs(position - destination) < errorMargin && Math.abs(velocity) < errorMargin;
- };
- /**
- * Spring animation
- */
- const spring =
- // default options
- ({ stiffness = 0.5, damping = 0.75, mass = 10 } = {}) =>
- // method definition
- {
- let target = null;
- let position = null;
- let velocity = 0;
- let resting = false;
- // updates spring state
- const interpolate = (ts, skipToEndState) => {
- // in rest, don't animate
- if (resting) return;
- // need at least a target or position to do springy things
- if (!(isNumber(target) && isNumber(position))) {
- resting = true;
- velocity = 0;
- return;
- }
- // calculate spring force
- const f = -(position - target) * stiffness;
- // update velocity by adding force based on mass
- velocity += f / mass;
- // update position by adding velocity
- position += velocity;
- // slow down based on amount of damping
- velocity *= damping;
- // we've arrived if we're near target and our velocity is near zero
- if (thereYet(position, target, velocity) || skipToEndState) {
- position = target;
- velocity = 0;
- resting = true;
- // we done
- api.onupdate(position);
- api.oncomplete(position);
- } else {
- // progress update
- api.onupdate(position);
- }
- };
- /**
- * Set new target value
- * @param value
- */
- const setTarget = value => {
- // if currently has no position, set target and position to this value
- if (isNumber(value) && !isNumber(position)) {
- position = value;
- }
- // next target value will not be animated to
- if (target === null) {
- target = value;
- position = value;
- }
- // let start moving to target
- target = value;
- // already at target
- if (position === target || typeof target === 'undefined') {
- // now resting as target is current position, stop moving
- resting = true;
- velocity = 0;
- // done!
- api.onupdate(position);
- api.oncomplete(position);
- return;
- }
- resting = false;
- };
- // need 'api' to call onupdate callback
- const api = createObject({
- interpolate,
- target: {
- set: setTarget,
- get: () => target,
- },
- resting: {
- get: () => resting,
- },
- onupdate: value => {},
- oncomplete: value => {},
- });
- return api;
- };
- const easeLinear = t => t;
- const easeInOutQuad = t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t);
- const tween =
- // default values
- ({ duration = 500, easing = easeInOutQuad, delay = 0 } = {}) =>
- // method definition
- {
- let start = null;
- let t;
- let p;
- let resting = true;
- let reverse = false;
- let target = null;
- const interpolate = (ts, skipToEndState) => {
- if (resting || target === null) return;
- if (start === null) {
- start = ts;
- }
- if (ts - start < delay) return;
- t = ts - start - delay;
- if (t >= duration || skipToEndState) {
- t = 1;
- p = reverse ? 0 : 1;
- api.onupdate(p * target);
- api.oncomplete(p * target);
- resting = true;
- } else {
- p = t / duration;
- api.onupdate((t >= 0 ? easing(reverse ? 1 - p : p) : 0) * target);
- }
- };
- // need 'api' to call onupdate callback
- const api = createObject({
- interpolate,
- target: {
- get: () => (reverse ? 0 : target),
- set: value => {
- // is initial value
- if (target === null) {
- target = value;
- api.onupdate(value);
- api.oncomplete(value);
- return;
- }
- // want to tween to a smaller value and have a current value
- if (value < target) {
- target = 1;
- reverse = true;
- } else {
- // not tweening to a smaller value
- reverse = false;
- target = value;
- }
- // let's go!
- resting = false;
- start = null;
- },
- },
- resting: {
- get: () => resting,
- },
- onupdate: value => {},
- oncomplete: value => {},
- });
- return api;
- };
- const animator = {
- spring,
- tween,
- };
- /*
- { type: 'spring', stiffness: .5, damping: .75, mass: 10 };
- { translation: { type: 'spring', ... }, ... }
- { translation: { x: { type: 'spring', ... } } }
- */
- const createAnimator = (definition, category, property) => {
- // default is single definition
- // we check if transform is set, if so, we check if property is set
- const def =
- definition[category] && typeof definition[category][property] === 'object'
- ? definition[category][property]
- : definition[category] || definition;
- const type = typeof def === 'string' ? def : def.type;
- const props = typeof def === 'object' ? { ...def } : {};
- return animator[type] ? animator[type](props) : null;
- };
- const addGetSet = (keys, obj, props, overwrite = false) => {
- obj = Array.isArray(obj) ? obj : [obj];
- obj.forEach(o => {
- keys.forEach(key => {
- let name = key;
- let getter = () => props[key];
- let setter = value => (props[key] = value);
- if (typeof key === 'object') {
- name = key.key;
- getter = key.getter || getter;
- setter = key.setter || setter;
- }
- if (o[name] && !overwrite) {
- return;
- }
- o[name] = {
- get: getter,
- set: setter,
- };
- });
- });
- };
- // add to state,
- // add getters and setters to internal and external api (if not set)
- // setup animators
- const animations = ({ mixinConfig, viewProps, viewInternalAPI, viewExternalAPI }) => {
- // initial properties
- const initialProps = { ...viewProps };
- // list of all active animations
- const animations = [];
- // setup animators
- forin(mixinConfig, (property, animation) => {
- const animator = createAnimator(animation);
- if (!animator) {
- return;
- }
- // when the animator updates, update the view state value
- animator.onupdate = value => {
- viewProps[property] = value;
- };
- // set animator target
- animator.target = initialProps[property];
- // when value is set, set the animator target value
- const prop = {
- key: property,
- setter: value => {
- // if already at target, we done!
- if (animator.target === value) {
- return;
- }
- animator.target = value;
- },
- getter: () => viewProps[property],
- };
- // add getters and setters
- addGetSet([prop], [viewInternalAPI, viewExternalAPI], viewProps, true);
- // add it to the list for easy updating from the _write method
- animations.push(animator);
- });
- // expose internal write api
- return {
- write: ts => {
- let skipToEndState = document.hidden;
- let resting = true;
- animations.forEach(animation => {
- if (!animation.resting) resting = false;
- animation.interpolate(ts, skipToEndState);
- });
- return resting;
- },
- destroy: () => {},
- };
- };
- const addEvent = element => (type, fn) => {
- element.addEventListener(type, fn);
- };
- const removeEvent = element => (type, fn) => {
- element.removeEventListener(type, fn);
- };
- // mixin
- const listeners = ({
- mixinConfig,
- viewProps,
- viewInternalAPI,
- viewExternalAPI,
- viewState,
- view,
- }) => {
- const events = [];
- const add = addEvent(view.element);
- const remove = removeEvent(view.element);
- viewExternalAPI.on = (type, fn) => {
- events.push({
- type,
- fn,
- });
- add(type, fn);
- };
- viewExternalAPI.off = (type, fn) => {
- events.splice(events.findIndex(event => event.type === type && event.fn === fn), 1);
- remove(type, fn);
- };
- return {
- write: () => {
- // not busy
- return true;
- },
- destroy: () => {
- events.forEach(event => {
- remove(event.type, event.fn);
- });
- },
- };
- };
- // add to external api and link to props
- const apis = ({ mixinConfig, viewProps, viewExternalAPI }) => {
- addGetSet(mixinConfig, viewExternalAPI, viewProps);
- };
- const isDefined = value => value != null;
- // add to state,
- // add getters and setters to internal and external api (if not set)
- // set initial state based on props in viewProps
- // apply as transforms each frame
- const defaults = {
- opacity: 1,
- scaleX: 1,
- scaleY: 1,
- translateX: 0,
- translateY: 0,
- rotateX: 0,
- rotateY: 0,
- rotateZ: 0,
- originX: 0,
- originY: 0,
- };
- const styles = ({ mixinConfig, viewProps, viewInternalAPI, viewExternalAPI, view }) => {
- // initial props
- const initialProps = { ...viewProps };
- // current props
- const currentProps = {};
- // we will add those properties to the external API and link them to the viewState
- addGetSet(mixinConfig, [viewInternalAPI, viewExternalAPI], viewProps);
- // override rect on internal and external rect getter so it takes in account transforms
- const getOffset = () => [viewProps['translateX'] || 0, viewProps['translateY'] || 0];
- const getScale = () => [viewProps['scaleX'] || 0, viewProps['scaleY'] || 0];
- const getRect = () =>
- view.rect ? getViewRect(view.rect, view.childViews, getOffset(), getScale()) : null;
- viewInternalAPI.rect = { get: getRect };
- viewExternalAPI.rect = { get: getRect };
- // apply view props
- mixinConfig.forEach(key => {
- viewProps[key] =
- typeof initialProps[key] === 'undefined' ? defaults[key] : initialProps[key];
- });
- // expose api
- return {
- write: () => {
- // see if props have changed
- if (!propsHaveChanged(currentProps, viewProps)) {
- return;
- }
- // moves element to correct position on screen
- applyStyles(view.element, viewProps);
- // store new transforms
- Object.assign(currentProps, { ...viewProps });
- // no longer busy
- return true;
- },
- destroy: () => {},
- };
- };
- const propsHaveChanged = (currentProps, newProps) => {
- // different amount of keys
- if (Object.keys(currentProps).length !== Object.keys(newProps).length) {
- return true;
- }
- // lets analyze the individual props
- for (const prop in newProps) {
- if (newProps[prop] !== currentProps[prop]) {
- return true;
- }
- }
- return false;
- };
- const applyStyles = (
- element,
- {
- opacity,
- perspective,
- translateX,
- translateY,
- scaleX,
- scaleY,
- rotateX,
- rotateY,
- rotateZ,
- originX,
- originY,
- width,
- height,
- }
- ) => {
- let transforms = '';
- let styles = '';
- // handle transform origin
- if (isDefined(originX) || isDefined(originY)) {
- styles += `transform-origin: ${originX || 0}px ${originY || 0}px;`;
- }
- // transform order is relevant
- // 0. perspective
- if (isDefined(perspective)) {
- transforms += `perspective(${perspective}px) `;
- }
- // 1. translate
- if (isDefined(translateX) || isDefined(translateY)) {
- transforms += `translate3d(${translateX || 0}px, ${translateY || 0}px, 0) `;
- }
- // 2. scale
- if (isDefined(scaleX) || isDefined(scaleY)) {
- transforms += `scale3d(${isDefined(scaleX) ? scaleX : 1}, ${
- isDefined(scaleY) ? scaleY : 1
- }, 1) `;
- }
- // 3. rotate
- if (isDefined(rotateZ)) {
- transforms += `rotateZ(${rotateZ}rad) `;
- }
- if (isDefined(rotateX)) {
- transforms += `rotateX(${rotateX}rad) `;
- }
- if (isDefined(rotateY)) {
- transforms += `rotateY(${rotateY}rad) `;
- }
- // add transforms
- if (transforms.length) {
- styles += `transform:${transforms};`;
- }
- // add opacity
- if (isDefined(opacity)) {
- styles += `opacity:${opacity};`;
- // if we reach zero, we make the element inaccessible
- if (opacity === 0) {
- styles += `visibility:hidden;`;
- }
- // if we're below 100% opacity this element can't be clicked
- if (opacity < 1) {
- styles += `pointer-events:none;`;
- }
- }
- // add height
- if (isDefined(height)) {
- styles += `height:${height}px;`;
- }
- // add width
- if (isDefined(width)) {
- styles += `width:${width}px;`;
- }
- // apply styles
- const elementCurrentStyle = element.elementCurrentStyle || '';
- // if new styles does not match current styles, lets update!
- if (styles.length !== elementCurrentStyle.length || styles !== elementCurrentStyle) {
- element.style.cssText = styles;
- // store current styles so we can compare them to new styles later on
- // _not_ getting the style value is faster
- element.elementCurrentStyle = styles;
- }
- };
- const Mixins = {
- styles,
- listeners,
- animations,
- apis,
- };
- const updateRect = (rect = {}, element = {}, style = {}) => {
- if (!element.layoutCalculated) {
- rect.paddingTop = parseInt(style.paddingTop, 10) || 0;
- rect.marginTop = parseInt(style.marginTop, 10) || 0;
- rect.marginRight = parseInt(style.marginRight, 10) || 0;
- rect.marginBottom = parseInt(style.marginBottom, 10) || 0;
- rect.marginLeft = parseInt(style.marginLeft, 10) || 0;
- element.layoutCalculated = true;
- }
- rect.left = element.offsetLeft || 0;
- rect.top = element.offsetTop || 0;
- rect.width = element.offsetWidth || 0;
- rect.height = element.offsetHeight || 0;
- rect.right = rect.left + rect.width;
- rect.bottom = rect.top + rect.height;
- rect.scrollTop = element.scrollTop;
- rect.hidden = element.offsetParent === null;
- return rect;
- };
- const createView =
- // default view definition
- ({
- // element definition
- tag = 'div',
- name = null,
- attributes = {},
- // view interaction
- read = () => {},
- write = () => {},
- create = () => {},
- destroy = () => {},
- // hooks
- filterFrameActionsForChild = (child, actions) => actions,
- didCreateView = () => {},
- didWriteView = () => {},
- // rect related
- ignoreRect = false,
- ignoreRectUpdate = false,
- // mixins
- mixins = [],
- } = {}) => (
- // each view requires reference to store
- store,
- // specific properties for this view
- props = {}
- ) => {
- // root element should not be changed
- const element = createElement(tag, `filepond--${name}`, attributes);
- // style reference should also not be changed
- const style = window.getComputedStyle(element, null);
- // element rectangle
- const rect = updateRect();
- let frameRect = null;
- // rest state
- let isResting = false;
- // pretty self explanatory
- const childViews = [];
- // loaded mixins
- const activeMixins = [];
- // references to created children
- const ref = {};
- // state used for each instance
- const state = {};
- // list of writers that will be called to update this view
- const writers = [
- write, // default writer
- ];
- const readers = [
- read, // default reader
- ];
- const destroyers = [
- destroy, // default destroy
- ];
- // core view methods
- const getElement = () => element;
- const getChildViews = () => childViews.concat();
- const getReference = () => ref;
- const createChildView = store => (view, props) => view(store, props);
- const getRect = () => {
- if (frameRect) {
- return frameRect;
- }
- frameRect = getViewRect(rect, childViews, [0, 0], [1, 1]);
- return frameRect;
- };
- const getStyle = () => style;
- /**
- * Read data from DOM
- * @private
- */
- const _read = () => {
- frameRect = null;
- // read child views
- childViews.forEach(child => child._read());
- const shouldUpdate = !(ignoreRectUpdate && rect.width && rect.height);
- if (shouldUpdate) {
- updateRect(rect, element, style);
- }
- // readers
- const api = { root: internalAPI, props, rect };
- readers.forEach(reader => reader(api));
- };
- /**
- * Write data to DOM
- * @private
- */
- const _write = (ts, frameActions, shouldOptimize) => {
- // if no actions, we assume that the view is resting
- let resting = frameActions.length === 0;
- // writers
- writers.forEach(writer => {
- const writerResting = writer({
- props,
- root: internalAPI,
- actions: frameActions,
- timestamp: ts,
- shouldOptimize,
- });
- if (writerResting === false) {
- resting = false;
- }
- });
- // run mixins
- activeMixins.forEach(mixin => {
- // if one of the mixins is still busy after write operation, we are not resting
- const mixinResting = mixin.write(ts);
- if (mixinResting === false) {
- resting = false;
- }
- });
- // updates child views that are currently attached to the DOM
- childViews
- .filter(child => !!child.element.parentNode)
- .forEach(child => {
- // if a child view is not resting, we are not resting
- const childResting = child._write(
- ts,
- filterFrameActionsForChild(child, frameActions),
- shouldOptimize
- );
- if (!childResting) {
- resting = false;
- }
- });
- // append new elements to DOM and update those
- childViews
- //.filter(child => !child.element.parentNode)
- .forEach((child, index) => {
- // skip
- if (child.element.parentNode) {
- return;
- }
- // append to DOM
- internalAPI.appendChild(child.element, index);
- // call read (need to know the size of these elements)
- child._read();
- // re-call write
- child._write(
- ts,
- filterFrameActionsForChild(child, frameActions),
- shouldOptimize
- );
- // we just added somthing to the dom, no rest
- resting = false;
- });
- // update resting state
- isResting = resting;
- didWriteView({
- props,
- root: internalAPI,
- actions: frameActions,
- timestamp: ts,
- });
- // let parent know if we are resting
- return resting;
- };
- const _destroy = () => {
- activeMixins.forEach(mixin => mixin.destroy());
- destroyers.forEach(destroyer => {
- destroyer({ root: internalAPI, props });
- });
- childViews.forEach(child => child._destroy());
- };
- // sharedAPI
- const sharedAPIDefinition = {
- element: {
- get: getElement,
- },
- style: {
- get: getStyle,
- },
- childViews: {
- get: getChildViews,
- },
- };
- // private API definition
- const internalAPIDefinition = {
- ...sharedAPIDefinition,
- rect: {
- get: getRect,
- },
- // access to custom children references
- ref: {
- get: getReference,
- },
- // dom modifiers
- is: needle => name === needle,
- appendChild: appendChild(element),
- createChildView: createChildView(store),
- linkView: view => {
- childViews.push(view);
- return view;
- },
- unlinkView: view => {
- childViews.splice(childViews.indexOf(view), 1);
- },
- appendChildView: appendChildView(element, childViews),
- removeChildView: removeChildView(element, childViews),
- registerWriter: writer => writers.push(writer),
- registerReader: reader => readers.push(reader),
- registerDestroyer: destroyer => destroyers.push(destroyer),
- invalidateLayout: () => (element.layoutCalculated = false),
- // access to data store
- dispatch: store.dispatch,
- query: store.query,
- };
- // public view API methods
- const externalAPIDefinition = {
- element: {
- get: getElement,
- },
- childViews: {
- get: getChildViews,
- },
- rect: {
- get: getRect,
- },
- resting: {
- get: () => isResting,
- },
- isRectIgnored: () => ignoreRect,
- _read,
- _write,
- _destroy,
- };
- // mixin API methods
- const mixinAPIDefinition = {
- ...sharedAPIDefinition,
- rect: {
- get: () => rect,
- },
- };
- // add mixin functionality
- Object.keys(mixins)
- .sort((a, b) => {
- // move styles to the back of the mixin list (so adjustments of other mixins are applied to the props correctly)
- if (a === 'styles') {
- return 1;
- } else if (b === 'styles') {
- return -1;
- }
- return 0;
- })
- .forEach(key => {
- const mixinAPI = Mixins[key]({
- mixinConfig: mixins[key],
- viewProps: props,
- viewState: state,
- viewInternalAPI: internalAPIDefinition,
- viewExternalAPI: externalAPIDefinition,
- view: createObject(mixinAPIDefinition),
- });
- if (mixinAPI) {
- activeMixins.push(mixinAPI);
- }
- });
- // construct private api
- const internalAPI = createObject(internalAPIDefinition);
- // create the view
- create({
- root: internalAPI,
- props,
- });
- // append created child views to root node
- const childCount = getChildCount(element); // need to know the current child count so appending happens in correct order
- childViews.forEach((child, index) => {
- internalAPI.appendChild(child.element, childCount + index);
- });
- // call did create
- didCreateView(internalAPI);
- // expose public api
- return createObject(externalAPIDefinition);
- };
- const createPainter = (read, write, fps = 60) => {
- const name = '__framePainter';
- // set global painter
- if (window[name]) {
- window[name].readers.push(read);
- window[name].writers.push(write);
- return;
- }
- window[name] = {
- readers: [read],
- writers: [write],
- };
- const painter = window[name];
- const interval = 1000 / fps;
- let last = null;
- let id = null;
- let requestTick = null;
- let cancelTick = null;
- const setTimerType = () => {
- if (document.hidden) {
- requestTick = () => window.setTimeout(() => tick(performance.now()), interval);
- cancelTick = () => window.clearTimeout(id);
- } else {
- requestTick = () => window.requestAnimationFrame(tick);
- cancelTick = () => window.cancelAnimationFrame(id);
- }
- };
- document.addEventListener('visibilitychange', () => {
- if (cancelTick) cancelTick();
- setTimerType();
- tick(performance.now());
- });
- const tick = ts => {
- // queue next tick
- id = requestTick(tick);
- // limit fps
- if (!last) {
- last = ts;
- }
- const delta = ts - last;
- if (delta <= interval) {
- // skip frame
- return;
- }
- // align next frame
- last = ts - (delta % interval);
- // update view
- painter.readers.forEach(read => read());
- painter.writers.forEach(write => write(ts));
- };
- setTimerType();
- tick(performance.now());
- return {
- pause: () => {
- cancelTick(id);
- },
- };
- };
- const createRoute = (routes, fn) => ({ root, props, actions = [], timestamp, shouldOptimize }) => {
- actions
- .filter(action => routes[action.type])
- .forEach(action =>
- routes[action.type]({ root, props, action: action.data, timestamp, shouldOptimize })
- );
- if (fn) {
- fn({ root, props, actions, timestamp, shouldOptimize });
- }
- };
- const insertBefore = (newNode, referenceNode) =>
- referenceNode.parentNode.insertBefore(newNode, referenceNode);
- const insertAfter = (newNode, referenceNode) => {
- return referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
- };
- const isArray = value => Array.isArray(value);
- const isEmpty = value => value == null;
- const trim = str => str.trim();
- const toString = value => '' + value;
- const toArray = (value, splitter = ',') => {
- if (isEmpty(value)) {
- return [];
- }
- if (isArray(value)) {
- return value;
- }
- return toString(value)
- .split(splitter)
- .map(trim)
- .filter(str => str.length);
- };
- const isBoolean = value => typeof value === 'boolean';
- const toBoolean = value => (isBoolean(value) ? value : value === 'true');
- const isString = value => typeof value === 'string';
- const toNumber = value =>
- isNumber(value) ? value : isString(value) ? toString(value).replace(/[a-z]+/gi, '') : 0;
- const toInt = value => parseInt(toNumber(value), 10);
- const toFloat = value => parseFloat(toNumber(value));
- const isInt = value => isNumber(value) && isFinite(value) && Math.floor(value) === value;
- const toBytes = (value, base = 1000) => {
- // is in bytes
- if (isInt(value)) {
- return value;
- }
- // is natural file size
- let naturalFileSize = toString(value).trim();
- // if is value in megabytes
- if (/MB$/i.test(naturalFileSize)) {
- naturalFileSize = naturalFileSize.replace(/MB$i/, '').trim();
- return toInt(naturalFileSize) * base * base;
- }
- // if is value in kilobytes
- if (/KB/i.test(naturalFileSize)) {
- naturalFileSize = naturalFileSize.replace(/KB$i/, '').trim();
- return toInt(naturalFileSize) * base;
- }
- return toInt(naturalFileSize);
- };
- const isFunction = value => typeof value === 'function';
- const toFunctionReference = string => {
- let ref = self;
- let levels = string.split('.');
- let level = null;
- while ((level = levels.shift())) {
- ref = ref[level];
- if (!ref) {
- return null;
- }
- }
- return ref;
- };
- const methods = {
- process: 'POST',
- patch: 'PATCH',
- revert: 'DELETE',
- fetch: 'GET',
- restore: 'GET',
- load: 'GET',
- };
- const createServerAPI = outline => {
- const api = {};
- api.url = isString(outline) ? outline : outline.url || '';
- api.timeout = outline.timeout ? parseInt(outline.timeout, 10) : 0;
- api.headers = outline.headers ? outline.headers : {};
- forin(methods, key => {
- api[key] = createAction(key, outline[key], methods[key], api.timeout, api.headers);
- });
- // remove process if no url or process on outline
- api.process = outline.process || isString(outline) || outline.url ? api.process : null;
- // special treatment for remove
- api.remove = outline.remove || null;
- // remove generic headers from api object
- delete api.headers;
- return api;
- };
- const createAction = (name, outline, method, timeout, headers) => {
- // is explicitely set to null so disable
- if (outline === null) {
- return null;
- }
- // if is custom function, done! Dev handles everything.
- if (typeof outline === 'function') {
- return outline;
- }
- // build action object
- const action = {
- url: method === 'GET' || method === 'PATCH' ? `?${name}=` : '',
- method,
- headers,
- withCredentials: false,
- timeout,
- onload: null,
- ondata: null,
- onerror: null,
- };
- // is a single url
- if (isString(outline)) {
- action.url = outline;
- return action;
- }
- // overwrite
- Object.assign(action, outline);
- // see if should reformat headers;
- if (isString(action.headers)) {
- const parts = action.headers.split(/:(.+)/);
- action.headers = {
- header: parts[0],
- value: parts[1],
- };
- }
- // if is bool withCredentials
- action.withCredentials = toBoolean(action.withCredentials);
- return action;
- };
- const toServerAPI = value => createServerAPI(value);
- const isNull = value => value === null;
- const isObject = value => typeof value === 'object' && value !== null;
- const isAPI = value => {
- return (
- isObject(value) &&
- isString(value.url) &&
- isObject(value.process) &&
- isObject(value.revert) &&
- isObject(value.restore) &&
- isObject(value.fetch)
- );
- };
- const getType = value => {
- if (isArray(value)) {
- return 'array';
- }
- if (isNull(value)) {
- return 'null';
- }
- if (isInt(value)) {
- return 'int';
- }
- if (/^[0-9]+ ?(?:GB|MB|KB)$/gi.test(value)) {
- return 'bytes';
- }
- if (isAPI(value)) {
- return 'api';
- }
- return typeof value;
- };
- const replaceSingleQuotes = str =>
- str
- .replace(/{\s*'/g, '{"')
- .replace(/'\s*}/g, '"}')
- .replace(/'\s*:/g, '":')
- .replace(/:\s*'/g, ':"')
- .replace(/,\s*'/g, ',"')
- .replace(/'\s*,/g, '",');
- const conversionTable = {
- array: toArray,
- boolean: toBoolean,
- int: value => (getType(value) === 'bytes' ? toBytes(value) : toInt(value)),
- number: toFloat,
- float: toFloat,
- bytes: toBytes,
- string: value => (isFunction(value) ? value : toString(value)),
- function: value => toFunctionReference(value),
- serverapi: toServerAPI,
- object: value => {
- try {
- return JSON.parse(replaceSingleQuotes(value));
- } catch (e) {
- return null;
- }
- },
- };
- const convertTo = (value, type) => conversionTable[type](value);
- const getValueByType = (newValue, defaultValue, valueType) => {
- // can always assign default value
- if (newValue === defaultValue) {
- return newValue;
- }
- // get the type of the new value
- let newValueType = getType(newValue);
- // is valid type?
- if (newValueType !== valueType) {
- // is string input, let's attempt to convert
- const convertedValue = convertTo(newValue, valueType);
- // what is the type now
- newValueType = getType(convertedValue);
- // no valid conversions found
- if (convertedValue === null) {
- throw `Trying to assign value with incorrect type to "${option}", allowed type: "${valueType}"`;
- } else {
- newValue = convertedValue;
- }
- }
- // assign new value
- return newValue;
- };
- const createOption = (defaultValue, valueType) => {
- let currentValue = defaultValue;
- return {
- enumerable: true,
- get: () => currentValue,
- set: newValue => {
- currentValue = getValueByType(newValue, defaultValue, valueType);
- },
- };
- };
- const createOptions = options => {
- const obj = {};
- forin(options, prop => {
- const optionDefinition = options[prop];
- obj[prop] = createOption(optionDefinition[0], optionDefinition[1]);
- });
- return createObject(obj);
- };
- const createInitialState = options => ({
- // model
- items: [],
- // timeout used for calling update items
- listUpdateTimeout: null,
- // timeout used for stacking metadata updates
- itemUpdateTimeout: null,
- // queue of items waiting to be processed
- processingQueue: [],
- // options
- options: createOptions(options),
- });
- const fromCamels = (string, separator = '-') =>
- string
- .split(/(?=[A-Z])/)
- .map(part => part.toLowerCase())
- .join(separator);
- const createOptionAPI = (store, options) => {
- const obj = {};
- forin(options, key => {
- obj[key] = {
- get: () => store.getState().options[key],
- set: value => {
- store.dispatch(`SET_${fromCamels(key, '_').toUpperCase()}`, {
- value,
- });
- },
- };
- });
- return obj;
- };
- const createOptionActions = options => (dispatch, query, state) => {
- const obj = {};
- forin(options, key => {
- const name = fromCamels(key, '_').toUpperCase();
- obj[`SET_${name}`] = action => {
- try {
- state.options[key] = action.value;
- } catch (e) {
- // nope, failed
- }
- // we successfully set the value of this option
- dispatch(`DID_SET_${name}`, { value: state.options[key] });
- };
- });
- return obj;
- };
- const createOptionQueries = options => state => {
- const obj = {};
- forin(options, key => {
- obj[`GET_${fromCamels(key, '_').toUpperCase()}`] = action => state.options[key];
- });
- return obj;
- };
- const InteractionMethod = {
- API: 1,
- DROP: 2,
- BROWSE: 3,
- PASTE: 4,
- NONE: 5,
- };
- const getUniqueId = () =>
- Math.random()
- .toString(36)
- .substring(2, 11);
- const arrayRemove = (arr, index) => arr.splice(index, 1);
- const run = (cb, sync) => {
- if (sync) {
- cb();
- } else if (document.hidden) {
- Promise.resolve(1).then(cb);
- } else {
- setTimeout(cb, 0);
- }
- };
- const on = () => {
- const listeners = [];
- const off = (event, cb) => {
- arrayRemove(
- listeners,
- listeners.findIndex(listener => listener.event === event && (listener.cb === cb || !cb))
- );
- };
- const fire = (event, args, sync) => {
- listeners
- .filter(listener => listener.event === event)
- .map(listener => listener.cb)
- .forEach(cb => run(() => cb(...args), sync));
- };
- return {
- fireSync: (event, ...args) => {
- fire(event, args, true);
- },
- fire: (event, ...args) => {
- fire(event, args, false);
- },
- on: (event, cb) => {
- listeners.push({ event, cb });
- },
- onOnce: (event, cb) => {
- listeners.push({
- event,
- cb: (...args) => {
- off(event, cb);
- cb(...args);
- },
- });
- },
- off,
- };
- };
- const copyObjectPropertiesToObject = (src, target, excluded) => {
- Object.getOwnPropertyNames(src)
- .filter(property => !excluded.includes(property))
- .forEach(key =>
- Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(src, key))
- );
- };
- const PRIVATE = [
- 'fire',
- 'process',
- 'revert',
- 'load',
- 'on',
- 'off',
- 'onOnce',
- 'retryLoad',
- 'extend',
- 'archive',
- 'archived',
- 'release',
- 'released',
- 'requestProcessing',
- 'freeze',
- ];
- const createItemAPI = item => {
- const api = {};
- copyObjectPropertiesToObject(item, api, PRIVATE);
- return api;
- };
- const removeReleasedItems = items => {
- items.forEach((item, index) => {
- if (item.released) {
- arrayRemove(items, index);
- }
- });
- };
- const ItemStatus = {
- INIT: 1,
- IDLE: 2,
- PROCESSING_QUEUED: 9,
- PROCESSING: 3,
- PROCESSING_COMPLETE: 5,
- PROCESSING_ERROR: 6,
- PROCESSING_REVERT_ERROR: 10,
- LOADING: 7,
- LOAD_ERROR: 8,
- };
- const FileOrigin = {
- INPUT: 1,
- LIMBO: 2,
- LOCAL: 3,
- };
- const getNonNumeric = str => /[^0-9]+/.exec(str);
- const getDecimalSeparator = () => getNonNumeric((1.1).toLocaleString())[0];
- const getThousandsSeparator = () => {
- // Added for browsers that do not return the thousands separator (happend on native browser Android 4.4.4)
- // We check against the normal toString output and if they're the same return a comma when decimal separator is a dot
- const decimalSeparator = getDecimalSeparator();
- const thousandsStringWithSeparator = (1000.0).toLocaleString();
- const thousandsStringWithoutSeparator = (1000.0).toString();
- if (thousandsStringWithSeparator !== thousandsStringWithoutSeparator) {
- return getNonNumeric(thousandsStringWithSeparator)[0];
- }
- return decimalSeparator === '.' ? ',' : '.';
- };
- const Type = {
- BOOLEAN: 'boolean',
- INT: 'int',
- NUMBER: 'number',
- STRING: 'string',
- ARRAY: 'array',
- OBJECT: 'object',
- FUNCTION: 'function',
- ACTION: 'action',
- SERVER_API: 'serverapi',
- REGEX: 'regex',
- };
- // all registered filters
- const filters = [];
- // loops over matching filters and passes options to each filter, returning the mapped results
- const applyFilterChain = (key, value, utils) =>
- new Promise((resolve, reject) => {
- // find matching filters for this key
- const matchingFilters = filters.filter(f => f.key === key).map(f => f.cb);
- // resolve now
- if (matchingFilters.length === 0) {
- resolve(value);
- return;
- }
- // first filter to kick things of
- const initialFilter = matchingFilters.shift();
- // chain filters
- matchingFilters
- .reduce(
- // loop over promises passing value to next promise
- (current, next) => current.then(value => next(value, utils)),
- // call initial filter, will return a promise
- initialFilter(value, utils)
- // all executed
- )
- .then(value => resolve(value))
- .catch(error => reject(error));
- });
- const applyFilters = (key, value, utils) =>
- filters.filter(f => f.key === key).map(f => f.cb(value, utils));
- // adds a new filter to the list
- const addFilter = (key, cb) => filters.push({ key, cb });
- const extendDefaultOptions = additionalOptions => Object.assign(defaultOptions, additionalOptions);
- const getOptions = () => ({ ...defaultOptions });
- const setOptions = opts => {
- forin(opts, (key, value) => {
- // key does not exist, so this option cannot be set
- if (!defaultOptions[key]) {
- return;
- }
- defaultOptions[key][0] = getValueByType(
- value,
- defaultOptions[key][0],
- defaultOptions[key][1]
- );
- });
- };
- // default options on app
- const defaultOptions = {
- // the id to add to the root element
- id: [null, Type.STRING],
- // input field name to use
- name: ['filepond', Type.STRING],
- // disable the field
- disabled: [false, Type.BOOLEAN],
- // classname to put on wrapper
- className: [null, Type.STRING],
- // is the field required
- required: [false, Type.BOOLEAN],
- // Allow media capture when value is set
- captureMethod: [null, Type.STRING],
- // - "camera", "microphone" or "camcorder",
- // - Does not work with multiple on apple devices
- // - If set, acceptedFileTypes must be made to match with media wildcard "image/*", "audio/*" or "video/*"
- // sync `acceptedFileTypes` property with `accept` attribute
- allowSyncAcceptAttribute: [true, Type.BOOLEAN],
- // Feature toggles
- allowDrop: [true, Type.BOOLEAN], // Allow dropping of files
- allowBrowse: [true, Type.BOOLEAN], // Allow browsing the file system
- allowPaste: [true, Type.BOOLEAN], // Allow pasting files
- allowMultiple: [false, Type.BOOLEAN], // Allow multiple files (disabled by default, as multiple attribute is also required on input to allow multiple)
- allowReplace: [true, Type.BOOLEAN], // Allow dropping a file on other file to replace it (only works when multiple is set to false)
- allowRevert: [true, Type.BOOLEAN], // Allows user to revert file upload
- allowRemove: [true, Type.BOOLEAN], // Allow user to remove a file
- allowProcess: [true, Type.BOOLEAN], // Allows user to process a file, when set to false, this removes the file upload button
- allowReorder: [false, Type.BOOLEAN], // Allow reordering of files
- allowDirectoriesOnly: [false, Type.BOOLEAN], // Allow only selecting directories with browse (no support for filtering dnd at this point)
- // Try store file if `server` not set
- storeAsFile: [false, Type.BOOLEAN],
- // Revert mode
- forceRevert: [false, Type.BOOLEAN], // Set to 'force' to require the file to be reverted before removal
- // Input requirements
- maxFiles: [null, Type.INT], // Max number of files
- checkValidity: [false, Type.BOOLEAN], // Enables custom validity messages
- // Where to put file
- itemInsertLocationFreedom: [true, Type.BOOLEAN], // Set to false to always add items to begin or end of list
- itemInsertLocation: ['before', Type.STRING], // Default index in list to add items that have been dropped at the top of the list
- itemInsertInterval: [75, Type.INT],
- // Drag 'n Drop related
- dropOnPage: [false, Type.BOOLEAN], // Allow dropping of files anywhere on page (prevents browser from opening file if dropped outside of Up)
- dropOnElement: [true, Type.BOOLEAN], // Drop needs to happen on element (set to false to also load drops outside of Up)
- dropValidation: [false, Type.BOOLEAN], // Enable or disable validating files on drop
- ignoredFiles: [['.ds_store', 'thumbs.db', 'desktop.ini'], Type.ARRAY],
- // Upload related
- instantUpload: [true, Type.BOOLEAN], // Should upload files immediately on drop
- maxParallelUploads: [2, Type.INT], // Maximum files to upload in parallel
- allowMinimumUploadDuration: [true, Type.BOOLEAN], // if true uploads take at least 750 ms, this ensures the user sees the upload progress giving trust the upload actually happened
- // Chunks
- chunkUploads: [false, Type.BOOLEAN], // Enable chunked uploads
- chunkForce: [false, Type.BOOLEAN], // Force use of chunk uploads even for files smaller than chunk size
- chunkSize: [5000000, Type.INT], // Size of chunks (5MB default)
- chunkRetryDelays: [[500, 1000, 3000], Type.ARRAY], // Amount of times to retry upload of a chunk when it fails
- // The server api end points to use for uploading (see docs)
- server: [null, Type.SERVER_API],
- // File size calculations, can set to 1024, this is only used for display, properties use file size base 1000
- fileSizeBase: [1000, Type.INT],
- // Labels and status messages
- labelFileSizeBytes: ['bytes', Type.STRING],
- labelFileSizeKilobytes: ['KB', Type.STRING],
- labelFileSizeMegabytes: ['MB', Type.STRING],
- labelFileSizeGigabytes: ['GB', Type.STRING],
- labelDecimalSeparator: [getDecimalSeparator(), Type.STRING], // Default is locale separator
- labelThousandsSeparator: [getThousandsSeparator(), Type.STRING], // Default is locale separator
- labelIdle: [
- 'Drag & Drop your files or <span class="filepond--label-action">Browse</span>',
- Type.STRING,
- ],
- labelInvalidField: ['Field contains invalid files', Type.STRING],
- labelFileWaitingForSize: ['Waiting for size', Type.STRING],
- labelFileSizeNotAvailable: ['Size not available', Type.STRING],
- labelFileCountSingular: ['file in list', Type.STRING],
- labelFileCountPlural: ['files in list', Type.STRING],
- labelFileLoading: ['Loading', Type.STRING],
- labelFileAdded: ['Added', Type.STRING], // assistive only
- labelFileLoadError: ['Error during load', Type.STRING],
- labelFileRemoved: ['Removed', Type.STRING], // assistive only
- labelFileRemoveError: ['Error during remove', Type.STRING],
- labelFileProcessing: ['Uploading', Type.STRING],
- labelFileProcessingComplete: ['Upload complete', Type.STRING],
- labelFileProcessingAborted: ['Upload cancelled', Type.STRING],
- labelFileProcessingError: ['Error during upload', Type.STRING],
- labelFileProcessingRevertError: ['Error during revert', Type.STRING],
- labelTapToCancel: ['tap to cancel', Type.STRING],
- labelTapToRetry: ['tap to retry', Type.STRING],
- labelTapToUndo: ['tap to undo', Type.STRING],
- labelButtonRemoveItem: ['Remove', Type.STRING],
- labelButtonAbortItemLoad: ['Abort', Type.STRING],
- labelButtonRetryItemLoad: ['Retry', Type.STRING],
- labelButtonAbortItemProcessing: ['Cancel', Type.STRING],
- labelButtonUndoItemProcessing: ['Undo', Type.STRING],
- labelButtonRetryItemProcessing: ['Retry', Type.STRING],
- labelButtonProcessItem: ['Upload', Type.STRING],
- // make sure width and height plus viewpox are even numbers so icons are nicely centered
- iconRemove: [
- '<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>',
- Type.STRING,
- ],
- iconProcess: [
- '<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>',
- Type.STRING,
- ],
- iconRetry: [
- '<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>',
- Type.STRING,
- ],
- iconUndo: [
- '<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>',
- Type.STRING,
- ],
- iconDone: [
- '<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>',
- Type.STRING,
- ],
- // event handlers
- oninit: [null, Type.FUNCTION],
- onwarning: [null, Type.FUNCTION],
- onerror: [null, Type.FUNCTION],
- onactivatefile: [null, Type.FUNCTION],
- oninitfile: [null, Type.FUNCTION],
- onaddfilestart: [null, Type.FUNCTION],
- onaddfileprogress: [null, Type.FUNCTION],
- onaddfile: [null, Type.FUNCTION],
- onprocessfilestart: [null, Type.FUNCTION],
- onprocessfileprogress: [null, Type.FUNCTION],
- onprocessfileabort: [null, Type.FUNCTION],
- onprocessfilerevert: [null, Type.FUNCTION],
- onprocessfile: [null, Type.FUNCTION],
- onprocessfiles: [null, Type.FUNCTION],
- onremovefile: [null, Type.FUNCTION],
- onpreparefile: [null, Type.FUNCTION],
- onupdatefiles: [null, Type.FUNCTION],
- onreorderfiles: [null, Type.FUNCTION],
- // hooks
- beforeDropFile: [null, Type.FUNCTION],
- beforeAddFile: [null, Type.FUNCTION],
- beforeRemoveFile: [null, Type.FUNCTION],
- beforePrepareFile: [null, Type.FUNCTION],
- // styles
- stylePanelLayout: [null, Type.STRING], // null 'integrated', 'compact', 'circle'
- stylePanelAspectRatio: [null, Type.STRING], // null or '3:2' or 1
- styleItemPanelAspectRatio: [null, Type.STRING],
- styleButtonRemoveItemPosition: ['left', Type.STRING],
- styleButtonProcessItemPosition: ['right', Type.STRING],
- styleLoadIndicatorPosition: ['right', Type.STRING],
- styleProgressIndicatorPosition: ['right', Type.STRING],
- styleButtonRemoveItemAlign: [false, Type.BOOLEAN],
- // custom initial files array
- files: [[], Type.ARRAY],
- // show support by displaying credits
- credits: [['https://pqina.nl/', 'Powered by PQINA'], Type.ARRAY],
- };
- const getItemByQuery = (items, query) => {
- // just return first index
- if (isEmpty(query)) {
- return items[0] || null;
- }
- // query is index
- if (isInt(query)) {
- return items[query] || null;
- }
- // if query is item, get the id
- if (typeof query === 'object') {
- query = query.id;
- }
- // assume query is a string and return item by id
- return items.find(item => item.id === query) || null;
- };
- const getNumericAspectRatioFromString = aspectRatio => {
- if (isEmpty(aspectRatio)) {
- return aspectRatio;
- }
- if (/:/.test(aspectRatio)) {
- const parts = aspectRatio.split(':');
- return parts[1] / parts[0];
- }
- return parseFloat(aspectRatio);
- };
- const getActiveItems = items => items.filter(item => !item.archived);
- const Status = {
- EMPTY: 0,
- IDLE: 1, // waiting
- ERROR: 2, // a file is in error state
- BUSY: 3, // busy processing or loading
- READY: 4, // all files uploaded
- };
- let res = null;
- const canUpdateFileInput = () => {
- if (res === null) {
- try {
- const dataTransfer = new DataTransfer();
- dataTransfer.items.add(new File(['hello world'], 'This_Works.txt'));
- const el = document.createElement('input');
- el.setAttribute('type', 'file');
- el.files = dataTransfer.files;
- res = el.files.length === 1;
- } catch (err) {
- res = false;
- }
- }
- return res;
- };
- const ITEM_ERROR = [
- ItemStatus.LOAD_ERROR,
- ItemStatus.PROCESSING_ERROR,
- ItemStatus.PROCESSING_REVERT_ERROR,
- ];
- const ITEM_BUSY = [
- ItemStatus.LOADING,
- ItemStatus.PROCESSING,
- ItemStatus.PROCESSING_QUEUED,
- ItemStatus.INIT,
- ];
- const ITEM_READY = [ItemStatus.PROCESSING_COMPLETE];
- const isItemInErrorState = item => ITEM_ERROR.includes(item.status);
- const isItemInBusyState = item => ITEM_BUSY.includes(item.status);
- const isItemInReadyState = item => ITEM_READY.includes(item.status);
- const isAsync = state =>
- isObject(state.options.server) &&
- (isObject(state.options.server.process) || isFunction(state.options.server.process));
- const queries = state => ({
- GET_STATUS: () => {
- const items = getActiveItems(state.items);
- const { EMPTY, ERROR, BUSY, IDLE, READY } = Status;
- if (items.length === 0) return EMPTY;
- if (items.some(isItemInErrorState)) return ERROR;
- if (items.some(isItemInBusyState)) return BUSY;
- if (items.some(isItemInReadyState)) return READY;
- return IDLE;
- },
- GET_ITEM: query => getItemByQuery(state.items, query),
- GET_ACTIVE_ITEM: query => getItemByQuery(getActiveItems(state.items), query),
- GET_ACTIVE_ITEMS: () => getActiveItems(state.items),
- GET_ITEMS: () => state.items,
- GET_ITEM_NAME: query => {
- const item = getItemByQuery(state.items, query);
- return item ? item.filename : null;
- },
- GET_ITEM_SIZE: query => {
- const item = getItemByQuery(state.items, query);
- return item ? item.fileSize : null;
- },
- GET_STYLES: () =>
- Object.keys(state.options)
- .filter(key => /^style/.test(key))
- .map(option => ({
- name: option,
- value: state.options[option],
- })),
- GET_PANEL_ASPECT_RATIO: () => {
- const isShapeCircle = /circle/.test(state.options.stylePanelLayout);
- const aspectRatio = isShapeCircle
- ? 1
- : getNumericAspectRatioFromString(state.options.stylePanelAspectRatio);
- return aspectRatio;
- },
- GET_ITEM_PANEL_ASPECT_RATIO: () => state.options.styleItemPanelAspectRatio,
- GET_ITEMS_BY_STATUS: status =>
- getActiveItems(state.items).filter(item => item.status === status),
- GET_TOTAL_ITEMS: () => getActiveItems(state.items).length,
- SHOULD_UPDATE_FILE_INPUT: () =>
- state.options.storeAsFile && canUpdateFileInput() && !isAsync(state),
- IS_ASYNC: () => isAsync(state),
- GET_FILE_SIZE_LABELS: query => ({
- labelBytes: query('GET_LABEL_FILE_SIZE_BYTES') || undefined,
- labelKilobytes: query('GET_LABEL_FILE_SIZE_KILOBYTES') || undefined,
- labelMegabytes: query('GET_LABEL_FILE_SIZE_MEGABYTES') || undefined,
- labelGigabytes: query('GET_LABEL_FILE_SIZE_GIGABYTES') || undefined,
- }),
- });
- const hasRoomForItem = state => {
- const count = getActiveItems(state.items).length;
- // if cannot have multiple items, to add one item it should currently not contain items
- if (!state.options.allowMultiple) {
- return count === 0;
- }
- // if allows multiple items, we check if a max item count has been set, if not, there's no limit
- const maxFileCount = state.options.maxFiles;
- if (maxFileCount === null) {
- return true;
- }
- // we check if the current count is smaller than the max count, if so, another file can still be added
- if (count < maxFileCount) {
- return true;
- }
- // no more room for another file
- return false;
- };
- const limit = (value, min, max) => Math.max(Math.min(max, value), min);
- const arrayInsert = (arr, index, item) => arr.splice(index, 0, item);
- const insertItem = (items, item, index) => {
- if (isEmpty(item)) {
- return null;
- }
- // if index is undefined, append
- if (typeof index === 'undefined') {
- items.push(item);
- return item;
- }
- // limit the index to the size of the items array
- index = limit(index, 0, items.length);
- // add item to array
- arrayInsert(items, index, item);
- // expose
- return item;
- };
- const isBase64DataURI = str =>
- /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s]*)\s*$/i.test(
- str
- );
- const getFilenameFromURL = url =>
- url
- .split('/')
- .pop()
- .split('?')
- .shift();
- const getExtensionFromFilename = name => name.split('.').pop();
- const guesstimateExtension = type => {
- // if no extension supplied, exit here
- if (typeof type !== 'string') {
- return '';
- }
- // get subtype
- const subtype = type.split('/').pop();
- // is svg subtype
- if (/svg/.test(subtype)) {
- return 'svg';
- }
- if (/zip|compressed/.test(subtype)) {
- return 'zip';
- }
- if (/plain/.test(subtype)) {
- return 'txt';
- }
- if (/msword/.test(subtype)) {
- return 'doc';
- }
- // if is valid subtype
- if (/[a-z]+/.test(subtype)) {
- // always use jpg extension
- if (subtype === 'jpeg') {
- return 'jpg';
- }
- // return subtype
- return subtype;
- }
- return '';
- };
- const leftPad = (value, padding = '') => (padding + value).slice(-padding.length);
- const getDateString = (date = new Date()) =>
- `${date.getFullYear()}-${leftPad(date.getMonth() + 1, '00')}-${leftPad(
- date.getDate(),
- '00'
- )}_${leftPad(date.getHours(), '00')}-${leftPad(date.getMinutes(), '00')}-${leftPad(
- date.getSeconds(),
- '00'
- )}`;
- const getFileFromBlob = (blob, filename, type = null, extension = null) => {
- const file =
- typeof type === 'string'
- ? blob.slice(0, blob.size, type)
- : blob.slice(0, blob.size, blob.type);
- file.lastModifiedDate = new Date();
- // copy relative path
- if (blob._relativePath) file._relativePath = blob._relativePath;
- // if blob has name property, use as filename if no filename supplied
- if (!isString(filename)) {
- filename = getDateString();
- }
- // if filename supplied but no extension and filename has extension
- if (filename && extension === null && getExtensionFromFilename(filename)) {
- file.name = filename;
- } else {
- extension = extension || guesstimateExtension(file.type);
- file.name = filename + (extension ? '.' + extension : '');
- }
- return file;
- };
- const getBlobBuilder = () => {
- return (window.BlobBuilder =
- window.BlobBuilder ||
- window.WebKitBlobBuilder ||
- window.MozBlobBuilder ||
- window.MSBlobBuilder);
- };
- const createBlob = (arrayBuffer, mimeType) => {
- const BB = getBlobBuilder();
- if (BB) {
- const bb = new BB();
- bb.append(arrayBuffer);
- return bb.getBlob(mimeType);
- }
- return new Blob([arrayBuffer], {
- type: mimeType,
- });
- };
- const getBlobFromByteStringWithMimeType = (byteString, mimeType) => {
- const ab = new ArrayBuffer(byteString.length);
- const ia = new Uint8Array(ab);
- for (let i = 0; i < byteString.length; i++) {
- ia[i] = byteString.charCodeAt(i);
- }
- return createBlob(ab, mimeType);
- };
- const getMimeTypeFromBase64DataURI = dataURI => {
- return (/^data:(.+);/.exec(dataURI) || [])[1] || null;
- };
- const getBase64DataFromBase64DataURI = dataURI => {
- // get data part of string (remove data:image/jpeg...,)
- const data = dataURI.split(',')[1];
- // remove any whitespace as that causes InvalidCharacterError in IE
- return data.replace(/\s/g, '');
- };
- const getByteStringFromBase64DataURI = dataURI => {
- return atob(getBase64DataFromBase64DataURI(dataURI));
- };
- const getBlobFromBase64DataURI = dataURI => {
- const mimeType = getMimeTypeFromBase64DataURI(dataURI);
- const byteString = getByteStringFromBase64DataURI(dataURI);
- return getBlobFromByteStringWithMimeType(byteString, mimeType);
- };
- const getFileFromBase64DataURI = (dataURI, filename, extension) => {
- return getFileFromBlob(getBlobFromBase64DataURI(dataURI), filename, null, extension);
- };
- const getFileNameFromHeader = header => {
- // test if is content disposition header, if not exit
- if (!/^content-disposition:/i.test(header)) return null;
- // get filename parts
- const matches = header
- .split(/filename=|filename\*=.+''/)
- .splice(1)
- .map(name => name.trim().replace(/^["']|[;"']{0,2}$/g, ''))
- .filter(name => name.length);
- return matches.length ? decodeURI(matches[matches.length - 1]) : null;
- };
- const getFileSizeFromHeader = header => {
- if (/content-length:/i.test(header)) {
- const size = header.match(/[0-9]+/)[0];
- return size ? parseInt(size, 10) : null;
- }
- return null;
- };
- const getTranfserIdFromHeader = header => {
- if (/x-content-transfer-id:/i.test(header)) {
- const id = (header.split(':')[1] || '').trim();
- return id || null;
- }
- return null;
- };
- const getFileInfoFromHeaders = headers => {
- const info = {
- source: null,
- name: null,
- size: null,
- };
- const rows = headers.split('\n');
- for (let header of rows) {
- const name = getFileNameFromHeader(header);
- if (name) {
- info.name = name;
- continue;
- }
- const size = getFileSizeFromHeader(header);
- if (size) {
- info.size = size;
- continue;
- }
- const source = getTranfserIdFromHeader(header);
- if (source) {
- info.source = source;
- continue;
- }
- }
- return info;
- };
- const createFileLoader = fetchFn => {
- const state = {
- source: null,
- complete: false,
- progress: 0,
- size: null,
- timestamp: null,
- duration: 0,
- request: null,
- };
- const getProgress = () => state.progress;
- const abort = () => {
- if (state.request && state.request.abort) {
- state.request.abort();
- }
- };
- // load source
- const load = () => {
- // get quick reference
- const source = state.source;
- api.fire('init', source);
- // Load Files
- if (source instanceof File) {
- api.fire('load', source);
- } else if (source instanceof Blob) {
- // Load blobs, set default name to current date
- api.fire('load', getFileFromBlob(source, source.name));
- } else if (isBase64DataURI(source)) {
- // Load base 64, set default name to current date
- api.fire('load', getFileFromBase64DataURI(source));
- } else {
- // Deal as if is external URL, let's load it!
- loadURL(source);
- }
- };
- // loads a url
- const loadURL = url => {
- // is remote url and no fetch method supplied
- if (!fetchFn) {
- api.fire('error', {
- type: 'error',
- body: "Can't load URL",
- code: 400,
- });
- return;
- }
- // set request start
- state.timestamp = Date.now();
- // load file
- state.request = fetchFn(
- url,
- response => {
- // update duration
- state.duration = Date.now() - state.timestamp;
- // done!
- state.complete = true;
- // turn blob response into a file
- if (response instanceof Blob) {
- response = getFileFromBlob(response, response.name || getFilenameFromURL(url));
- }
- api.fire(
- 'load',
- // if has received blob, we go with blob, if no response, we return null
- response instanceof Blob ? response : response ? response.body : null
- );
- },
- error => {
- api.fire(
- 'error',
- typeof error === 'string'
- ? {
- type: 'error',
- code: 0,
- body: error,
- }
- : error
- );
- },
- (computable, current, total) => {
- // collected some meta data already
- if (total) {
- state.size = total;
- }
- // update duration
- state.duration = Date.now() - state.timestamp;
- // if we can't compute progress, we're not going to fire progress events
- if (!computable) {
- state.progress = null;
- return;
- }
- // update progress percentage
- state.progress = current / total;
- // expose
- api.fire('progress', state.progress);
- },
- () => {
- api.fire('abort');
- },
- response => {
- const fileinfo = getFileInfoFromHeaders(
- typeof response === 'string' ? response : response.headers
- );
- api.fire('meta', {
- size: state.size || fileinfo.size,
- filename: fileinfo.name,
- source: fileinfo.source,
- });
- }
- );
- };
- const api = {
- ...on(),
- setSource: source => (state.source = source),
- getProgress, // file load progress
- abort, // abort file load
- load, // start load
- };
- return api;
- };
- const isGet = method => /GET|HEAD/.test(method);
- const sendRequest = (data, url, options) => {
- const api = {
- onheaders: () => {},
- onprogress: () => {},
- onload: () => {},
- ontimeout: () => {},
- onerror: () => {},
- onabort: () => {},
- abort: () => {
- aborted = true;
- xhr.abort();
- },
- };
- // timeout identifier, only used when timeout is defined
- let aborted = false;
- let headersReceived = false;
- // set default options
- options = {
- method: 'POST',
- headers: {},
- withCredentials: false,
- ...options,
- };
- // encode url
- url = encodeURI(url);
- // if method is GET, add any received data to url
- if (isGet(options.method) && data) {
- url = `${url}${encodeURIComponent(typeof data === 'string' ? data : JSON.stringify(data))}`;
- }
- // create request
- const xhr = new XMLHttpRequest();
- // progress of load
- const process = isGet(options.method) ? xhr : xhr.upload;
- process.onprogress = e => {
- // no progress event when aborted ( onprogress is called once after abort() )
- if (aborted) {
- return;
- }
- api.onprogress(e.lengthComputable, e.loaded, e.total);
- };
- // tries to get header info to the app as fast as possible
- xhr.onreadystatechange = () => {
- // not interesting in these states ('unsent' and 'openend' as they don't give us any additional info)
- if (xhr.readyState < 2) {
- return;
- }
- // no server response
- if (xhr.readyState === 4 && xhr.status === 0) {
- return;
- }
- if (headersReceived) {
- return;
- }
- headersReceived = true;
- // we've probably received some useful data in response headers
- api.onheaders(xhr);
- };
- // load successful
- xhr.onload = () => {
- // is classified as valid response
- if (xhr.status >= 200 && xhr.status < 300) {
- api.onload(xhr);
- } else {
- api.onerror(xhr);
- }
- };
- // error during load
- xhr.onerror = () => api.onerror(xhr);
- // request aborted
- xhr.onabort = () => {
- aborted = true;
- api.onabort();
- };
- // request timeout
- xhr.ontimeout = () => api.ontimeout(xhr);
- // open up open up!
- xhr.open(options.method, url, true);
- // set timeout if defined (do it after open so IE11 plays ball)
- if (isInt(options.timeout)) {
- xhr.timeout = options.timeout;
- }
- // add headers
- Object.keys(options.headers).forEach(key => {
- const value = unescape(encodeURIComponent(options.headers[key]));
- xhr.setRequestHeader(key, value);
- });
- // set type of response
- if (options.responseType) {
- xhr.responseType = options.responseType;
- }
- // set credentials
- if (options.withCredentials) {
- xhr.withCredentials = true;
- }
- // let's send our data
- xhr.send(data);
- return api;
- };
- const createResponse = (type, code, body, headers) => ({
- type,
- code,
- body,
- headers,
- });
- const createTimeoutResponse = cb => xhr => {
- cb(createResponse('error', 0, 'Timeout', xhr.getAllResponseHeaders()));
- };
- const hasQS = str => /\?/.test(str);
- const buildURL = (...parts) => {
- let url = '';
- parts.forEach(part => {
- url += hasQS(url) && hasQS(part) ? part.replace(/\?/, '&') : part;
- });
- return url;
- };
- const createFetchFunction = (apiUrl = '', action) => {
- // custom handler (should also handle file, load, error, progress and abort)
- if (typeof action === 'function') {
- return action;
- }
- // no action supplied
- if (!action || !isString(action.url)) {
- return null;
- }
- // set onload hanlder
- const onload = action.onload || (res => res);
- const onerror = action.onerror || (res => null);
- // internal handler
- return (url, load, error, progress, abort, headers) => {
- // do local or remote request based on if the url is external
- const request = sendRequest(url, buildURL(apiUrl, action.url), {
- ...action,
- responseType: 'blob',
- });
- request.onload = xhr => {
- // get headers
- const headers = xhr.getAllResponseHeaders();
- // get filename
- const filename = getFileInfoFromHeaders(headers).name || getFilenameFromURL(url);
- // create response
- load(
- createResponse(
- 'load',
- xhr.status,
- action.method === 'HEAD'
- ? null
- : getFileFromBlob(onload(xhr.response), filename),
- headers
- )
- );
- };
- request.onerror = xhr => {
- error(
- createResponse(
- 'error',
- xhr.status,
- onerror(xhr.response) || xhr.statusText,
- xhr.getAllResponseHeaders()
- )
- );
- };
- request.onheaders = xhr => {
- headers(createResponse('headers', xhr.status, null, xhr.getAllResponseHeaders()));
- };
- request.ontimeout = createTimeoutResponse(error);
- request.onprogress = progress;
- request.onabort = abort;
- // should return request
- return request;
- };
- };
- const ChunkStatus = {
- QUEUED: 0,
- COMPLETE: 1,
- PROCESSING: 2,
- ERROR: 3,
- WAITING: 4,
- };
- /*
- function signature:
- (file, metadata, load, error, progress, abort, transfer, options) => {
- return {
- abort:() => {}
- }
- }
- */
- // apiUrl, action, name, file, metadata, load, error, progress, abort, transfer, options
- const processFileChunked = (
- apiUrl,
- action,
- name,
- file,
- metadata,
- load,
- error,
- progress,
- abort,
- transfer,
- options
- ) => {
- // all chunks
- const chunks = [];
- const { chunkTransferId, chunkServer, chunkSize, chunkRetryDelays } = options;
- // default state
- const state = {
- serverId: chunkTransferId,
- aborted: false,
- };
- // set onload handlers
- const ondata = action.ondata || (fd => fd);
- const onload =
- action.onload ||
- ((xhr, method) =>
- method === 'HEAD' ? xhr.getResponseHeader('Upload-Offset') : xhr.response);
- const onerror = action.onerror || (res => null);
- // create server hook
- const requestTransferId = cb => {
- const formData = new FormData();
- // add metadata under same name
- if (isObject(metadata)) formData.append(name, JSON.stringify(metadata));
- const headers =
- typeof action.headers === 'function'
- ? action.headers(file, metadata)
- : {
- ...action.headers,
- 'Upload-Length': file.size,
- };
- const requestParams = {
- ...action,
- headers,
- };
- // send request object
- const request = sendRequest(ondata(formData), buildURL(apiUrl, action.url), requestParams);
- request.onload = xhr => cb(onload(xhr, requestParams.method));
- request.onerror = xhr =>
- error(
- createResponse(
- 'error',
- xhr.status,
- onerror(xhr.response) || xhr.statusText,
- xhr.getAllResponseHeaders()
- )
- );
- request.ontimeout = createTimeoutResponse(error);
- };
- const requestTransferOffset = cb => {
- const requestUrl = buildURL(apiUrl, chunkServer.url, state.serverId);
- const headers =
- typeof action.headers === 'function'
- ? action.headers(state.serverId)
- : {
- ...action.headers,
- };
- const requestParams = {
- headers,
- method: 'HEAD',
- };
- const request = sendRequest(null, requestUrl, requestParams);
- request.onload = xhr => cb(onload(xhr, requestParams.method));
- request.onerror = xhr =>
- error(
- createResponse(
- 'error',
- xhr.status,
- onerror(xhr.response) || xhr.statusText,
- xhr.getAllResponseHeaders()
- )
- );
- request.ontimeout = createTimeoutResponse(error);
- };
- // create chunks
- const lastChunkIndex = Math.floor(file.size / chunkSize);
- for (let i = 0; i <= lastChunkIndex; i++) {
- const offset = i * chunkSize;
- const data = file.slice(offset, offset + chunkSize, 'application/offset+octet-stream');
- chunks[i] = {
- index: i,
- size: data.size,
- offset,
- data,
- file,
- progress: 0,
- retries: [...chunkRetryDelays],
- status: ChunkStatus.QUEUED,
- error: null,
- request: null,
- timeout: null,
- };
- }
- const completeProcessingChunks = () => load(state.serverId);
- const canProcessChunk = chunk =>
- chunk.status === ChunkStatus.QUEUED || chunk.status === ChunkStatus.ERROR;
- const processChunk = chunk => {
- // processing is paused, wait here
- if (state.aborted) return;
- // get next chunk to process
- chunk = chunk || chunks.find(canProcessChunk);
- // no more chunks to process
- if (!chunk) {
- // all done?
- if (chunks.every(chunk => chunk.status === ChunkStatus.COMPLETE)) {
- completeProcessingChunks();
- }
- // no chunk to handle
- return;
- }
- // now processing this chunk
- chunk.status = ChunkStatus.PROCESSING;
- chunk.progress = null;
- // allow parsing of formdata
- const ondata = chunkServer.ondata || (fd => fd);
- const onerror = chunkServer.onerror || (res => null);
- // send request object
- const requestUrl = buildURL(apiUrl, chunkServer.url, state.serverId);
- const headers =
- typeof chunkServer.headers === 'function'
- ? chunkServer.headers(chunk)
- : {
- ...chunkServer.headers,
- 'Content-Type': 'application/offset+octet-stream',
- 'Upload-Offset': chunk.offset,
- 'Upload-Length': file.size,
- 'Upload-Name': file.name,
- };
- const request = (chunk.request = sendRequest(ondata(chunk.data), requestUrl, {
- ...chunkServer,
- headers,
- }));
- request.onload = () => {
- // done!
- chunk.status = ChunkStatus.COMPLETE;
- // remove request reference
- chunk.request = null;
- // start processing more chunks
- processChunks();
- };
- request.onprogress = (lengthComputable, loaded, total) => {
- chunk.progress = lengthComputable ? loaded : null;
- updateTotalProgress();
- };
- request.onerror = xhr => {
- chunk.status = ChunkStatus.ERROR;
- chunk.request = null;
- chunk.error = onerror(xhr.response) || xhr.statusText;
- if (!retryProcessChunk(chunk)) {
- error(
- createResponse(
- 'error',
- xhr.status,
- onerror(xhr.response) || xhr.statusText,
- xhr.getAllResponseHeaders()
- )
- );
- }
- };
- request.ontimeout = xhr => {
- chunk.status = ChunkStatus.ERROR;
- chunk.request = null;
- if (!retryProcessChunk(chunk)) {
- createTimeoutResponse(error)(xhr);
- }
- };
- request.onabort = () => {
- chunk.status = ChunkStatus.QUEUED;
- chunk.request = null;
- abort();
- };
- };
- const retryProcessChunk = chunk => {
- // no more retries left
- if (chunk.retries.length === 0) return false;
- // new retry
- chunk.status = ChunkStatus.WAITING;
- clearTimeout(chunk.timeout);
- chunk.timeout = setTimeout(() => {
- processChunk(chunk);
- }, chunk.retries.shift());
- // we're going to retry
- return true;
- };
- const updateTotalProgress = () => {
- // calculate total progress fraction
- const totalBytesTransfered = chunks.reduce((p, chunk) => {
- if (p === null || chunk.progress === null) return null;
- return p + chunk.progress;
- }, 0);
- // can't compute progress
- if (totalBytesTransfered === null) return progress(false, 0, 0);
- // calculate progress values
- const totalSize = chunks.reduce((total, chunk) => total + chunk.size, 0);
- // can update progress indicator
- progress(true, totalBytesTransfered, totalSize);
- };
- // process new chunks
- const processChunks = () => {
- const totalProcessing = chunks.filter(chunk => chunk.status === ChunkStatus.PROCESSING)
- .length;
- if (totalProcessing >= 1) return;
- processChunk();
- };
- const abortChunks = () => {
- chunks.forEach(chunk => {
- clearTimeout(chunk.timeout);
- if (chunk.request) {
- chunk.request.abort();
- }
- });
- };
- // let's go!
- if (!state.serverId) {
- requestTransferId(serverId => {
- // stop here if aborted, might have happened in between request and callback
- if (state.aborted) return;
- // pass back to item so we can use it if something goes wrong
- transfer(serverId);
- // store internally
- state.serverId = serverId;
- processChunks();
- });
- } else {
- requestTransferOffset(offset => {
- // stop here if aborted, might have happened in between request and callback
- if (state.aborted) return;
- // mark chunks with lower offset as complete
- chunks
- .filter(chunk => chunk.offset < offset)
- .forEach(chunk => {
- chunk.status = ChunkStatus.COMPLETE;
- chunk.progress = chunk.size;
- });
- // continue processing
- processChunks();
- });
- }
- return {
- abort: () => {
- state.aborted = true;
- abortChunks();
- },
- };
- };
- /*
- function signature:
- (file, metadata, load, error, progress, abort) => {
- return {
- abort:() => {}
- }
- }
- */
- const createFileProcessorFunction = (apiUrl, action, name, options) => (
- file,
- metadata,
- load,
- error,
- progress,
- abort,
- transfer
- ) => {
- // no file received
- if (!file) return;
- // if was passed a file, and we can chunk it, exit here
- const canChunkUpload = options.chunkUploads;
- const shouldChunkUpload = canChunkUpload && file.size > options.chunkSize;
- const willChunkUpload = canChunkUpload && (shouldChunkUpload || options.chunkForce);
- if (file instanceof Blob && willChunkUpload)
- return processFileChunked(
- apiUrl,
- action,
- name,
- file,
- metadata,
- load,
- error,
- progress,
- abort,
- transfer,
- options
- );
- // set handlers
- const ondata = action.ondata || (fd => fd);
- const onload = action.onload || (res => res);
- const onerror = action.onerror || (res => null);
- const headers =
- typeof action.headers === 'function'
- ? action.headers(file, metadata) || {}
- : {
- ...action.headers,
- };
- const requestParams = {
- ...action,
- headers,
- };
- // create formdata object
- var formData = new FormData();
- // add metadata under same name
- if (isObject(metadata)) {
- formData.append(name, JSON.stringify(metadata));
- }
- // Turn into an array of objects so no matter what the input, we can handle it the same way
- (file instanceof Blob ? [{ name: null, file }] : file).forEach(item => {
- formData.append(
- name,
- item.file,
- item.name === null ? item.file.name : `${item.name}${item.file.name}`
- );
- });
- // send request object
- const request = sendRequest(ondata(formData), buildURL(apiUrl, action.url), requestParams);
- request.onload = xhr => {
- load(createResponse('load', xhr.status, onload(xhr.response), xhr.getAllResponseHeaders()));
- };
- request.onerror = xhr => {
- error(
- createResponse(
- 'error',
- xhr.status,
- onerror(xhr.response) || xhr.statusText,
- xhr.getAllResponseHeaders()
- )
- );
- };
- request.ontimeout = createTimeoutResponse(error);
- request.onprogress = progress;
- request.onabort = abort;
- // should return request
- return request;
- };
- const createProcessorFunction = (apiUrl = '', action, name, options) => {
- // custom handler (should also handle file, load, error, progress and abort)
- if (typeof action === 'function') return (...params) => action(name, ...params, options);
- // no action supplied
- if (!action || !isString(action.url)) return null;
- // internal handler
- return createFileProcessorFunction(apiUrl, action, name, options);
- };
- /*
- function signature:
- (uniqueFileId, load, error) => { }
- */
- const createRevertFunction = (apiUrl = '', action) => {
- // is custom implementation
- if (typeof action === 'function') {
- return action;
- }
- // no action supplied, return stub function, interface will work, but file won't be removed
- if (!action || !isString(action.url)) {
- return (uniqueFileId, load) => load();
- }
- // set onload hanlder
- const onload = action.onload || (res => res);
- const onerror = action.onerror || (res => null);
- // internal implementation
- return (uniqueFileId, load, error) => {
- const request = sendRequest(
- uniqueFileId,
- apiUrl + action.url,
- action // contains method, headers and withCredentials properties
- );
- request.onload = xhr => {
- load(
- createResponse(
- 'load',
- xhr.status,
- onload(xhr.response),
- xhr.getAllResponseHeaders()
- )
- );
- };
- request.onerror = xhr => {
- error(
- createResponse(
- 'error',
- xhr.status,
- onerror(xhr.response) || xhr.statusText,
- xhr.getAllResponseHeaders()
- )
- );
- };
- request.ontimeout = createTimeoutResponse(error);
- return request;
- };
- };
- const getRandomNumber = (min = 0, max = 1) => min + Math.random() * (max - min);
- const createPerceivedPerformanceUpdater = (
- cb,
- duration = 1000,
- offset = 0,
- tickMin = 25,
- tickMax = 250
- ) => {
- let timeout = null;
- const start = Date.now();
- const tick = () => {
- let runtime = Date.now() - start;
- let delay = getRandomNumber(tickMin, tickMax);
- if (runtime + delay > duration) {
- delay = runtime + delay - duration;
- }
- let progress = runtime / duration;
- if (progress >= 1 || document.hidden) {
- cb(1);
- return;
- }
- cb(progress);
- timeout = setTimeout(tick, delay);
- };
- if (duration > 0) tick();
- return {
- clear: () => {
- clearTimeout(timeout);
- },
- };
- };
- const createFileProcessor = (processFn, options) => {
- const state = {
- complete: false,
- perceivedProgress: 0,
- perceivedPerformanceUpdater: null,
- progress: null,
- timestamp: null,
- perceivedDuration: 0,
- duration: 0,
- request: null,
- response: null,
- };
- const { allowMinimumUploadDuration } = options;
- const process = (file, metadata) => {
- const progressFn = () => {
- // we've not yet started the real download, stop here
- // the request might not go through, for instance, there might be some server trouble
- // if state.progress is null, the server does not allow computing progress and we show the spinner instead
- if (state.duration === 0 || state.progress === null) return;
- // as we're now processing, fire the progress event
- api.fire('progress', api.getProgress());
- };
- const completeFn = () => {
- state.complete = true;
- api.fire('load-perceived', state.response.body);
- };
- // let's start processing
- api.fire('start');
- // set request start
- state.timestamp = Date.now();
- // create perceived performance progress indicator
- state.perceivedPerformanceUpdater = createPerceivedPerformanceUpdater(
- progress => {
- state.perceivedProgress = progress;
- state.perceivedDuration = Date.now() - state.timestamp;
- progressFn();
- // if fake progress is done, and a response has been received,
- // and we've not yet called the complete method
- if (state.response && state.perceivedProgress === 1 && !state.complete) {
- // we done!
- completeFn();
- }
- },
- // random delay as in a list of files you start noticing
- // files uploading at the exact same speed
- allowMinimumUploadDuration ? getRandomNumber(750, 1500) : 0
- );
- // remember request so we can abort it later
- state.request = processFn(
- // the file to process
- file,
- // the metadata to send along
- metadata,
- // callbacks (load, error, progress, abort, transfer)
- // load expects the body to be a server id if
- // you want to make use of revert
- response => {
- // we put the response in state so we can access
- // it outside of this method
- state.response = isObject(response)
- ? response
- : {
- type: 'load',
- code: 200,
- body: `${response}`,
- headers: {},
- };
- // update duration
- state.duration = Date.now() - state.timestamp;
- // force progress to 1 as we're now done
- state.progress = 1;
- // actual load is done let's share results
- api.fire('load', state.response.body);
- // we are really done
- // if perceived progress is 1 ( wait for perceived progress to complete )
- // or if server does not support progress ( null )
- if (
- !allowMinimumUploadDuration ||
- (allowMinimumUploadDuration && state.perceivedProgress === 1)
- ) {
- completeFn();
- }
- },
- // error is expected to be an object with type, code, body
- error => {
- // cancel updater
- state.perceivedPerformanceUpdater.clear();
- // update others about this error
- api.fire(
- 'error',
- isObject(error)
- ? error
- : {
- type: 'error',
- code: 0,
- body: `${error}`,
- }
- );
- },
- // actual processing progress
- (computable, current, total) => {
- // update actual duration
- state.duration = Date.now() - state.timestamp;
- // update actual progress
- state.progress = computable ? current / total : null;
- progressFn();
- },
- // abort does not expect a value
- () => {
- // stop updater
- state.perceivedPerformanceUpdater.clear();
- // fire the abort event so we can switch visuals
- api.fire('abort', state.response ? state.response.body : null);
- },
- // register the id for this transfer
- transferId => {
- api.fire('transfer', transferId);
- }
- );
- };
- const abort = () => {
- // no request running, can't abort
- if (!state.request) return;
- // stop updater
- state.perceivedPerformanceUpdater.clear();
- // abort actual request
- if (state.request.abort) state.request.abort();
- // if has response object, we've completed the request
- state.complete = true;
- };
- const reset = () => {
- abort();
- state.complete = false;
- state.perceivedProgress = 0;
- state.progress = 0;
- state.timestamp = null;
- state.perceivedDuration = 0;
- state.duration = 0;
- state.request = null;
- state.response = null;
- };
- const getProgress = allowMinimumUploadDuration
- ? () => (state.progress ? Math.min(state.progress, state.perceivedProgress) : null)
- : () => state.progress || null;
- const getDuration = allowMinimumUploadDuration
- ? () => Math.min(state.duration, state.perceivedDuration)
- : () => state.duration;
- const api = {
- ...on(),
- process, // start processing file
- abort, // abort active process request
- getProgress,
- getDuration,
- reset,
- };
- return api;
- };
- const getFilenameWithoutExtension = name => name.substring(0, name.lastIndexOf('.')) || name;
- const createFileStub = source => {
- let data = [source.name, source.size, source.type];
- // is blob or base64, then we need to set the name
- if (source instanceof Blob || isBase64DataURI(source)) {
- data[0] = source.name || getDateString();
- } else if (isBase64DataURI(source)) {
- // if is base64 data uri we need to determine the average size and type
- data[1] = source.length;
- data[2] = getMimeTypeFromBase64DataURI(source);
- } else if (isString(source)) {
- // url
- data[0] = getFilenameFromURL(source);
- data[1] = 0;
- data[2] = 'application/octet-stream';
- }
- return {
- name: data[0],
- size: data[1],
- type: data[2],
- };
- };
- const isFile = value => !!(value instanceof File || (value instanceof Blob && value.name));
- const deepCloneObject = src => {
- if (!isObject(src)) return src;
- const target = isArray(src) ? [] : {};
- for (const key in src) {
- if (!src.hasOwnProperty(key)) continue;
- const v = src[key];
- target[key] = v && isObject(v) ? deepCloneObject(v) : v;
- }
- return target;
- };
- const createItem = (origin = null, serverFileReference = null, file = null) => {
- // unique id for this item, is used to identify the item across views
- const id = getUniqueId();
- /**
- * Internal item state
- */
- const state = {
- // is archived
- archived: false,
- // if is frozen, no longer fires events
- frozen: false,
- // removed from view
- released: false,
- // original source
- source: null,
- // file model reference
- file,
- // id of file on server
- serverFileReference,
- // id of file transfer on server
- transferId: null,
- // is aborted
- processingAborted: false,
- // current item status
- status: serverFileReference ? ItemStatus.PROCESSING_COMPLETE : ItemStatus.INIT,
- // active processes
- activeLoader: null,
- activeProcessor: null,
- };
- // callback used when abort processing is called to link back to the resolve method
- let abortProcessingRequestComplete = null;
- /**
- * Externally added item metadata
- */
- const metadata = {};
- // item data
- const setStatus = status => (state.status = status);
- // fire event unless the item has been archived
- const fire = (event, ...params) => {
- if (state.released || state.frozen) return;
- api.fire(event, ...params);
- };
- // file data
- const getFileExtension = () => getExtensionFromFilename(state.file.name);
- const getFileType = () => state.file.type;
- const getFileSize = () => state.file.size;
- const getFile = () => state.file;
- //
- // logic to load a file
- //
- const load = (source, loader, onload) => {
- // remember the original item source
- state.source = source;
- // source is known
- api.fireSync('init');
- // file stub is already there
- if (state.file) {
- api.fireSync('load-skip');
- return;
- }
- // set a stub file object while loading the actual data
- state.file = createFileStub(source);
- // starts loading
- loader.on('init', () => {
- fire('load-init');
- });
- // we'eve received a size indication, let's update the stub
- loader.on('meta', meta => {
- // set size of file stub
- state.file.size = meta.size;
- // set name of file stub
- state.file.filename = meta.filename;
- // if has received source, we done
- if (meta.source) {
- origin = FileOrigin.LIMBO;
- state.serverFileReference = meta.source;
- state.status = ItemStatus.PROCESSING_COMPLETE;
- }
- // size has been updated
- fire('load-meta');
- });
- // the file is now loading we need to update the progress indicators
- loader.on('progress', progress => {
- setStatus(ItemStatus.LOADING);
- fire('load-progress', progress);
- });
- // an error was thrown while loading the file, we need to switch to error state
- loader.on('error', error => {
- setStatus(ItemStatus.LOAD_ERROR);
- fire('load-request-error', error);
- });
- // user or another process aborted the file load (cannot retry)
- loader.on('abort', () => {
- setStatus(ItemStatus.INIT);
- fire('load-abort');
- });
- // done loading
- loader.on('load', file => {
- // as we've now loaded the file the loader is no longer required
- state.activeLoader = null;
- // called when file has loaded succesfully
- const success = result => {
- // set (possibly) transformed file
- state.file = isFile(result) ? result : state.file;
- // file received
- if (origin === FileOrigin.LIMBO && state.serverFileReference) {
- setStatus(ItemStatus.PROCESSING_COMPLETE);
- } else {
- setStatus(ItemStatus.IDLE);
- }
- fire('load');
- };
- const error = result => {
- // set original file
- state.file = file;
- fire('load-meta');
- setStatus(ItemStatus.LOAD_ERROR);
- fire('load-file-error', result);
- };
- // if we already have a server file reference, we don't need to call the onload method
- if (state.serverFileReference) {
- success(file);
- return;
- }
- // no server id, let's give this file the full treatment
- onload(file, success, error);
- });
- // set loader source data
- loader.setSource(source);
- // set as active loader
- state.activeLoader = loader;
- // load the source data
- loader.load();
- };
- const retryLoad = () => {
- if (!state.activeLoader) {
- return;
- }
- state.activeLoader.load();
- };
- const abortLoad = () => {
- if (state.activeLoader) {
- state.activeLoader.abort();
- return;
- }
- setStatus(ItemStatus.INIT);
- fire('load-abort');
- };
- //
- // logic to process a file
- //
- const process = (processor, onprocess) => {
- // processing was aborted
- if (state.processingAborted) {
- state.processingAborted = false;
- return;
- }
- // now processing
- setStatus(ItemStatus.PROCESSING);
- // reset abort callback
- abortProcessingRequestComplete = null;
- // if no file loaded we'll wait for the load event
- if (!(state.file instanceof Blob)) {
- api.on('load', () => {
- process(processor, onprocess);
- });
- return;
- }
- // setup processor
- processor.on('load', serverFileReference => {
- // need this id to be able to revert the upload
- state.transferId = null;
- state.serverFileReference = serverFileReference;
- });
- // register transfer id
- processor.on('transfer', transferId => {
- // need this id to be able to revert the upload
- state.transferId = transferId;
- });
- processor.on('load-perceived', serverFileReference => {
- // no longer required
- state.activeProcessor = null;
- // need this id to be able to rever the upload
- state.transferId = null;
- state.serverFileReference = serverFileReference;
- setStatus(ItemStatus.PROCESSING_COMPLETE);
- fire('process-complete', serverFileReference);
- });
- processor.on('start', () => {
- fire('process-start');
- });
- processor.on('error', error => {
- state.activeProcessor = null;
- setStatus(ItemStatus.PROCESSING_ERROR);
- fire('process-error', error);
- });
- processor.on('abort', serverFileReference => {
- state.activeProcessor = null;
- // if file was uploaded but processing was cancelled during perceived processor time store file reference
- state.serverFileReference = serverFileReference;
- setStatus(ItemStatus.IDLE);
- fire('process-abort');
- // has timeout so doesn't interfere with remove action
- if (abortProcessingRequestComplete) {
- abortProcessingRequestComplete();
- }
- });
- processor.on('progress', progress => {
- fire('process-progress', progress);
- });
- // when successfully transformed
- const success = file => {
- // if was archived in the mean time, don't process
- if (state.archived) return;
- // process file!
- processor.process(file, { ...metadata });
- };
- // something went wrong during transform phase
- const error = console.error;
- // start processing the file
- onprocess(state.file, success, error);
- // set as active processor
- state.activeProcessor = processor;
- };
- const requestProcessing = () => {
- state.processingAborted = false;
- setStatus(ItemStatus.PROCESSING_QUEUED);
- };
- const abortProcessing = () =>
- new Promise(resolve => {
- if (!state.activeProcessor) {
- state.processingAborted = true;
- setStatus(ItemStatus.IDLE);
- fire('process-abort');
- resolve();
- return;
- }
- abortProcessingRequestComplete = () => {
- resolve();
- };
- state.activeProcessor.abort();
- });
- //
- // logic to revert a processed file
- //
- const revert = (revertFileUpload, forceRevert) =>
- new Promise((resolve, reject) => {
- // a completed upload will have a serverFileReference, a failed chunked upload where
- // getting a serverId succeeded but >=0 chunks have been uploaded will have transferId set
- const serverTransferId =
- state.serverFileReference !== null ? state.serverFileReference : state.transferId;
- // cannot revert without a server id for this process
- if (serverTransferId === null) {
- resolve();
- return;
- }
- // revert the upload (fire and forget)
- revertFileUpload(
- serverTransferId,
- () => {
- // reset file server id and transfer id as now it's not available on the server
- state.serverFileReference = null;
- state.transferId = null;
- resolve();
- },
- error => {
- // don't set error state when reverting is optional, it will always resolve
- if (!forceRevert) {
- resolve();
- return;
- }
- // oh no errors
- setStatus(ItemStatus.PROCESSING_REVERT_ERROR);
- fire('process-revert-error');
- reject(error);
- }
- );
- // fire event
- setStatus(ItemStatus.IDLE);
- fire('process-revert');
- });
- // exposed methods
- const setMetadata = (key, value, silent) => {
- const keys = key.split('.');
- const root = keys[0];
- const last = keys.pop();
- let data = metadata;
- keys.forEach(key => (data = data[key]));
- // compare old value against new value, if they're the same, we're not updating
- if (JSON.stringify(data[last]) === JSON.stringify(value)) return;
- // update value
- data[last] = value;
- // fire update
- fire('metadata-update', {
- key: root,
- value: metadata[root],
- silent,
- });
- };
- const getMetadata = key => deepCloneObject(key ? metadata[key] : metadata);
- const api = {
- id: { get: () => id },
- origin: { get: () => origin, set: value => (origin = value) },
- serverId: { get: () => state.serverFileReference },
- transferId: { get: () => state.transferId },
- status: { get: () => state.status },
- filename: { get: () => state.file.name },
- filenameWithoutExtension: { get: () => getFilenameWithoutExtension(state.file.name) },
- fileExtension: { get: getFileExtension },
- fileType: { get: getFileType },
- fileSize: { get: getFileSize },
- file: { get: getFile },
- relativePath: { get: () => state.file._relativePath },
- source: { get: () => state.source },
- getMetadata,
- setMetadata: (key, value, silent) => {
- if (isObject(key)) {
- const data = key;
- Object.keys(data).forEach(key => {
- setMetadata(key, data[key], value);
- });
- return key;
- }
- setMetadata(key, value, silent);
- return value;
- },
- extend: (name, handler) => (itemAPI[name] = handler),
- abortLoad,
- retryLoad,
- requestProcessing,
- abortProcessing,
- load,
- process,
- revert,
- ...on(),
- freeze: () => (state.frozen = true),
- release: () => (state.released = true),
- released: { get: () => state.released },
- archive: () => (state.archived = true),
- archived: { get: () => state.archived },
- };
- // create it here instead of returning it instantly so we can extend it later
- const itemAPI = createObject(api);
- return itemAPI;
- };
- const getItemIndexByQuery = (items, query) => {
- // just return first index
- if (isEmpty(query)) {
- return 0;
- }
- // invalid queries
- if (!isString(query)) {
- return -1;
- }
- // return item by id (or -1 if not found)
- return items.findIndex(item => item.id === query);
- };
- const getItemById = (items, itemId) => {
- const index = getItemIndexByQuery(items, itemId);
- if (index < 0) {
- return;
- }
- return items[index] || null;
- };
- const fetchBlob = (url, load, error, progress, abort, headers) => {
- const request = sendRequest(null, url, {
- method: 'GET',
- responseType: 'blob',
- });
- request.onload = xhr => {
- // get headers
- const headers = xhr.getAllResponseHeaders();
- // get filename
- const filename = getFileInfoFromHeaders(headers).name || getFilenameFromURL(url);
- // create response
- load(createResponse('load', xhr.status, getFileFromBlob(xhr.response, filename), headers));
- };
- request.onerror = xhr => {
- error(createResponse('error', xhr.status, xhr.statusText, xhr.getAllResponseHeaders()));
- };
- request.onheaders = xhr => {
- headers(createResponse('headers', xhr.status, null, xhr.getAllResponseHeaders()));
- };
- request.ontimeout = createTimeoutResponse(error);
- request.onprogress = progress;
- request.onabort = abort;
- // should return request
- return request;
- };
- const getDomainFromURL = url => {
- if (url.indexOf('//') === 0) {
- url = location.protocol + url;
- }
- return url
- .toLowerCase()
- .replace('blob:', '')
- .replace(/([a-z])?:\/\//, '$1')
- .split('/')[0];
- };
- const isExternalURL = url =>
- (url.indexOf(':') > -1 || url.indexOf('//') > -1) &&
- getDomainFromURL(location.href) !== getDomainFromURL(url);
- const dynamicLabel = label => (...params) => (isFunction(label) ? label(...params) : label);
- const isMockItem = item => !isFile(item.file);
- const listUpdated = (dispatch, state) => {
- clearTimeout(state.listUpdateTimeout);
- state.listUpdateTimeout = setTimeout(() => {
- dispatch('DID_UPDATE_ITEMS', { items: getActiveItems(state.items) });
- }, 0);
- };
- const optionalPromise = (fn, ...params) =>
- new Promise(resolve => {
- if (!fn) {
- return resolve(true);
- }
- const result = fn(...params);
- if (result == null) {
- return resolve(true);
- }
- if (typeof result === 'boolean') {
- return resolve(result);
- }
- if (typeof result.then === 'function') {
- result.then(resolve);
- }
- });
- const sortItems = (state, compare) => {
- state.items.sort((a, b) => compare(createItemAPI(a), createItemAPI(b)));
- };
- // returns item based on state
- const getItemByQueryFromState = (state, itemHandler) => ({
- query,
- success = () => {},
- failure = () => {},
- ...options
- } = {}) => {
- const item = getItemByQuery(state.items, query);
- if (!item) {
- failure({
- error: createResponse('error', 0, 'Item not found'),
- file: null,
- });
- return;
- }
- itemHandler(item, success, failure, options || {});
- };
- const actions = (dispatch, query, state) => ({
- /**
- * Aborts all ongoing processes
- */
- ABORT_ALL: () => {
- getActiveItems(state.items).forEach(item => {
- item.freeze();
- item.abortLoad();
- item.abortProcessing();
- });
- },
- /**
- * Sets initial files
- */
- DID_SET_FILES: ({ value = [] }) => {
- // map values to file objects
- const files = value.map(file => ({
- source: file.source ? file.source : file,
- options: file.options,
- }));
- // loop over files, if file is in list, leave it be, if not, remove
- // test if items should be moved
- let activeItems = getActiveItems(state.items);
- activeItems.forEach(item => {
- // if item not is in new value, remove
- if (!files.find(file => file.source === item.source || file.source === item.file)) {
- dispatch('REMOVE_ITEM', { query: item, remove: false });
- }
- });
- // add new files
- activeItems = getActiveItems(state.items);
- files.forEach((file, index) => {
- // if file is already in list
- if (activeItems.find(item => item.source === file.source || item.file === file.source))
- return;
- // not in list, add
- dispatch('ADD_ITEM', {
- ...file,
- interactionMethod: InteractionMethod.NONE,
- index,
- });
- });
- },
- DID_UPDATE_ITEM_METADATA: ({ id, action, change }) => {
- // don't do anything
- if (change.silent) return;
- // if is called multiple times in close succession we combined all calls together to save resources
- clearTimeout(state.itemUpdateTimeout);
- state.itemUpdateTimeout = setTimeout(() => {
- const item = getItemById(state.items, id);
- // only revert and attempt to upload when we're uploading to a server
- if (!query('IS_ASYNC')) {
- // should we update the output data
- applyFilterChain('SHOULD_PREPARE_OUTPUT', false, {
- item,
- query,
- action,
- change,
- }).then(shouldPrepareOutput => {
- // plugins determined the output data should be prepared (or not), can be adjusted with beforePrepareOutput hook
- const beforePrepareFile = query('GET_BEFORE_PREPARE_FILE');
- if (beforePrepareFile)
- shouldPrepareOutput = beforePrepareFile(item, shouldPrepareOutput);
- if (!shouldPrepareOutput) return;
- dispatch(
- 'REQUEST_PREPARE_OUTPUT',
- {
- query: id,
- item,
- success: file => {
- dispatch('DID_PREPARE_OUTPUT', { id, file });
- },
- },
- true
- );
- });
- return;
- }
- // if is local item we need to enable upload button so change can be propagated to server
- if (item.origin === FileOrigin.LOCAL) {
- dispatch('DID_LOAD_ITEM', {
- id: item.id,
- error: null,
- serverFileReference: item.source,
- });
- }
- // for async scenarios
- const upload = () => {
- // we push this forward a bit so the interface is updated correctly
- setTimeout(() => {
- dispatch('REQUEST_ITEM_PROCESSING', { query: id });
- }, 32);
- };
- const revert = doUpload => {
- item.revert(
- createRevertFunction(state.options.server.url, state.options.server.revert),
- query('GET_FORCE_REVERT')
- )
- .then(doUpload ? upload : () => {})
- .catch(() => {});
- };
- const abort = doUpload => {
- item.abortProcessing().then(doUpload ? upload : () => {});
- };
- // if we should re-upload the file immediately
- if (item.status === ItemStatus.PROCESSING_COMPLETE) {
- return revert(state.options.instantUpload);
- }
- // if currently uploading, cancel upload
- if (item.status === ItemStatus.PROCESSING) {
- return abort(state.options.instantUpload);
- }
- if (state.options.instantUpload) {
- upload();
- }
- }, 0);
- },
- MOVE_ITEM: ({ query, index }) => {
- const item = getItemByQuery(state.items, query);
- if (!item) return;
- const currentIndex = state.items.indexOf(item);
- index = limit(index, 0, state.items.length - 1);
- if (currentIndex === index) return;
- state.items.splice(index, 0, state.items.splice(currentIndex, 1)[0]);
- },
- SORT: ({ compare }) => {
- sortItems(state, compare);
- dispatch('DID_SORT_ITEMS', {
- items: query('GET_ACTIVE_ITEMS'),
- });
- },
- ADD_ITEMS: ({ items, index, interactionMethod, success = () => {}, failure = () => {} }) => {
- let currentIndex = index;
- if (index === -1 || typeof index === 'undefined') {
- const insertLocation = query('GET_ITEM_INSERT_LOCATION');
- const totalItems = query('GET_TOTAL_ITEMS');
- currentIndex = insertLocation === 'before' ? 0 : totalItems;
- }
- const ignoredFiles = query('GET_IGNORED_FILES');
- const isValidFile = source =>
- isFile(source) ? !ignoredFiles.includes(source.name.toLowerCase()) : !isEmpty(source);
- const validItems = items.filter(isValidFile);
- const promises = validItems.map(
- source =>
- new Promise((resolve, reject) => {
- dispatch('ADD_ITEM', {
- interactionMethod,
- source: source.source || source,
- success: resolve,
- failure: reject,
- index: currentIndex++,
- options: source.options || {},
- });
- })
- );
- Promise.all(promises)
- .then(success)
- .catch(failure);
- },
- /**
- * @param source
- * @param index
- * @param interactionMethod
- */
- ADD_ITEM: ({
- source,
- index = -1,
- interactionMethod,
- success = () => {},
- failure = () => {},
- options = {},
- }) => {
- // if no source supplied
- if (isEmpty(source)) {
- failure({
- error: createResponse('error', 0, 'No source'),
- file: null,
- });
- return;
- }
- // filter out invalid file items, used to filter dropped directory contents
- if (isFile(source) && state.options.ignoredFiles.includes(source.name.toLowerCase())) {
- // fail silently
- return;
- }
- // test if there's still room in the list of files
- if (!hasRoomForItem(state)) {
- // if multiple allowed, we can't replace
- // or if only a single item is allowed but we're not allowed to replace it we exit
- if (
- state.options.allowMultiple ||
- (!state.options.allowMultiple && !state.options.allowReplace)
- ) {
- const error = createResponse('warning', 0, 'Max files');
- dispatch('DID_THROW_MAX_FILES', {
- source,
- error,
- });
- failure({ error, file: null });
- return;
- }
- // let's replace the item
- // id of first item we're about to remove
- const item = getActiveItems(state.items)[0];
- // if has been processed remove it from the server as well
- if (
- item.status === ItemStatus.PROCESSING_COMPLETE ||
- item.status === ItemStatus.PROCESSING_REVERT_ERROR
- ) {
- const forceRevert = query('GET_FORCE_REVERT');
- item.revert(
- createRevertFunction(state.options.server.url, state.options.server.revert),
- forceRevert
- )
- .then(() => {
- if (!forceRevert) return;
- // try to add now
- dispatch('ADD_ITEM', {
- source,
- index,
- interactionMethod,
- success,
- failure,
- options,
- });
- })
- .catch(() => {}); // no need to handle this catch state for now
- if (forceRevert) return;
- }
- // remove first item as it will be replaced by this item
- dispatch('REMOVE_ITEM', { query: item.id });
- }
- // where did the file originate
- const origin =
- options.type === 'local'
- ? FileOrigin.LOCAL
- : options.type === 'limbo'
- ? FileOrigin.LIMBO
- : FileOrigin.INPUT;
- // create a new blank item
- const item = createItem(
- // where did this file come from
- origin,
- // an input file never has a server file reference
- origin === FileOrigin.INPUT ? null : source,
- // file mock data, if defined
- options.file
- );
- // set initial meta data
- Object.keys(options.metadata || {}).forEach(key => {
- item.setMetadata(key, options.metadata[key]);
- });
- // created the item, let plugins add methods
- applyFilters('DID_CREATE_ITEM', item, { query, dispatch });
- // where to insert new items
- const itemInsertLocation = query('GET_ITEM_INSERT_LOCATION');
- // adjust index if is not allowed to pick location
- if (!state.options.itemInsertLocationFreedom) {
- index = itemInsertLocation === 'before' ? -1 : state.items.length;
- }
- // add item to list
- insertItem(state.items, item, index);
- // sort items in list
- if (isFunction(itemInsertLocation) && source) {
- sortItems(state, itemInsertLocation);
- }
- // get a quick reference to the item id
- const id = item.id;
- // observe item events
- item.on('init', () => {
- dispatch('DID_INIT_ITEM', { id });
- });
- item.on('load-init', () => {
- dispatch('DID_START_ITEM_LOAD', { id });
- });
- item.on('load-meta', () => {
- dispatch('DID_UPDATE_ITEM_META', { id });
- });
- item.on('load-progress', progress => {
- dispatch('DID_UPDATE_ITEM_LOAD_PROGRESS', { id, progress });
- });
- item.on('load-request-error', error => {
- const mainStatus = dynamicLabel(state.options.labelFileLoadError)(error);
- // is client error, no way to recover
- if (error.code >= 400 && error.code < 500) {
- dispatch('DID_THROW_ITEM_INVALID', {
- id,
- error,
- status: {
- main: mainStatus,
- sub: `${error.code} (${error.body})`,
- },
- });
- // reject the file so can be dealt with through API
- failure({ error, file: createItemAPI(item) });
- return;
- }
- // is possible server error, so might be possible to retry
- dispatch('DID_THROW_ITEM_LOAD_ERROR', {
- id,
- error,
- status: {
- main: mainStatus,
- sub: state.options.labelTapToRetry,
- },
- });
- });
- item.on('load-file-error', error => {
- dispatch('DID_THROW_ITEM_INVALID', {
- id,
- error: error.status,
- status: error.status,
- });
- failure({ error: error.status, file: createItemAPI(item) });
- });
- item.on('load-abort', () => {
- dispatch('REMOVE_ITEM', { query: id });
- });
- item.on('load-skip', () => {
- dispatch('COMPLETE_LOAD_ITEM', {
- query: id,
- item,
- data: {
- source,
- success,
- },
- });
- });
- item.on('load', () => {
- const handleAdd = shouldAdd => {
- // no should not add this file
- if (!shouldAdd) {
- dispatch('REMOVE_ITEM', {
- query: id,
- });
- return;
- }
- // now interested in metadata updates
- item.on('metadata-update', change => {
- dispatch('DID_UPDATE_ITEM_METADATA', { id, change });
- });
- // let plugins decide if the output data should be prepared at this point
- // means we'll do this and wait for idle state
- applyFilterChain('SHOULD_PREPARE_OUTPUT', false, { item, query }).then(
- shouldPrepareOutput => {
- // plugins determined the output data should be prepared (or not), can be adjusted with beforePrepareOutput hook
- const beforePrepareFile = query('GET_BEFORE_PREPARE_FILE');
- if (beforePrepareFile)
- shouldPrepareOutput = beforePrepareFile(item, shouldPrepareOutput);
- const loadComplete = () => {
- dispatch('COMPLETE_LOAD_ITEM', {
- query: id,
- item,
- data: {
- source,
- success,
- },
- });
- listUpdated(dispatch, state);
- };
- // exit
- if (shouldPrepareOutput) {
- // wait for idle state and then run PREPARE_OUTPUT
- dispatch(
- 'REQUEST_PREPARE_OUTPUT',
- {
- query: id,
- item,
- success: file => {
- dispatch('DID_PREPARE_OUTPUT', { id, file });
- loadComplete();
- },
- },
- true
- );
- return;
- }
- loadComplete();
- }
- );
- };
- // item loaded, allow plugins to
- // - read data (quickly)
- // - add metadata
- applyFilterChain('DID_LOAD_ITEM', item, { query, dispatch })
- .then(() => {
- optionalPromise(query('GET_BEFORE_ADD_FILE'), createItemAPI(item)).then(
- handleAdd
- );
- })
- .catch(e => {
- if (!e || !e.error || !e.status) return handleAdd(false);
- dispatch('DID_THROW_ITEM_INVALID', {
- id,
- error: e.error,
- status: e.status,
- });
- });
- });
- item.on('process-start', () => {
- dispatch('DID_START_ITEM_PROCESSING', { id });
- });
- item.on('process-progress', progress => {
- dispatch('DID_UPDATE_ITEM_PROCESS_PROGRESS', { id, progress });
- });
- item.on('process-error', error => {
- dispatch('DID_THROW_ITEM_PROCESSING_ERROR', {
- id,
- error,
- status: {
- main: dynamicLabel(state.options.labelFileProcessingError)(error),
- sub: state.options.labelTapToRetry,
- },
- });
- });
- item.on('process-revert-error', error => {
- dispatch('DID_THROW_ITEM_PROCESSING_REVERT_ERROR', {
- id,
- error,
- status: {
- main: dynamicLabel(state.options.labelFileProcessingRevertError)(error),
- sub: state.options.labelTapToRetry,
- },
- });
- });
- item.on('process-complete', serverFileReference => {
- dispatch('DID_COMPLETE_ITEM_PROCESSING', {
- id,
- error: null,
- serverFileReference,
- });
- dispatch('DID_DEFINE_VALUE', { id, value: serverFileReference });
- });
- item.on('process-abort', () => {
- dispatch('DID_ABORT_ITEM_PROCESSING', { id });
- });
- item.on('process-revert', () => {
- dispatch('DID_REVERT_ITEM_PROCESSING', { id });
- dispatch('DID_DEFINE_VALUE', { id, value: null });
- });
- // let view know the item has been inserted
- dispatch('DID_ADD_ITEM', { id, index, interactionMethod });
- listUpdated(dispatch, state);
- // start loading the source
- const { url, load, restore, fetch } = state.options.server || {};
- item.load(
- source,
- // this creates a function that loads the file based on the type of file (string, base64, blob, file) and location of file (local, remote, limbo)
- createFileLoader(
- origin === FileOrigin.INPUT
- ? // input, if is remote, see if should use custom fetch, else use default fetchBlob
- isString(source) && isExternalURL(source)
- ? fetch
- ? createFetchFunction(url, fetch)
- : fetchBlob // remote url
- : fetchBlob // try to fetch url
- : // limbo or local
- origin === FileOrigin.LIMBO
- ? createFetchFunction(url, restore) // limbo
- : createFetchFunction(url, load) // local
- ),
- // called when the file is loaded so it can be piped through the filters
- (file, success, error) => {
- // let's process the file
- applyFilterChain('LOAD_FILE', file, { query })
- .then(success)
- .catch(error);
- }
- );
- },
- REQUEST_PREPARE_OUTPUT: ({ item, success, failure = () => {} }) => {
- // error response if item archived
- const err = {
- error: createResponse('error', 0, 'Item not found'),
- file: null,
- };
- // don't handle archived items, an item could have been archived (load aborted) while waiting to be prepared
- if (item.archived) return failure(err);
- // allow plugins to alter the file data
- applyFilterChain('PREPARE_OUTPUT', item.file, { query, item }).then(result => {
- applyFilterChain('COMPLETE_PREPARE_OUTPUT', result, { query, item }).then(result => {
- // don't handle archived items, an item could have been archived (load aborted) while being prepared
- if (item.archived) return failure(err);
- // we done!
- success(result);
- });
- });
- },
- COMPLETE_LOAD_ITEM: ({ item, data }) => {
- const { success, source } = data;
- // sort items in list
- const itemInsertLocation = query('GET_ITEM_INSERT_LOCATION');
- if (isFunction(itemInsertLocation) && source) {
- sortItems(state, itemInsertLocation);
- }
- // let interface know the item has loaded
- dispatch('DID_LOAD_ITEM', {
- id: item.id,
- error: null,
- serverFileReference: item.origin === FileOrigin.INPUT ? null : source,
- });
- // item has been successfully loaded and added to the
- // list of items so can now be safely returned for use
- success(createItemAPI(item));
- // if this is a local server file we need to show a different state
- if (item.origin === FileOrigin.LOCAL) {
- dispatch('DID_LOAD_LOCAL_ITEM', { id: item.id });
- return;
- }
- // if is a temp server file we prevent async upload call here (as the file is already on the server)
- if (item.origin === FileOrigin.LIMBO) {
- dispatch('DID_COMPLETE_ITEM_PROCESSING', {
- id: item.id,
- error: null,
- serverFileReference: source,
- });
- dispatch('DID_DEFINE_VALUE', {
- id: item.id,
- value: item.serverId || source,
- });
- return;
- }
- // id we are allowed to upload the file immediately, lets do it
- if (query('IS_ASYNC') && state.options.instantUpload) {
- dispatch('REQUEST_ITEM_PROCESSING', { query: item.id });
- }
- },
- RETRY_ITEM_LOAD: getItemByQueryFromState(state, item => {
- // try loading the source one more time
- item.retryLoad();
- }),
- REQUEST_ITEM_PREPARE: getItemByQueryFromState(state, (item, success, failure) => {
- dispatch(
- 'REQUEST_PREPARE_OUTPUT',
- {
- query: item.id,
- item,
- success: file => {
- dispatch('DID_PREPARE_OUTPUT', { id: item.id, file });
- success({
- file: item,
- output: file,
- });
- },
- failure,
- },
- true
- );
- }),
- REQUEST_ITEM_PROCESSING: getItemByQueryFromState(state, (item, success, failure) => {
- // cannot be queued (or is already queued)
- const itemCanBeQueuedForProcessing =
- // waiting for something
- item.status === ItemStatus.IDLE ||
- // processing went wrong earlier
- item.status === ItemStatus.PROCESSING_ERROR;
- // not ready to be processed
- if (!itemCanBeQueuedForProcessing) {
- const processNow = () =>
- dispatch('REQUEST_ITEM_PROCESSING', { query: item, success, failure });
- const process = () => (document.hidden ? processNow() : setTimeout(processNow, 32));
- // if already done processing or tried to revert but didn't work, try again
- if (
- item.status === ItemStatus.PROCESSING_COMPLETE ||
- item.status === ItemStatus.PROCESSING_REVERT_ERROR
- ) {
- item.revert(
- createRevertFunction(state.options.server.url, state.options.server.revert),
- query('GET_FORCE_REVERT')
- )
- .then(process)
- .catch(() => {}); // don't continue with processing if something went wrong
- } else if (item.status === ItemStatus.PROCESSING) {
- item.abortProcessing().then(process);
- }
- return;
- }
- // already queued for processing
- if (item.status === ItemStatus.PROCESSING_QUEUED) return;
- item.requestProcessing();
- dispatch('DID_REQUEST_ITEM_PROCESSING', { id: item.id });
- dispatch('PROCESS_ITEM', { query: item, success, failure }, true);
- }),
- PROCESS_ITEM: getItemByQueryFromState(state, (item, success, failure) => {
- const maxParallelUploads = query('GET_MAX_PARALLEL_UPLOADS');
- const totalCurrentUploads = query('GET_ITEMS_BY_STATUS', ItemStatus.PROCESSING).length;
- // queue and wait till queue is freed up
- if (totalCurrentUploads === maxParallelUploads) {
- // queue for later processing
- state.processingQueue.push({
- id: item.id,
- success,
- failure,
- });
- // stop it!
- return;
- }
- // if was not queued or is already processing exit here
- if (item.status === ItemStatus.PROCESSING) return;
- const processNext = () => {
- // process queueud items
- const queueEntry = state.processingQueue.shift();
- // no items left
- if (!queueEntry) return;
- // get item reference
- const { id, success, failure } = queueEntry;
- const itemReference = getItemByQuery(state.items, id);
- // if item was archived while in queue, jump to next
- if (!itemReference || itemReference.archived) {
- processNext();
- return;
- }
- // process queued item
- dispatch('PROCESS_ITEM', { query: id, success, failure }, true);
- };
- // we done function
- item.onOnce('process-complete', () => {
- success(createItemAPI(item));
- processNext();
- // if origin is local, and we're instant uploading, trigger remove of original
- // as revert will remove file from list
- const server = state.options.server;
- const instantUpload = state.options.instantUpload;
- if (instantUpload && item.origin === FileOrigin.LOCAL && isFunction(server.remove)) {
- const noop = () => {};
- item.origin = FileOrigin.LIMBO;
- state.options.server.remove(item.source, noop, noop);
- }
- // All items processed? No errors?
- const allItemsProcessed =
- query('GET_ITEMS_BY_STATUS', ItemStatus.PROCESSING_COMPLETE).length ===
- state.items.length;
- if (allItemsProcessed) {
- dispatch('DID_COMPLETE_ITEM_PROCESSING_ALL');
- }
- });
- // we error function
- item.onOnce('process-error', error => {
- failure({ error, file: createItemAPI(item) });
- processNext();
- });
- // start file processing
- const options = state.options;
- item.process(
- createFileProcessor(
- createProcessorFunction(options.server.url, options.server.process, options.name, {
- chunkTransferId: item.transferId,
- chunkServer: options.server.patch,
- chunkUploads: options.chunkUploads,
- chunkForce: options.chunkForce,
- chunkSize: options.chunkSize,
- chunkRetryDelays: options.chunkRetryDelays,
- }),
- {
- allowMinimumUploadDuration: query('GET_ALLOW_MINIMUM_UPLOAD_DURATION'),
- }
- ),
- // called when the file is about to be processed so it can be piped through the transform filters
- (file, success, error) => {
- // allow plugins to alter the file data
- applyFilterChain('PREPARE_OUTPUT', file, { query, item })
- .then(file => {
- dispatch('DID_PREPARE_OUTPUT', { id: item.id, file });
- success(file);
- })
- .catch(error);
- }
- );
- }),
- RETRY_ITEM_PROCESSING: getItemByQueryFromState(state, item => {
- dispatch('REQUEST_ITEM_PROCESSING', { query: item });
- }),
- REQUEST_REMOVE_ITEM: getItemByQueryFromState(state, item => {
- optionalPromise(query('GET_BEFORE_REMOVE_FILE'), createItemAPI(item)).then(shouldRemove => {
- if (!shouldRemove) {
- return;
- }
- dispatch('REMOVE_ITEM', { query: item });
- });
- }),
- RELEASE_ITEM: getItemByQueryFromState(state, item => {
- item.release();
- }),
- REMOVE_ITEM: getItemByQueryFromState(state, (item, success, failure, options) => {
- const removeFromView = () => {
- // get id reference
- const id = item.id;
- // archive the item, this does not remove it from the list
- getItemById(state.items, id).archive();
- // tell the view the item has been removed
- dispatch('DID_REMOVE_ITEM', { error: null, id, item });
- // now the list has been modified
- listUpdated(dispatch, state);
- // correctly removed
- success(createItemAPI(item));
- };
- // if this is a local file and the `server.remove` function has been configured,
- // send source there so dev can remove file from server
- const server = state.options.server;
- if (
- item.origin === FileOrigin.LOCAL &&
- server &&
- isFunction(server.remove) &&
- options.remove !== false
- ) {
- dispatch('DID_START_ITEM_REMOVE', { id: item.id });
- server.remove(
- item.source,
- () => removeFromView(),
- status => {
- dispatch('DID_THROW_ITEM_REMOVE_ERROR', {
- id: item.id,
- error: createResponse('error', 0, status, null),
- status: {
- main: dynamicLabel(state.options.labelFileRemoveError)(status),
- sub: state.options.labelTapToRetry,
- },
- });
- }
- );
- } else {
- // if is requesting revert and can revert need to call revert handler (not calling request_ because that would also trigger beforeRemoveHook)
- if (
- (options.revert && item.origin !== FileOrigin.LOCAL && item.serverId !== null) ||
- // if chunked uploads are enabled and we're uploading in chunks for this specific file
- // or if the file isn't big enough for chunked uploads but chunkForce is set then call
- // revert before removing from the view...
- (state.options.chunkUploads && item.file.size > state.options.chunkSize) ||
- (state.options.chunkUploads && state.options.chunkForce)
- ) {
- item.revert(
- createRevertFunction(state.options.server.url, state.options.server.revert),
- query('GET_FORCE_REVERT')
- );
- }
- // can now safely remove from view
- removeFromView();
- }
- }),
- ABORT_ITEM_LOAD: getItemByQueryFromState(state, item => {
- item.abortLoad();
- }),
- ABORT_ITEM_PROCESSING: getItemByQueryFromState(state, item => {
- // test if is already processed
- if (item.serverId) {
- dispatch('REVERT_ITEM_PROCESSING', { id: item.id });
- return;
- }
- // abort
- item.abortProcessing().then(() => {
- const shouldRemove = state.options.instantUpload;
- if (shouldRemove) {
- dispatch('REMOVE_ITEM', { query: item.id });
- }
- });
- }),
- REQUEST_REVERT_ITEM_PROCESSING: getItemByQueryFromState(state, item => {
- // not instant uploading, revert immediately
- if (!state.options.instantUpload) {
- dispatch('REVERT_ITEM_PROCESSING', { query: item });
- return;
- }
- // if we're instant uploading the file will also be removed if we revert,
- // so if a before remove file hook is defined we need to run it now
- const handleRevert = shouldRevert => {
- if (!shouldRevert) return;
- dispatch('REVERT_ITEM_PROCESSING', { query: item });
- };
- const fn = query('GET_BEFORE_REMOVE_FILE');
- if (!fn) {
- return handleRevert(true);
- }
- const requestRemoveResult = fn(createItemAPI(item));
- if (requestRemoveResult == null) {
- // undefined or null
- return handleRevert(true);
- }
- if (typeof requestRemoveResult === 'boolean') {
- return handleRevert(requestRemoveResult);
- }
- if (typeof requestRemoveResult.then === 'function') {
- requestRemoveResult.then(handleRevert);
- }
- }),
- REVERT_ITEM_PROCESSING: getItemByQueryFromState(state, item => {
- item.revert(
- createRevertFunction(state.options.server.url, state.options.server.revert),
- query('GET_FORCE_REVERT')
- )
- .then(() => {
- const shouldRemove = state.options.instantUpload || isMockItem(item);
- if (shouldRemove) {
- dispatch('REMOVE_ITEM', { query: item.id });
- }
- })
- .catch(() => {});
- }),
- SET_OPTIONS: ({ options }) => {
- // get all keys passed
- const optionKeys = Object.keys(options);
- // get prioritized keyed to include (remove once not in options object)
- const prioritizedOptionKeys = PrioritizedOptions.filter(key => optionKeys.includes(key));
- // order the keys, prioritized first, then rest
- const orderedOptionKeys = [
- // add prioritized first if passed to options, else remove
- ...prioritizedOptionKeys,
- // prevent duplicate keys
- ...Object.keys(options).filter(key => !prioritizedOptionKeys.includes(key)),
- ];
- // dispatch set event for each option
- orderedOptionKeys.forEach(key => {
- dispatch(`SET_${fromCamels(key, '_').toUpperCase()}`, {
- value: options[key],
- });
- });
- },
- });
- const PrioritizedOptions = [
- 'server', // must be processed before "files"
- ];
- const formatFilename = name => name;
- const createElement$1 = tagName => {
- return document.createElement(tagName);
- };
- const text = (node, value) => {
- let textNode = node.childNodes[0];
- if (!textNode) {
- textNode = document.createTextNode(value);
- node.appendChild(textNode);
- } else if (value !== textNode.nodeValue) {
- textNode.nodeValue = value;
- }
- };
- const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => {
- const angleInRadians = (((angleInDegrees % 360) - 90) * Math.PI) / 180.0;
- return {
- x: centerX + radius * Math.cos(angleInRadians),
- y: centerY + radius * Math.sin(angleInRadians),
- };
- };
- const describeArc = (x, y, radius, startAngle, endAngle, arcSweep) => {
- const start = polarToCartesian(x, y, radius, endAngle);
- const end = polarToCartesian(x, y, radius, startAngle);
- return ['M', start.x, start.y, 'A', radius, radius, 0, arcSweep, 0, end.x, end.y].join(' ');
- };
- const percentageArc = (x, y, radius, from, to) => {
- let arcSweep = 1;
- if (to > from && to - from <= 0.5) {
- arcSweep = 0;
- }
- if (from > to && from - to >= 0.5) {
- arcSweep = 0;
- }
- return describeArc(
- x,
- y,
- radius,
- Math.min(0.9999, from) * 360,
- Math.min(0.9999, to) * 360,
- arcSweep
- );
- };
- const create = ({ root, props }) => {
- // start at 0
- props.spin = false;
- props.progress = 0;
- props.opacity = 0;
- // svg
- const svg = createElement('svg');
- root.ref.path = createElement('path', {
- 'stroke-width': 2,
- 'stroke-linecap': 'round',
- });
- svg.appendChild(root.ref.path);
- root.ref.svg = svg;
- root.appendChild(svg);
- };
- const write = ({ root, props }) => {
- if (props.opacity === 0) {
- return;
- }
- if (props.align) {
- root.element.dataset.align = props.align;
- }
- // get width of stroke
- const ringStrokeWidth = parseInt(attr(root.ref.path, 'stroke-width'), 10);
- // calculate size of ring
- const size = root.rect.element.width * 0.5;
- // ring state
- let ringFrom = 0;
- let ringTo = 0;
- // now in busy mode
- if (props.spin) {
- ringFrom = 0;
- ringTo = 0.5;
- } else {
- ringFrom = 0;
- ringTo = props.progress;
- }
- // get arc path
- const coordinates = percentageArc(size, size, size - ringStrokeWidth, ringFrom, ringTo);
- // update progress bar
- attr(root.ref.path, 'd', coordinates);
- // hide while contains 0 value
- attr(root.ref.path, 'stroke-opacity', props.spin || props.progress > 0 ? 1 : 0);
- };
- const progressIndicator = createView({
- tag: 'div',
- name: 'progress-indicator',
- ignoreRectUpdate: true,
- ignoreRect: true,
- create,
- write,
- mixins: {
- apis: ['progress', 'spin', 'align'],
- styles: ['opacity'],
- animations: {
- opacity: { type: 'tween', duration: 500 },
- progress: {
- type: 'spring',
- stiffness: 0.95,
- damping: 0.65,
- mass: 10,
- },
- },
- },
- });
- const create$1 = ({ root, props }) => {
- root.element.innerHTML = (props.icon || '') + `<span>${props.label}</span>`;
- props.isDisabled = false;
- };
- const write$1 = ({ root, props }) => {
- const { isDisabled } = props;
- const shouldDisable = root.query('GET_DISABLED') || props.opacity === 0;
- if (shouldDisable && !isDisabled) {
- props.isDisabled = true;
- attr(root.element, 'disabled', 'disabled');
- } else if (!shouldDisable && isDisabled) {
- props.isDisabled = false;
- root.element.removeAttribute('disabled');
- }
- };
- const fileActionButton = createView({
- tag: 'button',
- attributes: {
- type: 'button',
- },
- ignoreRect: true,
- ignoreRectUpdate: true,
- name: 'file-action-button',
- mixins: {
- apis: ['label'],
- styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity'],
- animations: {
- scaleX: 'spring',
- scaleY: 'spring',
- translateX: 'spring',
- translateY: 'spring',
- opacity: { type: 'tween', duration: 250 },
- },
- listeners: true,
- },
- create: create$1,
- write: write$1,
- });
- const toNaturalFileSize = (bytes, decimalSeparator = '.', base = 1000, options = {}) => {
- const {
- labelBytes = 'bytes',
- labelKilobytes = 'KB',
- labelMegabytes = 'MB',
- labelGigabytes = 'GB',
- } = options;
- // no negative byte sizes
- bytes = Math.round(Math.abs(bytes));
- const KB = base;
- const MB = base * base;
- const GB = base * base * base;
- // just bytes
- if (bytes < KB) {
- return `${bytes} ${labelBytes}`;
- }
- // kilobytes
- if (bytes < MB) {
- return `${Math.floor(bytes / KB)} ${labelKilobytes}`;
- }
- // megabytes
- if (bytes < GB) {
- return `${removeDecimalsWhenZero(bytes / MB, 1, decimalSeparator)} ${labelMegabytes}`;
- }
- // gigabytes
- return `${removeDecimalsWhenZero(bytes / GB, 2, decimalSeparator)} ${labelGigabytes}`;
- };
- const removeDecimalsWhenZero = (value, decimalCount, separator) => {
- return value
- .toFixed(decimalCount)
- .split('.')
- .filter(part => part !== '0')
- .join(separator);
- };
- const create$2 = ({ root, props }) => {
- // filename
- const fileName = createElement$1('span');
- fileName.className = 'filepond--file-info-main';
- // hide for screenreaders
- // the file is contained in a fieldset with legend that contains the filename
- // no need to read it twice
- attr(fileName, 'aria-hidden', 'true');
- root.appendChild(fileName);
- root.ref.fileName = fileName;
- // filesize
- const fileSize = createElement$1('span');
- fileSize.className = 'filepond--file-info-sub';
- root.appendChild(fileSize);
- root.ref.fileSize = fileSize;
- // set initial values
- text(fileSize, root.query('GET_LABEL_FILE_WAITING_FOR_SIZE'));
- text(fileName, formatFilename(root.query('GET_ITEM_NAME', props.id)));
- };
- const updateFile = ({ root, props }) => {
- text(
- root.ref.fileSize,
- toNaturalFileSize(
- root.query('GET_ITEM_SIZE', props.id),
- '.',
- root.query('GET_FILE_SIZE_BASE'),
- root.query('GET_FILE_SIZE_LABELS', root.query)
- )
- );
- text(root.ref.fileName, formatFilename(root.query('GET_ITEM_NAME', props.id)));
- };
- const updateFileSizeOnError = ({ root, props }) => {
- // if size is available don't fallback to unknown size message
- if (isInt(root.query('GET_ITEM_SIZE', props.id))) {
- updateFile({ root, props });
- return;
- }
- text(root.ref.fileSize, root.query('GET_LABEL_FILE_SIZE_NOT_AVAILABLE'));
- };
- const fileInfo = createView({
- name: 'file-info',
- ignoreRect: true,
- ignoreRectUpdate: true,
- write: createRoute({
- DID_LOAD_ITEM: updateFile,
- DID_UPDATE_ITEM_META: updateFile,
- DID_THROW_ITEM_LOAD_ERROR: updateFileSizeOnError,
- DID_THROW_ITEM_INVALID: updateFileSizeOnError,
- }),
- didCreateView: root => {
- applyFilters('CREATE_VIEW', { ...root, view: root });
- },
- create: create$2,
- mixins: {
- styles: ['translateX', 'translateY'],
- animations: {
- translateX: 'spring',
- translateY: 'spring',
- },
- },
- });
- const toPercentage = value => Math.round(value * 100);
- const create$3 = ({ root }) => {
- // main status
- const main = createElement$1('span');
- main.className = 'filepond--file-status-main';
- root.appendChild(main);
- root.ref.main = main;
- // sub status
- const sub = createElement$1('span');
- sub.className = 'filepond--file-status-sub';
- root.appendChild(sub);
- root.ref.sub = sub;
- didSetItemLoadProgress({ root, action: { progress: null } });
- };
- const didSetItemLoadProgress = ({ root, action }) => {
- const title =
- action.progress === null
- ? root.query('GET_LABEL_FILE_LOADING')
- : `${root.query('GET_LABEL_FILE_LOADING')} ${toPercentage(action.progress)}%`;
- text(root.ref.main, title);
- text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL'));
- };
- const didSetItemProcessProgress = ({ root, action }) => {
- const title =
- action.progress === null
- ? root.query('GET_LABEL_FILE_PROCESSING')
- : `${root.query('GET_LABEL_FILE_PROCESSING')} ${toPercentage(action.progress)}%`;
- text(root.ref.main, title);
- text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL'));
- };
- const didRequestItemProcessing = ({ root }) => {
- text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING'));
- text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL'));
- };
- const didAbortItemProcessing = ({ root }) => {
- text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING_ABORTED'));
- text(root.ref.sub, root.query('GET_LABEL_TAP_TO_RETRY'));
- };
- const didCompleteItemProcessing = ({ root }) => {
- text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING_COMPLETE'));
- text(root.ref.sub, root.query('GET_LABEL_TAP_TO_UNDO'));
- };
- const clear = ({ root }) => {
- text(root.ref.main, '');
- text(root.ref.sub, '');
- };
- const error = ({ root, action }) => {
- text(root.ref.main, action.status.main);
- text(root.ref.sub, action.status.sub);
- };
- const fileStatus = createView({
- name: 'file-status',
- ignoreRect: true,
- ignoreRectUpdate: true,
- write: createRoute({
- DID_LOAD_ITEM: clear,
- DID_REVERT_ITEM_PROCESSING: clear,
- DID_REQUEST_ITEM_PROCESSING: didRequestItemProcessing,
- DID_ABORT_ITEM_PROCESSING: didAbortItemProcessing,
- DID_COMPLETE_ITEM_PROCESSING: didCompleteItemProcessing,
- DID_UPDATE_ITEM_PROCESS_PROGRESS: didSetItemProcessProgress,
- DID_UPDATE_ITEM_LOAD_PROGRESS: didSetItemLoadProgress,
- DID_THROW_ITEM_LOAD_ERROR: error,
- DID_THROW_ITEM_INVALID: error,
- DID_THROW_ITEM_PROCESSING_ERROR: error,
- DID_THROW_ITEM_PROCESSING_REVERT_ERROR: error,
- DID_THROW_ITEM_REMOVE_ERROR: error,
- }),
- didCreateView: root => {
- applyFilters('CREATE_VIEW', { ...root, view: root });
- },
- create: create$3,
- mixins: {
- styles: ['translateX', 'translateY', 'opacity'],
- animations: {
- opacity: { type: 'tween', duration: 250 },
- translateX: 'spring',
- translateY: 'spring',
- },
- },
- });
- /**
- * Button definitions for the file view
- */
- const Buttons = {
- AbortItemLoad: {
- label: 'GET_LABEL_BUTTON_ABORT_ITEM_LOAD',
- action: 'ABORT_ITEM_LOAD',
- className: 'filepond--action-abort-item-load',
- align: 'LOAD_INDICATOR_POSITION', // right
- },
- RetryItemLoad: {
- label: 'GET_LABEL_BUTTON_RETRY_ITEM_LOAD',
- action: 'RETRY_ITEM_LOAD',
- icon: 'GET_ICON_RETRY',
- className: 'filepond--action-retry-item-load',
- align: 'BUTTON_PROCESS_ITEM_POSITION', // right
- },
- RemoveItem: {
- label: 'GET_LABEL_BUTTON_REMOVE_ITEM',
- action: 'REQUEST_REMOVE_ITEM',
- icon: 'GET_ICON_REMOVE',
- className: 'filepond--action-remove-item',
- align: 'BUTTON_REMOVE_ITEM_POSITION', // left
- },
- ProcessItem: {
- label: 'GET_LABEL_BUTTON_PROCESS_ITEM',
- action: 'REQUEST_ITEM_PROCESSING',
- icon: 'GET_ICON_PROCESS',
- className: 'filepond--action-process-item',
- align: 'BUTTON_PROCESS_ITEM_POSITION', // right
- },
- AbortItemProcessing: {
- label: 'GET_LABEL_BUTTON_ABORT_ITEM_PROCESSING',
- action: 'ABORT_ITEM_PROCESSING',
- className: 'filepond--action-abort-item-processing',
- align: 'BUTTON_PROCESS_ITEM_POSITION', // right
- },
- RetryItemProcessing: {
- label: 'GET_LABEL_BUTTON_RETRY_ITEM_PROCESSING',
- action: 'RETRY_ITEM_PROCESSING',
- icon: 'GET_ICON_RETRY',
- className: 'filepond--action-retry-item-processing',
- align: 'BUTTON_PROCESS_ITEM_POSITION', // right
- },
- RevertItemProcessing: {
- label: 'GET_LABEL_BUTTON_UNDO_ITEM_PROCESSING',
- action: 'REQUEST_REVERT_ITEM_PROCESSING',
- icon: 'GET_ICON_UNDO',
- className: 'filepond--action-revert-item-processing',
- align: 'BUTTON_PROCESS_ITEM_POSITION', // right
- },
- };
- // make a list of buttons, we can then remove buttons from this list if they're disabled
- const ButtonKeys = [];
- forin(Buttons, key => {
- ButtonKeys.push(key);
- });
- const calculateFileInfoOffset = root => {
- if (getRemoveIndicatorAligment(root) === 'right') return 0;
- const buttonRect = root.ref.buttonRemoveItem.rect.element;
- return buttonRect.hidden ? null : buttonRect.width + buttonRect.left;
- };
- const calculateButtonWidth = root => {
- const buttonRect = root.ref.buttonAbortItemLoad.rect.element;
- return buttonRect.width;
- };
- // Force on full pixels so text stays crips
- const calculateFileVerticalCenterOffset = root =>
- Math.floor(root.ref.buttonRemoveItem.rect.element.height / 4);
- const calculateFileHorizontalCenterOffset = root =>
- Math.floor(root.ref.buttonRemoveItem.rect.element.left / 2);
- const getLoadIndicatorAlignment = root => root.query('GET_STYLE_LOAD_INDICATOR_POSITION');
- const getProcessIndicatorAlignment = root => root.query('GET_STYLE_PROGRESS_INDICATOR_POSITION');
- const getRemoveIndicatorAligment = root => root.query('GET_STYLE_BUTTON_REMOVE_ITEM_POSITION');
- const DefaultStyle = {
- buttonAbortItemLoad: { opacity: 0 },
- buttonRetryItemLoad: { opacity: 0 },
- buttonRemoveItem: { opacity: 0 },
- buttonProcessItem: { opacity: 0 },
- buttonAbortItemProcessing: { opacity: 0 },
- buttonRetryItemProcessing: { opacity: 0 },
- buttonRevertItemProcessing: { opacity: 0 },
- loadProgressIndicator: { opacity: 0, align: getLoadIndicatorAlignment },
- processProgressIndicator: { opacity: 0, align: getProcessIndicatorAlignment },
- processingCompleteIndicator: { opacity: 0, scaleX: 0.75, scaleY: 0.75 },
- info: { translateX: 0, translateY: 0, opacity: 0 },
- status: { translateX: 0, translateY: 0, opacity: 0 },
- };
- const IdleStyle = {
- buttonRemoveItem: { opacity: 1 },
- buttonProcessItem: { opacity: 1 },
- info: { translateX: calculateFileInfoOffset },
- status: { translateX: calculateFileInfoOffset },
- };
- const ProcessingStyle = {
- buttonAbortItemProcessing: { opacity: 1 },
- processProgressIndicator: { opacity: 1 },
- status: { opacity: 1 },
- };
- const StyleMap = {
- DID_THROW_ITEM_INVALID: {
- buttonRemoveItem: { opacity: 1 },
- info: { translateX: calculateFileInfoOffset },
- status: { translateX: calculateFileInfoOffset, opacity: 1 },
- },
- DID_START_ITEM_LOAD: {
- buttonAbortItemLoad: { opacity: 1 },
- loadProgressIndicator: { opacity: 1 },
- status: { opacity: 1 },
- },
- DID_THROW_ITEM_LOAD_ERROR: {
- buttonRetryItemLoad: { opacity: 1 },
- buttonRemoveItem: { opacity: 1 },
- info: { translateX: calculateFileInfoOffset },
- status: { opacity: 1 },
- },
- DID_START_ITEM_REMOVE: {
- processProgressIndicator: { opacity: 1, align: getRemoveIndicatorAligment },
- info: { translateX: calculateFileInfoOffset },
- status: { opacity: 0 },
- },
- DID_THROW_ITEM_REMOVE_ERROR: {
- processProgressIndicator: { opacity: 0, align: getRemoveIndicatorAligment },
- buttonRemoveItem: { opacity: 1 },
- info: { translateX: calculateFileInfoOffset },
- status: { opacity: 1, translateX: calculateFileInfoOffset },
- },
- DID_LOAD_ITEM: IdleStyle,
- DID_LOAD_LOCAL_ITEM: {
- buttonRemoveItem: { opacity: 1 },
- info: { translateX: calculateFileInfoOffset },
- status: { translateX: calculateFileInfoOffset },
- },
- DID_START_ITEM_PROCESSING: ProcessingStyle,
- DID_REQUEST_ITEM_PROCESSING: ProcessingStyle,
- DID_UPDATE_ITEM_PROCESS_PROGRESS: ProcessingStyle,
- DID_COMPLETE_ITEM_PROCESSING: {
- buttonRevertItemProcessing: { opacity: 1 },
- info: { opacity: 1 },
- status: { opacity: 1 },
- },
- DID_THROW_ITEM_PROCESSING_ERROR: {
- buttonRemoveItem: { opacity: 1 },
- buttonRetryItemProcessing: { opacity: 1 },
- status: { opacity: 1 },
- info: { translateX: calculateFileInfoOffset },
- },
- DID_THROW_ITEM_PROCESSING_REVERT_ERROR: {
- buttonRevertItemProcessing: { opacity: 1 },
- status: { opacity: 1 },
- info: { opacity: 1 },
- },
- DID_ABORT_ITEM_PROCESSING: {
- buttonRemoveItem: { opacity: 1 },
- buttonProcessItem: { opacity: 1 },
- info: { translateX: calculateFileInfoOffset },
- status: { opacity: 1 },
- },
- DID_REVERT_ITEM_PROCESSING: IdleStyle,
- };
- // complete indicator view
- const processingCompleteIndicatorView = createView({
- create: ({ root }) => {
- root.element.innerHTML = root.query('GET_ICON_DONE');
- },
- name: 'processing-complete-indicator',
- ignoreRect: true,
- mixins: {
- styles: ['scaleX', 'scaleY', 'opacity'],
- animations: {
- scaleX: 'spring',
- scaleY: 'spring',
- opacity: { type: 'tween', duration: 250 },
- },
- },
- });
- /**
- * Creates the file view
- */
- const create$4 = ({ root, props }) => {
- // copy Buttons object
- const LocalButtons = Object.keys(Buttons).reduce((prev, curr) => {
- prev[curr] = { ...Buttons[curr] };
- return prev;
- }, {});
- const { id } = props;
- // allow reverting upload
- const allowRevert = root.query('GET_ALLOW_REVERT');
- // allow remove file
- const allowRemove = root.query('GET_ALLOW_REMOVE');
- // allow processing upload
- const allowProcess = root.query('GET_ALLOW_PROCESS');
- // is instant uploading, need this to determine the icon of the undo button
- const instantUpload = root.query('GET_INSTANT_UPLOAD');
- // is async set up
- const isAsync = root.query('IS_ASYNC');
- // should align remove item buttons
- const alignRemoveItemButton = root.query('GET_STYLE_BUTTON_REMOVE_ITEM_ALIGN');
- // enabled buttons array
- let buttonFilter;
- if (isAsync) {
- if (allowProcess && !allowRevert) {
- // only remove revert button
- buttonFilter = key => !/RevertItemProcessing/.test(key);
- } else if (!allowProcess && allowRevert) {
- // only remove process button
- buttonFilter = key => !/ProcessItem|RetryItemProcessing|AbortItemProcessing/.test(key);
- } else if (!allowProcess && !allowRevert) {
- // remove all process buttons
- buttonFilter = key => !/Process/.test(key);
- }
- } else {
- // no process controls available
- buttonFilter = key => !/Process/.test(key);
- }
- const enabledButtons = buttonFilter ? ButtonKeys.filter(buttonFilter) : ButtonKeys.concat();
- // update icon and label for revert button when instant uploading
- if (instantUpload && allowRevert) {
- LocalButtons['RevertItemProcessing'].label = 'GET_LABEL_BUTTON_REMOVE_ITEM';
- LocalButtons['RevertItemProcessing'].icon = 'GET_ICON_REMOVE';
- }
- // remove last button (revert) if not allowed
- if (isAsync && !allowRevert) {
- const map = StyleMap['DID_COMPLETE_ITEM_PROCESSING'];
- map.info.translateX = calculateFileHorizontalCenterOffset;
- map.info.translateY = calculateFileVerticalCenterOffset;
- map.status.translateY = calculateFileVerticalCenterOffset;
- map.processingCompleteIndicator = { opacity: 1, scaleX: 1, scaleY: 1 };
- }
- // should align center
- if (isAsync && !allowProcess) {
- [
- 'DID_START_ITEM_PROCESSING',
- 'DID_REQUEST_ITEM_PROCESSING',
- 'DID_UPDATE_ITEM_PROCESS_PROGRESS',
- 'DID_THROW_ITEM_PROCESSING_ERROR',
- ].forEach(key => {
- StyleMap[key].status.translateY = calculateFileVerticalCenterOffset;
- });
- StyleMap['DID_THROW_ITEM_PROCESSING_ERROR'].status.translateX = calculateButtonWidth;
- }
- // move remove button to right
- if (alignRemoveItemButton && allowRevert) {
- LocalButtons['RevertItemProcessing'].align = 'BUTTON_REMOVE_ITEM_POSITION';
- const map = StyleMap['DID_COMPLETE_ITEM_PROCESSING'];
- map.info.translateX = calculateFileInfoOffset;
- map.status.translateY = calculateFileVerticalCenterOffset;
- map.processingCompleteIndicator = { opacity: 1, scaleX: 1, scaleY: 1 };
- }
- // show/hide RemoveItem button
- if (!allowRemove) {
- LocalButtons['RemoveItem'].disabled = true;
- }
- // create the button views
- forin(LocalButtons, (key, definition) => {
- // create button
- const buttonView = root.createChildView(fileActionButton, {
- label: root.query(definition.label),
- icon: root.query(definition.icon),
- opacity: 0,
- });
- // should be appended?
- if (enabledButtons.includes(key)) {
- root.appendChildView(buttonView);
- }
- // toggle
- if (definition.disabled) {
- buttonView.element.setAttribute('disabled', 'disabled');
- buttonView.element.setAttribute('hidden', 'hidden');
- }
- // add position attribute
- buttonView.element.dataset.align = root.query(`GET_STYLE_${definition.align}`);
- // add class
- buttonView.element.classList.add(definition.className);
- // handle interactions
- buttonView.on('click', e => {
- e.stopPropagation();
- if (definition.disabled) return;
- root.dispatch(definition.action, { query: id });
- });
- // set reference
- root.ref[`button${key}`] = buttonView;
- });
- // checkmark
- root.ref.processingCompleteIndicator = root.appendChildView(
- root.createChildView(processingCompleteIndicatorView)
- );
- root.ref.processingCompleteIndicator.element.dataset.align = root.query(
- `GET_STYLE_BUTTON_PROCESS_ITEM_POSITION`
- );
- // create file info view
- root.ref.info = root.appendChildView(root.createChildView(fileInfo, { id }));
- // create file status view
- root.ref.status = root.appendChildView(root.createChildView(fileStatus, { id }));
- // add progress indicators
- const loadIndicatorView = root.appendChildView(
- root.createChildView(progressIndicator, {
- opacity: 0,
- align: root.query(`GET_STYLE_LOAD_INDICATOR_POSITION`),
- })
- );
- loadIndicatorView.element.classList.add('filepond--load-indicator');
- root.ref.loadProgressIndicator = loadIndicatorView;
- const progressIndicatorView = root.appendChildView(
- root.createChildView(progressIndicator, {
- opacity: 0,
- align: root.query(`GET_STYLE_PROGRESS_INDICATOR_POSITION`),
- })
- );
- progressIndicatorView.element.classList.add('filepond--process-indicator');
- root.ref.processProgressIndicator = progressIndicatorView;
- // current active styles
- root.ref.activeStyles = [];
- };
- const write$2 = ({ root, actions, props }) => {
- // route actions
- route({ root, actions, props });
- // select last state change action
- let action = actions
- .concat()
- .filter(action => /^DID_/.test(action.type))
- .reverse()
- .find(action => StyleMap[action.type]);
- // a new action happened, let's get the matching styles
- if (action) {
- // define new active styles
- root.ref.activeStyles = [];
- const stylesToApply = StyleMap[action.type];
- forin(DefaultStyle, (name, defaultStyles) => {
- // get reference to control
- const control = root.ref[name];
- // loop over all styles for this control
- forin(defaultStyles, (key, defaultValue) => {
- const value =
- stylesToApply[name] && typeof stylesToApply[name][key] !== 'undefined'
- ? stylesToApply[name][key]
- : defaultValue;
- root.ref.activeStyles.push({ control, key, value });
- });
- });
- }
- // apply active styles to element
- root.ref.activeStyles.forEach(({ control, key, value }) => {
- control[key] = typeof value === 'function' ? value(root) : value;
- });
- };
- const route = createRoute({
- DID_SET_LABEL_BUTTON_ABORT_ITEM_PROCESSING: ({ root, action }) => {
- root.ref.buttonAbortItemProcessing.label = action.value;
- },
- DID_SET_LABEL_BUTTON_ABORT_ITEM_LOAD: ({ root, action }) => {
- root.ref.buttonAbortItemLoad.label = action.value;
- },
- DID_SET_LABEL_BUTTON_ABORT_ITEM_REMOVAL: ({ root, action }) => {
- root.ref.buttonAbortItemRemoval.label = action.value;
- },
- DID_REQUEST_ITEM_PROCESSING: ({ root }) => {
- root.ref.processProgressIndicator.spin = true;
- root.ref.processProgressIndicator.progress = 0;
- },
- DID_START_ITEM_LOAD: ({ root }) => {
- root.ref.loadProgressIndicator.spin = true;
- root.ref.loadProgressIndicator.progress = 0;
- },
- DID_START_ITEM_REMOVE: ({ root }) => {
- root.ref.processProgressIndicator.spin = true;
- root.ref.processProgressIndicator.progress = 0;
- },
- DID_UPDATE_ITEM_LOAD_PROGRESS: ({ root, action }) => {
- root.ref.loadProgressIndicator.spin = false;
- root.ref.loadProgressIndicator.progress = action.progress;
- },
- DID_UPDATE_ITEM_PROCESS_PROGRESS: ({ root, action }) => {
- root.ref.processProgressIndicator.spin = false;
- root.ref.processProgressIndicator.progress = action.progress;
- },
- });
- const file = createView({
- create: create$4,
- write: write$2,
- didCreateView: root => {
- applyFilters('CREATE_VIEW', { ...root, view: root });
- },
- name: 'file',
- });
- /**
- * Creates the file view
- */
- const create$5 = ({ root, props }) => {
- // filename
- root.ref.fileName = createElement$1('legend');
- root.appendChild(root.ref.fileName);
- // file appended
- root.ref.file = root.appendChildView(root.createChildView(file, { id: props.id }));
- // data has moved to data.js
- root.ref.data = false;
- };
- /**
- * Data storage
- */
- const didLoadItem = ({ root, props }) => {
- // updates the legend of the fieldset so screenreaders can better group buttons
- text(root.ref.fileName, formatFilename(root.query('GET_ITEM_NAME', props.id)));
- };
- const fileWrapper = createView({
- create: create$5,
- ignoreRect: true,
- write: createRoute({
- DID_LOAD_ITEM: didLoadItem,
- }),
- didCreateView: root => {
- applyFilters('CREATE_VIEW', { ...root, view: root });
- },
- tag: 'fieldset',
- name: 'file-wrapper',
- });
- const PANEL_SPRING_PROPS = { type: 'spring', damping: 0.6, mass: 7 };
- const create$6 = ({ root, props }) => {
- [
- {
- name: 'top',
- },
- {
- name: 'center',
- props: {
- translateY: null,
- scaleY: null,
- },
- mixins: {
- animations: {
- scaleY: PANEL_SPRING_PROPS,
- },
- styles: ['translateY', 'scaleY'],
- },
- },
- {
- name: 'bottom',
- props: {
- translateY: null,
- },
- mixins: {
- animations: {
- translateY: PANEL_SPRING_PROPS,
- },
- styles: ['translateY'],
- },
- },
- ].forEach(section => {
- createSection(root, section, props.name);
- });
- root.element.classList.add(`filepond--${props.name}`);
- root.ref.scalable = null;
- };
- const createSection = (root, section, className) => {
- const viewConstructor = createView({
- name: `panel-${section.name} filepond--${className}`,
- mixins: section.mixins,
- ignoreRectUpdate: true,
- });
- const view = root.createChildView(viewConstructor, section.props);
- root.ref[section.name] = root.appendChildView(view);
- };
- const write$3 = ({ root, props }) => {
- // update scalable state
- if (root.ref.scalable === null || props.scalable !== root.ref.scalable) {
- root.ref.scalable = isBoolean(props.scalable) ? props.scalable : true;
- root.element.dataset.scalable = root.ref.scalable;
- }
- // no height, can't set
- if (!props.height) return;
- // get child rects
- const topRect = root.ref.top.rect.element;
- const bottomRect = root.ref.bottom.rect.element;
- // make sure height never is smaller than bottom and top seciton heights combined (will probably never happen, but who knows)
- const height = Math.max(topRect.height + bottomRect.height, props.height);
- // offset center part
- root.ref.center.translateY = topRect.height;
- // scale center part
- // use math ceil to prevent transparent lines because of rounding errors
- root.ref.center.scaleY = (height - topRect.height - bottomRect.height) / 100;
- // offset bottom part
- root.ref.bottom.translateY = height - bottomRect.height;
- };
- const panel = createView({
- name: 'panel',
- read: ({ root, props }) => (props.heightCurrent = root.ref.bottom.translateY),
- write: write$3,
- create: create$6,
- ignoreRect: true,
- mixins: {
- apis: ['height', 'heightCurrent', 'scalable'],
- },
- });
- const createDragHelper = items => {
- const itemIds = items.map(item => item.id);
- let prevIndex = undefined;
- return {
- setIndex: index => {
- prevIndex = index;
- },
- getIndex: () => prevIndex,
- getItemIndex: item => itemIds.indexOf(item.id),
- };
- };
- const ITEM_TRANSLATE_SPRING = {
- type: 'spring',
- stiffness: 0.75,
- damping: 0.45,
- mass: 10,
- };
- const ITEM_SCALE_SPRING = 'spring';
- const StateMap = {
- DID_START_ITEM_LOAD: 'busy',
- DID_UPDATE_ITEM_LOAD_PROGRESS: 'loading',
- DID_THROW_ITEM_INVALID: 'load-invalid',
- DID_THROW_ITEM_LOAD_ERROR: 'load-error',
- DID_LOAD_ITEM: 'idle',
- DID_THROW_ITEM_REMOVE_ERROR: 'remove-error',
- DID_START_ITEM_REMOVE: 'busy',
- DID_START_ITEM_PROCESSING: 'busy processing',
- DID_REQUEST_ITEM_PROCESSING: 'busy processing',
- DID_UPDATE_ITEM_PROCESS_PROGRESS: 'processing',
- DID_COMPLETE_ITEM_PROCESSING: 'processing-complete',
- DID_THROW_ITEM_PROCESSING_ERROR: 'processing-error',
- DID_THROW_ITEM_PROCESSING_REVERT_ERROR: 'processing-revert-error',
- DID_ABORT_ITEM_PROCESSING: 'cancelled',
- DID_REVERT_ITEM_PROCESSING: 'idle',
- };
- /**
- * Creates the file view
- */
- const create$7 = ({ root, props }) => {
- // select
- root.ref.handleClick = e => root.dispatch('DID_ACTIVATE_ITEM', { id: props.id });
- // set id
- root.element.id = `filepond--item-${props.id}`;
- root.element.addEventListener('click', root.ref.handleClick);
- // file view
- root.ref.container = root.appendChildView(root.createChildView(fileWrapper, { id: props.id }));
- // file panel
- root.ref.panel = root.appendChildView(root.createChildView(panel, { name: 'item-panel' }));
- // default start height
- root.ref.panel.height = null;
- // by default not marked for removal
- props.markedForRemoval = false;
- // if not allowed to reorder file items, exit here
- if (!root.query('GET_ALLOW_REORDER')) return;
- // set to idle so shows grab cursor
- root.element.dataset.dragState = 'idle';
- const grab = e => {
- if (!e.isPrimary) return;
- let removedActivateListener = false;
- const origin = {
- x: e.pageX,
- y: e.pageY,
- };
- props.dragOrigin = {
- x: root.translateX,
- y: root.translateY,
- };
- props.dragCenter = {
- x: e.offsetX,
- y: e.offsetY,
- };
- const dragState = createDragHelper(root.query('GET_ACTIVE_ITEMS'));
- root.dispatch('DID_GRAB_ITEM', { id: props.id, dragState });
- const drag = e => {
- if (!e.isPrimary) return;
- e.stopPropagation();
- e.preventDefault();
- props.dragOffset = {
- x: e.pageX - origin.x,
- y: e.pageY - origin.y,
- };
- // if dragged stop listening to clicks, will re-add when done dragging
- const dist =
- props.dragOffset.x * props.dragOffset.x + props.dragOffset.y * props.dragOffset.y;
- if (dist > 16 && !removedActivateListener) {
- removedActivateListener = true;
- root.element.removeEventListener('click', root.ref.handleClick);
- }
- root.dispatch('DID_DRAG_ITEM', { id: props.id, dragState });
- };
- const drop = e => {
- if (!e.isPrimary) return;
- document.removeEventListener('pointermove', drag);
- document.removeEventListener('pointerup', drop);
- props.dragOffset = {
- x: e.pageX - origin.x,
- y: e.pageY - origin.y,
- };
- root.dispatch('DID_DROP_ITEM', { id: props.id, dragState });
- // start listening to clicks again
- if (removedActivateListener) {
- setTimeout(() => root.element.addEventListener('click', root.ref.handleClick), 0);
- }
- };
- document.addEventListener('pointermove', drag);
- document.addEventListener('pointerup', drop);
- };
- root.element.addEventListener('pointerdown', grab);
- };
- const route$1 = createRoute({
- DID_UPDATE_PANEL_HEIGHT: ({ root, action }) => {
- root.height = action.height;
- },
- });
- const write$4 = createRoute(
- {
- DID_GRAB_ITEM: ({ root, props }) => {
- props.dragOrigin = {
- x: root.translateX,
- y: root.translateY,
- };
- },
- DID_DRAG_ITEM: ({ root }) => {
- root.element.dataset.dragState = 'drag';
- },
- DID_DROP_ITEM: ({ root, props }) => {
- props.dragOffset = null;
- props.dragOrigin = null;
- root.element.dataset.dragState = 'drop';
- },
- },
- ({ root, actions, props, shouldOptimize }) => {
- if (root.element.dataset.dragState === 'drop') {
- if (root.scaleX <= 1) {
- root.element.dataset.dragState = 'idle';
- }
- }
- // select last state change action
- let action = actions
- .concat()
- .filter(action => /^DID_/.test(action.type))
- .reverse()
- .find(action => StateMap[action.type]);
- // no need to set same state twice
- if (action && action.type !== props.currentState) {
- // set current state
- props.currentState = action.type;
- // set state
- root.element.dataset.filepondItemState = StateMap[props.currentState] || '';
- }
- // route actions
- const aspectRatio =
- root.query('GET_ITEM_PANEL_ASPECT_RATIO') || root.query('GET_PANEL_ASPECT_RATIO');
- if (!aspectRatio) {
- route$1({ root, actions, props });
- if (!root.height && root.ref.container.rect.element.height > 0) {
- root.height = root.ref.container.rect.element.height;
- }
- } else if (!shouldOptimize) {
- root.height = root.rect.element.width * aspectRatio;
- }
- // sync panel height with item height
- if (shouldOptimize) {
- root.ref.panel.height = null;
- }
- root.ref.panel.height = root.height;
- }
- );
- const item = createView({
- create: create$7,
- write: write$4,
- destroy: ({ root, props }) => {
- root.element.removeEventListener('click', root.ref.handleClick);
- root.dispatch('RELEASE_ITEM', { query: props.id });
- },
- tag: 'li',
- name: 'item',
- mixins: {
- apis: [
- 'id',
- 'interactionMethod',
- 'markedForRemoval',
- 'spawnDate',
- 'dragCenter',
- 'dragOrigin',
- 'dragOffset',
- ],
- styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity', 'height'],
- animations: {
- scaleX: ITEM_SCALE_SPRING,
- scaleY: ITEM_SCALE_SPRING,
- translateX: ITEM_TRANSLATE_SPRING,
- translateY: ITEM_TRANSLATE_SPRING,
- opacity: { type: 'tween', duration: 150 },
- },
- },
- });
- var getItemsPerRow = (horizontalSpace, itemWidth) => {
- // add one pixel leeway, when using percentages for item width total items can be 1.99 per row
- return Math.max(1, Math.floor((horizontalSpace + 1) / itemWidth));
- };
- const getItemIndexByPosition = (view, children, positionInView) => {
- if (!positionInView) return;
- const horizontalSpace = view.rect.element.width;
- // const children = view.childViews;
- const l = children.length;
- let last = null;
- // -1, don't move items to accomodate (either add to top or bottom)
- if (l === 0 || positionInView.top < children[0].rect.element.top) return -1;
- // let's get the item width
- const item = children[0];
- const itemRect = item.rect.element;
- const itemHorizontalMargin = itemRect.marginLeft + itemRect.marginRight;
- const itemWidth = itemRect.width + itemHorizontalMargin;
- const itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth);
- // stack
- if (itemsPerRow === 1) {
- for (let index = 0; index < l; index++) {
- const child = children[index];
- const childMid = child.rect.outer.top + child.rect.element.height * 0.5;
- if (positionInView.top < childMid) {
- return index;
- }
- }
- return l;
- }
- // grid
- const itemVerticalMargin = itemRect.marginTop + itemRect.marginBottom;
- const itemHeight = itemRect.height + itemVerticalMargin;
- for (let index = 0; index < l; index++) {
- const indexX = index % itemsPerRow;
- const indexY = Math.floor(index / itemsPerRow);
- const offsetX = indexX * itemWidth;
- const offsetY = indexY * itemHeight;
- const itemTop = offsetY - itemRect.marginTop;
- const itemRight = offsetX + itemWidth;
- const itemBottom = offsetY + itemHeight + itemRect.marginBottom;
- if (positionInView.top < itemBottom && positionInView.top > itemTop) {
- if (positionInView.left < itemRight) {
- return index;
- } else if (index !== l - 1) {
- last = index;
- } else {
- last = null;
- }
- }
- }
- if (last !== null) {
- return last;
- }
- return l;
- };
- const dropAreaDimensions = {
- height: 0,
- width: 0,
- get getHeight() {
- return this.height;
- },
- set setHeight(val) {
- if (this.height === 0 || val === 0) this.height = val;
- },
- get getWidth() {
- return this.width;
- },
- set setWidth(val) {
- if (this.width === 0 || val === 0) this.width = val;
- },
- setDimensions: function(height, width) {
- if (this.height === 0 || height === 0) this.height = height;
- if (this.width === 0 || width === 0) this.width = width;
- },
- };
- const create$8 = ({ root }) => {
- // need to set role to list as otherwise it won't be read as a list by VoiceOver
- attr(root.element, 'role', 'list');
- root.ref.lastItemSpanwDate = Date.now();
- };
- /**
- * Inserts a new item
- * @param root
- * @param action
- */
- const addItemView = ({ root, action }) => {
- const { id, index, interactionMethod } = action;
- root.ref.addIndex = index;
- const now = Date.now();
- let spawnDate = now;
- let opacity = 1;
- if (interactionMethod !== InteractionMethod.NONE) {
- opacity = 0;
- const cooldown = root.query('GET_ITEM_INSERT_INTERVAL');
- const dist = now - root.ref.lastItemSpanwDate;
- spawnDate = dist < cooldown ? now + (cooldown - dist) : now;
- }
- root.ref.lastItemSpanwDate = spawnDate;
- root.appendChildView(
- root.createChildView(
- // view type
- item,
- // props
- {
- spawnDate,
- id,
- opacity,
- interactionMethod,
- }
- ),
- index
- );
- };
- const moveItem = (item, x, y, vx = 0, vy = 1) => {
- // set to null to remove animation while dragging
- if (item.dragOffset) {
- item.translateX = null;
- item.translateY = null;
- item.translateX = item.dragOrigin.x + item.dragOffset.x;
- item.translateY = item.dragOrigin.y + item.dragOffset.y;
- item.scaleX = 1.025;
- item.scaleY = 1.025;
- } else {
- item.translateX = x;
- item.translateY = y;
- if (Date.now() > item.spawnDate) {
- // reveal element
- if (item.opacity === 0) {
- introItemView(item, x, y, vx, vy);
- }
- // make sure is default scale every frame
- item.scaleX = 1;
- item.scaleY = 1;
- item.opacity = 1;
- }
- }
- };
- const introItemView = (item, x, y, vx, vy) => {
- if (item.interactionMethod === InteractionMethod.NONE) {
- item.translateX = null;
- item.translateX = x;
- item.translateY = null;
- item.translateY = y;
- } else if (item.interactionMethod === InteractionMethod.DROP) {
- item.translateX = null;
- item.translateX = x - vx * 20;
- item.translateY = null;
- item.translateY = y - vy * 10;
- item.scaleX = 0.8;
- item.scaleY = 0.8;
- } else if (item.interactionMethod === InteractionMethod.BROWSE) {
- item.translateY = null;
- item.translateY = y - 30;
- } else if (item.interactionMethod === InteractionMethod.API) {
- item.translateX = null;
- item.translateX = x - 30;
- item.translateY = null;
- }
- };
- /**
- * Removes an existing item
- * @param root
- * @param action
- */
- const removeItemView = ({ root, action }) => {
- const { id } = action;
- // get the view matching the given id
- const view = root.childViews.find(child => child.id === id);
- // if no view found, exit
- if (!view) {
- return;
- }
- // animate view out of view
- view.scaleX = 0.9;
- view.scaleY = 0.9;
- view.opacity = 0;
- // mark for removal
- view.markedForRemoval = true;
- };
- const getItemHeight = child =>
- child.rect.element.height +
- child.rect.element.marginBottom * 0.5 +
- child.rect.element.marginTop * 0.5;
- const getItemWidth = child =>
- child.rect.element.width +
- child.rect.element.marginLeft * 0.5 +
- child.rect.element.marginRight * 0.5;
- const dragItem = ({ root, action }) => {
- const { id, dragState } = action;
- // reference to item
- const item = root.query('GET_ITEM', { id });
- // get the view matching the given id
- const view = root.childViews.find(child => child.id === id);
- const numItems = root.childViews.length;
- const oldIndex = dragState.getItemIndex(item);
- // if no view found, exit
- if (!view) return;
- const dragPosition = {
- x: view.dragOrigin.x + view.dragOffset.x + view.dragCenter.x,
- y: view.dragOrigin.y + view.dragOffset.y + view.dragCenter.y,
- };
- // get drag area dimensions
- const dragHeight = getItemHeight(view);
- const dragWidth = getItemWidth(view);
- // get rows and columns (There will always be at least one row and one column if a file is present)
- let cols = Math.floor(root.rect.outer.width / dragWidth);
- if (cols > numItems) cols = numItems;
- // rows are used to find when we have left the preview area bounding box
- const rows = Math.floor(numItems / cols + 1);
- dropAreaDimensions.setHeight = dragHeight * rows;
- dropAreaDimensions.setWidth = dragWidth * cols;
- // get new index of dragged item
- var location = {
- y: Math.floor(dragPosition.y / dragHeight),
- x: Math.floor(dragPosition.x / dragWidth),
- getGridIndex: function getGridIndex() {
- if (
- dragPosition.y > dropAreaDimensions.getHeight ||
- dragPosition.y < 0 ||
- dragPosition.x > dropAreaDimensions.getWidth ||
- dragPosition.x < 0
- )
- return oldIndex;
- return this.y * cols + this.x;
- },
- getColIndex: function getColIndex() {
- const items = root.query('GET_ACTIVE_ITEMS');
- const visibleChildren = root.childViews.filter(child => child.rect.element.height);
- const children = items.map(item =>
- visibleChildren.find(childView => childView.id === item.id)
- );
- const currentIndex = children.findIndex(child => child === view);
- const dragHeight = getItemHeight(view);
- const l = children.length;
- let idx = l;
- let childHeight = 0;
- let childBottom = 0;
- let childTop = 0;
- for (let i = 0; i < l; i++) {
- childHeight = getItemHeight(children[i]);
- childTop = childBottom;
- childBottom = childTop + childHeight;
- if (dragPosition.y < childBottom) {
- if (currentIndex > i) {
- if (dragPosition.y < childTop + dragHeight) {
- idx = i;
- break;
- }
- continue;
- }
- idx = i;
- break;
- }
- }
- return idx;
- },
- };
- // get new index
- const index = cols > 1 ? location.getGridIndex() : location.getColIndex();
- root.dispatch('MOVE_ITEM', { query: view, index });
- // if the index of the item changed, dispatch reorder action
- const currentIndex = dragState.getIndex();
- if (currentIndex === undefined || currentIndex !== index) {
- dragState.setIndex(index);
- if (currentIndex === undefined) return;
- root.dispatch('DID_REORDER_ITEMS', {
- items: root.query('GET_ACTIVE_ITEMS'),
- origin: oldIndex,
- target: index,
- });
- }
- };
- /**
- * Setup action routes
- */
- const route$2 = createRoute({
- DID_ADD_ITEM: addItemView,
- DID_REMOVE_ITEM: removeItemView,
- DID_DRAG_ITEM: dragItem,
- });
- /**
- * Write to view
- * @param root
- * @param actions
- * @param props
- */
- const write$5 = ({ root, props, actions, shouldOptimize }) => {
- // route actions
- route$2({ root, props, actions });
- const { dragCoordinates } = props;
- // available space on horizontal axis
- const horizontalSpace = root.rect.element.width;
- // only draw children that have dimensions
- const visibleChildren = root.childViews.filter(child => child.rect.element.height);
- // sort based on current active items
- const children = root
- .query('GET_ACTIVE_ITEMS')
- .map(item => visibleChildren.find(child => child.id === item.id))
- .filter(item => item);
- // get index
- const dragIndex = dragCoordinates
- ? getItemIndexByPosition(root, children, dragCoordinates)
- : null;
- // add index is used to reserve the dropped/added item index till the actual item is rendered
- const addIndex = root.ref.addIndex || null;
- // add index no longer needed till possibly next draw
- root.ref.addIndex = null;
- let dragIndexOffset = 0;
- let removeIndexOffset = 0;
- let addIndexOffset = 0;
- if (children.length === 0) return;
- const childRect = children[0].rect.element;
- const itemVerticalMargin = childRect.marginTop + childRect.marginBottom;
- const itemHorizontalMargin = childRect.marginLeft + childRect.marginRight;
- const itemWidth = childRect.width + itemHorizontalMargin;
- const itemHeight = childRect.height + itemVerticalMargin;
- const itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth);
- // stack
- if (itemsPerRow === 1) {
- let offsetY = 0;
- let dragOffset = 0;
- children.forEach((child, index) => {
- if (dragIndex) {
- let dist = index - dragIndex;
- if (dist === -2) {
- dragOffset = -itemVerticalMargin * 0.25;
- } else if (dist === -1) {
- dragOffset = -itemVerticalMargin * 0.75;
- } else if (dist === 0) {
- dragOffset = itemVerticalMargin * 0.75;
- } else if (dist === 1) {
- dragOffset = itemVerticalMargin * 0.25;
- } else {
- dragOffset = 0;
- }
- }
- if (shouldOptimize) {
- child.translateX = null;
- child.translateY = null;
- }
- if (!child.markedForRemoval) {
- moveItem(child, 0, offsetY + dragOffset);
- }
- let itemHeight = child.rect.element.height + itemVerticalMargin;
- let visualHeight = itemHeight * (child.markedForRemoval ? child.opacity : 1);
- offsetY += visualHeight;
- });
- }
- // grid
- else {
- let prevX = 0;
- let prevY = 0;
- children.forEach((child, index) => {
- if (index === dragIndex) {
- dragIndexOffset = 1;
- }
- if (index === addIndex) {
- addIndexOffset += 1;
- }
- if (child.markedForRemoval && child.opacity < 0.5) {
- removeIndexOffset -= 1;
- }
- const visualIndex = index + addIndexOffset + dragIndexOffset + removeIndexOffset;
- const indexX = visualIndex % itemsPerRow;
- const indexY = Math.floor(visualIndex / itemsPerRow);
- const offsetX = indexX * itemWidth;
- const offsetY = indexY * itemHeight;
- const vectorX = Math.sign(offsetX - prevX);
- const vectorY = Math.sign(offsetY - prevY);
- prevX = offsetX;
- prevY = offsetY;
- if (child.markedForRemoval) return;
- if (shouldOptimize) {
- child.translateX = null;
- child.translateY = null;
- }
- moveItem(child, offsetX, offsetY, vectorX, vectorY);
- });
- }
- };
- /**
- * Filters actions that are meant specifically for a certain child of the list
- * @param child
- * @param actions
- */
- const filterSetItemActions = (child, actions) =>
- actions.filter(action => {
- // if action has an id, filter out actions that don't have this child id
- if (action.data && action.data.id) {
- return child.id === action.data.id;
- }
- // allow all other actions
- return true;
- });
- const list = createView({
- create: create$8,
- write: write$5,
- tag: 'ul',
- name: 'list',
- didWriteView: ({ root }) => {
- root.childViews
- .filter(view => view.markedForRemoval && view.opacity === 0 && view.resting)
- .forEach(view => {
- view._destroy();
- root.removeChildView(view);
- });
- },
- filterFrameActionsForChild: filterSetItemActions,
- mixins: {
- apis: ['dragCoordinates'],
- },
- });
- const create$9 = ({ root, props }) => {
- root.ref.list = root.appendChildView(root.createChildView(list));
- props.dragCoordinates = null;
- props.overflowing = false;
- };
- const storeDragCoordinates = ({ root, props, action }) => {
- if (!root.query('GET_ITEM_INSERT_LOCATION_FREEDOM')) return;
- props.dragCoordinates = {
- left: action.position.scopeLeft - root.ref.list.rect.element.left,
- top:
- action.position.scopeTop -
- (root.rect.outer.top + root.rect.element.marginTop + root.rect.element.scrollTop),
- };
- };
- const clearDragCoordinates = ({ props }) => {
- props.dragCoordinates = null;
- };
- const route$3 = createRoute({
- DID_DRAG: storeDragCoordinates,
- DID_END_DRAG: clearDragCoordinates,
- });
- const write$6 = ({ root, props, actions }) => {
- // route actions
- route$3({ root, props, actions });
- // current drag position
- root.ref.list.dragCoordinates = props.dragCoordinates;
- // if currently overflowing but no longer received overflow
- if (props.overflowing && !props.overflow) {
- props.overflowing = false;
- // reset overflow state
- root.element.dataset.state = '';
- root.height = null;
- }
- // if is not overflowing currently but does receive overflow value
- if (props.overflow) {
- const newHeight = Math.round(props.overflow);
- if (newHeight !== root.height) {
- props.overflowing = true;
- root.element.dataset.state = 'overflow';
- root.height = newHeight;
- }
- }
- };
- const listScroller = createView({
- create: create$9,
- write: write$6,
- name: 'list-scroller',
- mixins: {
- apis: ['overflow', 'dragCoordinates'],
- styles: ['height', 'translateY'],
- animations: {
- translateY: 'spring',
- },
- },
- });
- const attrToggle = (element, name, state, enabledValue = '') => {
- if (state) {
- attr(element, name, enabledValue);
- } else {
- element.removeAttribute(name);
- }
- };
- const resetFileInput = input => {
- // no value, no need to reset
- if (!input || input.value === '') {
- return;
- }
- try {
- // for modern browsers
- input.value = '';
- } catch (err) {}
- // for IE10
- if (input.value) {
- // quickly append input to temp form and reset form
- const form = createElement$1('form');
- const parentNode = input.parentNode;
- const ref = input.nextSibling;
- form.appendChild(input);
- form.reset();
- // re-inject input where it originally was
- if (ref) {
- parentNode.insertBefore(input, ref);
- } else {
- parentNode.appendChild(input);
- }
- }
- };
- const create$a = ({ root, props }) => {
- // set id so can be referenced from outside labels
- root.element.id = `filepond--browser-${props.id}`;
- // set name of element (is removed when a value is set)
- attr(root.element, 'name', root.query('GET_NAME'));
- // we have to link this element to the status element
- attr(root.element, 'aria-controls', `filepond--assistant-${props.id}`);
- // set label, we use labelled by as otherwise the screenreader does not read the "browse" text in the label (as it has tabindex: 0)
- attr(root.element, 'aria-labelledby', `filepond--drop-label-${props.id}`);
- // set configurable props
- setAcceptedFileTypes({ root, action: { value: root.query('GET_ACCEPTED_FILE_TYPES') } });
- toggleAllowMultiple({ root, action: { value: root.query('GET_ALLOW_MULTIPLE') } });
- toggleDirectoryFilter({ root, action: { value: root.query('GET_ALLOW_DIRECTORIES_ONLY') } });
- toggleDisabled({ root });
- toggleRequired({ root, action: { value: root.query('GET_REQUIRED') } });
- setCaptureMethod({ root, action: { value: root.query('GET_CAPTURE_METHOD') } });
- // handle changes to the input field
- root.ref.handleChange = e => {
- if (!root.element.value) {
- return;
- }
- // extract files and move value of webkitRelativePath path to _relativePath
- const files = Array.from(root.element.files).map(file => {
- file._relativePath = file.webkitRelativePath;
- return file;
- });
- // we add a little delay so the OS file select window can move out of the way before we add our file
- setTimeout(() => {
- // load files
- props.onload(files);
- // reset input, it's just for exposing a method to drop files, should not retain any state
- resetFileInput(root.element);
- }, 250);
- };
- root.element.addEventListener('change', root.ref.handleChange);
- };
- const setAcceptedFileTypes = ({ root, action }) => {
- if (!root.query('GET_ALLOW_SYNC_ACCEPT_ATTRIBUTE')) return;
- attrToggle(root.element, 'accept', !!action.value, action.value ? action.value.join(',') : '');
- };
- const toggleAllowMultiple = ({ root, action }) => {
- attrToggle(root.element, 'multiple', action.value);
- };
- const toggleDirectoryFilter = ({ root, action }) => {
- attrToggle(root.element, 'webkitdirectory', action.value);
- };
- const toggleDisabled = ({ root }) => {
- const isDisabled = root.query('GET_DISABLED');
- const doesAllowBrowse = root.query('GET_ALLOW_BROWSE');
- const disableField = isDisabled || !doesAllowBrowse;
- attrToggle(root.element, 'disabled', disableField);
- };
- const toggleRequired = ({ root, action }) => {
- // want to remove required, always possible
- if (!action.value) {
- attrToggle(root.element, 'required', false);
- }
- // if want to make required, only possible when zero items
- else if (root.query('GET_TOTAL_ITEMS') === 0) {
- attrToggle(root.element, 'required', true);
- }
- };
- const setCaptureMethod = ({ root, action }) => {
- attrToggle(root.element, 'capture', !!action.value, action.value === true ? '' : action.value);
- };
- const updateRequiredStatus = ({ root }) => {
- const { element } = root;
- // always remove the required attribute when more than zero items
- if (root.query('GET_TOTAL_ITEMS') > 0) {
- attrToggle(element, 'required', false);
- attrToggle(element, 'name', false);
- } else {
- // add name attribute
- attrToggle(element, 'name', true, root.query('GET_NAME'));
- // remove any validation messages
- const shouldCheckValidity = root.query('GET_CHECK_VALIDITY');
- if (shouldCheckValidity) {
- element.setCustomValidity('');
- }
- // we only add required if the field has been deemed required
- if (root.query('GET_REQUIRED')) {
- attrToggle(element, 'required', true);
- }
- }
- };
- const updateFieldValidityStatus = ({ root }) => {
- const shouldCheckValidity = root.query('GET_CHECK_VALIDITY');
- if (!shouldCheckValidity) return;
- root.element.setCustomValidity(root.query('GET_LABEL_INVALID_FIELD'));
- };
- const browser = createView({
- tag: 'input',
- name: 'browser',
- ignoreRect: true,
- ignoreRectUpdate: true,
- attributes: {
- type: 'file',
- },
- create: create$a,
- destroy: ({ root }) => {
- root.element.removeEventListener('change', root.ref.handleChange);
- },
- write: createRoute({
- DID_LOAD_ITEM: updateRequiredStatus,
- DID_REMOVE_ITEM: updateRequiredStatus,
- DID_THROW_ITEM_INVALID: updateFieldValidityStatus,
- DID_SET_DISABLED: toggleDisabled,
- DID_SET_ALLOW_BROWSE: toggleDisabled,
- DID_SET_ALLOW_DIRECTORIES_ONLY: toggleDirectoryFilter,
- DID_SET_ALLOW_MULTIPLE: toggleAllowMultiple,
- DID_SET_ACCEPTED_FILE_TYPES: setAcceptedFileTypes,
- DID_SET_CAPTURE_METHOD: setCaptureMethod,
- DID_SET_REQUIRED: toggleRequired,
- }),
- });
- const Key = {
- ENTER: 13,
- SPACE: 32,
- };
- const create$b = ({ root, props }) => {
- // create the label and link it to the file browser
- const label = createElement$1('label');
- attr(label, 'for', `filepond--browser-${props.id}`);
- // use for labeling file input (aria-labelledby on file input)
- attr(label, 'id', `filepond--drop-label-${props.id}`);
- // 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.
- attr(label, 'aria-hidden', 'true');
- // handle keys
- root.ref.handleKeyDown = e => {
- const isActivationKey = e.keyCode === Key.ENTER || e.keyCode === Key.SPACE;
- if (!isActivationKey) return;
- // stops from triggering the element a second time
- e.preventDefault();
- // click link (will then in turn activate file input)
- root.ref.label.click();
- };
- root.ref.handleClick = e => {
- const isLabelClick = e.target === label || label.contains(e.target);
- // don't want to click twice
- if (isLabelClick) return;
- // click link (will then in turn activate file input)
- root.ref.label.click();
- };
- // attach events
- label.addEventListener('keydown', root.ref.handleKeyDown);
- root.element.addEventListener('click', root.ref.handleClick);
- // update
- updateLabelValue(label, props.caption);
- // add!
- root.appendChild(label);
- root.ref.label = label;
- };
- const updateLabelValue = (label, value) => {
- label.innerHTML = value;
- const clickable = label.querySelector('.filepond--label-action');
- if (clickable) {
- attr(clickable, 'tabindex', '0');
- }
- return value;
- };
- const dropLabel = createView({
- name: 'drop-label',
- ignoreRect: true,
- create: create$b,
- destroy: ({ root }) => {
- root.ref.label.addEventListener('keydown', root.ref.handleKeyDown);
- root.element.removeEventListener('click', root.ref.handleClick);
- },
- write: createRoute({
- DID_SET_LABEL_IDLE: ({ root, action }) => {
- updateLabelValue(root.ref.label, action.value);
- },
- }),
- mixins: {
- styles: ['opacity', 'translateX', 'translateY'],
- animations: {
- opacity: { type: 'tween', duration: 150 },
- translateX: 'spring',
- translateY: 'spring',
- },
- },
- });
- const blob = createView({
- name: 'drip-blob',
- ignoreRect: true,
- mixins: {
- styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity'],
- animations: {
- scaleX: 'spring',
- scaleY: 'spring',
- translateX: 'spring',
- translateY: 'spring',
- opacity: { type: 'tween', duration: 250 },
- },
- },
- });
- const addBlob = ({ root }) => {
- const centerX = root.rect.element.width * 0.5;
- const centerY = root.rect.element.height * 0.5;
- root.ref.blob = root.appendChildView(
- root.createChildView(blob, {
- opacity: 0,
- scaleX: 2.5,
- scaleY: 2.5,
- translateX: centerX,
- translateY: centerY,
- })
- );
- };
- const moveBlob = ({ root, action }) => {
- if (!root.ref.blob) {
- addBlob({ root });
- return;
- }
- root.ref.blob.translateX = action.position.scopeLeft;
- root.ref.blob.translateY = action.position.scopeTop;
- root.ref.blob.scaleX = 1;
- root.ref.blob.scaleY = 1;
- root.ref.blob.opacity = 1;
- };
- const hideBlob = ({ root }) => {
- if (!root.ref.blob) {
- return;
- }
- root.ref.blob.opacity = 0;
- };
- const explodeBlob = ({ root }) => {
- if (!root.ref.blob) {
- return;
- }
- root.ref.blob.scaleX = 2.5;
- root.ref.blob.scaleY = 2.5;
- root.ref.blob.opacity = 0;
- };
- const write$7 = ({ root, props, actions }) => {
- route$4({ root, props, actions });
- const { blob } = root.ref;
- if (actions.length === 0 && blob && blob.opacity === 0) {
- root.removeChildView(blob);
- root.ref.blob = null;
- }
- };
- const route$4 = createRoute({
- DID_DRAG: moveBlob,
- DID_DROP: explodeBlob,
- DID_END_DRAG: hideBlob,
- });
- const drip = createView({
- ignoreRect: true,
- ignoreRectUpdate: true,
- name: 'drip',
- write: write$7,
- });
- const setInputFiles = (element, files) => {
- try {
- // Create a DataTransfer instance and add a newly created file
- const dataTransfer = new DataTransfer();
- files.forEach(file => {
- if (file instanceof File) {
- dataTransfer.items.add(file);
- } else {
- dataTransfer.items.add(
- new File([file], file.name, {
- type: file.type,
- })
- );
- }
- });
- // Assign the DataTransfer files list to the file input
- element.files = dataTransfer.files;
- } catch (err) {
- return false;
- }
- return true;
- };
- const create$c = ({ root }) => (root.ref.fields = {});
- const getField = (root, id) => root.ref.fields[id];
- const syncFieldPositionsWithItems = root => {
- root.query('GET_ACTIVE_ITEMS').forEach(item => {
- if (!root.ref.fields[item.id]) return;
- root.element.appendChild(root.ref.fields[item.id]);
- });
- };
- const didReorderItems = ({ root }) => syncFieldPositionsWithItems(root);
- const didAddItem = ({ root, action }) => {
- const fileItem = root.query('GET_ITEM', action.id);
- const isLocalFile = fileItem.origin === FileOrigin.LOCAL;
- const shouldUseFileInput = !isLocalFile && root.query('SHOULD_UPDATE_FILE_INPUT');
- const dataContainer = createElement$1('input');
- dataContainer.type = shouldUseFileInput ? 'file' : 'hidden';
- dataContainer.name = root.query('GET_NAME');
- dataContainer.disabled = root.query('GET_DISABLED');
- root.ref.fields[action.id] = dataContainer;
- syncFieldPositionsWithItems(root);
- };
- const didLoadItem$1 = ({ root, action }) => {
- const field = getField(root, action.id);
- if (!field) return;
- // store server ref in hidden input
- if (action.serverFileReference !== null) field.value = action.serverFileReference;
- // store file item in file input
- if (!root.query('SHOULD_UPDATE_FILE_INPUT')) return;
- const fileItem = root.query('GET_ITEM', action.id);
- setInputFiles(field, [fileItem.file]);
- };
- const didPrepareOutput = ({ root, action }) => {
- // this timeout pushes the handler after 'load'
- if (!root.query('SHOULD_UPDATE_FILE_INPUT')) return;
- setTimeout(() => {
- const field = getField(root, action.id);
- if (!field) return;
- setInputFiles(field, [action.file]);
- }, 0);
- };
- const didSetDisabled = ({ root }) => {
- root.element.disabled = root.query('GET_DISABLED');
- };
- const didRemoveItem = ({ root, action }) => {
- const field = getField(root, action.id);
- if (!field) return;
- if (field.parentNode) field.parentNode.removeChild(field);
- delete root.ref.fields[action.id];
- };
- // only runs for server files (so doesn't deal with file input)
- const didDefineValue = ({ root, action }) => {
- const field = getField(root, action.id);
- if (!field) return;
- if (action.value === null) {
- // clear field value
- field.removeAttribute('value');
- } else {
- // set field value
- field.value = action.value;
- }
- syncFieldPositionsWithItems(root);
- };
- const write$8 = createRoute({
- DID_SET_DISABLED: didSetDisabled,
- DID_ADD_ITEM: didAddItem,
- DID_LOAD_ITEM: didLoadItem$1,
- DID_REMOVE_ITEM: didRemoveItem,
- DID_DEFINE_VALUE: didDefineValue,
- DID_PREPARE_OUTPUT: didPrepareOutput,
- DID_REORDER_ITEMS: didReorderItems,
- DID_SORT_ITEMS: didReorderItems,
- });
- const data = createView({
- tag: 'fieldset',
- name: 'data',
- create: create$c,
- write: write$8,
- ignoreRect: true,
- });
- const getRootNode = element => ('getRootNode' in element ? element.getRootNode() : document);
- const images = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'tiff'];
- const text$1 = ['css', 'csv', 'html', 'txt'];
- const map = {
- zip: 'zip|compressed',
- epub: 'application/epub+zip',
- };
- const guesstimateMimeType = (extension = '') => {
- extension = extension.toLowerCase();
- if (images.includes(extension)) {
- return (
- 'image/' + (extension === 'jpg' ? 'jpeg' : extension === 'svg' ? 'svg+xml' : extension)
- );
- }
- if (text$1.includes(extension)) {
- return 'text/' + extension;
- }
- return map[extension] || '';
- };
- const requestDataTransferItems = dataTransfer =>
- new Promise((resolve, reject) => {
- // try to get links from transfer, if found we'll exit immediately (unless a file is in the dataTransfer as well, this is because Firefox could represent the file as a URL and a file object at the same time)
- const links = getLinks(dataTransfer);
- if (links.length && !hasFiles(dataTransfer)) {
- return resolve(links);
- }
- // try to get files from the transfer
- getFiles(dataTransfer).then(resolve);
- });
- /**
- * Test if datatransfer has files
- */
- const hasFiles = dataTransfer => {
- if (dataTransfer.files) return dataTransfer.files.length > 0;
- return false;
- };
- /**
- * Extracts files from a DataTransfer object
- */
- const getFiles = dataTransfer =>
- new Promise((resolve, reject) => {
- // get the transfer items as promises
- const promisedFiles = (dataTransfer.items ? Array.from(dataTransfer.items) : [])
- // only keep file system items (files and directories)
- .filter(item => isFileSystemItem(item))
- // map each item to promise
- .map(item => getFilesFromItem(item));
- // if is empty, see if we can extract some info from the files property as a fallback
- if (!promisedFiles.length) {
- // TODO: test for directories (should not be allowed)
- // Use FileReader, problem is that the files property gets lost in the process
- resolve(dataTransfer.files ? Array.from(dataTransfer.files) : []);
- return;
- }
- // done!
- Promise.all(promisedFiles)
- .then(returnedFileGroups => {
- // flatten groups
- const files = [];
- returnedFileGroups.forEach(group => {
- files.push.apply(files, group);
- });
- // done (filter out empty files)!
- resolve(
- files
- .filter(file => file)
- .map(file => {
- if (!file._relativePath) file._relativePath = file.webkitRelativePath;
- return file;
- })
- );
- })
- .catch(console.error);
- });
- const isFileSystemItem = item => {
- if (isEntry(item)) {
- const entry = getAsEntry(item);
- if (entry) {
- return entry.isFile || entry.isDirectory;
- }
- }
- return item.kind === 'file';
- };
- const getFilesFromItem = item =>
- new Promise((resolve, reject) => {
- if (isDirectoryEntry(item)) {
- getFilesInDirectory(getAsEntry(item))
- .then(resolve)
- .catch(reject);
- return;
- }
- resolve([item.getAsFile()]);
- });
- const getFilesInDirectory = entry =>
- new Promise((resolve, reject) => {
- const files = [];
- // the total entries to read
- let dirCounter = 0;
- let fileCounter = 0;
- const resolveIfDone = () => {
- if (fileCounter === 0 && dirCounter === 0) {
- resolve(files);
- }
- };
- // the recursive function
- const readEntries = dirEntry => {
- dirCounter++;
- const directoryReader = dirEntry.createReader();
- // directories are returned in batches, we need to process all batches before we're done
- const readBatch = () => {
- directoryReader.readEntries(entries => {
- if (entries.length === 0) {
- dirCounter--;
- resolveIfDone();
- return;
- }
- entries.forEach(entry => {
- // recursively read more directories
- if (entry.isDirectory) {
- readEntries(entry);
- } else {
- // read as file
- fileCounter++;
- entry.file(file => {
- const correctedFile = correctMissingFileType(file);
- if (entry.fullPath) correctedFile._relativePath = entry.fullPath;
- files.push(correctedFile);
- fileCounter--;
- resolveIfDone();
- });
- }
- });
- // try to get next batch of files
- readBatch();
- }, reject);
- };
- // read first batch of files
- readBatch();
- };
- // go!
- readEntries(entry);
- });
- const correctMissingFileType = file => {
- if (file.type.length) return file;
- const date = file.lastModifiedDate;
- const name = file.name;
- const type = guesstimateMimeType(getExtensionFromFilename(file.name));
- if (!type.length) return file;
- file = file.slice(0, file.size, type);
- file.name = name;
- file.lastModifiedDate = date;
- return file;
- };
- const isDirectoryEntry = item => isEntry(item) && (getAsEntry(item) || {}).isDirectory;
- const isEntry = item => 'webkitGetAsEntry' in item;
- const getAsEntry = item => item.webkitGetAsEntry();
- /**
- * Extracts links from a DataTransfer object
- */
- const getLinks = dataTransfer => {
- let links = [];
- try {
- // look in meta data property
- links = getLinksFromTransferMetaData(dataTransfer);
- if (links.length) {
- return links;
- }
- links = getLinksFromTransferURLData(dataTransfer);
- } catch (e) {
- // nope nope nope (probably IE trouble)
- }
- return links;
- };
- const getLinksFromTransferURLData = dataTransfer => {
- let data = dataTransfer.getData('url');
- if (typeof data === 'string' && data.length) {
- return [data];
- }
- return [];
- };
- const getLinksFromTransferMetaData = dataTransfer => {
- let data = dataTransfer.getData('text/html');
- if (typeof data === 'string' && data.length) {
- const matches = data.match(/src\s*=\s*"(.+?)"/);
- if (matches) {
- return [matches[1]];
- }
- }
- return [];
- };
- const dragNDropObservers = [];
- const eventPosition = e => ({
- pageLeft: e.pageX,
- pageTop: e.pageY,
- scopeLeft: e.offsetX || e.layerX,
- scopeTop: e.offsetY || e.layerY,
- });
- const createDragNDropClient = (element, scopeToObserve, filterElement) => {
- const observer = getDragNDropObserver(scopeToObserve);
- const client = {
- element,
- filterElement,
- state: null,
- ondrop: () => {},
- onenter: () => {},
- ondrag: () => {},
- onexit: () => {},
- onload: () => {},
- allowdrop: () => {},
- };
- client.destroy = observer.addListener(client);
- return client;
- };
- const getDragNDropObserver = element => {
- // see if already exists, if so, return
- const observer = dragNDropObservers.find(item => item.element === element);
- if (observer) {
- return observer;
- }
- // create new observer, does not yet exist for this element
- const newObserver = createDragNDropObserver(element);
- dragNDropObservers.push(newObserver);
- return newObserver;
- };
- const createDragNDropObserver = element => {
- const clients = [];
- const routes = {
- dragenter,
- dragover,
- dragleave,
- drop,
- };
- const handlers = {};
- forin(routes, (event, createHandler) => {
- handlers[event] = createHandler(element, clients);
- element.addEventListener(event, handlers[event], false);
- });
- const observer = {
- element,
- addListener: client => {
- // add as client
- clients.push(client);
- // return removeListener function
- return () => {
- // remove client
- clients.splice(clients.indexOf(client), 1);
- // if no more clients, clean up observer
- if (clients.length === 0) {
- dragNDropObservers.splice(dragNDropObservers.indexOf(observer), 1);
- forin(routes, event => {
- element.removeEventListener(event, handlers[event], false);
- });
- }
- };
- },
- };
- return observer;
- };
- const elementFromPoint = (root, point) => {
- if (!('elementFromPoint' in root)) {
- root = document;
- }
- return root.elementFromPoint(point.x, point.y);
- };
- const isEventTarget = (e, target) => {
- // get root
- const root = getRootNode(target);
- // get element at position
- // if root is not actual shadow DOM and does not have elementFromPoint method, use the one on document
- const elementAtPosition = elementFromPoint(root, {
- x: e.pageX - window.pageXOffset,
- y: e.pageY - window.pageYOffset,
- });
- // test if target is the element or if one of its children is
- return elementAtPosition === target || target.contains(elementAtPosition);
- };
- let initialTarget = null;
- const setDropEffect = (dataTransfer, effect) => {
- // is in try catch as IE11 will throw error if not
- try {
- dataTransfer.dropEffect = effect;
- } catch (e) {}
- };
- const dragenter = (root, clients) => e => {
- e.preventDefault();
- initialTarget = e.target;
- clients.forEach(client => {
- const { element, onenter } = client;
- if (isEventTarget(e, element)) {
- client.state = 'enter';
- // fire enter event
- onenter(eventPosition(e));
- }
- });
- };
- const dragover = (root, clients) => e => {
- e.preventDefault();
- const dataTransfer = e.dataTransfer;
- requestDataTransferItems(dataTransfer).then(items => {
- let overDropTarget = false;
- clients.some(client => {
- const { filterElement, element, onenter, onexit, ondrag, allowdrop } = client;
- // by default we can drop
- setDropEffect(dataTransfer, 'copy');
- // allow transfer of these items
- const allowsTransfer = allowdrop(items);
- // only used when can be dropped on page
- if (!allowsTransfer) {
- setDropEffect(dataTransfer, 'none');
- return;
- }
- // targetting this client
- if (isEventTarget(e, element)) {
- overDropTarget = true;
- // had no previous state, means we are entering this client
- if (client.state === null) {
- client.state = 'enter';
- onenter(eventPosition(e));
- return;
- }
- // now over element (no matter if it allows the drop or not)
- client.state = 'over';
- // needs to allow transfer
- if (filterElement && !allowsTransfer) {
- setDropEffect(dataTransfer, 'none');
- return;
- }
- // dragging
- ondrag(eventPosition(e));
- } else {
- // should be over an element to drop
- if (filterElement && !overDropTarget) {
- setDropEffect(dataTransfer, 'none');
- }
- // might have just left this client?
- if (client.state) {
- client.state = null;
- onexit(eventPosition(e));
- }
- }
- });
- });
- };
- const drop = (root, clients) => e => {
- e.preventDefault();
- const dataTransfer = e.dataTransfer;
- requestDataTransferItems(dataTransfer).then(items => {
- clients.forEach(client => {
- const { filterElement, element, ondrop, onexit, allowdrop } = client;
- client.state = null;
- // if we're filtering on element we need to be over the element to drop
- if (filterElement && !isEventTarget(e, element)) return;
- // no transfer for this client
- if (!allowdrop(items)) return onexit(eventPosition(e));
- // we can drop these items on this client
- ondrop(eventPosition(e), items);
- });
- });
- };
- const dragleave = (root, clients) => e => {
- if (initialTarget !== e.target) {
- return;
- }
- clients.forEach(client => {
- const { onexit } = client;
- client.state = null;
- onexit(eventPosition(e));
- });
- };
- const createHopper = (scope, validateItems, options) => {
- // is now hopper scope
- scope.classList.add('filepond--hopper');
- // shortcuts
- const { catchesDropsOnPage, requiresDropOnElement, filterItems = items => items } = options;
- // create a dnd client
- const client = createDragNDropClient(
- scope,
- catchesDropsOnPage ? document.documentElement : scope,
- requiresDropOnElement
- );
- // current client state
- let lastState = '';
- let currentState = '';
- // determines if a file may be dropped
- client.allowdrop = items => {
- // TODO: if we can, throw error to indicate the items cannot by dropped
- return validateItems(filterItems(items));
- };
- client.ondrop = (position, items) => {
- const filteredItems = filterItems(items);
- if (!validateItems(filteredItems)) {
- api.ondragend(position);
- return;
- }
- currentState = 'drag-drop';
- api.onload(filteredItems, position);
- };
- client.ondrag = position => {
- api.ondrag(position);
- };
- client.onenter = position => {
- currentState = 'drag-over';
- api.ondragstart(position);
- };
- client.onexit = position => {
- currentState = 'drag-exit';
- api.ondragend(position);
- };
- const api = {
- updateHopperState: () => {
- if (lastState !== currentState) {
- scope.dataset.hopperState = currentState;
- lastState = currentState;
- }
- },
- onload: () => {},
- ondragstart: () => {},
- ondrag: () => {},
- ondragend: () => {},
- destroy: () => {
- // destroy client
- client.destroy();
- },
- };
- return api;
- };
- let listening = false;
- const listeners$1 = [];
- const handlePaste = e => {
- // if is pasting in input or textarea and the target is outside of a filepond scope, ignore
- const activeEl = document.activeElement;
- if (activeEl && /textarea|input/i.test(activeEl.nodeName)) {
- // test textarea or input is contained in filepond root
- let inScope = false;
- let element = activeEl;
- while (element !== document.body) {
- if (element.classList.contains('filepond--root')) {
- inScope = true;
- break;
- }
- element = element.parentNode;
- }
- if (!inScope) return;
- }
- requestDataTransferItems(e.clipboardData).then(files => {
- // no files received
- if (!files.length) {
- return;
- }
- // notify listeners of received files
- listeners$1.forEach(listener => listener(files));
- });
- };
- const listen = cb => {
- // can't add twice
- if (listeners$1.includes(cb)) {
- return;
- }
- // add initial listener
- listeners$1.push(cb);
- // setup paste listener for entire page
- if (listening) {
- return;
- }
- listening = true;
- document.addEventListener('paste', handlePaste);
- };
- const unlisten = listener => {
- arrayRemove(listeners$1, listeners$1.indexOf(listener));
- // clean up
- if (listeners$1.length === 0) {
- document.removeEventListener('paste', handlePaste);
- listening = false;
- }
- };
- const createPaster = () => {
- const cb = files => {
- api.onload(files);
- };
- const api = {
- destroy: () => {
- unlisten(cb);
- },
- onload: () => {},
- };
- listen(cb);
- return api;
- };
- /**
- * Creates the file view
- */
- const create$d = ({ root, props }) => {
- root.element.id = `filepond--assistant-${props.id}`;
- attr(root.element, 'role', 'status');
- attr(root.element, 'aria-live', 'polite');
- attr(root.element, 'aria-relevant', 'additions');
- };
- let addFilesNotificationTimeout = null;
- let notificationClearTimeout = null;
- const filenames = [];
- const assist = (root, message) => {
- root.element.textContent = message;
- };
- const clear$1 = root => {
- root.element.textContent = '';
- };
- const listModified = (root, filename, label) => {
- const total = root.query('GET_TOTAL_ITEMS');
- assist(
- root,
- `${label} ${filename}, ${total} ${
- total === 1
- ? root.query('GET_LABEL_FILE_COUNT_SINGULAR')
- : root.query('GET_LABEL_FILE_COUNT_PLURAL')
- }`
- );
- // clear group after set amount of time so the status is not read twice
- clearTimeout(notificationClearTimeout);
- notificationClearTimeout = setTimeout(() => {
- clear$1(root);
- }, 1500);
- };
- const isUsingFilePond = root => root.element.parentNode.contains(document.activeElement);
- const itemAdded = ({ root, action }) => {
- if (!isUsingFilePond(root)) {
- return;
- }
- root.element.textContent = '';
- const item = root.query('GET_ITEM', action.id);
- filenames.push(item.filename);
- clearTimeout(addFilesNotificationTimeout);
- addFilesNotificationTimeout = setTimeout(() => {
- listModified(root, filenames.join(', '), root.query('GET_LABEL_FILE_ADDED'));
- filenames.length = 0;
- }, 750);
- };
- const itemRemoved = ({ root, action }) => {
- if (!isUsingFilePond(root)) {
- return;
- }
- const item = action.item;
- listModified(root, item.filename, root.query('GET_LABEL_FILE_REMOVED'));
- };
- const itemProcessed = ({ root, action }) => {
- // will also notify the user when FilePond is not being used, as the user might be occupied with other activities while uploading a file
- const item = root.query('GET_ITEM', action.id);
- const filename = item.filename;
- const label = root.query('GET_LABEL_FILE_PROCESSING_COMPLETE');
- assist(root, `${filename} ${label}`);
- };
- const itemProcessedUndo = ({ root, action }) => {
- const item = root.query('GET_ITEM', action.id);
- const filename = item.filename;
- const label = root.query('GET_LABEL_FILE_PROCESSING_ABORTED');
- assist(root, `${filename} ${label}`);
- };
- const itemError = ({ root, action }) => {
- const item = root.query('GET_ITEM', action.id);
- const filename = item.filename;
- // will also notify the user when FilePond is not being used, as the user might be occupied with other activities while uploading a file
- assist(root, `${action.status.main} ${filename} ${action.status.sub}`);
- };
- const assistant = createView({
- create: create$d,
- ignoreRect: true,
- ignoreRectUpdate: true,
- write: createRoute({
- DID_LOAD_ITEM: itemAdded,
- DID_REMOVE_ITEM: itemRemoved,
- DID_COMPLETE_ITEM_PROCESSING: itemProcessed,
- DID_ABORT_ITEM_PROCESSING: itemProcessedUndo,
- DID_REVERT_ITEM_PROCESSING: itemProcessedUndo,
- DID_THROW_ITEM_REMOVE_ERROR: itemError,
- DID_THROW_ITEM_LOAD_ERROR: itemError,
- DID_THROW_ITEM_INVALID: itemError,
- DID_THROW_ITEM_PROCESSING_ERROR: itemError,
- }),
- tag: 'span',
- name: 'assistant',
- });
- const toCamels = (string, separator = '-') =>
- string.replace(new RegExp(`${separator}.`, 'g'), sub => sub.charAt(1).toUpperCase());
- const debounce = (func, interval = 16, immidiateOnly = true) => {
- let last = Date.now();
- let timeout = null;
- return (...args) => {
- clearTimeout(timeout);
- const dist = Date.now() - last;
- const fn = () => {
- last = Date.now();
- func(...args);
- };
- if (dist < interval) {
- // we need to delay by the difference between interval and dist
- // for example: if distance is 10 ms and interval is 16 ms,
- // we need to wait an additional 6ms before calling the function)
- if (!immidiateOnly) {
- timeout = setTimeout(fn, interval - dist);
- }
- } else {
- // go!
- fn();
- }
- };
- };
- const MAX_FILES_LIMIT = 1000000;
- const prevent = e => e.preventDefault();
- const create$e = ({ root, props }) => {
- // Add id
- const id = root.query('GET_ID');
- if (id) {
- root.element.id = id;
- }
- // Add className
- const className = root.query('GET_CLASS_NAME');
- if (className) {
- className
- .split(' ')
- .filter(name => name.length)
- .forEach(name => {
- root.element.classList.add(name);
- });
- }
- // Field label
- root.ref.label = root.appendChildView(
- root.createChildView(dropLabel, {
- ...props,
- translateY: null,
- caption: root.query('GET_LABEL_IDLE'),
- })
- );
- // List of items
- root.ref.list = root.appendChildView(root.createChildView(listScroller, { translateY: null }));
- // Background panel
- root.ref.panel = root.appendChildView(root.createChildView(panel, { name: 'panel-root' }));
- // Assistant notifies assistive tech when content changes
- root.ref.assistant = root.appendChildView(root.createChildView(assistant, { ...props }));
- // Data
- root.ref.data = root.appendChildView(root.createChildView(data, { ...props }));
- // Measure (tests if fixed height was set)
- // DOCTYPE needs to be set for this to work
- root.ref.measure = createElement$1('div');
- root.ref.measure.style.height = '100%';
- root.element.appendChild(root.ref.measure);
- // information on the root height or fixed height status
- root.ref.bounds = null;
- // apply initial style properties
- root.query('GET_STYLES')
- .filter(style => !isEmpty(style.value))
- .map(({ name, value }) => {
- root.element.dataset[name] = value;
- });
- // determine if width changed
- root.ref.widthPrevious = null;
- root.ref.widthUpdated = debounce(() => {
- root.ref.updateHistory = [];
- root.dispatch('DID_RESIZE_ROOT');
- }, 250);
- // history of updates
- root.ref.previousAspectRatio = null;
- root.ref.updateHistory = [];
- // prevent scrolling and zooming on iOS (only if supports pointer events, for then we can enable reorder)
- const canHover = window.matchMedia('(pointer: fine) and (hover: hover)').matches;
- const hasPointerEvents = 'PointerEvent' in window;
- if (root.query('GET_ALLOW_REORDER') && hasPointerEvents && !canHover) {
- root.element.addEventListener('touchmove', prevent, { passive: false });
- root.element.addEventListener('gesturestart', prevent);
- }
- // add credits
- const credits = root.query('GET_CREDITS');
- const hasCredits = credits.length === 2;
- if (hasCredits) {
- const frag = document.createElement('a');
- frag.className = 'filepond--credits';
- frag.setAttribute('aria-hidden', 'true');
- frag.href = credits[0];
- frag.tabindex = -1;
- frag.target = '_blank';
- frag.rel = 'noopener noreferrer';
- frag.textContent = credits[1];
- root.element.appendChild(frag);
- root.ref.credits = frag;
- }
- };
- const write$9 = ({ root, props, actions }) => {
- // route actions
- route$5({ root, props, actions });
- // apply style properties
- actions
- .filter(action => /^DID_SET_STYLE_/.test(action.type))
- .filter(action => !isEmpty(action.data.value))
- .map(({ type, data }) => {
- const name = toCamels(type.substring(8).toLowerCase(), '_');
- root.element.dataset[name] = data.value;
- root.invalidateLayout();
- });
- if (root.rect.element.hidden) return;
- if (root.rect.element.width !== root.ref.widthPrevious) {
- root.ref.widthPrevious = root.rect.element.width;
- root.ref.widthUpdated();
- }
- // get box bounds, we do this only once
- let bounds = root.ref.bounds;
- if (!bounds) {
- bounds = root.ref.bounds = calculateRootBoundingBoxHeight(root);
- // destroy measure element
- root.element.removeChild(root.ref.measure);
- root.ref.measure = null;
- }
- // get quick references to various high level parts of the upload tool
- const { hopper, label, list, panel } = root.ref;
- // sets correct state to hopper scope
- if (hopper) {
- hopper.updateHopperState();
- }
- // bool to indicate if we're full or not
- const aspectRatio = root.query('GET_PANEL_ASPECT_RATIO');
- const isMultiItem = root.query('GET_ALLOW_MULTIPLE');
- const totalItems = root.query('GET_TOTAL_ITEMS');
- const maxItems = isMultiItem ? root.query('GET_MAX_FILES') || MAX_FILES_LIMIT : 1;
- const atMaxCapacity = totalItems === maxItems;
- // action used to add item
- const addAction = actions.find(action => action.type === 'DID_ADD_ITEM');
- // if reached max capacity and we've just reached it
- if (atMaxCapacity && addAction) {
- // get interaction type
- const interactionMethod = addAction.data.interactionMethod;
- // hide label
- label.opacity = 0;
- if (isMultiItem) {
- label.translateY = -40;
- } else {
- if (interactionMethod === InteractionMethod.API) {
- label.translateX = 40;
- } else if (interactionMethod === InteractionMethod.BROWSE) {
- label.translateY = 40;
- } else {
- label.translateY = 30;
- }
- }
- } else if (!atMaxCapacity) {
- label.opacity = 1;
- label.translateX = 0;
- label.translateY = 0;
- }
- const listItemMargin = calculateListItemMargin(root);
- const listHeight = calculateListHeight(root);
- const labelHeight = label.rect.element.height;
- const currentLabelHeight = !isMultiItem || atMaxCapacity ? 0 : labelHeight;
- const listMarginTop = atMaxCapacity ? list.rect.element.marginTop : 0;
- const listMarginBottom = totalItems === 0 ? 0 : list.rect.element.marginBottom;
- const visualHeight = currentLabelHeight + listMarginTop + listHeight.visual + listMarginBottom;
- const boundsHeight = currentLabelHeight + listMarginTop + listHeight.bounds + listMarginBottom;
- // link list to label bottom position
- list.translateY =
- Math.max(0, currentLabelHeight - list.rect.element.marginTop) - listItemMargin.top;
- if (aspectRatio) {
- // fixed aspect ratio
- // calculate height based on width
- const width = root.rect.element.width;
- const height = width * aspectRatio;
- // clear history if aspect ratio has changed
- if (aspectRatio !== root.ref.previousAspectRatio) {
- root.ref.previousAspectRatio = aspectRatio;
- root.ref.updateHistory = [];
- }
- // remember this width
- const history = root.ref.updateHistory;
- history.push(width);
- const MAX_BOUNCES = 2;
- if (history.length > MAX_BOUNCES * 2) {
- const l = history.length;
- const bottom = l - 10;
- let bounces = 0;
- for (let i = l; i >= bottom; i--) {
- if (history[i] === history[i - 2]) {
- bounces++;
- }
- if (bounces >= MAX_BOUNCES) {
- // dont adjust height
- return;
- }
- }
- }
- // fix height of panel so it adheres to aspect ratio
- panel.scalable = false;
- panel.height = height;
- // available height for list
- const listAvailableHeight =
- // the height of the panel minus the label height
- height -
- currentLabelHeight -
- // the room we leave open between the end of the list and the panel bottom
- (listMarginBottom - listItemMargin.bottom) -
- // if we're full we need to leave some room between the top of the panel and the list
- (atMaxCapacity ? listMarginTop : 0);
- if (listHeight.visual > listAvailableHeight) {
- list.overflow = listAvailableHeight;
- } else {
- list.overflow = null;
- }
- // set container bounds (so pushes siblings downwards)
- root.height = height;
- } else if (bounds.fixedHeight) {
- // fixed height
- // fix height of panel
- panel.scalable = false;
- // available height for list
- const listAvailableHeight =
- // the height of the panel minus the label height
- bounds.fixedHeight -
- currentLabelHeight -
- // the room we leave open between the end of the list and the panel bottom
- (listMarginBottom - listItemMargin.bottom) -
- // if we're full we need to leave some room between the top of the panel and the list
- (atMaxCapacity ? listMarginTop : 0);
- // set list height
- if (listHeight.visual > listAvailableHeight) {
- list.overflow = listAvailableHeight;
- } else {
- list.overflow = null;
- }
- // no need to set container bounds as these are handles by CSS fixed height
- } else if (bounds.cappedHeight) {
- // max-height
- // not a fixed height panel
- const isCappedHeight = visualHeight >= bounds.cappedHeight;
- const panelHeight = Math.min(bounds.cappedHeight, visualHeight);
- panel.scalable = true;
- panel.height = isCappedHeight
- ? panelHeight
- : panelHeight - listItemMargin.top - listItemMargin.bottom;
- // available height for list
- const listAvailableHeight =
- // the height of the panel minus the label height
- panelHeight -
- currentLabelHeight -
- // the room we leave open between the end of the list and the panel bottom
- (listMarginBottom - listItemMargin.bottom) -
- // if we're full we need to leave some room between the top of the panel and the list
- (atMaxCapacity ? listMarginTop : 0);
- // set list height (if is overflowing)
- if (visualHeight > bounds.cappedHeight && listHeight.visual > listAvailableHeight) {
- list.overflow = listAvailableHeight;
- } else {
- list.overflow = null;
- }
- // set container bounds (so pushes siblings downwards)
- root.height = Math.min(
- bounds.cappedHeight,
- boundsHeight - listItemMargin.top - listItemMargin.bottom
- );
- } else {
- // flexible height
- // not a fixed height panel
- const itemMargin = totalItems > 0 ? listItemMargin.top + listItemMargin.bottom : 0;
- panel.scalable = true;
- panel.height = Math.max(labelHeight, visualHeight - itemMargin);
- // set container bounds (so pushes siblings downwards)
- root.height = Math.max(labelHeight, boundsHeight - itemMargin);
- }
- // move credits to bottom
- if (root.ref.credits && panel.heightCurrent)
- root.ref.credits.style.transform = `translateY(${panel.heightCurrent}px)`;
- };
- const calculateListItemMargin = root => {
- const item = root.ref.list.childViews[0].childViews[0];
- return item
- ? {
- top: item.rect.element.marginTop,
- bottom: item.rect.element.marginBottom,
- }
- : {
- top: 0,
- bottom: 0,
- };
- };
- const calculateListHeight = root => {
- let visual = 0;
- let bounds = 0;
- // get file list reference
- const scrollList = root.ref.list;
- const itemList = scrollList.childViews[0];
- const visibleChildren = itemList.childViews.filter(child => child.rect.element.height);
- const children = root
- .query('GET_ACTIVE_ITEMS')
- .map(item => visibleChildren.find(child => child.id === item.id))
- .filter(item => item);
- // no children, done!
- if (children.length === 0) return { visual, bounds };
- const horizontalSpace = itemList.rect.element.width;
- const dragIndex = getItemIndexByPosition(itemList, children, scrollList.dragCoordinates);
- const childRect = children[0].rect.element;
- const itemVerticalMargin = childRect.marginTop + childRect.marginBottom;
- const itemHorizontalMargin = childRect.marginLeft + childRect.marginRight;
- const itemWidth = childRect.width + itemHorizontalMargin;
- const itemHeight = childRect.height + itemVerticalMargin;
- const newItem = typeof dragIndex !== 'undefined' && dragIndex >= 0 ? 1 : 0;
- const removedItem = children.find(child => child.markedForRemoval && child.opacity < 0.45)
- ? -1
- : 0;
- const verticalItemCount = children.length + newItem + removedItem;
- const itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth);
- // stack
- if (itemsPerRow === 1) {
- children.forEach(item => {
- const height = item.rect.element.height + itemVerticalMargin;
- bounds += height;
- visual += height * item.opacity;
- });
- }
- // grid
- else {
- bounds = Math.ceil(verticalItemCount / itemsPerRow) * itemHeight;
- visual = bounds;
- }
- return { visual, bounds };
- };
- const calculateRootBoundingBoxHeight = root => {
- const height = root.ref.measureHeight || null;
- const cappedHeight = parseInt(root.style.maxHeight, 10) || null;
- const fixedHeight = height === 0 ? null : height;
- return {
- cappedHeight,
- fixedHeight,
- };
- };
- const exceedsMaxFiles = (root, items) => {
- const allowReplace = root.query('GET_ALLOW_REPLACE');
- const allowMultiple = root.query('GET_ALLOW_MULTIPLE');
- const totalItems = root.query('GET_TOTAL_ITEMS');
- let maxItems = root.query('GET_MAX_FILES');
- // total amount of items being dragged
- const totalBrowseItems = items.length;
- // if does not allow multiple items and dragging more than one item
- if (!allowMultiple && totalBrowseItems > 1) {
- root.dispatch('DID_THROW_MAX_FILES', {
- source: items,
- error: createResponse('warning', 0, 'Max files'),
- });
- return true;
- }
- // limit max items to one if not allowed to drop multiple items
- maxItems = allowMultiple ? maxItems : 1;
- if (!allowMultiple && allowReplace) {
- // There is only one item, so there is room to replace or add an item
- return false;
- }
- // no more room?
- const hasMaxItems = isInt(maxItems);
- if (hasMaxItems && totalItems + totalBrowseItems > maxItems) {
- root.dispatch('DID_THROW_MAX_FILES', {
- source: items,
- error: createResponse('warning', 0, 'Max files'),
- });
- return true;
- }
- return false;
- };
- const getDragIndex = (list, children, position) => {
- const itemList = list.childViews[0];
- return getItemIndexByPosition(itemList, children, {
- left: position.scopeLeft - itemList.rect.element.left,
- top:
- position.scopeTop -
- (list.rect.outer.top + list.rect.element.marginTop + list.rect.element.scrollTop),
- });
- };
- /**
- * Enable or disable file drop functionality
- */
- const toggleDrop = root => {
- const isAllowed = root.query('GET_ALLOW_DROP');
- const isDisabled = root.query('GET_DISABLED');
- const enabled = isAllowed && !isDisabled;
- if (enabled && !root.ref.hopper) {
- const hopper = createHopper(
- root.element,
- items => {
- // allow quick validation of dropped items
- const beforeDropFile = root.query('GET_BEFORE_DROP_FILE') || (() => true);
- // all items should be validated by all filters as valid
- const dropValidation = root.query('GET_DROP_VALIDATION');
- return dropValidation
- ? items.every(
- item =>
- applyFilters('ALLOW_HOPPER_ITEM', item, {
- query: root.query,
- }).every(result => result === true) && beforeDropFile(item)
- )
- : true;
- },
- {
- filterItems: items => {
- const ignoredFiles = root.query('GET_IGNORED_FILES');
- return items.filter(item => {
- if (isFile(item)) {
- return !ignoredFiles.includes(item.name.toLowerCase());
- }
- return true;
- });
- },
- catchesDropsOnPage: root.query('GET_DROP_ON_PAGE'),
- requiresDropOnElement: root.query('GET_DROP_ON_ELEMENT'),
- }
- );
- hopper.onload = (items, position) => {
- // get item children elements and sort based on list sort
- const list = root.ref.list.childViews[0];
- const visibleChildren = list.childViews.filter(child => child.rect.element.height);
- const children = root
- .query('GET_ACTIVE_ITEMS')
- .map(item => visibleChildren.find(child => child.id === item.id))
- .filter(item => item);
- applyFilterChain('ADD_ITEMS', items, { dispatch: root.dispatch }).then(queue => {
- // these files don't fit so stop here
- if (exceedsMaxFiles(root, queue)) return false;
- // go
- root.dispatch('ADD_ITEMS', {
- items: queue,
- index: getDragIndex(root.ref.list, children, position),
- interactionMethod: InteractionMethod.DROP,
- });
- });
- root.dispatch('DID_DROP', { position });
- root.dispatch('DID_END_DRAG', { position });
- };
- hopper.ondragstart = position => {
- root.dispatch('DID_START_DRAG', { position });
- };
- hopper.ondrag = debounce(position => {
- root.dispatch('DID_DRAG', { position });
- });
- hopper.ondragend = position => {
- root.dispatch('DID_END_DRAG', { position });
- };
- root.ref.hopper = hopper;
- root.ref.drip = root.appendChildView(root.createChildView(drip));
- } else if (!enabled && root.ref.hopper) {
- root.ref.hopper.destroy();
- root.ref.hopper = null;
- root.removeChildView(root.ref.drip);
- }
- };
- /**
- * Enable or disable browse functionality
- */
- const toggleBrowse = (root, props) => {
- const isAllowed = root.query('GET_ALLOW_BROWSE');
- const isDisabled = root.query('GET_DISABLED');
- const enabled = isAllowed && !isDisabled;
- if (enabled && !root.ref.browser) {
- root.ref.browser = root.appendChildView(
- root.createChildView(browser, {
- ...props,
- onload: items => {
- applyFilterChain('ADD_ITEMS', items, {
- dispatch: root.dispatch,
- }).then(queue => {
- // these files don't fit so stop here
- if (exceedsMaxFiles(root, queue)) return false;
- // add items!
- root.dispatch('ADD_ITEMS', {
- items: queue,
- index: -1,
- interactionMethod: InteractionMethod.BROWSE,
- });
- });
- },
- }),
- 0
- );
- } else if (!enabled && root.ref.browser) {
- root.removeChildView(root.ref.browser);
- root.ref.browser = null;
- }
- };
- /**
- * Enable or disable paste functionality
- */
- const togglePaste = root => {
- const isAllowed = root.query('GET_ALLOW_PASTE');
- const isDisabled = root.query('GET_DISABLED');
- const enabled = isAllowed && !isDisabled;
- if (enabled && !root.ref.paster) {
- root.ref.paster = createPaster();
- root.ref.paster.onload = items => {
- applyFilterChain('ADD_ITEMS', items, { dispatch: root.dispatch }).then(queue => {
- // these files don't fit so stop here
- if (exceedsMaxFiles(root, queue)) return false;
- // add items!
- root.dispatch('ADD_ITEMS', {
- items: queue,
- index: -1,
- interactionMethod: InteractionMethod.PASTE,
- });
- });
- };
- } else if (!enabled && root.ref.paster) {
- root.ref.paster.destroy();
- root.ref.paster = null;
- }
- };
- /**
- * Route actions
- */
- const route$5 = createRoute({
- DID_SET_ALLOW_BROWSE: ({ root, props }) => {
- toggleBrowse(root, props);
- },
- DID_SET_ALLOW_DROP: ({ root }) => {
- toggleDrop(root);
- },
- DID_SET_ALLOW_PASTE: ({ root }) => {
- togglePaste(root);
- },
- DID_SET_DISABLED: ({ root, props }) => {
- toggleDrop(root);
- togglePaste(root);
- toggleBrowse(root, props);
- const isDisabled = root.query('GET_DISABLED');
- if (isDisabled) {
- root.element.dataset.disabled = 'disabled';
- } else {
- // delete root.element.dataset.disabled; <= this does not work on iOS 10
- root.element.removeAttribute('data-disabled');
- }
- },
- });
- const root = createView({
- name: 'root',
- read: ({ root }) => {
- if (root.ref.measure) {
- root.ref.measureHeight = root.ref.measure.offsetHeight;
- }
- },
- create: create$e,
- write: write$9,
- destroy: ({ root }) => {
- if (root.ref.paster) {
- root.ref.paster.destroy();
- }
- if (root.ref.hopper) {
- root.ref.hopper.destroy();
- }
- root.element.removeEventListener('touchmove', prevent);
- root.element.removeEventListener('gesturestart', prevent);
- },
- mixins: {
- styles: ['height'],
- },
- });
- // creates the app
- const createApp = (initialOptions = {}) => {
- // let element
- let originalElement = null;
- // get default options
- const defaultOptions = getOptions();
- // create the data store, this will contain all our app info
- const store = createStore(
- // initial state (should be serializable)
- createInitialState(defaultOptions),
- // queries
- [queries, createOptionQueries(defaultOptions)],
- // action handlers
- [actions, createOptionActions(defaultOptions)]
- );
- // set initial options
- store.dispatch('SET_OPTIONS', { options: initialOptions });
- // kick thread if visibility changes
- const visibilityHandler = () => {
- if (document.hidden) return;
- store.dispatch('KICK');
- };
- document.addEventListener('visibilitychange', visibilityHandler);
- // re-render on window resize start and finish
- let resizeDoneTimer = null;
- let isResizing = false;
- let isResizingHorizontally = false;
- let initialWindowWidth = null;
- let currentWindowWidth = null;
- const resizeHandler = () => {
- if (!isResizing) {
- isResizing = true;
- }
- clearTimeout(resizeDoneTimer);
- resizeDoneTimer = setTimeout(() => {
- isResizing = false;
- initialWindowWidth = null;
- currentWindowWidth = null;
- if (isResizingHorizontally) {
- isResizingHorizontally = false;
- store.dispatch('DID_STOP_RESIZE');
- }
- }, 500);
- };
- window.addEventListener('resize', resizeHandler);
- // render initial view
- const view = root(store, { id: getUniqueId() });
- //
- // PRIVATE API -------------------------------------------------------------------------------------
- //
- let isResting = false;
- let isHidden = false;
- const readWriteApi = {
- // necessary for update loop
- /**
- * Reads from dom (never call manually)
- * @private
- */
- _read: () => {
- // test if we're resizing horizontally
- // TODO: see if we can optimize this by measuring root rect
- if (isResizing) {
- currentWindowWidth = window.innerWidth;
- if (!initialWindowWidth) {
- initialWindowWidth = currentWindowWidth;
- }
- if (!isResizingHorizontally && currentWindowWidth !== initialWindowWidth) {
- store.dispatch('DID_START_RESIZE');
- isResizingHorizontally = true;
- }
- }
- if (isHidden && isResting) {
- // test if is no longer hidden
- isResting = view.element.offsetParent === null;
- }
- // if resting, no need to read as numbers will still all be correct
- if (isResting) return;
- // read view data
- view._read();
- // if is hidden we need to know so we exit rest mode when revealed
- isHidden = view.rect.element.hidden;
- },
- /**
- * Writes to dom (never call manually)
- * @private
- */
- _write: ts => {
- // get all actions from store
- const actions = store
- .processActionQueue()
- // filter out set actions (these will automatically trigger DID_SET)
- .filter(action => !/^SET_/.test(action.type));
- // if was idling and no actions stop here
- if (isResting && !actions.length) return;
- // some actions might trigger events
- routeActionsToEvents(actions);
- // update the view
- isResting = view._write(ts, actions, isResizingHorizontally);
- // will clean up all archived items
- removeReleasedItems(store.query('GET_ITEMS'));
- // now idling
- if (isResting) {
- store.processDispatchQueue();
- }
- },
- };
- //
- // EXPOSE EVENTS -------------------------------------------------------------------------------------
- //
- const createEvent = name => data => {
- // create default event
- const event = {
- type: name,
- };
- // no data to add
- if (!data) {
- return event;
- }
- // copy relevant props
- if (data.hasOwnProperty('error')) {
- event.error = data.error ? { ...data.error } : null;
- }
- if (data.status) {
- event.status = { ...data.status };
- }
- if (data.file) {
- event.output = data.file;
- }
- // only source is available, else add item if possible
- if (data.source) {
- event.file = data.source;
- } else if (data.item || data.id) {
- const item = data.item ? data.item : store.query('GET_ITEM', data.id);
- event.file = item ? createItemAPI(item) : null;
- }
- // map all items in a possible items array
- if (data.items) {
- event.items = data.items.map(createItemAPI);
- }
- // if this is a progress event add the progress amount
- if (/progress/.test(name)) {
- event.progress = data.progress;
- }
- // copy relevant props
- if (data.hasOwnProperty('origin') && data.hasOwnProperty('target')) {
- event.origin = data.origin;
- event.target = data.target;
- }
- return event;
- };
- const eventRoutes = {
- DID_DESTROY: createEvent('destroy'),
- DID_INIT: createEvent('init'),
- DID_THROW_MAX_FILES: createEvent('warning'),
- DID_INIT_ITEM: createEvent('initfile'),
- DID_START_ITEM_LOAD: createEvent('addfilestart'),
- DID_UPDATE_ITEM_LOAD_PROGRESS: createEvent('addfileprogress'),
- DID_LOAD_ITEM: createEvent('addfile'),
- DID_THROW_ITEM_INVALID: [createEvent('error'), createEvent('addfile')],
- DID_THROW_ITEM_LOAD_ERROR: [createEvent('error'), createEvent('addfile')],
- DID_THROW_ITEM_REMOVE_ERROR: [createEvent('error'), createEvent('removefile')],
- DID_PREPARE_OUTPUT: createEvent('preparefile'),
- DID_START_ITEM_PROCESSING: createEvent('processfilestart'),
- DID_UPDATE_ITEM_PROCESS_PROGRESS: createEvent('processfileprogress'),
- DID_ABORT_ITEM_PROCESSING: createEvent('processfileabort'),
- DID_COMPLETE_ITEM_PROCESSING: createEvent('processfile'),
- DID_COMPLETE_ITEM_PROCESSING_ALL: createEvent('processfiles'),
- DID_REVERT_ITEM_PROCESSING: createEvent('processfilerevert'),
- DID_THROW_ITEM_PROCESSING_ERROR: [createEvent('error'), createEvent('processfile')],
- DID_REMOVE_ITEM: createEvent('removefile'),
- DID_UPDATE_ITEMS: createEvent('updatefiles'),
- DID_ACTIVATE_ITEM: createEvent('activatefile'),
- DID_REORDER_ITEMS: createEvent('reorderfiles'),
- };
- const exposeEvent = event => {
- // create event object to be dispatched
- const detail = { pond: exports, ...event };
- delete detail.type;
- view.element.dispatchEvent(
- new CustomEvent(`FilePond:${event.type}`, {
- // event info
- detail,
- // event behaviour
- bubbles: true,
- cancelable: true,
- composed: true, // triggers listeners outside of shadow root
- })
- );
- // event object to params used for `on()` event handlers and callbacks `oninit()`
- const params = [];
- // if is possible error event, make it the first param
- if (event.hasOwnProperty('error')) {
- params.push(event.error);
- }
- // file is always section
- if (event.hasOwnProperty('file')) {
- params.push(event.file);
- }
- // append other props
- const filtered = ['type', 'error', 'file'];
- Object.keys(event)
- .filter(key => !filtered.includes(key))
- .forEach(key => params.push(event[key]));
- // on(type, () => { })
- exports.fire(event.type, ...params);
- // oninit = () => {}
- const handler = store.query(`GET_ON${event.type.toUpperCase()}`);
- if (handler) {
- handler(...params);
- }
- };
- const routeActionsToEvents = actions => {
- if (!actions.length) return;
- actions
- .filter(action => eventRoutes[action.type])
- .forEach(action => {
- const routes = eventRoutes[action.type];
- (Array.isArray(routes) ? routes : [routes]).forEach(route => {
- // this isn't fantastic, but because of the stacking of settimeouts plugins can handle the did_load before the did_init
- if (action.type === 'DID_INIT_ITEM') {
- exposeEvent(route(action.data));
- } else {
- setTimeout(() => {
- exposeEvent(route(action.data));
- }, 0);
- }
- });
- });
- };
- //
- // PUBLIC API -------------------------------------------------------------------------------------
- //
- const setOptions = options => store.dispatch('SET_OPTIONS', { options });
- const getFile = query => store.query('GET_ACTIVE_ITEM', query);
- const prepareFile = query =>
- new Promise((resolve, reject) => {
- store.dispatch('REQUEST_ITEM_PREPARE', {
- query,
- success: item => {
- resolve(item);
- },
- failure: error => {
- reject(error);
- },
- });
- });
- const addFile = (source, options = {}) =>
- new Promise((resolve, reject) => {
- addFiles([{ source, options }], { index: options.index })
- .then(items => resolve(items && items[0]))
- .catch(reject);
- });
- const isFilePondFile = obj => obj.file && obj.id;
- const removeFile = (query, options) => {
- // if only passed options
- if (typeof query === 'object' && !isFilePondFile(query) && !options) {
- options = query;
- query = undefined;
- }
- // request item removal
- store.dispatch('REMOVE_ITEM', { ...options, query });
- // see if item has been removed
- return store.query('GET_ACTIVE_ITEM', query) === null;
- };
- const addFiles = (...args) =>
- new Promise((resolve, reject) => {
- const sources = [];
- const options = {};
- // user passed a sources array
- if (isArray(args[0])) {
- sources.push.apply(sources, args[0]);
- Object.assign(options, args[1] || {});
- } else {
- // user passed sources as arguments, last one might be options object
- const lastArgument = args[args.length - 1];
- if (typeof lastArgument === 'object' && !(lastArgument instanceof Blob)) {
- Object.assign(options, args.pop());
- }
- // add rest to sources
- sources.push(...args);
- }
- store.dispatch('ADD_ITEMS', {
- items: sources,
- index: options.index,
- interactionMethod: InteractionMethod.API,
- success: resolve,
- failure: reject,
- });
- });
- const getFiles = () => store.query('GET_ACTIVE_ITEMS');
- const processFile = query =>
- new Promise((resolve, reject) => {
- store.dispatch('REQUEST_ITEM_PROCESSING', {
- query,
- success: item => {
- resolve(item);
- },
- failure: error => {
- reject(error);
- },
- });
- });
- const prepareFiles = (...args) => {
- const queries = Array.isArray(args[0]) ? args[0] : args;
- const items = queries.length ? queries : getFiles();
- return Promise.all(items.map(prepareFile));
- };
- const processFiles = (...args) => {
- const queries = Array.isArray(args[0]) ? args[0] : args;
- if (!queries.length) {
- const files = getFiles().filter(
- item =>
- !(item.status === ItemStatus.IDLE && item.origin === FileOrigin.LOCAL) &&
- item.status !== ItemStatus.PROCESSING &&
- item.status !== ItemStatus.PROCESSING_COMPLETE &&
- item.status !== ItemStatus.PROCESSING_REVERT_ERROR
- );
- return Promise.all(files.map(processFile));
- }
- return Promise.all(queries.map(processFile));
- };
- const removeFiles = (...args) => {
- const queries = Array.isArray(args[0]) ? args[0] : args;
- let options;
- if (typeof queries[queries.length - 1] === 'object') {
- options = queries.pop();
- } else if (Array.isArray(args[0])) {
- options = args[1];
- }
- const files = getFiles();
- if (!queries.length) return Promise.all(files.map(file => removeFile(file, options)));
- // when removing by index the indexes shift after each file removal so we need to convert indexes to ids
- const mappedQueries = queries
- .map(query => (isNumber(query) ? (files[query] ? files[query].id : null) : query))
- .filter(query => query);
- return mappedQueries.map(q => removeFile(q, options));
- };
- const exports = {
- // supports events
- ...on(),
- // inject private api methods
- ...readWriteApi,
- // inject all getters and setters
- ...createOptionAPI(store, defaultOptions),
- /**
- * Override options defined in options object
- * @param options
- */
- setOptions,
- /**
- * Load the given file
- * @param source - the source of the file (either a File, base64 data uri or url)
- * @param options - object, { index: 0 }
- */
- addFile,
- /**
- * Load the given files
- * @param sources - the sources of the files to load
- * @param options - object, { index: 0 }
- */
- addFiles,
- /**
- * Returns the file objects matching the given query
- * @param query { string, number, null }
- */
- getFile,
- /**
- * Upload file with given name
- * @param query { string, number, null }
- */
- processFile,
- /**
- * Request prepare output for file with given name
- * @param query { string, number, null }
- */
- prepareFile,
- /**
- * Removes a file by its name
- * @param query { string, number, null }
- */
- removeFile,
- /**
- * Moves a file to a new location in the files list
- */
- moveFile: (query, index) => store.dispatch('MOVE_ITEM', { query, index }),
- /**
- * Returns all files (wrapped in public api)
- */
- getFiles,
- /**
- * Starts uploading all files
- */
- processFiles,
- /**
- * Clears all files from the files list
- */
- removeFiles,
- /**
- * Starts preparing output of all files
- */
- prepareFiles,
- /**
- * Sort list of files
- */
- sort: compare => store.dispatch('SORT', { compare }),
- /**
- * Browse the file system for a file
- */
- browse: () => {
- // needs to be trigger directly as user action needs to be traceable (is not traceable in requestAnimationFrame)
- var input = view.element.querySelector('input[type=file]');
- if (input) {
- input.click();
- }
- },
- /**
- * Destroys the app
- */
- destroy: () => {
- // request destruction
- exports.fire('destroy', view.element);
- // stop active processes (file uploads, fetches, stuff like that)
- // loop over items and depending on states call abort for ongoing processes
- store.dispatch('ABORT_ALL');
- // destroy view
- view._destroy();
- // stop listening to resize
- window.removeEventListener('resize', resizeHandler);
- // stop listening to the visiblitychange event
- document.removeEventListener('visibilitychange', visibilityHandler);
- // dispatch destroy
- store.dispatch('DID_DESTROY');
- },
- /**
- * Inserts the plugin before the target element
- */
- insertBefore: element => insertBefore(view.element, element),
- /**
- * Inserts the plugin after the target element
- */
- insertAfter: element => insertAfter(view.element, element),
- /**
- * Appends the plugin to the target element
- */
- appendTo: element => element.appendChild(view.element),
- /**
- * Replaces an element with the app
- */
- replaceElement: element => {
- // insert the app before the element
- insertBefore(view.element, element);
- // remove the original element
- element.parentNode.removeChild(element);
- // remember original element
- originalElement = element;
- },
- /**
- * Restores the original element
- */
- restoreElement: () => {
- if (!originalElement) {
- return; // no element to restore
- }
- // restore original element
- insertAfter(originalElement, view.element);
- // remove our element
- view.element.parentNode.removeChild(view.element);
- // remove reference
- originalElement = null;
- },
- /**
- * Returns true if the app root is attached to given element
- * @param element
- */
- isAttachedTo: element => view.element === element || originalElement === element,
- /**
- * Returns the root element
- */
- element: {
- get: () => view.element,
- },
- /**
- * Returns the current pond status
- */
- status: {
- get: () => store.query('GET_STATUS'),
- },
- };
- // Done!
- store.dispatch('DID_INIT');
- // create actual api object
- return createObject(exports);
- };
- const createAppObject = (customOptions = {}) => {
- // default options
- const defaultOptions = {};
- forin(getOptions(), (key, value) => {
- defaultOptions[key] = value[0];
- });
- // set app options
- const app = createApp({
- // default options
- ...defaultOptions,
- // custom options
- ...customOptions,
- });
- // return the plugin instance
- return app;
- };
- const lowerCaseFirstLetter = string => string.charAt(0).toLowerCase() + string.slice(1);
- const attributeNameToPropertyName = attributeName => toCamels(attributeName.replace(/^data-/, ''));
- const mapObject = (object, propertyMap) => {
- // remove unwanted
- forin(propertyMap, (selector, mapping) => {
- forin(object, (property, value) => {
- // create regexp shortcut
- const selectorRegExp = new RegExp(selector);
- // tests if
- const matches = selectorRegExp.test(property);
- // no match, skip
- if (!matches) {
- return;
- }
- // if there's a mapping, the original property is always removed
- delete object[property];
- // should only remove, we done!
- if (mapping === false) {
- return;
- }
- // move value to new property
- if (isString(mapping)) {
- object[mapping] = value;
- return;
- }
- // move to group
- const group = mapping.group;
- if (isObject(mapping) && !object[group]) {
- object[group] = {};
- }
- object[group][lowerCaseFirstLetter(property.replace(selectorRegExp, ''))] = value;
- });
- // do submapping
- if (mapping.mapping) {
- mapObject(object[mapping.group], mapping.mapping);
- }
- });
- };
- const getAttributesAsObject = (node, attributeMapping = {}) => {
- // turn attributes into object
- const attributes = [];
- forin(node.attributes, index => {
- attributes.push(node.attributes[index]);
- });
- const output = attributes
- .filter(attribute => attribute.name)
- .reduce((obj, attribute) => {
- const value = attr(node, attribute.name);
- obj[attributeNameToPropertyName(attribute.name)] =
- value === attribute.name ? true : value;
- return obj;
- }, {});
- // do mapping of object properties
- mapObject(output, attributeMapping);
- return output;
- };
- const createAppAtElement = (element, options = {}) => {
- // how attributes of the input element are mapped to the options for the plugin
- const attributeMapping = {
- // translate to other name
- '^class$': 'className',
- '^multiple$': 'allowMultiple',
- '^capture$': 'captureMethod',
- '^webkitdirectory$': 'allowDirectoriesOnly',
- // group under single property
- '^server': {
- group: 'server',
- mapping: {
- '^process': {
- group: 'process',
- },
- '^revert': {
- group: 'revert',
- },
- '^fetch': {
- group: 'fetch',
- },
- '^restore': {
- group: 'restore',
- },
- '^load': {
- group: 'load',
- },
- },
- },
- // don't include in object
- '^type$': false,
- '^files$': false,
- };
- // add additional option translators
- applyFilters('SET_ATTRIBUTE_TO_OPTION_MAP', attributeMapping);
- // create final options object by setting options object and then overriding options supplied on element
- const mergedOptions = {
- ...options,
- };
- const attributeOptions = getAttributesAsObject(
- element.nodeName === 'FIELDSET' ? element.querySelector('input[type=file]') : element,
- attributeMapping
- );
- // merge with options object
- Object.keys(attributeOptions).forEach(key => {
- if (isObject(attributeOptions[key])) {
- if (!isObject(mergedOptions[key])) {
- mergedOptions[key] = {};
- }
- Object.assign(mergedOptions[key], attributeOptions[key]);
- } else {
- mergedOptions[key] = attributeOptions[key];
- }
- });
- // if parent is a fieldset, get files from parent by selecting all input fields that are not file upload fields
- // these will then be automatically set to the initial files
- mergedOptions.files = (options.files || []).concat(
- Array.from(element.querySelectorAll('input:not([type=file])')).map(input => ({
- source: input.value,
- options: {
- type: input.dataset.type,
- },
- }))
- );
- // build plugin
- const app = createAppObject(mergedOptions);
- // add already selected files
- if (element.files) {
- Array.from(element.files).forEach(file => {
- app.addFile(file);
- });
- }
- // replace the target element
- app.replaceElement(element);
- // expose
- return app;
- };
- // if an element is passed, we create the instance at that element, if not, we just create an up object
- const createApp$1 = (...args) =>
- isNode(args[0]) ? createAppAtElement(...args) : createAppObject(...args);
- const PRIVATE_METHODS = ['fire', '_read', '_write'];
- const createAppAPI = app => {
- const api = {};
- copyObjectPropertiesToObject(app, api, PRIVATE_METHODS);
- return api;
- };
- /**
- * Replaces placeholders in given string with replacements
- * @param string - "Foo {bar}""
- * @param replacements - { "bar": 10 }
- */
- const replaceInString = (string, replacements) =>
- string.replace(/(?:{([a-zA-Z]+)})/g, (match, group) => replacements[group]);
- const createWorker = fn => {
- const workerBlob = new Blob(['(', fn.toString(), ')()'], {
- type: 'application/javascript',
- });
- const workerURL = URL.createObjectURL(workerBlob);
- const worker = new Worker(workerURL);
- return {
- transfer: (message, cb) => {},
- post: (message, cb, transferList) => {
- const id = getUniqueId();
- worker.onmessage = e => {
- if (e.data.id === id) {
- cb(e.data.message);
- }
- };
- worker.postMessage(
- {
- id,
- message,
- },
- transferList
- );
- },
- terminate: () => {
- worker.terminate();
- URL.revokeObjectURL(workerURL);
- },
- };
- };
- const loadImage = url =>
- new Promise((resolve, reject) => {
- const img = new Image();
- img.onload = () => {
- resolve(img);
- };
- img.onerror = e => {
- reject(e);
- };
- img.src = url;
- });
- const renameFile = (file, name) => {
- const renamedFile = file.slice(0, file.size, file.type);
- renamedFile.lastModifiedDate = file.lastModifiedDate;
- renamedFile.name = name;
- return renamedFile;
- };
- const copyFile = file => renameFile(file, file.name);
- // already registered plugins (can't register twice)
- const registeredPlugins = [];
- // pass utils to plugin
- const createAppPlugin = plugin => {
- // already registered
- if (registeredPlugins.includes(plugin)) {
- return;
- }
- // remember this plugin
- registeredPlugins.push(plugin);
- // setup!
- const pluginOutline = plugin({
- addFilter,
- utils: {
- Type,
- forin,
- isString,
- isFile,
- toNaturalFileSize,
- replaceInString,
- getExtensionFromFilename,
- getFilenameWithoutExtension,
- guesstimateMimeType,
- getFileFromBlob,
- getFilenameFromURL,
- createRoute,
- createWorker,
- createView,
- createItemAPI,
- loadImage,
- copyFile,
- renameFile,
- createBlob,
- applyFilterChain,
- text,
- getNumericAspectRatioFromString,
- },
- views: {
- fileActionButton,
- },
- });
- // add plugin options to default options
- extendDefaultOptions(pluginOutline.options);
- };
- // feature detection used by supported() method
- const isOperaMini = () => Object.prototype.toString.call(window.operamini) === '[object OperaMini]';
- const hasPromises = () => 'Promise' in window;
- const hasBlobSlice = () => 'slice' in Blob.prototype;
- const hasCreateObjectURL = () => 'URL' in window && 'createObjectURL' in window.URL;
- const hasVisibility = () => 'visibilityState' in document;
- const hasTiming = () => 'performance' in window; // iOS 8.x
- const hasCSSSupports = () => 'supports' in (window.CSS || {}); // use to detect Safari 9+
- const isIE11 = () => /MSIE|Trident/.test(window.navigator.userAgent);
- const supported = (() => {
- // Runs immediately and then remembers result for subsequent calls
- const isSupported =
- // Has to be a browser
- isBrowser() &&
- // Can't run on Opera Mini due to lack of everything
- !isOperaMini() &&
- // Require these APIs to feature detect a modern browser
- hasVisibility() &&
- hasPromises() &&
- hasBlobSlice() &&
- hasCreateObjectURL() &&
- hasTiming() &&
- // doesn't need CSSSupports but is a good way to detect Safari 9+ (we do want to support IE11 though)
- (hasCSSSupports() || isIE11());
- return () => isSupported;
- })();
- /**
- * Plugin internal state (over all instances)
- */
- const state = {
- // active app instances, used to redraw the apps and to find the later
- apps: [],
- };
- // plugin name
- const name = 'filepond';
- /**
- * Public Plugin methods
- */
- const fn = () => {};
- let Status$1 = {};
- let FileStatus = {};
- let FileOrigin$1 = {};
- let OptionTypes = {};
- let create$f = fn;
- let destroy = fn;
- let parse = fn;
- let find = fn;
- let registerPlugin = fn;
- let getOptions$1 = fn;
- let setOptions$1 = fn;
- // if not supported, no API
- if (supported()) {
- // start painter and fire load event
- createPainter(
- () => {
- state.apps.forEach(app => app._read());
- },
- ts => {
- state.apps.forEach(app => app._write(ts));
- }
- );
- // fire loaded event so we know when FilePond is available
- const dispatch = () => {
- // let others know we have area ready
- document.dispatchEvent(
- new CustomEvent('FilePond:loaded', {
- detail: {
- supported,
- create: create$f,
- destroy,
- parse,
- find,
- registerPlugin,
- setOptions: setOptions$1,
- },
- })
- );
- // clean up event
- document.removeEventListener('DOMContentLoaded', dispatch);
- };
- if (document.readyState !== 'loading') {
- // move to back of execution queue, FilePond should have been exported by then
- setTimeout(() => dispatch(), 0);
- } else {
- document.addEventListener('DOMContentLoaded', dispatch);
- }
- // updates the OptionTypes object based on the current options
- const updateOptionTypes = () =>
- forin(getOptions(), (key, value) => {
- OptionTypes[key] = value[1];
- });
- Status$1 = { ...Status };
- FileOrigin$1 = { ...FileOrigin };
- FileStatus = { ...ItemStatus };
- OptionTypes = {};
- updateOptionTypes();
- // create method, creates apps and adds them to the app array
- create$f = (...args) => {
- const app = createApp$1(...args);
- app.on('destroy', destroy);
- state.apps.push(app);
- return createAppAPI(app);
- };
- // destroys apps and removes them from the app array
- destroy = hook => {
- // returns true if the app was destroyed successfully
- const indexToRemove = state.apps.findIndex(app => app.isAttachedTo(hook));
- if (indexToRemove >= 0) {
- // remove from apps
- const app = state.apps.splice(indexToRemove, 1)[0];
- // restore original dom element
- app.restoreElement();
- return true;
- }
- return false;
- };
- // parses the given context for plugins (does not include the context element itself)
- parse = context => {
- // get all possible hooks
- const matchedHooks = Array.from(context.querySelectorAll(`.${name}`));
- // filter out already active hooks
- const newHooks = matchedHooks.filter(
- newHook => !state.apps.find(app => app.isAttachedTo(newHook))
- );
- // create new instance for each hook
- return newHooks.map(hook => create$f(hook));
- };
- // returns an app based on the given element hook
- find = hook => {
- const app = state.apps.find(app => app.isAttachedTo(hook));
- if (!app) {
- return null;
- }
- return createAppAPI(app);
- };
- // adds a plugin extension
- registerPlugin = (...plugins) => {
- // register plugins
- plugins.forEach(createAppPlugin);
- // update OptionTypes, each plugin might have extended the default options
- updateOptionTypes();
- };
- getOptions$1 = () => {
- const opts = {};
- forin(getOptions(), (key, value) => {
- opts[key] = value[0];
- });
- return opts;
- };
- setOptions$1 = opts => {
- if (isObject(opts)) {
- // update existing plugins
- state.apps.forEach(app => {
- app.setOptions(opts);
- });
- // override defaults
- setOptions(opts);
- }
- // return new options
- return getOptions$1();
- };
- }
- export {
- FileOrigin$1 as FileOrigin,
- FileStatus,
- OptionTypes,
- Status$1 as Status,
- create$f as create,
- destroy,
- find,
- getOptions$1 as getOptions,
- parse,
- registerPlugin,
- setOptions$1 as setOptions,
- supported,
- };
|