You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Escalator.java 278KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207
  1. /*
  2. * Copyright 2000-2018 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.client.widgets;
  17. import java.util.ArrayList;
  18. import java.util.Arrays;
  19. import java.util.Collection;
  20. import java.util.Collections;
  21. import java.util.HashMap;
  22. import java.util.LinkedList;
  23. import java.util.List;
  24. import java.util.ListIterator;
  25. import java.util.Map;
  26. import java.util.Map.Entry;
  27. import java.util.Optional;
  28. import java.util.TreeMap;
  29. import java.util.function.Consumer;
  30. import java.util.logging.Level;
  31. import java.util.logging.Logger;
  32. import java.util.stream.Stream;
  33. import com.google.gwt.animation.client.Animation;
  34. import com.google.gwt.animation.client.AnimationScheduler;
  35. import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback;
  36. import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle;
  37. import com.google.gwt.core.client.Duration;
  38. import com.google.gwt.core.client.JavaScriptObject;
  39. import com.google.gwt.core.client.JsArray;
  40. import com.google.gwt.core.client.Scheduler;
  41. import com.google.gwt.core.client.Scheduler.ScheduledCommand;
  42. import com.google.gwt.dom.client.DivElement;
  43. import com.google.gwt.dom.client.Document;
  44. import com.google.gwt.dom.client.Element;
  45. import com.google.gwt.dom.client.NativeEvent;
  46. import com.google.gwt.dom.client.Node;
  47. import com.google.gwt.dom.client.NodeList;
  48. import com.google.gwt.dom.client.Style;
  49. import com.google.gwt.dom.client.Style.Display;
  50. import com.google.gwt.dom.client.Style.Unit;
  51. import com.google.gwt.dom.client.TableCellElement;
  52. import com.google.gwt.dom.client.TableRowElement;
  53. import com.google.gwt.dom.client.TableSectionElement;
  54. import com.google.gwt.dom.client.Touch;
  55. import com.google.gwt.event.dom.client.KeyCodes;
  56. import com.google.gwt.event.shared.HandlerRegistration;
  57. import com.google.gwt.logging.client.LogConfiguration;
  58. import com.google.gwt.user.client.DOM;
  59. import com.google.gwt.user.client.Event;
  60. import com.google.gwt.user.client.Window;
  61. import com.google.gwt.user.client.ui.RequiresResize;
  62. import com.google.gwt.user.client.ui.RootPanel;
  63. import com.google.gwt.user.client.ui.UIObject;
  64. import com.google.gwt.user.client.ui.Widget;
  65. import com.vaadin.client.BrowserInfo;
  66. import com.vaadin.client.ComputedStyle;
  67. import com.vaadin.client.DeferredWorker;
  68. import com.vaadin.client.Profiler;
  69. import com.vaadin.client.WidgetUtil;
  70. import com.vaadin.client.ui.SubPartAware;
  71. import com.vaadin.client.widget.escalator.Cell;
  72. import com.vaadin.client.widget.escalator.ColumnConfiguration;
  73. import com.vaadin.client.widget.escalator.EscalatorUpdater;
  74. import com.vaadin.client.widget.escalator.FlyweightCell;
  75. import com.vaadin.client.widget.escalator.FlyweightRow;
  76. import com.vaadin.client.widget.escalator.PositionFunction;
  77. import com.vaadin.client.widget.escalator.PositionFunction.Translate3DPosition;
  78. import com.vaadin.client.widget.escalator.PositionFunction.TranslatePosition;
  79. import com.vaadin.client.widget.escalator.PositionFunction.WebkitTranslate3DPosition;
  80. import com.vaadin.client.widget.escalator.Row;
  81. import com.vaadin.client.widget.escalator.RowContainer;
  82. import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer;
  83. import com.vaadin.client.widget.escalator.RowVisibilityChangeEvent;
  84. import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler;
  85. import com.vaadin.client.widget.escalator.ScrollbarBundle;
  86. import com.vaadin.client.widget.escalator.ScrollbarBundle.HorizontalScrollbarBundle;
  87. import com.vaadin.client.widget.escalator.ScrollbarBundle.VerticalScrollbarBundle;
  88. import com.vaadin.client.widget.escalator.Spacer;
  89. import com.vaadin.client.widget.escalator.SpacerUpdater;
  90. import com.vaadin.client.widget.escalator.events.RowHeightChangedEvent;
  91. import com.vaadin.client.widget.escalator.events.SpacerIndexChangedEvent;
  92. import com.vaadin.client.widget.escalator.events.SpacerVisibilityChangedEvent;
  93. import com.vaadin.client.widget.grid.events.ScrollEvent;
  94. import com.vaadin.client.widget.grid.events.ScrollHandler;
  95. import com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle;
  96. import com.vaadin.shared.Range;
  97. import com.vaadin.shared.ui.grid.HeightMode;
  98. import com.vaadin.shared.ui.grid.ScrollDestination;
  99. import com.vaadin.shared.util.SharedUtil;
  100. /*-
  101. Maintenance Notes! Reading these might save your day.
  102. (note for editors: line width is 80 chars, including the
  103. one-space indentation)
  104. == Row Container Structure
  105. AbstractRowContainer
  106. |-- AbstractStaticRowContainer
  107. | |-- HeaderRowContainer
  108. | `-- FooterContainer
  109. `---- BodyRowContainerImpl
  110. AbstractRowContainer is intended to contain all common logic
  111. between RowContainers. It manages the bookkeeping of row
  112. count, makes sure that all individual cells are rendered
  113. the same way, and so on.
  114. AbstractStaticRowContainer has some special logic that is
  115. required by all RowContainers that don't scroll (hence the
  116. word "static"). HeaderRowContainer and FooterRowContainer
  117. are pretty thin special cases of a StaticRowContainer
  118. (mostly relating to positioning of the root element).
  119. BodyRowContainerImpl could also be split into an additional
  120. "AbstractScrollingRowContainer", but I felt that no more
  121. inner classes were needed. So it contains both logic
  122. required for making things scroll about, and equivalent
  123. special cases for layouting, as are found in
  124. Header/FooterRowContainers.
  125. == The Three Indices
  126. Each RowContainer can be thought to have three levels of
  127. indices for any given displayed row (but the distinction
  128. matters primarily for the BodyRowContainerImpl, because of
  129. the way it scrolls through data):
  130. - Logical index
  131. - Physical (or DOM) index
  132. - Visual index
  133. LOGICAL INDEX is the index that is linked to the data
  134. source. If you want your data source to represent a SQL
  135. database with 10 000 rows, the 7 000:th row in the SQL has a
  136. logical index of 6 999, since the index is 0-based (unless
  137. that data source does some funky logic).
  138. PHYSICAL INDEX is the index for a row that you see in a
  139. browser's DOM inspector. If your row is the second <tr>
  140. element within a <tbody> tag, it has a physical index of 1
  141. (because of 0-based indices). In Header and
  142. FooterRowContainers, you are safe to assume that the logical
  143. index is the same as the physical index. But because the
  144. BodyRowContainerImpl never displays large data sources
  145. entirely in the DOM, a physical index usually has no
  146. apparent direct relationship with its logical index.
  147. VISUAL INDEX is the index relating to the order that you
  148. see a row in, in the browser, as it is rendered. The
  149. topmost row is 0, the second is 1, and so on. The visual
  150. index is similar to the physical index in the sense that
  151. Header and FooterRowContainers can assume a 1:1
  152. relationship between visual index and logical index. And
  153. again, BodyRowContainerImpl has no such relationship. The
  154. body's visual index has additionally no apparent
  155. relationship with its physical index. Because the <tr> tags
  156. are reused in the body and visually repositioned with CSS
  157. as the user scrolls, the relationship between physical
  158. index and visual index is quickly broken. You can get an
  159. element's visual index via the field
  160. BodyRowContainerImpl.visualRowOrder.
  161. Currently, the physical and visual indices are kept in sync
  162. _most of the time_ by a deferred rearrangement of rows.
  163. They become desynced when scrolling. This is to help screen
  164. readers to read the contents from the DOM in a natural
  165. order. See BodyRowContainerImpl.DeferredDomSorter for more
  166. about that.
  167. */
  168. /**
  169. * A workaround-class for GWT and JSNI.
  170. * <p>
  171. * GWT is unable to handle some method calls to Java methods in inner-classes
  172. * from within JSNI blocks. Having that inner class extend a non-inner-class (or
  173. * implement such an interface), makes it possible for JSNI to indirectly refer
  174. * to the inner class, by invoking methods and fields in the non-inner-class
  175. * API.
  176. *
  177. * @see Escalator.Scroller
  178. */
  179. abstract class JsniWorkaround {
  180. /**
  181. * A JavaScript function that handles the scroll DOM event, and passes it on
  182. * to Java code.
  183. *
  184. * @see #createScrollListenerFunction(Escalator)
  185. * @see Escalator.Scroller#onScroll()
  186. */
  187. protected final JavaScriptObject scrollListenerFunction;
  188. /**
  189. * A JavaScript function that handles the mousewheel DOM event, and passes
  190. * it on to Java code.
  191. *
  192. * @see #createMousewheelListenerFunction(Escalator)
  193. * @see Escalator.Scroller#onScroll()
  194. */
  195. protected final JavaScriptObject mousewheelListenerFunction;
  196. /**
  197. * A JavaScript function that handles the touch start DOM event, and passes
  198. * it on to Java code.
  199. *
  200. * @see TouchHandlerBundle#touchStart(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
  201. */
  202. protected JavaScriptObject touchStartFunction;
  203. /**
  204. * A JavaScript function that handles the touch move DOM event, and passes
  205. * it on to Java code.
  206. *
  207. * @see TouchHandlerBundle#touchMove(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
  208. */
  209. protected JavaScriptObject touchMoveFunction;
  210. /**
  211. * A JavaScript function that handles the touch end and cancel DOM events,
  212. * and passes them on to Java code.
  213. *
  214. * @see TouchHandlerBundle#touchEnd(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
  215. */
  216. protected JavaScriptObject touchEndFunction;
  217. protected TouchHandlerBundle touchHandlerBundle;
  218. protected JsniWorkaround(final Escalator escalator) {
  219. scrollListenerFunction = createScrollListenerFunction(escalator);
  220. mousewheelListenerFunction = createMousewheelListenerFunction(
  221. escalator);
  222. touchHandlerBundle = new TouchHandlerBundle(escalator);
  223. touchStartFunction = touchHandlerBundle.getTouchStartHandler();
  224. touchMoveFunction = touchHandlerBundle.getTouchMoveHandler();
  225. touchEndFunction = touchHandlerBundle.getTouchEndHandler();
  226. }
  227. /**
  228. * A method that constructs the JavaScript function that will be stored into
  229. * {@link #scrollListenerFunction}.
  230. *
  231. * @param esc
  232. * a reference to the current instance of {@link Escalator}
  233. * @see Escalator.Scroller#onScroll()
  234. */
  235. protected abstract JavaScriptObject createScrollListenerFunction(
  236. Escalator esc);
  237. /**
  238. * A method that constructs the JavaScript function that will be stored into
  239. * {@link #mousewheelListenerFunction}.
  240. *
  241. * @param esc
  242. * a reference to the current instance of {@link Escalator}
  243. * @see Escalator.Scroller#onScroll()
  244. */
  245. protected abstract JavaScriptObject createMousewheelListenerFunction(
  246. Escalator esc);
  247. }
  248. /**
  249. * A low-level table-like widget that features a scrolling virtual viewport and
  250. * lazily generated rows.
  251. *
  252. * @since 7.4
  253. * @author Vaadin Ltd
  254. */
  255. public class Escalator extends Widget
  256. implements RequiresResize, DeferredWorker, SubPartAware {
  257. // todo comments legend
  258. /*
  259. * [[optimize]]: There's an opportunity to rewrite the code in such a way
  260. * that it _might_ perform better (remember to measure, implement,
  261. * re-measure)
  262. */
  263. /*
  264. * [[mpixscroll]]: This code will require alterations that are relevant for
  265. * supporting the scrolling through more pixels than some browsers normally
  266. * would support. (i.e. when we support more than "a million" pixels in the
  267. * escalator DOM). NOTE: these bits can most often also be identified by
  268. * searching for code that call scrollElem.getScrollTop();.
  269. */
  270. /*
  271. * [[spacer]]: Code that is important to make spacers work.
  272. */
  273. /**
  274. * A utility class that contains utility methods that are usually called
  275. * from JSNI.
  276. * <p>
  277. * The methods are moved in this class to minimize the amount of JSNI code
  278. * as much as feasible.
  279. */
  280. static class JsniUtil {
  281. public static class TouchHandlerBundle {
  282. public static final String POINTER_EVENT_TYPE_TOUCH = "touch";
  283. public static final int SIGNIFICANT_MOVE_THRESHOLD = 3;
  284. /**
  285. * A <a href=
  286. * "http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsOverlay.html"
  287. * >JavaScriptObject overlay</a> for the
  288. * <a href="http://www.w3.org/TR/touch-events/">JavaScript
  289. * TouchEvent</a> object.
  290. * <p>
  291. * This needs to be used in the touch event handlers, since GWT's
  292. * {@link com.google.gwt.event.dom.client.TouchEvent TouchEvent}
  293. * can't be cast from the JSNI call, and the
  294. * {@link com.google.gwt.dom.client.NativeEvent NativeEvent} isn't
  295. * properly populated with the correct values.
  296. */
  297. private static final class CustomTouchEvent
  298. extends JavaScriptObject {
  299. protected CustomTouchEvent() {
  300. }
  301. public native NativeEvent getNativeEvent()
  302. /*-{
  303. return this;
  304. }-*/;
  305. public native int getPageX()
  306. /*-{
  307. return this.targetTouches[0].pageX;
  308. }-*/;
  309. public native int getPageY()
  310. /*-{
  311. return this.targetTouches[0].pageY;
  312. }-*/;
  313. public native String getPointerType()
  314. /*-{
  315. return this.pointerType;
  316. }-*/;
  317. }
  318. private final Escalator escalator;
  319. public TouchHandlerBundle(final Escalator escalator) {
  320. this.escalator = escalator;
  321. }
  322. public native JavaScriptObject getTouchStartHandler()
  323. /*-{
  324. // we need to store "this", since it won't be preserved on call.
  325. var self = this;
  326. return $entry(function (e) {
  327. self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchStart(*)(e);
  328. });
  329. }-*/;
  330. public native JavaScriptObject getTouchMoveHandler()
  331. /*-{
  332. // we need to store "this", since it won't be preserved on call.
  333. var self = this;
  334. return $entry(function (e) {
  335. self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchMove(*)(e);
  336. });
  337. }-*/;
  338. public native JavaScriptObject getTouchEndHandler()
  339. /*-{
  340. // we need to store "this", since it won't be preserved on call.
  341. var self = this;
  342. return $entry(function (e) {
  343. self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchEnd(*)(e);
  344. });
  345. }-*/;
  346. // Duration of the inertial scrolling simulation. Devices with
  347. // larger screens take longer durations.
  348. private static final int DURATION = Window.getClientHeight();
  349. // multiply scroll velocity with repeated touching
  350. private int acceleration = 1;
  351. private boolean touching = false;
  352. // Two movement objects for storing status and processing touches
  353. private Movement yMov, xMov;
  354. // true if moved significantly since touch start
  355. private boolean movedSignificantly = false;
  356. private double touchStartTime;
  357. final double MIN_VEL = 0.6, MAX_VEL = 4, F_VEL = 1500, F_ACC = 0.7,
  358. F_AXIS = 1;
  359. // The object to deal with one direction scrolling
  360. private class Movement {
  361. final List<Double> speeds = new ArrayList<>();
  362. final ScrollbarBundle scroll;
  363. double position, offset, velocity, prevPos, prevTime, delta;
  364. boolean run, vertical;
  365. public Movement(boolean vertical) {
  366. this.vertical = vertical;
  367. scroll = vertical ? escalator.verticalScrollbar
  368. : escalator.horizontalScrollbar;
  369. }
  370. public void startTouch(CustomTouchEvent event) {
  371. speeds.clear();
  372. prevPos = pagePosition(event);
  373. prevTime = Duration.currentTimeMillis();
  374. }
  375. public void moveTouch(CustomTouchEvent event) {
  376. double pagePosition = pagePosition(event);
  377. if (pagePosition > -1) {
  378. delta = prevPos - pagePosition;
  379. double now = Duration.currentTimeMillis();
  380. double ellapsed = now - prevTime;
  381. velocity = delta / ellapsed;
  382. // if last speed was so low, reset speeds and start
  383. // storing again
  384. if (!speeds.isEmpty() && !validSpeed(speeds.get(0))) {
  385. speeds.clear();
  386. run = true;
  387. }
  388. speeds.add(0, velocity);
  389. prevTime = now;
  390. prevPos = pagePosition;
  391. }
  392. }
  393. public void endTouch(CustomTouchEvent event) {
  394. // Compute average speed
  395. velocity = 0;
  396. for (double s : speeds) {
  397. velocity += s / speeds.size();
  398. }
  399. position = scroll.getScrollPos();
  400. // Compute offset, and adjust it with an easing curve so as
  401. // movement is smoother.
  402. offset = F_VEL * velocity * acceleration
  403. * easingInOutCos(velocity, MAX_VEL);
  404. // Enable or disable inertia movement in this axis
  405. run = validSpeed(velocity);
  406. if (run) {
  407. event.getNativeEvent().preventDefault();
  408. }
  409. }
  410. void validate(Movement other) {
  411. if (!run || other.velocity > 0
  412. && Math.abs(velocity / other.velocity) < F_AXIS) {
  413. delta = offset = 0;
  414. run = false;
  415. }
  416. }
  417. void stepAnimation(double progress) {
  418. scroll.setScrollPos(position + offset * progress);
  419. }
  420. int pagePosition(CustomTouchEvent event) {
  421. // Use native event's screen x and y for IE11 and Edge
  422. // since there is no touches for these browsers (#18737)
  423. if (isCurrentBrowserIE11OrEdge()) {
  424. return vertical
  425. ? event.getNativeEvent().getClientY()
  426. + Window.getScrollTop()
  427. : event.getNativeEvent().getClientX()
  428. + Window.getScrollLeft();
  429. }
  430. JsArray<Touch> a = event.getNativeEvent().getTouches();
  431. return vertical ? a.get(0).getPageY() : a.get(0).getPageX();
  432. }
  433. boolean validSpeed(double speed) {
  434. return Math.abs(speed) > MIN_VEL;
  435. }
  436. }
  437. // Using GWT animations which take care of native animation frames.
  438. private Animation animation = new Animation() {
  439. @Override
  440. public void onUpdate(double progress) {
  441. xMov.stepAnimation(progress);
  442. yMov.stepAnimation(progress);
  443. }
  444. @Override
  445. public double interpolate(double progress) {
  446. return easingOutCirc(progress);
  447. };
  448. @Override
  449. public void onComplete() {
  450. touching = false;
  451. escalator.body.domSorter.reschedule();
  452. };
  453. @Override
  454. public void run(int duration) {
  455. if (xMov.run || yMov.run) {
  456. super.run(duration);
  457. } else {
  458. onComplete();
  459. }
  460. };
  461. };
  462. public void touchStart(final CustomTouchEvent event) {
  463. if (allowTouch(event)) {
  464. if (yMov == null) {
  465. yMov = new Movement(true);
  466. xMov = new Movement(false);
  467. }
  468. if (animation.isRunning()) {
  469. acceleration += F_ACC;
  470. event.getNativeEvent().preventDefault();
  471. animation.cancel();
  472. } else {
  473. acceleration = 1;
  474. }
  475. xMov.startTouch(event);
  476. yMov.startTouch(event);
  477. touchStartTime = Duration.currentTimeMillis();
  478. touching = true;
  479. movedSignificantly = false;
  480. } else {
  481. touching = false;
  482. animation.cancel();
  483. acceleration = 1;
  484. }
  485. }
  486. public void touchMove(final CustomTouchEvent event) {
  487. if (touching) {
  488. if (!movedSignificantly) {
  489. double distanceSquared = Math.abs(xMov.delta)
  490. * Math.abs(xMov.delta)
  491. + Math.abs(yMov.delta) * Math.abs(yMov.delta);
  492. movedSignificantly = distanceSquared > SIGNIFICANT_MOVE_THRESHOLD
  493. * SIGNIFICANT_MOVE_THRESHOLD;
  494. }
  495. // allow handling long press differently, without triggering
  496. // scrolling
  497. if (escalator.getDelayToCancelTouchScroll() >= 0
  498. && !movedSignificantly
  499. && Duration.currentTimeMillis()
  500. - touchStartTime > escalator
  501. .getDelayToCancelTouchScroll()) {
  502. // cancel touch handling, don't prevent event
  503. touching = false;
  504. animation.cancel();
  505. acceleration = 1;
  506. return;
  507. }
  508. xMov.moveTouch(event);
  509. yMov.moveTouch(event);
  510. xMov.validate(yMov);
  511. yMov.validate(xMov);
  512. moveScrollFromEvent(escalator, xMov.delta, yMov.delta,
  513. event.getNativeEvent());
  514. }
  515. }
  516. public void touchEnd(final CustomTouchEvent event) {
  517. if (touching) {
  518. xMov.endTouch(event);
  519. yMov.endTouch(event);
  520. xMov.validate(yMov);
  521. yMov.validate(xMov);
  522. // Adjust duration so as longer movements take more duration
  523. boolean vert = !xMov.run || yMov.run
  524. && Math.abs(yMov.offset) > Math.abs(xMov.offset);
  525. double delta = Math.abs((vert ? yMov : xMov).offset);
  526. animation.run((int) (3 * DURATION * easingOutExp(delta)));
  527. }
  528. }
  529. // Allow touchStart for IE11 and Edge even though there is no touch
  530. // (#18737),
  531. // otherwise allow touch only if there is a single touch in the
  532. // event
  533. private boolean allowTouch(
  534. final TouchHandlerBundle.CustomTouchEvent event) {
  535. if (isCurrentBrowserIE11OrEdge()) {
  536. return (POINTER_EVENT_TYPE_TOUCH
  537. .equals(event.getPointerType()));
  538. } else {
  539. return (event.getNativeEvent().getTouches().length() == 1);
  540. }
  541. }
  542. private double easingInOutCos(double val, double max) {
  543. return 0.5 - 0.5 * Math.cos(Math.PI * Math.signum(val)
  544. * Math.min(Math.abs(val), max) / max);
  545. }
  546. private double easingOutExp(double delta) {
  547. return (1 - Math.pow(2, -delta / 1000));
  548. }
  549. private double easingOutCirc(double progress) {
  550. return Math.sqrt(1 - (progress - 1) * (progress - 1));
  551. }
  552. }
  553. public static void moveScrollFromEvent(final Escalator escalator,
  554. final double deltaX, final double deltaY,
  555. final NativeEvent event) {
  556. boolean scrollPosXChanged = false;
  557. boolean scrollPosYChanged = false;
  558. if (!Double.isNaN(deltaX)) {
  559. double oldScrollPosX = escalator.horizontalScrollbar
  560. .getScrollPos();
  561. escalator.horizontalScrollbar.setScrollPosByDelta(deltaX);
  562. if (oldScrollPosX != escalator.horizontalScrollbar
  563. .getScrollPos()) {
  564. scrollPosXChanged = true;
  565. }
  566. }
  567. if (!Double.isNaN(deltaY)) {
  568. double oldScrollPosY = escalator.verticalScrollbar
  569. .getScrollPos();
  570. escalator.verticalScrollbar.setScrollPosByDelta(deltaY);
  571. if (oldScrollPosY != escalator.verticalScrollbar
  572. .getScrollPos()) {
  573. scrollPosYChanged = true;
  574. }
  575. }
  576. /*
  577. * Only prevent if internal scrolling happened. If there's no more
  578. * room to scroll internally, allow the event to pass further.
  579. */
  580. final boolean warrantedYScroll = deltaY != 0 && scrollPosYChanged
  581. && escalator.verticalScrollbar.showsScrollHandle();
  582. final boolean warrantedXScroll = deltaX != 0 && scrollPosXChanged
  583. && escalator.horizontalScrollbar.showsScrollHandle();
  584. if (warrantedYScroll || warrantedXScroll) {
  585. event.preventDefault();
  586. }
  587. }
  588. }
  589. /**
  590. * ScrollDestination case-specific handling logic.
  591. */
  592. private static double getScrollPos(final ScrollDestination destination,
  593. final double targetStartPx, final double targetEndPx,
  594. final double viewportStartPx, final double viewportEndPx,
  595. final double padding) {
  596. final double viewportLength = viewportEndPx - viewportStartPx;
  597. switch (destination) {
  598. /*
  599. * Scroll as little as possible to show the target element. If the
  600. * element fits into view, this works as START or END depending on the
  601. * current scroll position. If the element does not fit into view, this
  602. * works as START.
  603. */
  604. case ANY: {
  605. final double startScrollPos = targetStartPx - padding;
  606. final double endScrollPos = targetEndPx + padding - viewportLength;
  607. if (startScrollPos < viewportStartPx) {
  608. return startScrollPos;
  609. } else if (targetEndPx + padding > viewportEndPx) {
  610. return endScrollPos;
  611. } else {
  612. // NOOP, it's already visible
  613. return viewportStartPx;
  614. }
  615. }
  616. /*
  617. * Scrolls so that the element is shown at the end of the viewport. The
  618. * viewport will, however, not scroll before its first element.
  619. */
  620. case END: {
  621. return targetEndPx + padding - viewportLength;
  622. }
  623. /*
  624. * Scrolls so that the element is shown in the middle of the viewport.
  625. * The viewport will, however, not scroll beyond its contents, given
  626. * more elements than what the viewport is able to show at once. Under
  627. * no circumstances will the viewport scroll before its first element.
  628. */
  629. case MIDDLE: {
  630. final double targetMiddle = targetStartPx
  631. + (targetEndPx - targetStartPx) / 2;
  632. return targetMiddle - viewportLength / 2;
  633. }
  634. /*
  635. * Scrolls so that the element is shown at the start of the viewport.
  636. * The viewport will, however, not scroll beyond its contents.
  637. */
  638. case START: {
  639. return targetStartPx - padding;
  640. }
  641. /*
  642. * Throw an error if we're here. This can only mean that
  643. * ScrollDestination has been carelessly amended..
  644. */
  645. default: {
  646. throw new IllegalArgumentException(
  647. "Internal: ScrollDestination has been modified, "
  648. + "but Escalator.getScrollPos has not been updated "
  649. + "to match new values.");
  650. }
  651. }
  652. }
  653. /** An inner class that handles all logic related to scrolling. */
  654. private class Scroller extends JsniWorkaround {
  655. private double lastScrollTop = 0;
  656. private double lastScrollLeft = 0;
  657. public Scroller() {
  658. super(Escalator.this);
  659. }
  660. @Override
  661. protected native JavaScriptObject createScrollListenerFunction(
  662. Escalator esc)
  663. /*-{
  664. var vScroll = esc.@com.vaadin.client.widgets.Escalator::verticalScrollbar;
  665. var vScrollElem = vScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::getElement()();
  666. var hScroll = esc.@com.vaadin.client.widgets.Escalator::horizontalScrollbar;
  667. var hScrollElem = hScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::getElement()();
  668. return $entry(function(e) {
  669. var target = e.target;
  670. // in case the scroll event was native (i.e. scrollbars were dragged, or
  671. // the scrollTop/Left was manually modified), the bundles have old cache
  672. // values. We need to make sure that the caches are kept up to date.
  673. if (target === vScrollElem) {
  674. vScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::updateScrollPosFromDom()();
  675. } else if (target === hScrollElem) {
  676. hScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::updateScrollPosFromDom()();
  677. } else {
  678. $wnd.console.error("unexpected scroll target: "+target);
  679. }
  680. });
  681. }-*/;
  682. @Override
  683. protected native JavaScriptObject createMousewheelListenerFunction(
  684. Escalator esc)
  685. /*-{
  686. return $entry(function(e) {
  687. var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX;
  688. var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY;
  689. // Delta mode 0 is in pixels; we don't need to do anything...
  690. // A delta mode of 1 means we're scrolling by lines instead of pixels
  691. // We need to scale the number of lines by the default line height
  692. if (e.deltaMode === 1) {
  693. var brc = esc.@com.vaadin.client.widgets.Escalator::body;
  694. deltaY *= brc.@com.vaadin.client.widgets.Escalator.AbstractRowContainer::getDefaultRowHeight()();
  695. }
  696. // Other delta modes aren't supported
  697. if ((e.deltaMode !== undefined) && (e.deltaMode >= 2 || e.deltaMode < 0)) {
  698. var msg = "Unsupported wheel delta mode \"" + e.deltaMode + "\"";
  699. // Print warning message
  700. esc.@com.vaadin.client.widgets.Escalator::logWarning(*)(msg);
  701. }
  702. // IE8 has only delta y
  703. if (isNaN(deltaY)) {
  704. deltaY = -0.5*e.wheelDelta;
  705. }
  706. @com.vaadin.client.widgets.Escalator.JsniUtil::moveScrollFromEvent(*)(esc, deltaX, deltaY, e);
  707. });
  708. }-*/;
  709. /**
  710. * Recalculates the virtual viewport represented by the scrollbars, so
  711. * that the sizes of the scroll handles appear correct in the browser
  712. */
  713. public void recalculateScrollbarsForVirtualViewport() {
  714. double scrollContentHeight = body.calculateTotalRowHeight()
  715. + body.spacerContainer.getSpacerHeightsSum();
  716. double scrollContentWidth = columnConfiguration.calculateRowWidth();
  717. double tableWrapperHeight = heightOfEscalator;
  718. double tableWrapperWidth = widthOfEscalator;
  719. boolean verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
  720. + WidgetUtil.PIXEL_EPSILON - header.getHeightOfSection()
  721. - footer.getHeightOfSection();
  722. boolean horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth
  723. + WidgetUtil.PIXEL_EPSILON;
  724. // One dimension got scrollbars, but not the other. Recheck time!
  725. if (verticalScrollNeeded != horizontalScrollNeeded) {
  726. if (!verticalScrollNeeded && horizontalScrollNeeded) {
  727. verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
  728. + WidgetUtil.PIXEL_EPSILON
  729. - header.getHeightOfSection()
  730. - footer.getHeightOfSection()
  731. - horizontalScrollbar.getScrollbarThickness();
  732. } else {
  733. horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth
  734. + WidgetUtil.PIXEL_EPSILON
  735. - verticalScrollbar.getScrollbarThickness();
  736. }
  737. }
  738. // let's fix the table wrapper size, since it's now stable.
  739. if (verticalScrollNeeded) {
  740. tableWrapperWidth -= verticalScrollbar.getScrollbarThickness();
  741. tableWrapperWidth = Math.max(0, tableWrapperWidth);
  742. }
  743. if (horizontalScrollNeeded) {
  744. tableWrapperHeight -= horizontalScrollbar
  745. .getScrollbarThickness();
  746. tableWrapperHeight = Math.max(0, tableWrapperHeight);
  747. }
  748. tableWrapper.getStyle().setHeight(tableWrapperHeight, Unit.PX);
  749. tableWrapper.getStyle().setWidth(tableWrapperWidth, Unit.PX);
  750. double footerHeight = footer.getHeightOfSection();
  751. double headerHeight = header.getHeightOfSection();
  752. double vScrollbarHeight = Math.max(0,
  753. tableWrapperHeight - footerHeight - headerHeight);
  754. verticalScrollbar.setOffsetSize(vScrollbarHeight);
  755. verticalScrollbar.setScrollSize(scrollContentHeight);
  756. /*
  757. * If decreasing the amount of frozen columns, and scrolled to the
  758. * right, the scroll position might reset. So we need to remember
  759. * the scroll position, and re-apply it once the scrollbar size has
  760. * been adjusted.
  761. */
  762. double prevScrollPos = horizontalScrollbar.getScrollPos();
  763. double unfrozenPixels = columnConfiguration
  764. .getCalculatedColumnsWidth(Range.between(
  765. columnConfiguration.getFrozenColumnCount(),
  766. columnConfiguration.getColumnCount()));
  767. double frozenPixels = scrollContentWidth - unfrozenPixels;
  768. double hScrollOffsetWidth = tableWrapperWidth - frozenPixels;
  769. horizontalScrollbar.setOffsetSize(hScrollOffsetWidth);
  770. horizontalScrollbar.setScrollSize(unfrozenPixels);
  771. horizontalScrollbar.getElement().getStyle().setLeft(frozenPixels,
  772. Unit.PX);
  773. horizontalScrollbar.setScrollPos(prevScrollPos);
  774. /*
  775. * only show the scrollbar wrapper if the scrollbar itself is
  776. * visible.
  777. */
  778. if (horizontalScrollbar.showsScrollHandle()) {
  779. horizontalScrollbarDeco.getStyle().clearDisplay();
  780. } else {
  781. horizontalScrollbarDeco.getStyle().setDisplay(Display.NONE);
  782. }
  783. /*
  784. * only show corner background divs if the vertical scrollbar is
  785. * visible.
  786. */
  787. Style hCornerStyle = headerDeco.getStyle();
  788. Style fCornerStyle = footerDeco.getStyle();
  789. if (verticalScrollbar.showsScrollHandle()) {
  790. hCornerStyle.clearDisplay();
  791. fCornerStyle.clearDisplay();
  792. if (horizontalScrollbar.showsScrollHandle()) {
  793. double offset = horizontalScrollbar.getScrollbarThickness();
  794. fCornerStyle.setBottom(offset, Unit.PX);
  795. } else {
  796. fCornerStyle.clearBottom();
  797. }
  798. } else {
  799. hCornerStyle.setDisplay(Display.NONE);
  800. fCornerStyle.setDisplay(Display.NONE);
  801. }
  802. }
  803. /**
  804. * Logical scrolling event handler for the entire widget.
  805. */
  806. public void onScroll() {
  807. final double scrollTop = verticalScrollbar.getScrollPos();
  808. final double scrollLeft = horizontalScrollbar.getScrollPos();
  809. if (lastScrollLeft != scrollLeft) {
  810. for (int i = 0; i < columnConfiguration.frozenColumns; i++) {
  811. header.updateFreezePosition(i, scrollLeft);
  812. body.updateFreezePosition(i, scrollLeft);
  813. footer.updateFreezePosition(i, scrollLeft);
  814. }
  815. position.set(headElem, -scrollLeft, 0);
  816. position.set(footElem, -scrollLeft, 0);
  817. lastScrollLeft = scrollLeft;
  818. }
  819. body.setBodyScrollPosition(scrollLeft, scrollTop);
  820. lastScrollTop = scrollTop;
  821. body.updateEscalatorRowsOnScroll();
  822. body.spacerContainer.updateSpacerDecosVisibility();
  823. /*
  824. * TODO [[optimize]]: Might avoid a reflow by first calculating new
  825. * scrolltop and scrolleft, then doing the escalator magic based on
  826. * those numbers and only updating the positions after that.
  827. */
  828. }
  829. public native void attachScrollListener(Element element)
  830. /*
  831. * Attaching events with JSNI instead of the GWT event mechanism because
  832. * GWT didn't provide enough details in events, or triggering the event
  833. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  834. * and skill, it could be done with better success. JavaScript overlay
  835. * types might work. This might also get rid of the JsniWorkaround
  836. * class.
  837. */
  838. /*-{
  839. if (element.addEventListener) {
  840. element.addEventListener("scroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
  841. } else {
  842. element.attachEvent("onscroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
  843. }
  844. }-*/;
  845. public native void detachScrollListener(Element element)
  846. /*
  847. * Detaching events with JSNI instead of the GWT event mechanism because
  848. * GWT didn't provide enough details in events, or triggering the event
  849. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  850. * and skill, it could be done with better success. JavaScript overlay
  851. * types might work. This might also get rid of the JsniWorkaround
  852. * class.
  853. */
  854. /*-{
  855. if (element.addEventListener) {
  856. element.removeEventListener("scroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
  857. } else {
  858. element.detachEvent("onscroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
  859. }
  860. }-*/;
  861. public native void attachMousewheelListener(Element element)
  862. /*
  863. * Attaching events with JSNI instead of the GWT event mechanism because
  864. * GWT didn't provide enough details in events, or triggering the event
  865. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  866. * and skill, it could be done with better success. JavaScript overlay
  867. * types might work. This might also get rid of the JsniWorkaround
  868. * class.
  869. */
  870. /*-{
  871. // firefox likes "wheel", while others use "mousewheel"
  872. var eventName = 'onmousewheel' in element ? 'mousewheel' : 'wheel';
  873. element.addEventListener(eventName, this.@com.vaadin.client.widgets.JsniWorkaround::mousewheelListenerFunction);
  874. }-*/;
  875. public native void detachMousewheelListener(Element element)
  876. /*
  877. * Detaching events with JSNI instead of the GWT event mechanism because
  878. * GWT didn't provide enough details in events, or triggering the event
  879. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  880. * and skill, it could be done with better success. JavaScript overlay
  881. * types might work. This might also get rid of the JsniWorkaround
  882. * class.
  883. */
  884. /*-{
  885. // firefox likes "wheel", while others use "mousewheel"
  886. var eventName = element.onwheel===undefined?"mousewheel":"wheel";
  887. element.removeEventListener(eventName, this.@com.vaadin.client.widgets.JsniWorkaround::mousewheelListenerFunction);
  888. }-*/;
  889. public native void attachTouchListeners(Element element)
  890. /*
  891. * Detaching events with JSNI instead of the GWT event mechanism because
  892. * GWT didn't provide enough details in events, or triggering the event
  893. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  894. * and skill, it could be done with better success. JavaScript overlay
  895. * types might work. This might also get rid of the JsniWorkaround
  896. * class.
  897. */
  898. /*-{
  899. element.addEventListener("touchstart", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
  900. element.addEventListener("touchmove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
  901. element.addEventListener("touchend", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  902. element.addEventListener("touchcancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  903. }-*/;
  904. public native void detachTouchListeners(Element element)
  905. /*
  906. * Detaching events with JSNI instead of the GWT event mechanism because
  907. * GWT didn't provide enough details in events, or triggering the event
  908. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  909. * and skill, it could be done with better success. JavaScript overlay
  910. * types might work. This might also get rid of the JsniWorkaround
  911. * class.
  912. */
  913. /*-{
  914. element.removeEventListener("touchstart", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
  915. element.removeEventListener("touchmove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
  916. element.removeEventListener("touchend", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  917. element.removeEventListener("touchcancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  918. }-*/;
  919. /**
  920. * Using pointerdown, pointermove, pointerup, and pointercancel for IE11
  921. * and Edge instead of touch* listeners (#18737)
  922. *
  923. * @param element
  924. */
  925. public native void attachPointerEventListeners(Element element)
  926. /*
  927. * Attaching events with JSNI instead of the GWT event mechanism because
  928. * GWT didn't provide enough details in events, or triggering the event
  929. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  930. * and skill, it could be done with better success. JavaScript overlay
  931. * types might work. This might also get rid of the JsniWorkaround
  932. * class.
  933. */
  934. /*-{
  935. element.addEventListener("pointerdown", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
  936. element.addEventListener("pointermove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
  937. element.addEventListener("pointerup", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  938. element.addEventListener("pointercancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  939. }-*/;
  940. /**
  941. * Using pointerdown, pointermove, pointerup, and pointercancel for IE11
  942. * and Edge instead of touch* listeners (#18737)
  943. *
  944. * @param element
  945. */
  946. public native void detachPointerEventListeners(Element element)
  947. /*
  948. * Detaching events with JSNI instead of the GWT event mechanism because
  949. * GWT didn't provide enough details in events, or triggering the event
  950. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  951. * and skill, it could be done with better success. JavaScript overlay
  952. * types might work. This might also get rid of the JsniWorkaround
  953. * class.
  954. */
  955. /*-{
  956. element.removeEventListener("pointerdown", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
  957. element.removeEventListener("pointermove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
  958. element.removeEventListener("pointerup", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  959. element.removeEventListener("pointercancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  960. }-*/;
  961. public void scrollToColumn(final int columnIndex,
  962. final ScrollDestination destination, final int padding) {
  963. assert columnIndex >= columnConfiguration.frozenColumns : "Can't scroll to a frozen column";
  964. /*
  965. * To cope with frozen columns, we just pretend those columns are
  966. * not there at all when calculating the position of the target
  967. * column and the boundaries of the viewport. The resulting
  968. * scrollLeft will be correct without compensation since the DOM
  969. * structure effectively means that scrollLeft also ignores the
  970. * frozen columns.
  971. */
  972. final double frozenPixels = columnConfiguration
  973. .getCalculatedColumnsWidth(Range.withLength(0,
  974. columnConfiguration.frozenColumns));
  975. final double targetStartPx = columnConfiguration
  976. .getCalculatedColumnsWidth(Range.withLength(0, columnIndex))
  977. - frozenPixels;
  978. final double targetEndPx = targetStartPx
  979. + columnConfiguration.getColumnWidthActual(columnIndex);
  980. final double viewportStartPx = getScrollLeft();
  981. double viewportEndPx = viewportStartPx
  982. + getBoundingWidth(getElement()) - frozenPixels;
  983. if (verticalScrollbar.showsScrollHandle()) {
  984. viewportEndPx -= WidgetUtil.getNativeScrollbarSize();
  985. }
  986. final double scrollLeft = getScrollPos(destination, targetStartPx,
  987. targetEndPx, viewportStartPx, viewportEndPx, padding);
  988. /*
  989. * note that it doesn't matter if the scroll would go beyond the
  990. * content, since the browser will adjust for that, and everything
  991. * fall into line accordingly.
  992. */
  993. setScrollLeft(scrollLeft);
  994. }
  995. public void scrollToRow(final int rowIndex,
  996. final ScrollDestination destination, final double padding) {
  997. final double targetStartPx = (body.getDefaultRowHeight() * rowIndex)
  998. + body.spacerContainer
  999. .getSpacerHeightsSumUntilIndex(rowIndex);
  1000. final double targetEndPx = targetStartPx
  1001. + body.getDefaultRowHeight();
  1002. final double viewportStartPx = getScrollTop();
  1003. final double viewportEndPx = viewportStartPx
  1004. + body.getHeightOfSection();
  1005. final double scrollTop = getScrollPos(destination, targetStartPx,
  1006. targetEndPx, viewportStartPx, viewportEndPx, padding);
  1007. /*
  1008. * note that it doesn't matter if the scroll would go beyond the
  1009. * content, since the browser will adjust for that, and everything
  1010. * falls into line accordingly.
  1011. */
  1012. setScrollTop(scrollTop);
  1013. }
  1014. }
  1015. /**
  1016. * Helper class that helps to implement the WAI-ARIA functionality for the
  1017. * Grid and TreeGrid component.
  1018. * <p>
  1019. * The following WAI-ARIA attributes are added through this class:
  1020. *
  1021. * <ul>
  1022. * <li>aria-rowcount (since 8.2)</li>
  1023. * <li>roles provided by {@link AriaGridRole} (since 8.2)</li>
  1024. * </ul>
  1025. *
  1026. * @since 8.2
  1027. */
  1028. public class AriaGridHelper {
  1029. /**
  1030. * This field contains the total number of rows from the grid including
  1031. * rows from thead, tbody and tfoot.
  1032. *
  1033. * @since 8.2
  1034. */
  1035. private int allRows;
  1036. /**
  1037. * Adds the given numberOfRows to allRows and calls
  1038. * {@link #updateAriaRowCount()}.
  1039. *
  1040. * @param numberOfRows
  1041. * number of rows that were added to the grid
  1042. *
  1043. * @since 8.2
  1044. */
  1045. public void addRows(int numberOfRows) {
  1046. allRows += numberOfRows;
  1047. updateAriaRowCount();
  1048. }
  1049. /**
  1050. * Removes the given numberOfRows from allRows and calls
  1051. * {@link #updateAriaRowCount()}.
  1052. *
  1053. * @param numberOfRows
  1054. * number of rows that were removed from the grid
  1055. *
  1056. * @since 8.2
  1057. */
  1058. public void removeRows(int numberOfRows) {
  1059. allRows -= numberOfRows;
  1060. updateAriaRowCount();
  1061. }
  1062. /**
  1063. * Sets the aria-rowcount attribute with the current value of
  1064. * {@link AriaGridHelper#allRows} if the grid is attached and
  1065. * {@link AriaGridHelper#allRows} > 0.
  1066. *
  1067. * @since 8.2
  1068. */
  1069. public void updateAriaRowCount() {
  1070. if (!isAttached() || 0 > allRows) {
  1071. return;
  1072. }
  1073. getTable().setAttribute("aria-rowcount", String.valueOf(allRows));
  1074. }
  1075. /**
  1076. * Sets the {@code role} attribute to the given element.
  1077. *
  1078. * @param element
  1079. * element that should get the role attribute
  1080. * @param role
  1081. * role to be added
  1082. *
  1083. * @since 8.2
  1084. */
  1085. public void updateRole(final Element element, AriaGridRole role) {
  1086. element.setAttribute("role", role.getName());
  1087. }
  1088. }
  1089. /**
  1090. * Holds the currently used aria roles within the grid for rows and cells.
  1091. *
  1092. * @since 8.2
  1093. */
  1094. public enum AriaGridRole {
  1095. ROW("row"), ROWHEADER("rowheader"), ROWGROUP("rowgroup"), GRIDCELL(
  1096. "gridcell"), COLUMNHEADER("columnheader");
  1097. private final String name;
  1098. AriaGridRole(String name) {
  1099. this.name = name;
  1100. }
  1101. /**
  1102. * Return the name of the {@link AriaGridRole}.
  1103. *
  1104. * @return String name to be used as role attribute
  1105. */
  1106. public String getName() {
  1107. return name;
  1108. }
  1109. }
  1110. public abstract class AbstractRowContainer implements RowContainer {
  1111. private EscalatorUpdater updater = EscalatorUpdater.NULL;
  1112. private int rows;
  1113. /**
  1114. * The table section element ({@code <thead>}, {@code <tbody>} or
  1115. * {@code <tfoot>}) the rows (i.e. <code>&lt;tr&gt;</code> tags) are
  1116. * contained in.
  1117. */
  1118. protected final TableSectionElement root;
  1119. /**
  1120. * The primary style name of the escalator. Most commonly provided by
  1121. * Escalator as "v-escalator".
  1122. */
  1123. private String primaryStyleName = null;
  1124. private boolean defaultRowHeightShouldBeAutodetected = true;
  1125. private double defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT;
  1126. private boolean initialColumnSizesCalculated = false;
  1127. private boolean autodetectingRowHeightLater = false;
  1128. public AbstractRowContainer(
  1129. final TableSectionElement rowContainerElement) {
  1130. root = rowContainerElement;
  1131. ariaGridHelper.updateRole(root, AriaGridRole.ROWGROUP);
  1132. }
  1133. @Override
  1134. public TableSectionElement getElement() {
  1135. return root;
  1136. }
  1137. /**
  1138. * Gets the tag name of an element to represent a cell in a row.
  1139. * <p>
  1140. * Usually {@code "th"} or {@code "td"}.
  1141. * <p>
  1142. * <em>Note:</em> To actually <em>create</em> such an element, use
  1143. * {@link #createCellElement(double)} instead.
  1144. *
  1145. * @return the tag name for the element to represent cells as
  1146. * @see #createCellElement(double)
  1147. */
  1148. protected abstract String getCellElementTagName();
  1149. /**
  1150. * Gets the role attribute of an element to represent a cell in a row.
  1151. * <p>
  1152. * Usually {@link AriaGridRole#GRIDCELL} except for a cell in the
  1153. * header.
  1154. *
  1155. * @return the role attribute for the element to represent cells
  1156. *
  1157. * @since 8.2
  1158. */
  1159. protected AriaGridRole getCellElementRole() {
  1160. return AriaGridRole.GRIDCELL;
  1161. }
  1162. /**
  1163. * Gets the role attribute of an element to represent a row in a grid.
  1164. * <p>
  1165. * Usually {@link AriaGridRole#ROW} except for a row in the header.
  1166. *
  1167. * @return the role attribute for the element to represent rows
  1168. *
  1169. * @since 8.2
  1170. */
  1171. protected AriaGridRole getRowElementRole() {
  1172. return AriaGridRole.ROW;
  1173. }
  1174. @Override
  1175. public EscalatorUpdater getEscalatorUpdater() {
  1176. return updater;
  1177. }
  1178. /**
  1179. * {@inheritDoc}
  1180. * <p>
  1181. * <em>Implementation detail:</em> This method does no DOM modifications
  1182. * (i.e. is very cheap to call) if there is no data for rows or columns
  1183. * when this method is called.
  1184. *
  1185. * @see #hasColumnAndRowData()
  1186. */
  1187. @Override
  1188. public void setEscalatorUpdater(
  1189. final EscalatorUpdater escalatorUpdater) {
  1190. if (escalatorUpdater == null) {
  1191. throw new IllegalArgumentException(
  1192. "escalator updater cannot be null");
  1193. }
  1194. updater = escalatorUpdater;
  1195. if (hasColumnAndRowData() && getRowCount() > 0) {
  1196. refreshRows(0, getRowCount());
  1197. }
  1198. }
  1199. /**
  1200. * {@inheritDoc}
  1201. * <p>
  1202. * <em>Implementation detail:</em> This method does no DOM modifications
  1203. * (i.e. is very cheap to call) if there are no rows in the DOM when
  1204. * this method is called.
  1205. *
  1206. * @see #hasSomethingInDom()
  1207. */
  1208. @Override
  1209. public void removeRows(final int index, final int numberOfRows) {
  1210. assertArgumentsAreValidAndWithinRange(index, numberOfRows);
  1211. rows -= numberOfRows;
  1212. ariaGridHelper.removeRows(numberOfRows);
  1213. if (!isAttached()) {
  1214. return;
  1215. }
  1216. if (hasSomethingInDom()) {
  1217. paintRemoveRows(index, numberOfRows);
  1218. }
  1219. }
  1220. /**
  1221. * Removes those row elements from the DOM that correspond to the given
  1222. * range of logical indices. This may be fewer than {@code numberOfRows}
  1223. * , even zero, if not all the removed rows are actually visible.
  1224. * <p>
  1225. * The implementation must call
  1226. * {@link #paintRemoveRow(TableRowElement, int)} for each row that is
  1227. * removed from the DOM.
  1228. *
  1229. * @param index
  1230. * the logical index of the first removed row
  1231. * @param numberOfRows
  1232. * number of logical rows to remove
  1233. */
  1234. protected abstract void paintRemoveRows(final int index,
  1235. final int numberOfRows);
  1236. /**
  1237. * Removes a row element from the DOM, invoking
  1238. * {@link #getEscalatorUpdater()}
  1239. * {@link EscalatorUpdater#preDetach(Row, Iterable) preDetach} and
  1240. * {@link EscalatorUpdater#postDetach(Row, Iterable) postDetach} before
  1241. * and after removing the row, respectively.
  1242. * <p>
  1243. * This method must be called for each removed DOM row by any
  1244. * {@link #paintRemoveRows(int, int)} implementation.
  1245. *
  1246. * @param tr
  1247. * the row element to remove.
  1248. */
  1249. protected void paintRemoveRow(final TableRowElement tr,
  1250. final int logicalRowIndex) {
  1251. flyweightRow.setup(tr, logicalRowIndex,
  1252. columnConfiguration.getCalculatedColumnWidths());
  1253. getEscalatorUpdater().preDetach(flyweightRow,
  1254. flyweightRow.getCells());
  1255. tr.removeFromParent();
  1256. getEscalatorUpdater().postDetach(flyweightRow,
  1257. flyweightRow.getCells());
  1258. /*
  1259. * the "assert" guarantees that this code is run only during
  1260. * development/debugging.
  1261. */
  1262. assert flyweightRow.teardown();
  1263. }
  1264. protected void assertArgumentsAreValidAndWithinRange(final int index,
  1265. final int numberOfRows)
  1266. throws IllegalArgumentException, IndexOutOfBoundsException {
  1267. if (numberOfRows < 1) {
  1268. throw new IllegalArgumentException(
  1269. "Number of rows must be 1 or greater (was "
  1270. + numberOfRows + ")");
  1271. }
  1272. if (index < 0 || index + numberOfRows > getRowCount()) {
  1273. throw new IndexOutOfBoundsException("The given " + "row range ("
  1274. + index + ".." + (index + numberOfRows)
  1275. + ") was outside of the current number of rows ("
  1276. + getRowCount() + ")");
  1277. }
  1278. }
  1279. @Override
  1280. public int getRowCount() {
  1281. return rows;
  1282. }
  1283. /**
  1284. * This method calculates the current row count directly from the DOM.
  1285. * <p>
  1286. * While Escalator is stable, this value should equal to
  1287. * {@link #getRowCount()}, but while row counts are being updated, these
  1288. * two values might differ for a short while.
  1289. * <p>
  1290. * Any extra content, such as spacers for the body, should not be
  1291. * included in this count.
  1292. *
  1293. * @since 7.5.0
  1294. *
  1295. * @return the actual DOM count of rows
  1296. */
  1297. public abstract int getDomRowCount();
  1298. /**
  1299. * {@inheritDoc}
  1300. * <p>
  1301. * <em>Implementation detail:</em> This method does no DOM modifications
  1302. * (i.e. is very cheap to call) if there is no data for columns when
  1303. * this method is called.
  1304. *
  1305. * @see #hasColumnAndRowData()
  1306. */
  1307. @Override
  1308. public void insertRows(final int index, final int numberOfRows) {
  1309. if (index < 0 || index > getRowCount()) {
  1310. throw new IndexOutOfBoundsException("The given index (" + index
  1311. + ") was outside of the current number of rows (0.."
  1312. + getRowCount() + ")");
  1313. }
  1314. if (numberOfRows < 1) {
  1315. throw new IllegalArgumentException(
  1316. "Number of rows must be 1 or greater (was "
  1317. + numberOfRows + ")");
  1318. }
  1319. rows += numberOfRows;
  1320. ariaGridHelper.addRows(numberOfRows);
  1321. /*
  1322. * only add items in the DOM if the widget itself is attached to the
  1323. * DOM. We can't calculate sizes otherwise.
  1324. */
  1325. if (isAttached()) {
  1326. paintInsertRows(index, numberOfRows);
  1327. /*
  1328. * We are inserting the first rows in this container. We
  1329. * potentially need to set the widths for the cells for the
  1330. * first time.
  1331. */
  1332. if (rows == numberOfRows) {
  1333. Scheduler.get().scheduleFinally(() -> {
  1334. if (initialColumnSizesCalculated) {
  1335. return;
  1336. }
  1337. initialColumnSizesCalculated = true;
  1338. Map<Integer, Double> colWidths = new HashMap<>();
  1339. for (int i = 0; i < getColumnConfiguration()
  1340. .getColumnCount(); i++) {
  1341. Double width = Double.valueOf(
  1342. getColumnConfiguration().getColumnWidth(i));
  1343. Integer col = Integer.valueOf(i);
  1344. colWidths.put(col, width);
  1345. }
  1346. getColumnConfiguration().setColumnWidths(colWidths);
  1347. });
  1348. }
  1349. }
  1350. }
  1351. /**
  1352. * Actually add rows into the DOM, now that everything can be
  1353. * calculated.
  1354. *
  1355. * @param visualIndex
  1356. * the DOM index to add rows into
  1357. * @param numberOfRows
  1358. * the number of rows to insert
  1359. */
  1360. protected abstract void paintInsertRows(final int visualIndex,
  1361. final int numberOfRows);
  1362. protected List<TableRowElement> paintInsertStaticRows(
  1363. final int visualIndex, final int numberOfRows) {
  1364. assert isAttached() : "Can't paint rows if Escalator is not attached";
  1365. final List<TableRowElement> addedRows = new ArrayList<>();
  1366. if (numberOfRows < 1) {
  1367. return addedRows;
  1368. }
  1369. Node referenceRow;
  1370. if (root.getChildCount() != 0 && visualIndex != 0) {
  1371. // get the row node we're inserting stuff after
  1372. referenceRow = root.getChild(visualIndex - 1);
  1373. } else {
  1374. // index is 0, so just prepend.
  1375. referenceRow = null;
  1376. }
  1377. for (int row = visualIndex; row < visualIndex
  1378. + numberOfRows; row++) {
  1379. final TableRowElement tr = TableRowElement.as(DOM.createTR());
  1380. addedRows.add(tr);
  1381. tr.addClassName(getStylePrimaryName() + "-row");
  1382. ariaGridHelper.updateRole(tr, getRowElementRole());
  1383. for (int col = 0; col < columnConfiguration
  1384. .getColumnCount(); col++) {
  1385. final double colWidth = columnConfiguration
  1386. .getColumnWidthActual(col);
  1387. final TableCellElement cellElem = createCellElement(
  1388. colWidth);
  1389. tr.appendChild(cellElem);
  1390. // Set stylename and position if new cell is frozen
  1391. if (col < columnConfiguration.frozenColumns) {
  1392. cellElem.addClassName("frozen");
  1393. position.set(cellElem, scroller.lastScrollLeft, 0);
  1394. }
  1395. if (columnConfiguration.frozenColumns > 0
  1396. && col == columnConfiguration.frozenColumns - 1) {
  1397. cellElem.addClassName("last-frozen");
  1398. }
  1399. }
  1400. referenceRow = paintInsertRow(referenceRow, tr, row);
  1401. }
  1402. reapplyRowWidths();
  1403. recalculateSectionHeight();
  1404. return addedRows;
  1405. }
  1406. /**
  1407. * Inserts a single row into the DOM, invoking
  1408. * {@link #getEscalatorUpdater()}
  1409. * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and
  1410. * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before
  1411. * and after inserting the row, respectively. The row should have its
  1412. * cells already inserted.
  1413. *
  1414. * @param referenceRow
  1415. * the row after which to insert or null if insert as first
  1416. * @param tr
  1417. * the row to be inserted
  1418. * @param logicalRowIndex
  1419. * the logical index of the inserted row
  1420. * @return the inserted row to be used as the new reference
  1421. */
  1422. protected Node paintInsertRow(Node referenceRow,
  1423. final TableRowElement tr, int logicalRowIndex) {
  1424. flyweightRow.setup(tr, logicalRowIndex,
  1425. columnConfiguration.getCalculatedColumnWidths());
  1426. getEscalatorUpdater().preAttach(flyweightRow,
  1427. flyweightRow.getCells());
  1428. referenceRow = insertAfterReferenceAndUpdateIt(root, tr,
  1429. referenceRow);
  1430. getEscalatorUpdater().postAttach(flyweightRow,
  1431. flyweightRow.getCells());
  1432. updater.update(flyweightRow, flyweightRow.getCells());
  1433. /*
  1434. * the "assert" guarantees that this code is run only during
  1435. * development/debugging.
  1436. */
  1437. assert flyweightRow.teardown();
  1438. return referenceRow;
  1439. }
  1440. private Node insertAfterReferenceAndUpdateIt(final Element parent,
  1441. final Element elem, final Node referenceNode) {
  1442. if (referenceNode != null) {
  1443. parent.insertAfter(elem, referenceNode);
  1444. } else {
  1445. /*
  1446. * referencenode being null means we have offset 0, i.e. make it
  1447. * the first row
  1448. */
  1449. /*
  1450. * TODO [[optimize]]: Is insertFirst or append faster for an
  1451. * empty root?
  1452. */
  1453. parent.insertFirst(elem);
  1454. }
  1455. return elem;
  1456. }
  1457. protected abstract void recalculateSectionHeight();
  1458. /**
  1459. * Returns the height of all rows in the row container.
  1460. */
  1461. protected double calculateTotalRowHeight() {
  1462. return getDefaultRowHeight() * getRowCount();
  1463. }
  1464. /**
  1465. * {@inheritDoc}
  1466. * <p>
  1467. * <em>Implementation detail:</em> This method does no DOM modifications
  1468. * (i.e. is very cheap to call) if there is no data for columns when
  1469. * this method is called.
  1470. *
  1471. * @see #hasColumnAndRowData()
  1472. */
  1473. @Override
  1474. // overridden because of JavaDoc
  1475. public void refreshRows(final int index, final int numberOfRows) {
  1476. Range rowRange = Range.withLength(index, numberOfRows);
  1477. Range colRange = Range.withLength(0,
  1478. getColumnConfiguration().getColumnCount());
  1479. refreshCells(rowRange, colRange);
  1480. }
  1481. protected abstract void refreshCells(Range logicalRowRange,
  1482. Range colRange);
  1483. void refreshRow(TableRowElement tr, int logicalRowIndex) {
  1484. refreshRow(tr, logicalRowIndex, Range.withLength(0,
  1485. getColumnConfiguration().getColumnCount()));
  1486. }
  1487. void refreshRow(final TableRowElement tr, final int logicalRowIndex,
  1488. Range colRange) {
  1489. flyweightRow.setup(tr, logicalRowIndex,
  1490. columnConfiguration.getCalculatedColumnWidths());
  1491. Iterable<FlyweightCell> cellsToUpdate = flyweightRow
  1492. .getCells(colRange.getStart(), colRange.length());
  1493. updater.update(flyweightRow, cellsToUpdate);
  1494. /*
  1495. * the "assert" guarantees that this code is run only during
  1496. * development/debugging.
  1497. */
  1498. assert flyweightRow.teardown();
  1499. }
  1500. /**
  1501. * Create and setup an empty cell element.
  1502. *
  1503. * @param width
  1504. * the width of the cell, in pixels
  1505. *
  1506. * @return a set-up empty cell element
  1507. */
  1508. public TableCellElement createCellElement(final double width) {
  1509. final TableCellElement cellElem = TableCellElement
  1510. .as(DOM.createElement(getCellElementTagName()));
  1511. final double height = getDefaultRowHeight();
  1512. assert height >= 0 : "defaultRowHeight was negative. There's a setter leak somewhere.";
  1513. cellElem.getStyle().setHeight(height, Unit.PX);
  1514. if (width >= 0) {
  1515. cellElem.getStyle().setWidth(width, Unit.PX);
  1516. }
  1517. cellElem.addClassName(getStylePrimaryName() + "-cell");
  1518. ariaGridHelper.updateRole(cellElem, getCellElementRole());
  1519. return cellElem;
  1520. }
  1521. @Override
  1522. public TableRowElement getRowElement(int index) {
  1523. return getTrByVisualIndex(index);
  1524. }
  1525. /**
  1526. * Gets the child element that is visually at a certain index.
  1527. *
  1528. * @param index
  1529. * the index of the element to retrieve
  1530. * @return the element at position {@code index}
  1531. * @throws IndexOutOfBoundsException
  1532. * if {@code index} is not valid within {@link #root}
  1533. */
  1534. protected abstract TableRowElement getTrByVisualIndex(int index)
  1535. throws IndexOutOfBoundsException;
  1536. protected void paintRemoveColumns(final int offset,
  1537. final int numberOfColumns) {
  1538. for (int i = 0; i < getDomRowCount(); i++) {
  1539. TableRowElement row = getTrByVisualIndex(i);
  1540. flyweightRow.setup(row, i,
  1541. columnConfiguration.getCalculatedColumnWidths());
  1542. Iterable<FlyweightCell> attachedCells = flyweightRow
  1543. .getCells(offset, numberOfColumns);
  1544. getEscalatorUpdater().preDetach(flyweightRow, attachedCells);
  1545. for (int j = 0; j < numberOfColumns; j++) {
  1546. row.getCells().getItem(offset).removeFromParent();
  1547. }
  1548. Iterable<FlyweightCell> detachedCells = flyweightRow
  1549. .getUnattachedCells(offset, numberOfColumns);
  1550. getEscalatorUpdater().postDetach(flyweightRow, detachedCells);
  1551. assert flyweightRow.teardown();
  1552. }
  1553. }
  1554. protected void paintInsertColumns(final int offset,
  1555. final int numberOfColumns, boolean frozen) {
  1556. for (int row = 0; row < getDomRowCount(); row++) {
  1557. final TableRowElement tr = getTrByVisualIndex(row);
  1558. int logicalRowIndex = getLogicalRowIndex(tr);
  1559. paintInsertCells(tr, logicalRowIndex, offset, numberOfColumns);
  1560. }
  1561. reapplyRowWidths();
  1562. if (frozen) {
  1563. for (int col = offset; col < offset + numberOfColumns; col++) {
  1564. setColumnFrozen(col, true);
  1565. }
  1566. }
  1567. }
  1568. /**
  1569. * Inserts new cell elements into a single row element, invoking
  1570. * {@link #getEscalatorUpdater()}
  1571. * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and
  1572. * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before
  1573. * and after inserting the cells, respectively.
  1574. * <p>
  1575. * Precondition: The row must be already attached to the DOM and the
  1576. * FlyweightCell instances corresponding to the new columns added to
  1577. * {@code flyweightRow}.
  1578. *
  1579. * @param tr
  1580. * the row in which to insert the cells
  1581. * @param logicalRowIndex
  1582. * the index of the row
  1583. * @param offset
  1584. * the index of the first cell
  1585. * @param numberOfCells
  1586. * the number of cells to insert
  1587. */
  1588. private void paintInsertCells(final TableRowElement tr,
  1589. int logicalRowIndex, final int offset,
  1590. final int numberOfCells) {
  1591. assert root.isOrHasChild(
  1592. tr) : "The row must be attached to the document";
  1593. flyweightRow.setup(tr, logicalRowIndex,
  1594. columnConfiguration.getCalculatedColumnWidths());
  1595. Iterable<FlyweightCell> cells = flyweightRow
  1596. .getUnattachedCells(offset, numberOfCells);
  1597. for (FlyweightCell cell : cells) {
  1598. final double colWidth = columnConfiguration
  1599. .getColumnWidthActual(cell.getColumn());
  1600. final TableCellElement cellElem = createCellElement(colWidth);
  1601. cell.setElement(cellElem);
  1602. }
  1603. getEscalatorUpdater().preAttach(flyweightRow, cells);
  1604. Node referenceCell;
  1605. if (offset != 0) {
  1606. referenceCell = tr.getChild(offset - 1);
  1607. } else {
  1608. referenceCell = null;
  1609. }
  1610. for (FlyweightCell cell : cells) {
  1611. referenceCell = insertAfterReferenceAndUpdateIt(tr,
  1612. cell.getElement(), referenceCell);
  1613. }
  1614. getEscalatorUpdater().postAttach(flyweightRow, cells);
  1615. getEscalatorUpdater().update(flyweightRow, cells);
  1616. assert flyweightRow.teardown();
  1617. }
  1618. public void setColumnFrozen(int column, boolean frozen) {
  1619. toggleFrozenColumnClass(column, frozen, "frozen");
  1620. if (frozen) {
  1621. updateFreezePosition(column, scroller.lastScrollLeft);
  1622. }
  1623. }
  1624. private void toggleFrozenColumnClass(int column, boolean frozen,
  1625. String className) {
  1626. final NodeList<TableRowElement> childRows = root.getRows();
  1627. for (int row = 0; row < childRows.getLength(); row++) {
  1628. final TableRowElement tr = childRows.getItem(row);
  1629. if (!rowCanBeFrozen(tr)) {
  1630. continue;
  1631. }
  1632. TableCellElement cell = tr.getCells().getItem(column);
  1633. if (frozen) {
  1634. cell.addClassName(className);
  1635. } else {
  1636. cell.removeClassName(className);
  1637. position.reset(cell);
  1638. }
  1639. }
  1640. }
  1641. public void setColumnLastFrozen(int column, boolean lastFrozen) {
  1642. toggleFrozenColumnClass(column, lastFrozen, "last-frozen");
  1643. }
  1644. public void updateFreezePosition(int column, double scrollLeft) {
  1645. final NodeList<TableRowElement> childRows = root.getRows();
  1646. for (int row = 0; row < childRows.getLength(); row++) {
  1647. final TableRowElement tr = childRows.getItem(row);
  1648. if (rowCanBeFrozen(tr)) {
  1649. TableCellElement cell = tr.getCells().getItem(column);
  1650. position.set(cell, scrollLeft, 0);
  1651. }
  1652. }
  1653. }
  1654. /**
  1655. * Checks whether a row is an element, or contains such elements, that
  1656. * can be frozen.
  1657. * <p>
  1658. * In practice, this applies for all header and footer rows. For body
  1659. * rows, it applies for all rows except spacer rows.
  1660. *
  1661. * @since 7.5.0
  1662. *
  1663. * @param tr
  1664. * the row element to check whether it, or any of its its
  1665. * descendants can be frozen
  1666. * @return <code>true</code> if the given element, or any of its
  1667. * descendants, can be frozen
  1668. */
  1669. protected abstract boolean rowCanBeFrozen(TableRowElement tr);
  1670. /**
  1671. * Iterates through all the cells in a column and returns the width of
  1672. * the widest element in this RowContainer.
  1673. *
  1674. * @param index
  1675. * the index of the column to inspect
  1676. * @return the pixel width of the widest element in the indicated column
  1677. */
  1678. public double calculateMaxColWidth(int index) {
  1679. TableRowElement row = TableRowElement
  1680. .as(root.getFirstChildElement());
  1681. double maxWidth = 0;
  1682. while (row != null) {
  1683. final TableCellElement cell = row.getCells().getItem(index);
  1684. final boolean isVisible = !cell.getStyle().getDisplay()
  1685. .equals(Display.NONE.getCssName());
  1686. if (isVisible) {
  1687. maxWidth = Math.max(maxWidth, getBoundingWidth(cell));
  1688. }
  1689. row = TableRowElement.as(row.getNextSiblingElement());
  1690. }
  1691. return maxWidth;
  1692. }
  1693. /**
  1694. * Reapplies all the cells' widths according to the calculated widths in
  1695. * the column configuration.
  1696. */
  1697. public void reapplyColumnWidths() {
  1698. Element row = root.getFirstChildElement();
  1699. while (row != null) {
  1700. // Only handle non-spacer rows
  1701. if (!body.spacerContainer.isSpacer(row)) {
  1702. Element cell = row.getFirstChildElement();
  1703. int columnIndex = 0;
  1704. while (cell != null) {
  1705. final double width = getCalculatedColumnWidthWithColspan(
  1706. cell, columnIndex);
  1707. /*
  1708. * TODO Should Escalator implement ProvidesResize at
  1709. * some point, this is where we need to do that.
  1710. */
  1711. cell.getStyle().setWidth(width, Unit.PX);
  1712. cell = cell.getNextSiblingElement();
  1713. columnIndex++;
  1714. }
  1715. }
  1716. row = row.getNextSiblingElement();
  1717. }
  1718. reapplyRowWidths();
  1719. }
  1720. private double getCalculatedColumnWidthWithColspan(final Element cell,
  1721. final int columnIndex) {
  1722. final int colspan = cell.getPropertyInt(FlyweightCell.COLSPAN_ATTR);
  1723. Range spannedColumns = Range.withLength(columnIndex, colspan);
  1724. /*
  1725. * Since browsers don't explode with overflowing colspans, escalator
  1726. * shouldn't either.
  1727. */
  1728. if (spannedColumns.getEnd() > columnConfiguration
  1729. .getColumnCount()) {
  1730. spannedColumns = Range.between(columnIndex,
  1731. columnConfiguration.getColumnCount());
  1732. }
  1733. return columnConfiguration
  1734. .getCalculatedColumnsWidth(spannedColumns);
  1735. }
  1736. /**
  1737. * Applies the total length of the columns to each row element.
  1738. * <p>
  1739. * <em>Note:</em> In contrast to {@link #reapplyColumnWidths()}, this
  1740. * method only modifies the width of the {@code
  1741. *
  1742. <tr>
  1743. * } element, not the cells within.
  1744. */
  1745. protected void reapplyRowWidths() {
  1746. double rowWidth = columnConfiguration.calculateRowWidth();
  1747. if (rowWidth < 0) {
  1748. return;
  1749. }
  1750. Element row = root.getFirstChildElement();
  1751. while (row != null) {
  1752. // IF there is a rounding error when summing the columns, we
  1753. // need to round the tr width up to ensure that columns fit and
  1754. // do not wrap
  1755. // E.g.122.95+123.25+103.75+209.25+83.52+88.57+263.45+131.21+126.85+113.13=1365.9299999999998
  1756. // For this we must set 1365.93 or the last column will wrap
  1757. row.getStyle().setWidth(WidgetUtil.roundSizeUp(rowWidth),
  1758. Unit.PX);
  1759. row = row.getNextSiblingElement();
  1760. }
  1761. }
  1762. /**
  1763. * The primary style name for the container.
  1764. *
  1765. * @param primaryStyleName
  1766. * the style name to use as prefix for all row and cell style
  1767. * names.
  1768. */
  1769. protected void setStylePrimaryName(String primaryStyleName) {
  1770. String oldStyle = getStylePrimaryName();
  1771. if (SharedUtil.equals(oldStyle, primaryStyleName)) {
  1772. return;
  1773. }
  1774. this.primaryStyleName = primaryStyleName;
  1775. // Update already rendered rows and cells
  1776. Element row = root.getRows().getItem(0);
  1777. while (row != null) {
  1778. UIObject.setStylePrimaryName(row, primaryStyleName + "-row");
  1779. Element cell = TableRowElement.as(row).getCells().getItem(0);
  1780. while (cell != null) {
  1781. assert TableCellElement.is(cell);
  1782. UIObject.setStylePrimaryName(cell,
  1783. primaryStyleName + "-cell");
  1784. cell = cell.getNextSiblingElement();
  1785. }
  1786. row = row.getNextSiblingElement();
  1787. }
  1788. }
  1789. /**
  1790. * Returns the primary style name of the container.
  1791. *
  1792. * @return The primary style name or <code>null</code> if not set.
  1793. */
  1794. protected String getStylePrimaryName() {
  1795. return primaryStyleName;
  1796. }
  1797. @Override
  1798. public void setDefaultRowHeight(double px)
  1799. throws IllegalArgumentException {
  1800. if (px < 1) {
  1801. throw new IllegalArgumentException(
  1802. "Height must be positive. " + px + " was given.");
  1803. }
  1804. defaultRowHeightShouldBeAutodetected = false;
  1805. defaultRowHeight = px;
  1806. reapplyDefaultRowHeights();
  1807. applyHeightByRows();
  1808. }
  1809. @Override
  1810. public double getDefaultRowHeight() {
  1811. return defaultRowHeight;
  1812. }
  1813. /**
  1814. * The default height of rows has (most probably) changed.
  1815. * <p>
  1816. * Make sure that the displayed rows with a default height are updated
  1817. * in height and top position.
  1818. * <p>
  1819. * <em>Note:</em>This implementation should not call
  1820. * {@link Escalator#recalculateElementSizes()} - it is done by the
  1821. * discretion of the caller of this method.
  1822. */
  1823. protected abstract void reapplyDefaultRowHeights();
  1824. protected void reapplyRowHeight(final TableRowElement tr,
  1825. final double heightPx) {
  1826. assert heightPx >= 0 : "Height must not be negative";
  1827. Element cellElem = tr.getFirstChildElement();
  1828. while (cellElem != null) {
  1829. cellElem.getStyle().setHeight(heightPx, Unit.PX);
  1830. cellElem = cellElem.getNextSiblingElement();
  1831. }
  1832. /*
  1833. * no need to apply height to tr-element, it'll be resized
  1834. * implicitly.
  1835. */
  1836. }
  1837. protected void setRowPosition(final TableRowElement tr, final int x,
  1838. final double y) {
  1839. positions.set(tr, x, y);
  1840. }
  1841. /**
  1842. * Returns <em>the assigned</em> top position for the given element.
  1843. * <p>
  1844. * <em>Note:</em> This method does not calculate what a row's top
  1845. * position should be. It just returns an assigned value, correct or
  1846. * not.
  1847. *
  1848. * @param tr
  1849. * the table row element to measure
  1850. * @return the current top position for {@code tr}
  1851. * @see BodyRowContainerImpl#getRowTop(int)
  1852. */
  1853. protected double getRowTop(final TableRowElement tr) {
  1854. return positions.getTop(tr);
  1855. }
  1856. protected void removeRowPosition(TableRowElement tr) {
  1857. positions.remove(tr);
  1858. }
  1859. public void autodetectRowHeightLater() {
  1860. autodetectingRowHeightLater = true;
  1861. Scheduler.get().scheduleFinally(() -> {
  1862. if (defaultRowHeightShouldBeAutodetected && isAttached()) {
  1863. autodetectRowHeightNow();
  1864. defaultRowHeightShouldBeAutodetected = false;
  1865. }
  1866. autodetectingRowHeightLater = false;
  1867. });
  1868. }
  1869. @Override
  1870. public boolean isAutodetectingRowHeightLater() {
  1871. return autodetectingRowHeightLater;
  1872. }
  1873. private void fireRowHeightChangedEventFinally() {
  1874. if (!rowHeightChangedEventFired) {
  1875. rowHeightChangedEventFired = true;
  1876. Scheduler.get().scheduleFinally(() -> {
  1877. fireEvent(new RowHeightChangedEvent());
  1878. rowHeightChangedEventFired = false;
  1879. });
  1880. }
  1881. }
  1882. public void autodetectRowHeightNow() {
  1883. if (!isAttached()) {
  1884. // Run again when attached
  1885. defaultRowHeightShouldBeAutodetected = true;
  1886. return;
  1887. }
  1888. final double oldRowHeight = defaultRowHeight;
  1889. final Element detectionTr = DOM.createTR();
  1890. detectionTr.setClassName(getStylePrimaryName() + "-row");
  1891. final Element cellElem = DOM.createElement(getCellElementTagName());
  1892. cellElem.setClassName(getStylePrimaryName() + "-cell");
  1893. cellElem.setInnerText("Ij");
  1894. detectionTr.appendChild(cellElem);
  1895. root.appendChild(detectionTr);
  1896. double boundingHeight = getBoundingHeight(cellElem);
  1897. defaultRowHeight = Math.max(1.0d, boundingHeight);
  1898. root.removeChild(detectionTr);
  1899. if (root.hasChildNodes()) {
  1900. reapplyDefaultRowHeights();
  1901. applyHeightByRows();
  1902. }
  1903. if (oldRowHeight != defaultRowHeight) {
  1904. fireRowHeightChangedEventFinally();
  1905. }
  1906. }
  1907. @Override
  1908. public Cell getCell(final Element element) {
  1909. if (element == null) {
  1910. throw new IllegalArgumentException("Element cannot be null");
  1911. }
  1912. /*
  1913. * Ensure that element is not root nor the direct descendant of root
  1914. * (a row) and ensure the element is inside the dom hierarchy of the
  1915. * root element. If not, return.
  1916. */
  1917. if (root == element || element.getParentElement() == root
  1918. || !root.isOrHasChild(element)) {
  1919. return null;
  1920. }
  1921. /*
  1922. * Ensure element is the cell element by iterating up the DOM
  1923. * hierarchy until reaching cell element.
  1924. */
  1925. Element cellElementCandidate = element;
  1926. while (cellElementCandidate.getParentElement()
  1927. .getParentElement() != root) {
  1928. cellElementCandidate = cellElementCandidate.getParentElement();
  1929. }
  1930. final TableCellElement cellElement = TableCellElement
  1931. .as(cellElementCandidate);
  1932. // Find dom column
  1933. int domColumnIndex = -1;
  1934. for (Element e = cellElement; e != null; e = e
  1935. .getPreviousSiblingElement()) {
  1936. domColumnIndex++;
  1937. }
  1938. // Find dom row
  1939. int domRowIndex = -1;
  1940. for (Element e = cellElement.getParentElement(); e != null; e = e
  1941. .getPreviousSiblingElement()) {
  1942. domRowIndex++;
  1943. }
  1944. return new Cell(domRowIndex, domColumnIndex, cellElement);
  1945. }
  1946. double measureCellWidth(TableCellElement cell, boolean withContent) {
  1947. /*
  1948. * To get the actual width of the contents, we need to get the cell
  1949. * content without any hardcoded height or width.
  1950. *
  1951. * But we don't want to modify the existing column, because that
  1952. * might trigger some unnecessary listeners and whatnot. So,
  1953. * instead, we make a deep clone of that cell, but without any
  1954. * explicit dimensions, and measure that instead.
  1955. */
  1956. TableCellElement cellClone = TableCellElement
  1957. .as((Element) cell.cloneNode(withContent));
  1958. cellClone.getStyle().clearHeight();
  1959. cellClone.getStyle().clearWidth();
  1960. cell.getParentElement().insertBefore(cellClone, cell);
  1961. double requiredWidth = getBoundingWidth(cellClone);
  1962. if (BrowserInfo.get().isIE()) {
  1963. /*
  1964. * IE browsers have some issues with subpixels. Occasionally
  1965. * content is overflown even if not necessary. Increase the
  1966. * counted required size by 0.01 just to be on the safe side.
  1967. */
  1968. requiredWidth += 0.01;
  1969. }
  1970. cellClone.removeFromParent();
  1971. return requiredWidth;
  1972. }
  1973. /**
  1974. * Gets the minimum width needed to display the cell properly.
  1975. *
  1976. * @param colIndex
  1977. * index of column to measure
  1978. * @param withContent
  1979. * <code>true</code> if content is taken into account,
  1980. * <code>false</code> if not
  1981. * @return cell width needed for displaying correctly
  1982. */
  1983. double measureMinCellWidth(int colIndex, boolean withContent) {
  1984. assert isAttached() : "Can't measure max width of cell, since Escalator is not attached to the DOM.";
  1985. double minCellWidth = -1;
  1986. NodeList<TableRowElement> rows = root.getRows();
  1987. for (int row = 0; row < rows.getLength(); row++) {
  1988. TableCellElement cell = rows.getItem(row).getCells()
  1989. .getItem(colIndex);
  1990. if (cell != null && !cellIsPartOfSpan(cell)) {
  1991. double cellWidth = measureCellWidth(cell, withContent);
  1992. minCellWidth = Math.max(minCellWidth, cellWidth);
  1993. }
  1994. }
  1995. return minCellWidth;
  1996. }
  1997. private boolean cellIsPartOfSpan(TableCellElement cell) {
  1998. boolean cellHasColspan = cell.getColSpan() > 1;
  1999. boolean cellIsHidden = Display.NONE.getCssName()
  2000. .equals(cell.getStyle().getDisplay());
  2001. return cellHasColspan || cellIsHidden;
  2002. }
  2003. void refreshColumns(int index, int numberOfColumns) {
  2004. if (getRowCount() > 0) {
  2005. Range rowRange = Range.withLength(0, getRowCount());
  2006. Range colRange = Range.withLength(index, numberOfColumns);
  2007. refreshCells(rowRange, colRange);
  2008. }
  2009. }
  2010. /**
  2011. * The height of this table section.
  2012. * <p>
  2013. * Note that {@link Escalator#getBody() the body} will calculate its
  2014. * height, while the others will return a precomputed value.
  2015. *
  2016. * @since 7.5.0
  2017. *
  2018. * @return the height of this table section
  2019. */
  2020. protected abstract double getHeightOfSection();
  2021. /**
  2022. * Gets the logical row index for the given table row element.
  2023. *
  2024. * @param tr
  2025. * the table row element inside this container.
  2026. * @return the logical index of the given element
  2027. */
  2028. public int getLogicalRowIndex(final TableRowElement tr) {
  2029. return tr.getSectionRowIndex();
  2030. };
  2031. }
  2032. private abstract class AbstractStaticRowContainer
  2033. extends AbstractRowContainer {
  2034. /** The height of the combined rows in the DOM. Never negative. */
  2035. private double heightOfSection = 0;
  2036. public AbstractStaticRowContainer(
  2037. final TableSectionElement headElement) {
  2038. super(headElement);
  2039. }
  2040. @Override
  2041. public int getDomRowCount() {
  2042. return root.getChildCount();
  2043. }
  2044. @Override
  2045. protected void paintRemoveRows(final int index,
  2046. final int numberOfRows) {
  2047. for (int i = index; i < index + numberOfRows; i++) {
  2048. final TableRowElement tr = root.getRows().getItem(index);
  2049. paintRemoveRow(tr, index);
  2050. }
  2051. recalculateSectionHeight();
  2052. }
  2053. @Override
  2054. protected TableRowElement getTrByVisualIndex(final int index)
  2055. throws IndexOutOfBoundsException {
  2056. if (index >= 0 && index < root.getChildCount()) {
  2057. return root.getRows().getItem(index);
  2058. } else {
  2059. throw new IndexOutOfBoundsException(
  2060. "No such visual index: " + index);
  2061. }
  2062. }
  2063. @Override
  2064. public void insertRows(int index, int numberOfRows) {
  2065. super.insertRows(index, numberOfRows);
  2066. recalculateElementSizes();
  2067. applyHeightByRows();
  2068. }
  2069. @Override
  2070. public void removeRows(int index, int numberOfRows) {
  2071. /*
  2072. * While the rows in a static section are removed, the scrollbar is
  2073. * temporarily shrunk and then re-expanded. This leads to the fact
  2074. * that the scroll position is scooted up a bit. This means that we
  2075. * need to reset the position here.
  2076. *
  2077. * If Escalator, at some point, gets a JIT evaluation functionality,
  2078. * this re-setting is a strong candidate for removal.
  2079. */
  2080. double oldScrollPos = verticalScrollbar.getScrollPos();
  2081. super.removeRows(index, numberOfRows);
  2082. recalculateElementSizes();
  2083. applyHeightByRows();
  2084. verticalScrollbar.setScrollPos(oldScrollPos);
  2085. }
  2086. @Override
  2087. protected void reapplyDefaultRowHeights() {
  2088. if (root.getChildCount() == 0) {
  2089. return;
  2090. }
  2091. Profiler.enter(
  2092. "Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
  2093. Element tr = root.getRows().getItem(0);
  2094. while (tr != null) {
  2095. reapplyRowHeight(TableRowElement.as(tr), getDefaultRowHeight());
  2096. tr = tr.getNextSiblingElement();
  2097. }
  2098. /*
  2099. * Because all rows are immediately displayed in the static row
  2100. * containers, the section's overall height has most probably
  2101. * changed.
  2102. */
  2103. recalculateSectionHeight();
  2104. Profiler.leave(
  2105. "Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
  2106. }
  2107. @Override
  2108. protected void recalculateSectionHeight() {
  2109. Profiler.enter(
  2110. "Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
  2111. double newHeight = calculateTotalRowHeight();
  2112. if (newHeight != heightOfSection) {
  2113. heightOfSection = newHeight;
  2114. sectionHeightCalculated();
  2115. /*
  2116. * We need to update the scrollbar dimension at this point. If
  2117. * we are scrolled too far down and the static section shrinks,
  2118. * the body will try to render rows that don't exist during
  2119. * body.verifyEscalatorCount. This is because the logical row
  2120. * indices are calculated from the scrollbar position.
  2121. */
  2122. verticalScrollbar.setOffsetSize(
  2123. heightOfEscalator - header.getHeightOfSection()
  2124. - footer.getHeightOfSection());
  2125. body.verifyEscalatorCount();
  2126. body.spacerContainer.updateSpacerDecosVisibility();
  2127. }
  2128. Profiler.leave(
  2129. "Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
  2130. }
  2131. /**
  2132. * Informs the row container that the height of its respective table
  2133. * section has changed.
  2134. * <p>
  2135. * These calculations might affect some layouting logic, such as the
  2136. * body is being offset by the footer, the footer needs to be readjusted
  2137. * according to its height, and so on.
  2138. * <p>
  2139. * A table section is either header, body or footer.
  2140. */
  2141. protected abstract void sectionHeightCalculated();
  2142. @Override
  2143. protected void refreshCells(Range logicalRowRange, Range colRange) {
  2144. assertArgumentsAreValidAndWithinRange(logicalRowRange.getStart(),
  2145. logicalRowRange.length());
  2146. if (!isAttached()) {
  2147. return;
  2148. }
  2149. Profiler.enter("Escalator.AbstractStaticRowContainer.refreshCells");
  2150. if (hasColumnAndRowData()) {
  2151. for (int row = logicalRowRange.getStart(); row < logicalRowRange
  2152. .getEnd(); row++) {
  2153. final TableRowElement tr = getTrByVisualIndex(row);
  2154. refreshRow(tr, row, colRange);
  2155. }
  2156. }
  2157. Profiler.leave("Escalator.AbstractStaticRowContainer.refreshCells");
  2158. }
  2159. @Override
  2160. protected void paintInsertRows(int visualIndex, int numberOfRows) {
  2161. paintInsertStaticRows(visualIndex, numberOfRows);
  2162. }
  2163. @Override
  2164. protected boolean rowCanBeFrozen(TableRowElement tr) {
  2165. assert root.isOrHasChild(
  2166. tr) : "Row does not belong to this table section";
  2167. return true;
  2168. }
  2169. @Override
  2170. protected double getHeightOfSection() {
  2171. return Math.max(0, heightOfSection);
  2172. }
  2173. }
  2174. private class HeaderRowContainer extends AbstractStaticRowContainer {
  2175. public HeaderRowContainer(final TableSectionElement headElement) {
  2176. super(headElement);
  2177. }
  2178. @Override
  2179. protected void sectionHeightCalculated() {
  2180. double heightOfSection = getHeightOfSection();
  2181. bodyElem.getStyle().setMarginTop(heightOfSection, Unit.PX);
  2182. spacerDecoContainer.getStyle().setMarginTop(heightOfSection,
  2183. Unit.PX);
  2184. verticalScrollbar.getElement().getStyle().setTop(heightOfSection,
  2185. Unit.PX);
  2186. headerDeco.getStyle().setHeight(heightOfSection, Unit.PX);
  2187. }
  2188. @Override
  2189. protected String getCellElementTagName() {
  2190. return "th";
  2191. }
  2192. @Override
  2193. protected AriaGridRole getRowElementRole() {
  2194. return AriaGridRole.ROWHEADER;
  2195. }
  2196. @Override
  2197. protected AriaGridRole getCellElementRole() {
  2198. return AriaGridRole.COLUMNHEADER;
  2199. }
  2200. @Override
  2201. public void setStylePrimaryName(String primaryStyleName) {
  2202. super.setStylePrimaryName(primaryStyleName);
  2203. UIObject.setStylePrimaryName(root, primaryStyleName + "-header");
  2204. }
  2205. }
  2206. private class FooterRowContainer extends AbstractStaticRowContainer {
  2207. public FooterRowContainer(final TableSectionElement footElement) {
  2208. super(footElement);
  2209. }
  2210. @Override
  2211. public void setStylePrimaryName(String primaryStyleName) {
  2212. super.setStylePrimaryName(primaryStyleName);
  2213. UIObject.setStylePrimaryName(root, primaryStyleName + "-footer");
  2214. }
  2215. @Override
  2216. protected String getCellElementTagName() {
  2217. return "td";
  2218. }
  2219. @Override
  2220. protected void sectionHeightCalculated() {
  2221. double headerHeight = header.getHeightOfSection();
  2222. double footerHeight = footer.getHeightOfSection();
  2223. int vscrollHeight = (int) Math
  2224. .floor(heightOfEscalator - headerHeight - footerHeight);
  2225. final boolean horizontalScrollbarNeeded = columnConfiguration
  2226. .calculateRowWidth() > widthOfEscalator;
  2227. if (horizontalScrollbarNeeded) {
  2228. vscrollHeight -= horizontalScrollbar.getScrollbarThickness();
  2229. }
  2230. footerDeco.getStyle().setHeight(footer.getHeightOfSection(),
  2231. Unit.PX);
  2232. verticalScrollbar.setOffsetSize(vscrollHeight);
  2233. }
  2234. }
  2235. private class BodyRowContainerImpl extends AbstractRowContainer
  2236. implements BodyRowContainer {
  2237. /*
  2238. * TODO [[optimize]]: check whether a native JsArray might be faster
  2239. * than LinkedList
  2240. */
  2241. /**
  2242. * The order in which row elements are rendered visually in the browser,
  2243. * with the help of CSS tricks. Usually has nothing to do with the DOM
  2244. * order.
  2245. *
  2246. * @see #sortDomElements()
  2247. */
  2248. private final LinkedList<TableRowElement> visualRowOrder = new LinkedList<>();
  2249. /**
  2250. * The logical index of the topmost row.
  2251. *
  2252. * @deprecated Use the accessors {@link #setTopRowLogicalIndex(int)},
  2253. * {@link #updateTopRowLogicalIndex(int)} and
  2254. * {@link #getTopRowLogicalIndex()} instead
  2255. */
  2256. @Deprecated
  2257. private int topRowLogicalIndex = 0;
  2258. /**
  2259. * A callback function to be executed after new rows are added to the
  2260. * escalator.
  2261. */
  2262. private Consumer<List<TableRowElement>> newEscalatorRowCallback;
  2263. private void setTopRowLogicalIndex(int topRowLogicalIndex) {
  2264. if (LogConfiguration.loggingIsEnabled(Level.INFO)) {
  2265. Logger.getLogger("Escalator.BodyRowContainer")
  2266. .fine("topRowLogicalIndex: " + this.topRowLogicalIndex
  2267. + " -> " + topRowLogicalIndex);
  2268. }
  2269. assert topRowLogicalIndex >= 0 : "topRowLogicalIndex became negative (top left cell contents: "
  2270. + visualRowOrder.getFirst().getCells().getItem(0)
  2271. .getInnerText()
  2272. + ") ";
  2273. /*
  2274. * if there's a smart way of evaluating and asserting the max index,
  2275. * this would be a nice place to put it. I haven't found out an
  2276. * effective and generic solution.
  2277. */
  2278. this.topRowLogicalIndex = topRowLogicalIndex;
  2279. }
  2280. public int getTopRowLogicalIndex() {
  2281. return topRowLogicalIndex;
  2282. }
  2283. private void updateTopRowLogicalIndex(int diff) {
  2284. setTopRowLogicalIndex(topRowLogicalIndex + diff);
  2285. }
  2286. private class DeferredDomSorter {
  2287. private static final int SORT_DELAY_MILLIS = 50;
  2288. // as it happens, 3 frames = 50ms @ 60fps.
  2289. private static final int REQUIRED_FRAMES_PASSED = 3;
  2290. private final AnimationCallback frameCounter = new AnimationCallback() {
  2291. @Override
  2292. public void execute(double timestamp) {
  2293. framesPassed++;
  2294. boolean domWasSorted = sortIfConditionsMet();
  2295. if (!domWasSorted) {
  2296. animationHandle = AnimationScheduler.get()
  2297. .requestAnimationFrame(this);
  2298. } else {
  2299. waiting = false;
  2300. }
  2301. }
  2302. };
  2303. private int framesPassed;
  2304. private double startTime;
  2305. private AnimationHandle animationHandle;
  2306. /** <code>true</code> if a sort is scheduled */
  2307. public boolean waiting = false;
  2308. public void reschedule() {
  2309. waiting = true;
  2310. resetConditions();
  2311. animationHandle = AnimationScheduler.get()
  2312. .requestAnimationFrame(frameCounter);
  2313. }
  2314. private boolean sortIfConditionsMet() {
  2315. boolean enoughFramesHavePassed = framesPassed >= REQUIRED_FRAMES_PASSED;
  2316. boolean enoughTimeHasPassed = (Duration.currentTimeMillis()
  2317. - startTime) >= SORT_DELAY_MILLIS;
  2318. boolean notTouchActivity = !scroller.touchHandlerBundle.touching;
  2319. boolean conditionsMet = enoughFramesHavePassed
  2320. && enoughTimeHasPassed && notTouchActivity;
  2321. if (conditionsMet) {
  2322. resetConditions();
  2323. sortDomElements();
  2324. }
  2325. return conditionsMet;
  2326. }
  2327. private void resetConditions() {
  2328. if (animationHandle != null) {
  2329. animationHandle.cancel();
  2330. animationHandle = null;
  2331. }
  2332. startTime = Duration.currentTimeMillis();
  2333. framesPassed = 0;
  2334. }
  2335. }
  2336. private DeferredDomSorter domSorter = new DeferredDomSorter();
  2337. private final SpacerContainer spacerContainer = new SpacerContainer();
  2338. public BodyRowContainerImpl(final TableSectionElement bodyElement) {
  2339. super(bodyElement);
  2340. }
  2341. @Override
  2342. public void setStylePrimaryName(String primaryStyleName) {
  2343. super.setStylePrimaryName(primaryStyleName);
  2344. UIObject.setStylePrimaryName(root, primaryStyleName + "-body");
  2345. spacerContainer.setStylePrimaryName(primaryStyleName);
  2346. }
  2347. public void updateEscalatorRowsOnScroll() {
  2348. if (visualRowOrder.isEmpty()) {
  2349. return;
  2350. }
  2351. boolean rowsWereMoved = false;
  2352. final double topElementPosition;
  2353. final double nextRowBottomOffset;
  2354. SpacerContainer.SpacerImpl topSpacer = spacerContainer
  2355. .getSpacer(getTopRowLogicalIndex() - 1);
  2356. if (topSpacer != null) {
  2357. topElementPosition = topSpacer.getTop();
  2358. nextRowBottomOffset = topSpacer.getHeight()
  2359. + getDefaultRowHeight();
  2360. } else {
  2361. topElementPosition = getRowTop(visualRowOrder.getFirst());
  2362. nextRowBottomOffset = getDefaultRowHeight();
  2363. }
  2364. // TODO [[mpixscroll]]
  2365. final double scrollTop = tBodyScrollTop;
  2366. final double viewportOffset = topElementPosition - scrollTop;
  2367. /*
  2368. * TODO [[optimize]] this if-else can most probably be refactored
  2369. * into a neater block of code
  2370. */
  2371. if (viewportOffset > 0) {
  2372. // there's empty room on top
  2373. double rowPx = getRowHeightsSumBetweenPx(scrollTop,
  2374. topElementPosition);
  2375. int originalRowsToMove = (int) Math
  2376. .ceil(rowPx / getDefaultRowHeight());
  2377. int rowsToMove = Math.min(originalRowsToMove,
  2378. visualRowOrder.size());
  2379. final int end = visualRowOrder.size();
  2380. final int start = end - rowsToMove;
  2381. final int logicalRowIndex = getLogicalRowIndex(scrollTop);
  2382. moveAndUpdateEscalatorRows(Range.between(start, end), 0,
  2383. logicalRowIndex);
  2384. setTopRowLogicalIndex(logicalRowIndex);
  2385. rowsWereMoved = true;
  2386. } else if (viewportOffset + nextRowBottomOffset <= 0) {
  2387. /*
  2388. * the viewport has been scrolled more than the topmost visual
  2389. * row.
  2390. */
  2391. double rowPx = getRowHeightsSumBetweenPx(topElementPosition,
  2392. scrollTop);
  2393. int originalRowsToMove = (int) (rowPx / getDefaultRowHeight());
  2394. int rowsToMove = Math.min(originalRowsToMove,
  2395. visualRowOrder.size());
  2396. int logicalRowIndex;
  2397. if (rowsToMove < visualRowOrder.size()) {
  2398. /*
  2399. * We scroll so little that we can just keep adding the rows
  2400. * below the current escalator
  2401. */
  2402. logicalRowIndex = getLogicalRowIndex(
  2403. visualRowOrder.getLast()) + 1;
  2404. } else {
  2405. /*
  2406. * Since we're moving all escalator rows, we need to
  2407. * calculate the first logical row index from the scroll
  2408. * position.
  2409. */
  2410. logicalRowIndex = getLogicalRowIndex(scrollTop);
  2411. }
  2412. /*
  2413. * Since we're moving the viewport downwards, the visual index
  2414. * is always at the bottom. Note: Due to how
  2415. * moveAndUpdateEscalatorRows works, this will work out even if
  2416. * we move all the rows, and try to place them "at the end".
  2417. */
  2418. final int targetVisualIndex = visualRowOrder.size();
  2419. // make sure that we don't move rows over the data boundary
  2420. boolean aRowWasLeftBehind = false;
  2421. if (logicalRowIndex + rowsToMove > getRowCount()) {
  2422. /*
  2423. * TODO [[spacer]]: with constant row heights, there's
  2424. * always exactly one row that will be moved beyond the data
  2425. * source, when viewport is scrolled to the end. This,
  2426. * however, isn't guaranteed anymore once row heights start
  2427. * varying.
  2428. */
  2429. rowsToMove--;
  2430. aRowWasLeftBehind = true;
  2431. }
  2432. /*
  2433. * Make sure we don't scroll beyond the row content. This can
  2434. * happen if we have spacers for the last rows.
  2435. */
  2436. rowsToMove = Math.max(0,
  2437. Math.min(rowsToMove, getRowCount() - logicalRowIndex));
  2438. moveAndUpdateEscalatorRows(Range.between(0, rowsToMove),
  2439. targetVisualIndex, logicalRowIndex);
  2440. if (aRowWasLeftBehind) {
  2441. /*
  2442. * To keep visualRowOrder as a spatially contiguous block of
  2443. * rows, let's make sure that the one row we didn't move
  2444. * visually still stays with the pack.
  2445. */
  2446. final Range strayRow = Range.withOnly(0);
  2447. /*
  2448. * We cannot trust getLogicalRowIndex, because it hasn't yet
  2449. * been updated. But since we're leaving rows behind, it
  2450. * means we've scrolled to the bottom. So, instead, we
  2451. * simply count backwards from the end.
  2452. */
  2453. final int topLogicalIndex = getRowCount()
  2454. - visualRowOrder.size();
  2455. moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex);
  2456. }
  2457. final int naiveNewLogicalIndex = getTopRowLogicalIndex()
  2458. + originalRowsToMove;
  2459. final int maxLogicalIndex = getRowCount()
  2460. - visualRowOrder.size();
  2461. setTopRowLogicalIndex(
  2462. Math.min(naiveNewLogicalIndex, maxLogicalIndex));
  2463. rowsWereMoved = true;
  2464. }
  2465. if (rowsWereMoved) {
  2466. fireRowVisibilityChangeEvent();
  2467. domSorter.reschedule();
  2468. }
  2469. }
  2470. private double getRowHeightsSumBetweenPx(double y1, double y2) {
  2471. assert y1 < y2 : "y1 must be smaller than y2";
  2472. double viewportPx = y2 - y1;
  2473. double spacerPx = spacerContainer.getSpacerHeightsSumBetweenPx(y1,
  2474. SpacerInclusionStrategy.PARTIAL, y2,
  2475. SpacerInclusionStrategy.PARTIAL);
  2476. return viewportPx - spacerPx;
  2477. }
  2478. private int getLogicalRowIndex(final double px) {
  2479. double rowPx = px - spacerContainer.getSpacerHeightsSumUntilPx(px);
  2480. return (int) (rowPx / getDefaultRowHeight());
  2481. }
  2482. @Override
  2483. public void insertRows(int index, int numberOfRows) {
  2484. super.insertRows(index, numberOfRows);
  2485. if (heightMode == HeightMode.UNDEFINED) {
  2486. setHeightByRows(getRowCount());
  2487. }
  2488. }
  2489. @Override
  2490. public void removeRows(int index, int numberOfRows) {
  2491. super.removeRows(index, numberOfRows);
  2492. if (heightMode == HeightMode.UNDEFINED) {
  2493. setHeightByRows(getRowCount());
  2494. }
  2495. }
  2496. @Override
  2497. protected void paintInsertRows(final int index,
  2498. final int numberOfRows) {
  2499. if (numberOfRows == 0) {
  2500. return;
  2501. }
  2502. spacerContainer.shiftSpacersByRows(index, numberOfRows);
  2503. /*
  2504. * TODO: this method should probably only add physical rows, and not
  2505. * populate them - let everything be populated as appropriate by the
  2506. * logic that follows.
  2507. *
  2508. * This also would lead to the fact that paintInsertRows wouldn't
  2509. * need to return anything.
  2510. */
  2511. final List<TableRowElement> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
  2512. index, numberOfRows);
  2513. /*
  2514. * insertRows will always change the number of rows - update the
  2515. * scrollbar sizes.
  2516. */
  2517. scroller.recalculateScrollbarsForVirtualViewport();
  2518. double spacerHeightsSumUntilIndex = spacerContainer
  2519. .getSpacerHeightsSumUntilIndex(index);
  2520. final boolean addedRowsAboveCurrentViewport = index
  2521. * getDefaultRowHeight()
  2522. + spacerHeightsSumUntilIndex < getScrollTop();
  2523. final boolean addedRowsBelowCurrentViewport = index
  2524. * getDefaultRowHeight()
  2525. + spacerHeightsSumUntilIndex > getScrollTop()
  2526. + getHeightOfSection();
  2527. if (addedRowsAboveCurrentViewport) {
  2528. /*
  2529. * We need to tweak the virtual viewport (scroll handle
  2530. * positions, table "scroll position" and row locations), but
  2531. * without re-evaluating any rows.
  2532. */
  2533. final double yDelta = numberOfRows * getDefaultRowHeight();
  2534. moveViewportAndContent(yDelta);
  2535. updateTopRowLogicalIndex(numberOfRows);
  2536. } else if (addedRowsBelowCurrentViewport) {
  2537. // NOOP, we already recalculated scrollbars.
  2538. } else {
  2539. // some rows were added inside the current viewport
  2540. final int unupdatedLogicalStart = index + addedRows.size();
  2541. final int visualOffset = getLogicalRowIndex(
  2542. visualRowOrder.getFirst());
  2543. /*
  2544. * At this point, we have added new escalator rows, if so
  2545. * needed.
  2546. *
  2547. * If more rows were added than the new escalator rows can
  2548. * account for, we need to start to spin the escalator to update
  2549. * the remaining rows as well.
  2550. */
  2551. final int rowsStillNeeded = numberOfRows - addedRows.size();
  2552. if (rowsStillNeeded > 0) {
  2553. final Range unupdatedVisual = convertToVisual(
  2554. Range.withLength(unupdatedLogicalStart,
  2555. rowsStillNeeded));
  2556. final int end = getDomRowCount();
  2557. final int start = end - unupdatedVisual.length();
  2558. final int visualTargetIndex = unupdatedLogicalStart
  2559. - visualOffset;
  2560. moveAndUpdateEscalatorRows(Range.between(start, end),
  2561. visualTargetIndex, unupdatedLogicalStart);
  2562. // move the surrounding rows to their correct places.
  2563. double rowTop = (unupdatedLogicalStart + (end - start))
  2564. * getDefaultRowHeight();
  2565. // TODO: Get rid of this try/catch block by fixing the
  2566. // underlying issue. The reason for this erroneous behavior
  2567. // might be that Escalator actually works 'by mistake', and
  2568. // the order of operations is, in fact, wrong.
  2569. try {
  2570. final ListIterator<TableRowElement> i = visualRowOrder
  2571. .listIterator(
  2572. visualTargetIndex + (end - start));
  2573. int logicalRowIndexCursor = unupdatedLogicalStart;
  2574. while (i.hasNext()) {
  2575. rowTop += spacerContainer
  2576. .getSpacerHeight(logicalRowIndexCursor++);
  2577. final TableRowElement tr = i.next();
  2578. setRowPosition(tr, 0, rowTop);
  2579. rowTop += getDefaultRowHeight();
  2580. }
  2581. } catch (Exception e) {
  2582. Logger logger = getLogger();
  2583. logger.warning(
  2584. "Ignored out-of-bounds row element access");
  2585. logger.warning("Escalator state: start=" + start
  2586. + ", end=" + end + ", visualTargetIndex="
  2587. + visualTargetIndex + ", visualRowOrder.size()="
  2588. + visualRowOrder.size());
  2589. logger.warning(e.toString());
  2590. }
  2591. }
  2592. fireRowVisibilityChangeEvent();
  2593. sortDomElements();
  2594. }
  2595. }
  2596. /**
  2597. * Move escalator rows around, and make sure everything gets
  2598. * appropriately repositioned and repainted.
  2599. *
  2600. * @param visualSourceRange
  2601. * the range of rows to move to a new place
  2602. * @param visualTargetIndex
  2603. * the visual index where the rows will be placed to
  2604. * @param logicalTargetIndex
  2605. * the logical index to be assigned to the first moved row
  2606. */
  2607. private void moveAndUpdateEscalatorRows(final Range visualSourceRange,
  2608. final int visualTargetIndex, final int logicalTargetIndex)
  2609. throws IllegalArgumentException {
  2610. if (visualSourceRange.isEmpty()) {
  2611. return;
  2612. }
  2613. assert visualSourceRange.getStart() >= 0 : "Visual source start "
  2614. + "must be 0 or greater (was "
  2615. + visualSourceRange.getStart() + ")";
  2616. assert logicalTargetIndex >= 0 : "Logical target must be 0 or "
  2617. + "greater (was " + logicalTargetIndex + ")";
  2618. assert visualTargetIndex >= 0 : "Visual target must be 0 or greater (was "
  2619. + visualTargetIndex + ")";
  2620. assert visualTargetIndex <= getDomRowCount() : "Visual target "
  2621. + "must not be greater than the number of escalator rows (was "
  2622. + visualTargetIndex + ", escalator rows " + getDomRowCount()
  2623. + ")";
  2624. assert logicalTargetIndex
  2625. + visualSourceRange.length() <= getRowCount() : "Logical "
  2626. + "target leads to rows outside of the data range ("
  2627. + Range.withLength(logicalTargetIndex,
  2628. visualSourceRange.length())
  2629. + " goes beyond "
  2630. + Range.withLength(0, getRowCount()) + ")";
  2631. /*
  2632. * Since we move a range into another range, the indices might move
  2633. * about. Having 10 rows, if we move 0..1 to index 10 (to the end of
  2634. * the collection), the target range will end up being 8..9, instead
  2635. * of 10..11.
  2636. *
  2637. * This applies only if we move elements forward in the collection,
  2638. * not backward.
  2639. */
  2640. final int adjustedVisualTargetIndex;
  2641. if (visualSourceRange.getStart() < visualTargetIndex) {
  2642. adjustedVisualTargetIndex = visualTargetIndex
  2643. - visualSourceRange.length();
  2644. } else {
  2645. adjustedVisualTargetIndex = visualTargetIndex;
  2646. }
  2647. if (visualSourceRange.getStart() != adjustedVisualTargetIndex) {
  2648. /*
  2649. * Reorder the rows to their correct places within
  2650. * visualRowOrder (unless rows are moved back to their original
  2651. * places)
  2652. */
  2653. /*
  2654. * TODO [[optimize]]: move whichever set is smaller: the ones
  2655. * explicitly moved, or the others. So, with 10 escalator rows,
  2656. * if we are asked to move idx[0..8] to the end of the list,
  2657. * it's faster to just move idx[9] to the beginning.
  2658. */
  2659. final List<TableRowElement> removedRows = new ArrayList<>(
  2660. visualSourceRange.length());
  2661. for (int i = 0; i < visualSourceRange.length(); i++) {
  2662. final TableRowElement tr = visualRowOrder
  2663. .remove(visualSourceRange.getStart());
  2664. removedRows.add(tr);
  2665. }
  2666. visualRowOrder.addAll(adjustedVisualTargetIndex, removedRows);
  2667. }
  2668. { // Refresh the contents of the affected rows
  2669. final ListIterator<TableRowElement> iter = visualRowOrder
  2670. .listIterator(adjustedVisualTargetIndex);
  2671. for (int logicalIndex = logicalTargetIndex; logicalIndex < logicalTargetIndex
  2672. + visualSourceRange.length(); logicalIndex++) {
  2673. final TableRowElement tr = iter.next();
  2674. refreshRow(tr, logicalIndex);
  2675. }
  2676. }
  2677. { // Reposition the rows that were moved
  2678. double newRowTop = getRowTop(logicalTargetIndex);
  2679. final ListIterator<TableRowElement> iter = visualRowOrder
  2680. .listIterator(adjustedVisualTargetIndex);
  2681. for (int i = 0; i < visualSourceRange.length(); i++) {
  2682. final TableRowElement tr = iter.next();
  2683. setRowPosition(tr, 0, newRowTop);
  2684. newRowTop += getDefaultRowHeight();
  2685. newRowTop += spacerContainer
  2686. .getSpacerHeight(logicalTargetIndex + i);
  2687. }
  2688. }
  2689. }
  2690. /**
  2691. * Adjust the scroll position and move the contained rows.
  2692. * <p>
  2693. * The difference between using this method and simply scrolling is that
  2694. * this method "takes the rows and spacers with it" and renders them
  2695. * appropriately. The viewport may be scrolled any arbitrary amount, and
  2696. * the contents are moved appropriately, but always snapped into a
  2697. * plausible place.
  2698. * <p>
  2699. * <dl>
  2700. * <dt>Example 1</dt>
  2701. * <dd>An Escalator with default row height 20px. Adjusting the scroll
  2702. * position with 7.5px will move the viewport 7.5px down, but leave the
  2703. * row where it is.</dd>
  2704. * <dt>Example 2</dt>
  2705. * <dd>An Escalator with default row height 20px. Adjusting the scroll
  2706. * position with 27.5px will move the viewport 27.5px down, and place
  2707. * the row at 20px.</dd>
  2708. * </dl>
  2709. *
  2710. * @param yDelta
  2711. * the delta of pixels by which to move the viewport and
  2712. * content. A positive value moves everything downwards,
  2713. * while a negative value moves everything upwards
  2714. */
  2715. public void moveViewportAndContent(final double yDelta) {
  2716. if (yDelta == 0) {
  2717. return;
  2718. }
  2719. double newTop = tBodyScrollTop + yDelta;
  2720. verticalScrollbar.setScrollPos(newTop);
  2721. final double defaultRowHeight = getDefaultRowHeight();
  2722. double rowPxDelta = yDelta - (yDelta % defaultRowHeight);
  2723. int rowIndexDelta = (int) (yDelta / defaultRowHeight);
  2724. if (!WidgetUtil.pixelValuesEqual(rowPxDelta, 0)) {
  2725. Collection<SpacerContainer.SpacerImpl> spacers = spacerContainer
  2726. .getSpacersAfterPx(tBodyScrollTop,
  2727. SpacerInclusionStrategy.PARTIAL);
  2728. for (SpacerContainer.SpacerImpl spacer : spacers) {
  2729. spacer.setPositionDiff(0, rowPxDelta);
  2730. spacer.setRowIndex(spacer.getRow() + rowIndexDelta);
  2731. }
  2732. for (TableRowElement tr : visualRowOrder) {
  2733. setRowPosition(tr, 0, getRowTop(tr) + rowPxDelta);
  2734. }
  2735. }
  2736. setBodyScrollPosition(tBodyScrollLeft, newTop);
  2737. }
  2738. /**
  2739. * Adds new physical escalator rows to the DOM at the given index if
  2740. * there's still a need for more escalator rows.
  2741. * <p>
  2742. * If Escalator already is at (or beyond) max capacity, this method does
  2743. * nothing to the DOM.
  2744. *
  2745. * @param index
  2746. * the index at which to add new escalator rows.
  2747. * <em>Note:</em>It is assumed that the index is both the
  2748. * visual index and the logical index.
  2749. * @param numberOfRows
  2750. * the number of rows to add at <code>index</code>
  2751. * @return a list of the added rows
  2752. */
  2753. private List<TableRowElement> fillAndPopulateEscalatorRowsIfNeeded(
  2754. final int index, final int numberOfRows) {
  2755. final int escalatorRowsStillFit = getMaxVisibleRowCount()
  2756. - getDomRowCount();
  2757. final int escalatorRowsNeeded = Math.min(numberOfRows,
  2758. escalatorRowsStillFit);
  2759. if (escalatorRowsNeeded > 0) {
  2760. final List<TableRowElement> addedRows = paintInsertStaticRows(
  2761. index, escalatorRowsNeeded);
  2762. visualRowOrder.addAll(index, addedRows);
  2763. double y = index * getDefaultRowHeight()
  2764. + spacerContainer.getSpacerHeightsSumUntilIndex(index);
  2765. for (int i = index; i < visualRowOrder.size(); i++) {
  2766. final TableRowElement tr;
  2767. if (i - index < addedRows.size()) {
  2768. tr = addedRows.get(i - index);
  2769. } else {
  2770. tr = visualRowOrder.get(i);
  2771. }
  2772. setRowPosition(tr, 0, y);
  2773. y += getDefaultRowHeight();
  2774. y += spacerContainer.getSpacerHeight(i);
  2775. }
  2776. // Execute the registered callback function for newly created
  2777. // rows
  2778. Optional.ofNullable(newEscalatorRowCallback)
  2779. .ifPresent(callback -> callback.accept(addedRows));
  2780. return addedRows;
  2781. } else {
  2782. return Collections.emptyList();
  2783. }
  2784. }
  2785. private int getMaxVisibleRowCount() {
  2786. double heightOfSection = getHeightOfSection();
  2787. // By including the possibly shown scrollbar height, we get a
  2788. // consistent count and do not add/remove rows whenever a scrollbar
  2789. // is shown
  2790. heightOfSection += horizontalScrollbarDeco.getOffsetHeight();
  2791. double defaultRowHeight = getDefaultRowHeight();
  2792. final int maxVisibleRowCount = (int) Math
  2793. .ceil(heightOfSection / defaultRowHeight) + 1;
  2794. /*
  2795. * maxVisibleRowCount can become negative if the headers and footers
  2796. * start to overlap. This is a crazy situation, but Vaadin blinks
  2797. * the components a lot, so it's feasible.
  2798. */
  2799. return Math.max(0, maxVisibleRowCount);
  2800. }
  2801. @Override
  2802. protected void paintRemoveRows(final int index,
  2803. final int numberOfRows) {
  2804. if (numberOfRows == 0) {
  2805. return;
  2806. }
  2807. final Range viewportRange = getVisibleRowRange();
  2808. final Range removedRowsRange = Range.withLength(index,
  2809. numberOfRows);
  2810. /*
  2811. * Removing spacers as the very first step will correct the
  2812. * scrollbars and row offsets right away.
  2813. *
  2814. * TODO: actually, it kinda sounds like a Grid feature that a spacer
  2815. * would be associated with a particular row. Maybe it would be
  2816. * better to have a spacer separate from rows, and simply collapse
  2817. * them if they happen to end up on top of each other. This would
  2818. * probably make supporting the -1 row pretty easy, too.
  2819. */
  2820. spacerContainer.paintRemoveSpacers(removedRowsRange);
  2821. final Range[] partitions = removedRowsRange
  2822. .partitionWith(viewportRange);
  2823. final Range removedAbove = partitions[0];
  2824. final Range removedLogicalInside = partitions[1];
  2825. final Range removedVisualInside = convertToVisual(
  2826. removedLogicalInside);
  2827. /*
  2828. * TODO: extract the following if-block to a separate method. I'll
  2829. * leave this be inlined for now, to make linediff-based code
  2830. * reviewing easier. Probably will be moved in the following patch
  2831. * set.
  2832. */
  2833. /*
  2834. * Adjust scroll position in one of two scenarios:
  2835. *
  2836. * 1) Rows were removed above. Then we just need to adjust the
  2837. * scrollbar by the height of the removed rows.
  2838. *
  2839. * 2) There are no logical rows above, and at least the first (if
  2840. * not more) visual row is removed. Then we need to snap the scroll
  2841. * position to the first visible row (i.e. reset scroll position to
  2842. * absolute 0)
  2843. *
  2844. * The logic is optimized in such a way that the
  2845. * moveViewportAndContent is called only once, to avoid extra
  2846. * reflows, and thus the code might seem a bit obscure.
  2847. */
  2848. final boolean firstVisualRowIsRemoved = !removedVisualInside
  2849. .isEmpty() && removedVisualInside.getStart() == 0;
  2850. if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) {
  2851. final double yDelta = removedAbove.length()
  2852. * getDefaultRowHeight();
  2853. final double firstLogicalRowHeight = getDefaultRowHeight();
  2854. final boolean removalScrollsToShowFirstLogicalRow = verticalScrollbar
  2855. .getScrollPos() - yDelta < firstLogicalRowHeight;
  2856. if (removedVisualInside.isEmpty()
  2857. && (!removalScrollsToShowFirstLogicalRow
  2858. || !firstVisualRowIsRemoved)) {
  2859. /*
  2860. * rows were removed from above the viewport, so all we need
  2861. * to do is to adjust the scroll position to account for the
  2862. * removed rows
  2863. */
  2864. moveViewportAndContent(-yDelta);
  2865. } else if (removalScrollsToShowFirstLogicalRow) {
  2866. /*
  2867. * It seems like we've removed all rows from above, and also
  2868. * into the current viewport. This means we'll need to even
  2869. * out the scroll position to exactly 0 (i.e. adjust by the
  2870. * current negative scrolltop, presto!), so that it isn't
  2871. * aligned funnily
  2872. */
  2873. moveViewportAndContent(-verticalScrollbar.getScrollPos());
  2874. }
  2875. }
  2876. // ranges evaluated, let's do things.
  2877. if (!removedVisualInside.isEmpty()) {
  2878. int escalatorRowCount = body.getDomRowCount();
  2879. /*
  2880. * remember: the rows have already been subtracted from the row
  2881. * count at this point
  2882. */
  2883. int rowsLeft = getRowCount();
  2884. if (rowsLeft < escalatorRowCount) {
  2885. /*
  2886. * Remove extra DOM rows and refresh contents.
  2887. */
  2888. for (int i = escalatorRowCount - 1; i >= rowsLeft; i--) {
  2889. final TableRowElement tr = visualRowOrder.remove(i);
  2890. paintRemoveRow(tr, i);
  2891. removeRowPosition(tr);
  2892. }
  2893. // Move rest of the rows to the Escalator's top
  2894. Range visualRange = Range.withLength(0,
  2895. visualRowOrder.size());
  2896. moveAndUpdateEscalatorRows(visualRange, 0, 0);
  2897. sortDomElements();
  2898. setTopRowLogicalIndex(0);
  2899. scroller.recalculateScrollbarsForVirtualViewport();
  2900. fireRowVisibilityChangeEvent();
  2901. return;
  2902. } else {
  2903. // No escalator rows need to be removed.
  2904. /*
  2905. * Two things (or a combination thereof) can happen:
  2906. *
  2907. * 1) We're scrolled to the bottom, the last rows are
  2908. * removed. SOLUTION: moveAndUpdateEscalatorRows the
  2909. * bottommost rows, and place them at the top to be
  2910. * refreshed.
  2911. *
  2912. * 2) We're scrolled somewhere in the middle, arbitrary rows
  2913. * are removed. SOLUTION: moveAndUpdateEscalatorRows the
  2914. * removed rows, and place them at the bottom to be
  2915. * refreshed.
  2916. *
  2917. * Since a combination can also happen, we need to handle
  2918. * this in a smart way, all while avoiding
  2919. * double-refreshing.
  2920. */
  2921. final double contentBottom = getRowCount()
  2922. * getDefaultRowHeight();
  2923. final double viewportBottom = tBodyScrollTop
  2924. + getHeightOfSection();
  2925. if (viewportBottom <= contentBottom) {
  2926. /*
  2927. * We're in the middle of the row container, everything
  2928. * is added to the bottom
  2929. */
  2930. paintRemoveRowsAtMiddle(removedLogicalInside,
  2931. removedVisualInside, 0);
  2932. } else if (removedVisualInside.contains(0)
  2933. && numberOfRows >= visualRowOrder.size()) {
  2934. /*
  2935. * We're removing so many rows that the viewport is
  2936. * pushed up more than a screenful. This means we can
  2937. * simply scroll up and everything will work without a
  2938. * sweat.
  2939. */
  2940. double left = horizontalScrollbar.getScrollPos();
  2941. double top = contentBottom
  2942. - visualRowOrder.size() * getDefaultRowHeight();
  2943. setBodyScrollPosition(left, top);
  2944. Range allEscalatorRows = Range.withLength(0,
  2945. visualRowOrder.size());
  2946. int logicalTargetIndex = getRowCount()
  2947. - allEscalatorRows.length();
  2948. moveAndUpdateEscalatorRows(allEscalatorRows, 0,
  2949. logicalTargetIndex);
  2950. /*
  2951. * moveAndUpdateEscalatorRows recalculates the rows, but
  2952. * logical top row index bookkeeping is handled in this
  2953. * method.
  2954. *
  2955. * TODO: Redesign how to keep it easy to track this.
  2956. */
  2957. updateTopRowLogicalIndex(
  2958. -removedLogicalInside.length());
  2959. /*
  2960. * Scrolling the body to the correct location will be
  2961. * fixed automatically. Because the amount of rows is
  2962. * decreased, the viewport is pushed up as the scrollbar
  2963. * shrinks. So no need to do anything there.
  2964. *
  2965. * TODO [[optimize]]: This might lead to a double body
  2966. * refresh. Needs investigation.
  2967. */
  2968. } else if (contentBottom
  2969. + (numberOfRows * getDefaultRowHeight())
  2970. - viewportBottom < getDefaultRowHeight()) {
  2971. /*
  2972. * We're at the end of the row container, everything is
  2973. * added to the top.
  2974. */
  2975. /*
  2976. * FIXME [[spacer]]: above if-clause is coded to only
  2977. * work with default row heights - will not work with
  2978. * variable row heights
  2979. */
  2980. paintRemoveRowsAtBottom(removedLogicalInside,
  2981. removedVisualInside);
  2982. updateTopRowLogicalIndex(
  2983. -removedLogicalInside.length());
  2984. } else {
  2985. /*
  2986. * We're in a combination, where we need to both scroll
  2987. * up AND show new rows at the bottom.
  2988. *
  2989. * Example: Scrolled down to show the second to last
  2990. * row. Remove two. Viewport scrolls up, revealing the
  2991. * row above row. The last element collapses up and into
  2992. * view.
  2993. *
  2994. * Reminder: this use case handles only the case when
  2995. * there are enough escalator rows to still render a
  2996. * full view. I.e. all escalator rows will _always_ be
  2997. * populated
  2998. */
  2999. /*-
  3000. * 1 1 |1| <- newly rendered
  3001. * |2| |2| |2|
  3002. * |3| ==> |*| ==> |5| <- newly rendered
  3003. * |4| |*|
  3004. * 5 5
  3005. *
  3006. * 1 1 |1| <- newly rendered
  3007. * |2| |*| |4|
  3008. * |3| ==> |*| ==> |5| <- newly rendered
  3009. * |4| |4|
  3010. * 5 5
  3011. */
  3012. /*
  3013. * STEP 1:
  3014. *
  3015. * reorganize deprecated escalator rows to bottom, but
  3016. * don't re-render anything yet
  3017. */
  3018. /*-
  3019. * 1 1 1
  3020. * |2| |*| |4|
  3021. * |3| ==> |*| ==> |*|
  3022. * |4| |4| |*|
  3023. * 5 5 5
  3024. */
  3025. double newTop = getRowTop(visualRowOrder
  3026. .get(removedVisualInside.getStart()));
  3027. for (int i = 0; i < removedVisualInside.length(); i++) {
  3028. final TableRowElement tr = visualRowOrder
  3029. .remove(removedVisualInside.getStart());
  3030. visualRowOrder.addLast(tr);
  3031. }
  3032. for (int i = removedVisualInside
  3033. .getStart(); i < escalatorRowCount; i++) {
  3034. final TableRowElement tr = visualRowOrder.get(i);
  3035. setRowPosition(tr, 0, (int) newTop);
  3036. newTop += getDefaultRowHeight();
  3037. newTop += spacerContainer.getSpacerHeight(
  3038. i + removedLogicalInside.getStart());
  3039. }
  3040. /*
  3041. * STEP 2:
  3042. *
  3043. * manually scroll
  3044. */
  3045. /*-
  3046. * 1 |1| <-- newly rendered (by scrolling)
  3047. * |4| |4|
  3048. * |*| ==> |*|
  3049. * |*|
  3050. * 5 5
  3051. */
  3052. final double newScrollTop = contentBottom
  3053. - getHeightOfSection();
  3054. setScrollTop(newScrollTop);
  3055. /*
  3056. * Manually call the scroll handler, so we get immediate
  3057. * effects in the escalator.
  3058. */
  3059. scroller.onScroll();
  3060. /*
  3061. * Move the bottommost (n+1:th) escalator row to top,
  3062. * because scrolling up doesn't handle that for us
  3063. * automatically
  3064. */
  3065. moveAndUpdateEscalatorRows(
  3066. Range.withOnly(escalatorRowCount - 1), 0,
  3067. getLogicalRowIndex(visualRowOrder.getFirst())
  3068. - 1);
  3069. updateTopRowLogicalIndex(-1);
  3070. /*
  3071. * STEP 3:
  3072. *
  3073. * update remaining escalator rows
  3074. */
  3075. /*-
  3076. * |1| |1|
  3077. * |4| ==> |4|
  3078. * |*| |5| <-- newly rendered
  3079. *
  3080. * 5
  3081. */
  3082. final int rowsScrolled = (int) (Math
  3083. .ceil((viewportBottom - contentBottom)
  3084. / getDefaultRowHeight()));
  3085. final int start = escalatorRowCount
  3086. - (removedVisualInside.length() - rowsScrolled);
  3087. final Range visualRefreshRange = Range.between(start,
  3088. escalatorRowCount);
  3089. final int logicalTargetIndex = getLogicalRowIndex(
  3090. visualRowOrder.getFirst()) + start;
  3091. // in-place move simply re-renders the rows.
  3092. moveAndUpdateEscalatorRows(visualRefreshRange, start,
  3093. logicalTargetIndex);
  3094. }
  3095. }
  3096. fireRowVisibilityChangeEvent();
  3097. sortDomElements();
  3098. }
  3099. updateTopRowLogicalIndex(-removedAbove.length());
  3100. /*
  3101. * this needs to be done after the escalator has been shrunk down,
  3102. * or it won't work correctly (due to setScrollTop invocation)
  3103. */
  3104. scroller.recalculateScrollbarsForVirtualViewport();
  3105. }
  3106. private void paintRemoveRowsAtMiddle(final Range removedLogicalInside,
  3107. final Range removedVisualInside, final int logicalOffset) {
  3108. /*-
  3109. * : : :
  3110. * |2| |2| |2|
  3111. * |3| ==> |*| ==> |4|
  3112. * |4| |4| |6| <- newly rendered
  3113. * : : :
  3114. */
  3115. final int escalatorRowCount = visualRowOrder.size();
  3116. final int logicalTargetIndex = getLogicalRowIndex(
  3117. visualRowOrder.getLast())
  3118. - (removedVisualInside.length() - 1) + logicalOffset;
  3119. moveAndUpdateEscalatorRows(removedVisualInside, escalatorRowCount,
  3120. logicalTargetIndex);
  3121. // move the surrounding rows to their correct places.
  3122. final ListIterator<TableRowElement> iterator = visualRowOrder
  3123. .listIterator(removedVisualInside.getStart());
  3124. double rowTop = getRowTop(
  3125. removedLogicalInside.getStart() + logicalOffset);
  3126. for (int i = removedVisualInside.getStart(); i < escalatorRowCount
  3127. - removedVisualInside.length(); i++) {
  3128. final TableRowElement tr = iterator.next();
  3129. setRowPosition(tr, 0, rowTop);
  3130. rowTop += getDefaultRowHeight();
  3131. rowTop += spacerContainer
  3132. .getSpacerHeight(i + removedLogicalInside.getStart());
  3133. }
  3134. }
  3135. private void paintRemoveRowsAtBottom(final Range removedLogicalInside,
  3136. final Range removedVisualInside) {
  3137. /*-
  3138. * :
  3139. * : : |4| <- newly rendered
  3140. * |5| |5| |5|
  3141. * |6| ==> |*| ==> |7|
  3142. * |7| |7|
  3143. */
  3144. final int logicalTargetIndex = getLogicalRowIndex(
  3145. visualRowOrder.getFirst()) - removedVisualInside.length();
  3146. moveAndUpdateEscalatorRows(removedVisualInside, 0,
  3147. logicalTargetIndex);
  3148. // move the surrounding rows to their correct places.
  3149. int firstUpdatedIndex = removedVisualInside.getEnd();
  3150. final ListIterator<TableRowElement> iterator = visualRowOrder
  3151. .listIterator(firstUpdatedIndex);
  3152. double rowTop = getRowTop(removedLogicalInside.getStart());
  3153. int i = 0;
  3154. while (iterator.hasNext()) {
  3155. final TableRowElement tr = iterator.next();
  3156. setRowPosition(tr, 0, rowTop);
  3157. rowTop += getDefaultRowHeight();
  3158. rowTop += spacerContainer
  3159. .getSpacerHeight(firstUpdatedIndex + i++);
  3160. }
  3161. }
  3162. @Override
  3163. public int getLogicalRowIndex(final TableRowElement tr) {
  3164. assert tr
  3165. .getParentNode() == root : "The given element isn't a row element in the body";
  3166. int internalIndex = visualRowOrder.indexOf(tr);
  3167. return getTopRowLogicalIndex() + internalIndex;
  3168. }
  3169. @Override
  3170. protected void recalculateSectionHeight() {
  3171. // NOOP for body, since it doesn't make any sense.
  3172. }
  3173. /**
  3174. * Adjusts the row index and number to be relevant for the current
  3175. * virtual viewport.
  3176. * <p>
  3177. * It converts a logical range of rows index to the matching visual
  3178. * range, truncating the resulting range with the viewport.
  3179. * <p>
  3180. * <ul>
  3181. * <li>Escalator contains logical rows 0..100
  3182. * <li>Current viewport showing logical rows 20..29
  3183. * <li>convertToVisual([20..29]) &rarr; [0..9]
  3184. * <li>convertToVisual([15..24]) &rarr; [0..4]
  3185. * <li>convertToVisual([25..29]) &rarr; [5..9]
  3186. * <li>convertToVisual([26..39]) &rarr; [6..9]
  3187. * <li>convertToVisual([0..5]) &rarr; [0..-1] <em>(empty)</em>
  3188. * <li>convertToVisual([35..1]) &rarr; [0..-1] <em>(empty)</em>
  3189. * <li>convertToVisual([0..100]) &rarr; [0..9]
  3190. * </ul>
  3191. *
  3192. * @return a logical range converted to a visual range, truncated to the
  3193. * current viewport. The first visual row has the index 0.
  3194. */
  3195. private Range convertToVisual(final Range logicalRange) {
  3196. if (logicalRange.isEmpty()) {
  3197. return logicalRange;
  3198. } else if (visualRowOrder.isEmpty()) {
  3199. // empty range
  3200. return Range.withLength(0, 0);
  3201. }
  3202. /*
  3203. * TODO [[spacer]]: these assumptions will be totally broken with
  3204. * spacers.
  3205. */
  3206. final int maxVisibleRowCount = getMaxVisibleRowCount();
  3207. final int currentTopRowIndex = getLogicalRowIndex(
  3208. visualRowOrder.getFirst());
  3209. final Range[] partitions = logicalRange.partitionWith(
  3210. Range.withLength(currentTopRowIndex, maxVisibleRowCount));
  3211. final Range insideRange = partitions[1];
  3212. return insideRange.offsetBy(-currentTopRowIndex);
  3213. }
  3214. @Override
  3215. protected String getCellElementTagName() {
  3216. return "td";
  3217. }
  3218. @Override
  3219. protected double getHeightOfSection() {
  3220. final int tableHeight = tableWrapper.getOffsetHeight();
  3221. final double footerHeight = footer.getHeightOfSection();
  3222. final double headerHeight = header.getHeightOfSection();
  3223. double heightOfSection = tableHeight - footerHeight - headerHeight;
  3224. return Math.max(0, heightOfSection);
  3225. }
  3226. @Override
  3227. protected void refreshCells(Range logicalRowRange, Range colRange) {
  3228. Profiler.enter("Escalator.BodyRowContainer.refreshRows");
  3229. final Range visualRange = convertToVisual(logicalRowRange);
  3230. if (!visualRange.isEmpty()) {
  3231. final int firstLogicalRowIndex = getLogicalRowIndex(
  3232. visualRowOrder.getFirst());
  3233. for (int rowNumber = visualRange
  3234. .getStart(); rowNumber < visualRange
  3235. .getEnd(); rowNumber++) {
  3236. refreshRow(visualRowOrder.get(rowNumber),
  3237. firstLogicalRowIndex + rowNumber, colRange);
  3238. }
  3239. }
  3240. Profiler.leave("Escalator.BodyRowContainer.refreshRows");
  3241. }
  3242. @Override
  3243. protected TableRowElement getTrByVisualIndex(final int index)
  3244. throws IndexOutOfBoundsException {
  3245. if (index >= 0 && index < visualRowOrder.size()) {
  3246. return visualRowOrder.get(index);
  3247. } else {
  3248. throw new IndexOutOfBoundsException(
  3249. "No such visual index: " + index);
  3250. }
  3251. }
  3252. @Override
  3253. public TableRowElement getRowElement(int index) {
  3254. if (index < 0 || index >= getRowCount()) {
  3255. throw new IndexOutOfBoundsException(
  3256. "No such logical index: " + index);
  3257. }
  3258. int visualIndex = index
  3259. - getLogicalRowIndex(visualRowOrder.getFirst());
  3260. if (visualIndex >= 0 && visualIndex < visualRowOrder.size()) {
  3261. return super.getRowElement(visualIndex);
  3262. } else {
  3263. throw new IllegalStateException("Row with logical index "
  3264. + index + " is currently not available in the DOM");
  3265. }
  3266. }
  3267. private void setBodyScrollPosition(final double scrollLeft,
  3268. final double scrollTop) {
  3269. tBodyScrollLeft = scrollLeft;
  3270. tBodyScrollTop = scrollTop;
  3271. position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop);
  3272. position.set(spacerDecoContainer, 0, -tBodyScrollTop);
  3273. }
  3274. /**
  3275. * Make sure that there is a correct amount of escalator rows: Add more
  3276. * if needed, or remove any superfluous ones.
  3277. * <p>
  3278. * This method should be called when e.g. the height of the Escalator
  3279. * changes.
  3280. * <p>
  3281. * <em>Note:</em> This method will make sure that the escalator rows are
  3282. * placed in the proper places. By default new rows are added below, but
  3283. * if the content is scrolled down, the rows are populated on top
  3284. * instead.
  3285. */
  3286. public void verifyEscalatorCount() {
  3287. /*
  3288. * This method indeed has a smell very similar to paintRemoveRows
  3289. * and paintInsertRows.
  3290. *
  3291. * Unfortunately, those the code can't trivially be shared, since
  3292. * there are some slight differences in the respective
  3293. * responsibilities. The "paint" methods fake the addition and
  3294. * removal of rows, and make sure to either push existing data out
  3295. * of view, or draw new data into view. Only in some special cases
  3296. * will the DOM element count change.
  3297. *
  3298. * This method, however, has the explicit responsibility to verify
  3299. * that when "something" happens, we still have the correct amount
  3300. * of escalator rows in the DOM, and if not, we make sure to modify
  3301. * that count. Only in some special cases do we need to take into
  3302. * account other things than simply modifying the DOM element count.
  3303. */
  3304. Profiler.enter("Escalator.BodyRowContainer.verifyEscalatorCount");
  3305. if (!isAttached()) {
  3306. return;
  3307. }
  3308. final int maxVisibleRowCount = getMaxVisibleRowCount();
  3309. final int neededEscalatorRows = Math.min(maxVisibleRowCount,
  3310. body.getRowCount());
  3311. final int neededEscalatorRowsDiff = neededEscalatorRows
  3312. - visualRowOrder.size();
  3313. if (neededEscalatorRowsDiff > 0) {
  3314. // needs more
  3315. /*
  3316. * This is a workaround for the issue where we might be scrolled
  3317. * to the bottom, and the widget expands beyond the content
  3318. * range
  3319. */
  3320. final int index = visualRowOrder.size();
  3321. final int nextLastLogicalIndex;
  3322. if (!visualRowOrder.isEmpty()) {
  3323. nextLastLogicalIndex = getLogicalRowIndex(
  3324. visualRowOrder.getLast()) + 1;
  3325. } else {
  3326. nextLastLogicalIndex = 0;
  3327. }
  3328. final boolean contentWillFit = nextLastLogicalIndex < getRowCount()
  3329. - neededEscalatorRowsDiff;
  3330. if (contentWillFit) {
  3331. final List<TableRowElement> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
  3332. index, neededEscalatorRowsDiff);
  3333. /*
  3334. * Since fillAndPopulateEscalatorRowsIfNeeded operates on
  3335. * the assumption that index == visual index == logical
  3336. * index, we thank for the added escalator rows, but since
  3337. * they're painted in the wrong CSS position, we need to
  3338. * move them to their actual locations.
  3339. *
  3340. * Note: this is the second (see body.paintInsertRows)
  3341. * occasion where fillAndPopulateEscalatorRowsIfNeeded would
  3342. * behave "more correctly" if it only would add escalator
  3343. * rows to the DOM and appropriate bookkeping, and not
  3344. * actually populate them :/
  3345. */
  3346. moveAndUpdateEscalatorRows(
  3347. Range.withLength(index, addedRows.size()), index,
  3348. nextLastLogicalIndex);
  3349. } else {
  3350. /*
  3351. * TODO [[optimize]]
  3352. *
  3353. * We're scrolled so far down that all rows can't be simply
  3354. * appended at the end, since we might start displaying
  3355. * escalator rows that don't exist. To avoid the mess that
  3356. * is body.paintRemoveRows, this is a dirty hack that dumbs
  3357. * the problem down to a more basic and already-solved
  3358. * problem:
  3359. *
  3360. * 1) scroll all the way up 2) add the missing escalator
  3361. * rows 3) scroll back to the original position.
  3362. *
  3363. * Letting the browser scroll back to our original position
  3364. * will automatically solve any possible overflow problems,
  3365. * since the browser will not allow us to scroll beyond the
  3366. * actual content.
  3367. */
  3368. final double oldScrollTop = getScrollTop();
  3369. setScrollTop(0);
  3370. scroller.onScroll();
  3371. fillAndPopulateEscalatorRowsIfNeeded(index,
  3372. neededEscalatorRowsDiff);
  3373. setScrollTop(oldScrollTop);
  3374. scroller.onScroll();
  3375. }
  3376. } else if (neededEscalatorRowsDiff < 0) {
  3377. // needs less
  3378. final ListIterator<TableRowElement> iter = visualRowOrder
  3379. .listIterator(visualRowOrder.size());
  3380. for (int i = 0; i < -neededEscalatorRowsDiff; i++) {
  3381. final Element last = iter.previous();
  3382. last.removeFromParent();
  3383. iter.remove();
  3384. }
  3385. /*
  3386. * If we were scrolled to the bottom so that we didn't have an
  3387. * extra escalator row at the bottom, we'll probably end up with
  3388. * blank space at the bottom of the escalator, and one extra row
  3389. * above the header.
  3390. *
  3391. * Experimentation idea #1: calculate "scrollbottom" vs content
  3392. * bottom and remove one row from top, rest from bottom. This
  3393. * FAILED, since setHeight has already happened, thus we never
  3394. * will detect ourselves having been scrolled all the way to the
  3395. * bottom.
  3396. */
  3397. if (!visualRowOrder.isEmpty()) {
  3398. final double firstRowTop = getRowTop(
  3399. visualRowOrder.getFirst());
  3400. final double firstRowMinTop = tBodyScrollTop
  3401. - getDefaultRowHeight();
  3402. if (firstRowTop < firstRowMinTop) {
  3403. final int newLogicalIndex = getLogicalRowIndex(
  3404. visualRowOrder.getLast()) + 1;
  3405. moveAndUpdateEscalatorRows(Range.withOnly(0),
  3406. visualRowOrder.size(), newLogicalIndex);
  3407. updateTopRowLogicalIndex(1);
  3408. }
  3409. }
  3410. }
  3411. if (neededEscalatorRowsDiff != 0) {
  3412. fireRowVisibilityChangeEvent();
  3413. }
  3414. Profiler.leave("Escalator.BodyRowContainer.verifyEscalatorCount");
  3415. }
  3416. @Override
  3417. protected void reapplyDefaultRowHeights() {
  3418. if (visualRowOrder.isEmpty()) {
  3419. return;
  3420. }
  3421. Profiler.enter(
  3422. "Escalator.BodyRowContainer.reapplyDefaultRowHeights");
  3423. double spacerHeights = 0;
  3424. /* step 1: resize and reposition rows */
  3425. for (int i = 0; i < visualRowOrder.size(); i++) {
  3426. TableRowElement tr = visualRowOrder.get(i);
  3427. reapplyRowHeight(tr, getDefaultRowHeight());
  3428. final int logicalIndex = getTopRowLogicalIndex() + i;
  3429. setRowPosition(tr, 0,
  3430. logicalIndex * getDefaultRowHeight() + spacerHeights);
  3431. spacerHeights += spacerContainer.getSpacerHeight(logicalIndex);
  3432. }
  3433. /*
  3434. * step 2: move scrollbar so that it corresponds to its previous
  3435. * place
  3436. */
  3437. /*
  3438. * This ratio needs to be calculated with the scrollsize (not max
  3439. * scroll position) in order to align the top row with the new
  3440. * scroll position.
  3441. */
  3442. double scrollRatio = verticalScrollbar.getScrollPos()
  3443. / verticalScrollbar.getScrollSize();
  3444. scroller.recalculateScrollbarsForVirtualViewport();
  3445. verticalScrollbar.setScrollPos((int) (getDefaultRowHeight()
  3446. * getRowCount() * scrollRatio));
  3447. setBodyScrollPosition(horizontalScrollbar.getScrollPos(),
  3448. verticalScrollbar.getScrollPos());
  3449. scroller.onScroll();
  3450. /*
  3451. * step 3: make sure we have the correct amount of escalator rows.
  3452. */
  3453. verifyEscalatorCount();
  3454. int logicalLogical = (int) (getRowTop(visualRowOrder.getFirst())
  3455. / getDefaultRowHeight());
  3456. setTopRowLogicalIndex(logicalLogical);
  3457. Profiler.leave(
  3458. "Escalator.BodyRowContainer.reapplyDefaultRowHeights");
  3459. }
  3460. /**
  3461. * Sorts the rows in the DOM to correspond to the visual order.
  3462. *
  3463. * @see #visualRowOrder
  3464. */
  3465. private void sortDomElements() {
  3466. final String profilingName = "Escalator.BodyRowContainer.sortDomElements";
  3467. Profiler.enter(profilingName);
  3468. /*
  3469. * Focus is lost from an element if that DOM element is (or any of
  3470. * its parents are) removed from the document. Therefore, we sort
  3471. * everything around that row instead.
  3472. */
  3473. final TableRowElement focusedRow = getRowWithFocus();
  3474. if (focusedRow != null) {
  3475. assert focusedRow
  3476. .getParentElement() == root : "Trying to sort around a row that doesn't exist in body";
  3477. assert visualRowOrder.contains(focusedRow)
  3478. || body.spacerContainer.isSpacer(
  3479. focusedRow) : "Trying to sort around a row that doesn't exist in visualRowOrder or is not a spacer.";
  3480. }
  3481. /*
  3482. * Two cases handled simultaneously:
  3483. *
  3484. * 1) No focus on rows. We iterate visualRowOrder backwards, and
  3485. * take the respective element in the DOM, and place it as the first
  3486. * child in the body element. Then we take the next-to-last from
  3487. * visualRowOrder, and put that first, pushing the previous row as
  3488. * the second child. And so on...
  3489. *
  3490. * 2) Focus on some row within Escalator body. Again, we iterate
  3491. * visualRowOrder backwards. This time, we use the focused row as a
  3492. * pivot: Instead of placing rows from the bottom of visualRowOrder
  3493. * and placing it first, we place it underneath the focused row.
  3494. * Once we hit the focused row, we don't move it (to not reset
  3495. * focus) but change sorting mode. After that, we place all rows as
  3496. * the first child.
  3497. */
  3498. List<TableRowElement> orderedBodyRows = new ArrayList<>(
  3499. visualRowOrder);
  3500. Map<Integer, SpacerContainer.SpacerImpl> spacers = body.spacerContainer
  3501. .getSpacers();
  3502. /*
  3503. * Start at -1 to include a spacer that is rendered above the
  3504. * viewport, but its parent row is still not shown
  3505. */
  3506. for (int i = -1; i < visualRowOrder.size(); i++) {
  3507. SpacerContainer.SpacerImpl spacer = spacers
  3508. .remove(Integer.valueOf(getTopRowLogicalIndex() + i));
  3509. if (spacer != null) {
  3510. orderedBodyRows.add(i + 1, spacer.getRootElement());
  3511. spacer.show();
  3512. }
  3513. }
  3514. /*
  3515. * At this point, invisible spacers aren't reordered, so their
  3516. * position in the DOM will remain undefined.
  3517. */
  3518. // If a spacer was not reordered, it means that it's out of view.
  3519. for (SpacerContainer.SpacerImpl unmovedSpacer : spacers.values()) {
  3520. unmovedSpacer.hide();
  3521. }
  3522. /*
  3523. * If we have a focused row, start in the mode where we put
  3524. * everything underneath that row. Otherwise, all rows are placed as
  3525. * first child.
  3526. */
  3527. boolean insertFirst = (focusedRow == null);
  3528. final ListIterator<TableRowElement> i = orderedBodyRows
  3529. .listIterator(orderedBodyRows.size());
  3530. while (i.hasPrevious()) {
  3531. TableRowElement tr = i.previous();
  3532. if (tr == focusedRow) {
  3533. insertFirst = true;
  3534. } else if (insertFirst) {
  3535. // remove row explicitly to work around an IE11 bug (#9850)
  3536. if (BrowserInfo.get().isIE11()
  3537. && tr.equals(root.getFirstChildElement())) {
  3538. root.removeChild(tr);
  3539. }
  3540. root.insertFirst(tr);
  3541. } else {
  3542. root.insertAfter(tr, focusedRow);
  3543. }
  3544. }
  3545. Profiler.leave(profilingName);
  3546. }
  3547. /**
  3548. * Get the {@literal <tbody>} row that contains (or has) focus.
  3549. *
  3550. * @return The {@literal <tbody>} row that contains a focused DOM
  3551. * element, or <code>null</code> if focus is outside of a body
  3552. * row.
  3553. */
  3554. private TableRowElement getRowWithFocus() {
  3555. TableRowElement rowContainingFocus = null;
  3556. final Element focusedElement = WidgetUtil.getFocusedElement();
  3557. if (focusedElement != null && root.isOrHasChild(focusedElement)) {
  3558. Element e = focusedElement;
  3559. while (e != null && e != root) {
  3560. /*
  3561. * You never know if there's several tables embedded in a
  3562. * cell... We'll take the deepest one.
  3563. */
  3564. if (TableRowElement.is(e)) {
  3565. rowContainingFocus = TableRowElement.as(e);
  3566. }
  3567. e = e.getParentElement();
  3568. }
  3569. }
  3570. return rowContainingFocus;
  3571. }
  3572. @Override
  3573. public Cell getCell(Element element) {
  3574. Cell cell = super.getCell(element);
  3575. if (cell == null) {
  3576. return null;
  3577. }
  3578. // Convert DOM coordinates to logical coordinates for rows
  3579. TableRowElement rowElement = (TableRowElement) cell.getElement()
  3580. .getParentElement();
  3581. return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(),
  3582. cell.getElement());
  3583. }
  3584. @Override
  3585. public void setSpacer(int rowIndex, double height)
  3586. throws IllegalArgumentException {
  3587. spacerContainer.setSpacer(rowIndex, height);
  3588. }
  3589. @Override
  3590. public boolean spacerExists(int rowIndex) {
  3591. return spacerContainer.spacerExists(rowIndex);
  3592. }
  3593. @Override
  3594. public void setSpacerUpdater(SpacerUpdater spacerUpdater)
  3595. throws IllegalArgumentException {
  3596. spacerContainer.setSpacerUpdater(spacerUpdater);
  3597. }
  3598. @Override
  3599. public SpacerUpdater getSpacerUpdater() {
  3600. return spacerContainer.getSpacerUpdater();
  3601. }
  3602. /**
  3603. * <em>Calculates</em> the correct top position of a row at a logical
  3604. * index, regardless if there is one there or not.
  3605. * <p>
  3606. * A correct result requires that both {@link #getDefaultRowHeight()} is
  3607. * consistent, and the placement and height of all spacers above the
  3608. * given logical index are consistent.
  3609. *
  3610. * @param logicalIndex
  3611. * the logical index of the row for which to calculate the
  3612. * top position
  3613. * @return the position at which to place a row in {@code logicalIndex}
  3614. * @see #getRowTop(TableRowElement)
  3615. */
  3616. private double getRowTop(int logicalIndex) {
  3617. double top = spacerContainer
  3618. .getSpacerHeightsSumUntilIndex(logicalIndex);
  3619. return top + (logicalIndex * getDefaultRowHeight());
  3620. }
  3621. public void shiftRowPositions(int row, double diff) {
  3622. for (TableRowElement tr : getVisibleRowsAfter(row)) {
  3623. setRowPosition(tr, 0, getRowTop(tr) + diff);
  3624. }
  3625. }
  3626. private List<TableRowElement> getVisibleRowsAfter(int logicalRow) {
  3627. Range visibleRowLogicalRange = getVisibleRowRange();
  3628. boolean allRowsAreInView = logicalRow < visibleRowLogicalRange
  3629. .getStart();
  3630. boolean noRowsAreInView = logicalRow >= visibleRowLogicalRange
  3631. .getEnd() - 1;
  3632. if (allRowsAreInView) {
  3633. return Collections.unmodifiableList(visualRowOrder);
  3634. } else if (noRowsAreInView) {
  3635. return Collections.emptyList();
  3636. } else {
  3637. int fromIndex = (logicalRow - visibleRowLogicalRange.getStart())
  3638. + 1;
  3639. int toIndex = visibleRowLogicalRange.length();
  3640. List<TableRowElement> sublist = visualRowOrder
  3641. .subList(fromIndex, toIndex);
  3642. return Collections.unmodifiableList(sublist);
  3643. }
  3644. }
  3645. @Override
  3646. public int getDomRowCount() {
  3647. return root.getChildCount()
  3648. - spacerContainer.getSpacersInDom().size();
  3649. }
  3650. @Override
  3651. protected boolean rowCanBeFrozen(TableRowElement tr) {
  3652. return visualRowOrder.contains(tr);
  3653. }
  3654. void reapplySpacerWidths() {
  3655. spacerContainer.reapplySpacerWidths();
  3656. }
  3657. void scrollToSpacer(int spacerIndex, ScrollDestination destination,
  3658. int padding) {
  3659. spacerContainer.scrollToSpacer(spacerIndex, destination, padding);
  3660. }
  3661. @Override
  3662. public void setNewRowCallback(
  3663. Consumer<List<TableRowElement>> callback) {
  3664. newEscalatorRowCallback = callback;
  3665. }
  3666. }
  3667. private class ColumnConfigurationImpl implements ColumnConfiguration {
  3668. public class Column {
  3669. public static final double DEFAULT_COLUMN_WIDTH_PX = 100;
  3670. private double definedWidth = -1;
  3671. private double calculatedWidth = DEFAULT_COLUMN_WIDTH_PX;
  3672. private boolean measuringRequested = false;
  3673. public void setWidth(double px) {
  3674. Profiler.enter(
  3675. "Escalator.ColumnConfigurationImpl.Column.setWidth");
  3676. definedWidth = px;
  3677. if (px < 0) {
  3678. if (isAttached()) {
  3679. calculateWidth();
  3680. } else {
  3681. /*
  3682. * the column's width is calculated at Escalator.onLoad
  3683. * via measureAndSetWidthIfNeeded!
  3684. */
  3685. measuringRequested = true;
  3686. }
  3687. } else {
  3688. calculatedWidth = px;
  3689. }
  3690. Profiler.leave(
  3691. "Escalator.ColumnConfigurationImpl.Column.setWidth");
  3692. }
  3693. public double getDefinedWidth() {
  3694. return definedWidth;
  3695. }
  3696. /**
  3697. * Returns the actual width in the DOM.
  3698. *
  3699. * @return the width in pixels in the DOM. Returns -1 if the column
  3700. * needs measuring, but has not been yet measured
  3701. */
  3702. public double getCalculatedWidth() {
  3703. /*
  3704. * This might return an untrue value (e.g. during init/onload),
  3705. * since we haven't had a proper chance to actually calculate
  3706. * widths yet.
  3707. *
  3708. * This is fixed during Escalator.onLoad, by the call to
  3709. * "measureAndSetWidthIfNeeded", which fixes "everything".
  3710. */
  3711. if (!measuringRequested) {
  3712. return calculatedWidth;
  3713. } else {
  3714. return -1;
  3715. }
  3716. }
  3717. /**
  3718. * Checks if the column needs measuring, and then measures it.
  3719. * <p>
  3720. * Called by {@link Escalator#onLoad()}.
  3721. */
  3722. public boolean measureAndSetWidthIfNeeded() {
  3723. assert isAttached() : "Column.measureAndSetWidthIfNeeded() was called even though Escalator was not attached!";
  3724. if (measuringRequested) {
  3725. measuringRequested = false;
  3726. setWidth(definedWidth);
  3727. return true;
  3728. }
  3729. return false;
  3730. }
  3731. private void calculateWidth() {
  3732. calculatedWidth = getMaxCellWidth(columns.indexOf(this));
  3733. }
  3734. }
  3735. private final List<Column> columns = new ArrayList<>();
  3736. private int frozenColumns = 0;
  3737. /*
  3738. * TODO: this is a bit of a duplicate functionality with the
  3739. * Column.calculatedWidth caching. Probably should use one or the other,
  3740. * not both
  3741. */
  3742. /**
  3743. * A cached array of all the calculated column widths.
  3744. *
  3745. * @see #getCalculatedColumnWidths()
  3746. */
  3747. private double[] widthsArray = null;
  3748. /**
  3749. * {@inheritDoc}
  3750. * <p>
  3751. * <em>Implementation detail:</em> This method does no DOM modifications
  3752. * (i.e. is very cheap to call) if there are no rows in the DOM when
  3753. * this method is called.
  3754. *
  3755. * @see #hasSomethingInDom()
  3756. */
  3757. @Override
  3758. public void removeColumns(final int index, final int numberOfColumns) {
  3759. if (numberOfColumns == 0) {
  3760. return;
  3761. }
  3762. // Validate
  3763. assertArgumentsAreValidAndWithinRange(index, numberOfColumns);
  3764. // Move the horizontal scrollbar to the left, if removed columns are
  3765. // to the left of the viewport
  3766. removeColumnsAdjustScrollbar(index, numberOfColumns);
  3767. // Remove from DOM
  3768. header.paintRemoveColumns(index, numberOfColumns);
  3769. body.paintRemoveColumns(index, numberOfColumns);
  3770. footer.paintRemoveColumns(index, numberOfColumns);
  3771. // Remove from bookkeeping
  3772. flyweightRow.removeCells(index, numberOfColumns);
  3773. columns.subList(index, index + numberOfColumns).clear();
  3774. // Adjust frozen columns
  3775. if (index < getFrozenColumnCount()) {
  3776. if (index + numberOfColumns < frozenColumns) {
  3777. /*
  3778. * Last removed column was frozen, meaning that all removed
  3779. * columns were frozen. Just decrement the number of frozen
  3780. * columns accordingly.
  3781. */
  3782. frozenColumns -= numberOfColumns;
  3783. } else {
  3784. /*
  3785. * If last removed column was not frozen, we have removed
  3786. * columns beyond the frozen range, so all remaining frozen
  3787. * columns are to the left of the removed columns.
  3788. */
  3789. frozenColumns = index;
  3790. }
  3791. }
  3792. scroller.recalculateScrollbarsForVirtualViewport();
  3793. body.verifyEscalatorCount();
  3794. if (getColumnConfiguration().getColumnCount() > 0) {
  3795. reapplyRowWidths(header);
  3796. reapplyRowWidths(body);
  3797. reapplyRowWidths(footer);
  3798. }
  3799. /*
  3800. * Colspans make any kind of automatic clever content re-rendering
  3801. * impossible: As soon as anything has colspans, removing one might
  3802. * reveal further colspans, modifying the DOM structure once again,
  3803. * ending in a cascade of updates. Because we don't know how the
  3804. * data is updated.
  3805. *
  3806. * So, instead, we don't do anything. The client code is responsible
  3807. * for re-rendering the content (if so desired). Everything Just
  3808. * Works (TM) if colspans aren't used.
  3809. */
  3810. }
  3811. private void reapplyRowWidths(AbstractRowContainer container) {
  3812. if (container.getRowCount() > 0) {
  3813. container.reapplyRowWidths();
  3814. }
  3815. }
  3816. private void removeColumnsAdjustScrollbar(int index,
  3817. int numberOfColumns) {
  3818. if (horizontalScrollbar.getOffsetSize() >= horizontalScrollbar
  3819. .getScrollSize()) {
  3820. return;
  3821. }
  3822. double leftPosOfFirstColumnToRemove = getCalculatedColumnsWidth(
  3823. Range.between(0, index));
  3824. double widthOfColumnsToRemove = getCalculatedColumnsWidth(
  3825. Range.withLength(index, numberOfColumns));
  3826. double scrollLeft = horizontalScrollbar.getScrollPos();
  3827. if (scrollLeft <= leftPosOfFirstColumnToRemove) {
  3828. /*
  3829. * viewport is scrolled to the left of the first removed column,
  3830. * so there's no need to adjust anything
  3831. */
  3832. return;
  3833. }
  3834. double adjustedScrollLeft = Math.max(leftPosOfFirstColumnToRemove,
  3835. scrollLeft - widthOfColumnsToRemove);
  3836. horizontalScrollbar.setScrollPos(adjustedScrollLeft);
  3837. }
  3838. /**
  3839. * Calculate the width of a row, as the sum of columns' widths.
  3840. *
  3841. * @return the width of a row, in pixels
  3842. */
  3843. public double calculateRowWidth() {
  3844. return getCalculatedColumnsWidth(
  3845. Range.between(0, getColumnCount()));
  3846. }
  3847. private void assertArgumentsAreValidAndWithinRange(final int index,
  3848. final int numberOfColumns) {
  3849. if (numberOfColumns < 1) {
  3850. throw new IllegalArgumentException(
  3851. "Number of columns can't be less than 1 (was "
  3852. + numberOfColumns + ")");
  3853. }
  3854. if (index < 0 || index + numberOfColumns > getColumnCount()) {
  3855. throw new IndexOutOfBoundsException("The given "
  3856. + "column range (" + index + ".."
  3857. + (index + numberOfColumns)
  3858. + ") was outside of the current "
  3859. + "number of columns (" + getColumnCount() + ")");
  3860. }
  3861. }
  3862. /**
  3863. * {@inheritDoc}
  3864. * <p>
  3865. * <em>Implementation detail:</em> This method does no DOM modifications
  3866. * (i.e. is very cheap to call) if there is no data for rows when this
  3867. * method is called.
  3868. *
  3869. * @see #hasColumnAndRowData()
  3870. */
  3871. @Override
  3872. public void insertColumns(final int index, final int numberOfColumns) {
  3873. if (numberOfColumns == 0) {
  3874. return;
  3875. }
  3876. // Validate
  3877. if (index < 0 || index > getColumnCount()) {
  3878. throw new IndexOutOfBoundsException("The given index(" + index
  3879. + ") was outside of the current number of columns (0.."
  3880. + getColumnCount() + ")");
  3881. }
  3882. if (numberOfColumns < 1) {
  3883. throw new IllegalArgumentException(
  3884. "Number of columns must be 1 or greater (was "
  3885. + numberOfColumns);
  3886. }
  3887. // Add to bookkeeping
  3888. flyweightRow.addCells(index, numberOfColumns);
  3889. for (int i = 0; i < numberOfColumns; i++) {
  3890. columns.add(index, new Column());
  3891. }
  3892. // Adjust frozen columns
  3893. boolean frozen = index < frozenColumns;
  3894. if (frozen) {
  3895. frozenColumns += numberOfColumns;
  3896. }
  3897. // Add to DOM
  3898. header.paintInsertColumns(index, numberOfColumns, frozen);
  3899. body.paintInsertColumns(index, numberOfColumns, frozen);
  3900. footer.paintInsertColumns(index, numberOfColumns, frozen);
  3901. // this needs to be before the scrollbar adjustment.
  3902. boolean scrollbarWasNeeded = horizontalScrollbar
  3903. .getOffsetSize() < horizontalScrollbar.getScrollSize();
  3904. scroller.recalculateScrollbarsForVirtualViewport();
  3905. boolean scrollbarIsNowNeeded = horizontalScrollbar
  3906. .getOffsetSize() < horizontalScrollbar.getScrollSize();
  3907. if (!scrollbarWasNeeded && scrollbarIsNowNeeded) {
  3908. // This might as a side effect move rows around (when scrolled
  3909. // all the way down) and require the DOM to be up to date, i.e.
  3910. // the column to be added
  3911. body.verifyEscalatorCount();
  3912. }
  3913. // fix initial width
  3914. if (header.getRowCount() > 0 || body.getRowCount() > 0
  3915. || footer.getRowCount() > 0) {
  3916. Map<Integer, Double> colWidths = new HashMap<>();
  3917. Double width = Double.valueOf(Column.DEFAULT_COLUMN_WIDTH_PX);
  3918. for (int i = index; i < index + numberOfColumns; i++) {
  3919. Integer col = Integer.valueOf(i);
  3920. colWidths.put(col, width);
  3921. }
  3922. getColumnConfiguration().setColumnWidths(colWidths);
  3923. }
  3924. // Adjust scrollbar
  3925. double pixelsToInsertedColumn = columnConfiguration
  3926. .getCalculatedColumnsWidth(Range.withLength(0, index));
  3927. final boolean columnsWereAddedToTheLeftOfViewport = scroller.lastScrollLeft > pixelsToInsertedColumn;
  3928. if (columnsWereAddedToTheLeftOfViewport) {
  3929. double insertedColumnsWidth = columnConfiguration
  3930. .getCalculatedColumnsWidth(
  3931. Range.withLength(index, numberOfColumns));
  3932. horizontalScrollbar.setScrollPos(
  3933. scroller.lastScrollLeft + insertedColumnsWidth);
  3934. }
  3935. /*
  3936. * Colspans make any kind of automatic clever content re-rendering
  3937. * impossible: As soon as anything has colspans, adding one might
  3938. * affect surrounding colspans, modifying the DOM structure once
  3939. * again, ending in a cascade of updates. Because we don't know how
  3940. * the data is updated.
  3941. *
  3942. * So, instead, we don't do anything. The client code is responsible
  3943. * for re-rendering the content (if so desired). Everything Just
  3944. * Works (TM) if colspans aren't used.
  3945. */
  3946. }
  3947. @Override
  3948. public int getColumnCount() {
  3949. return columns.size();
  3950. }
  3951. @Override
  3952. public void setFrozenColumnCount(int count)
  3953. throws IllegalArgumentException {
  3954. if (count < 0 || count > getColumnCount()) {
  3955. throw new IllegalArgumentException(
  3956. "count must be between 0 and the current number of columns ("
  3957. + getColumnCount() + ")");
  3958. }
  3959. int oldCount = frozenColumns;
  3960. if (count == oldCount) {
  3961. return;
  3962. }
  3963. frozenColumns = count;
  3964. if (hasSomethingInDom()) {
  3965. // Are we freezing or unfreezing?
  3966. boolean frozen = count > oldCount;
  3967. int firstAffectedCol;
  3968. int firstUnaffectedCol;
  3969. if (frozen) {
  3970. firstAffectedCol = oldCount;
  3971. firstUnaffectedCol = count;
  3972. } else {
  3973. firstAffectedCol = count;
  3974. firstUnaffectedCol = oldCount;
  3975. }
  3976. if (oldCount > 0) {
  3977. header.setColumnLastFrozen(oldCount - 1, false);
  3978. body.setColumnLastFrozen(oldCount - 1, false);
  3979. footer.setColumnLastFrozen(oldCount - 1, false);
  3980. }
  3981. if (count > 0) {
  3982. header.setColumnLastFrozen(count - 1, true);
  3983. body.setColumnLastFrozen(count - 1, true);
  3984. footer.setColumnLastFrozen(count - 1, true);
  3985. }
  3986. for (int col = firstAffectedCol; col < firstUnaffectedCol; col++) {
  3987. header.setColumnFrozen(col, frozen);
  3988. body.setColumnFrozen(col, frozen);
  3989. footer.setColumnFrozen(col, frozen);
  3990. }
  3991. }
  3992. scroller.recalculateScrollbarsForVirtualViewport();
  3993. }
  3994. @Override
  3995. public int getFrozenColumnCount() {
  3996. return frozenColumns;
  3997. }
  3998. @Override
  3999. public void setColumnWidth(int index, double px)
  4000. throws IllegalArgumentException {
  4001. setColumnWidths(Collections.singletonMap(Integer.valueOf(index),
  4002. Double.valueOf(px)));
  4003. }
  4004. @Override
  4005. public void setColumnWidths(Map<Integer, Double> indexWidthMap)
  4006. throws IllegalArgumentException {
  4007. if (indexWidthMap == null) {
  4008. throw new IllegalArgumentException("indexWidthMap was null");
  4009. }
  4010. if (indexWidthMap.isEmpty()) {
  4011. return;
  4012. }
  4013. Profiler.enter("Escalator.ColumnConfigurationImpl.setColumnWidths");
  4014. try {
  4015. for (Entry<Integer, Double> entry : indexWidthMap.entrySet()) {
  4016. int index = entry.getKey().intValue();
  4017. double width = entry.getValue().doubleValue();
  4018. checkValidColumnIndex(index);
  4019. // Not all browsers will accept any fractional size..
  4020. width = WidgetUtil.roundSizeDown(width);
  4021. columns.get(index).setWidth(width);
  4022. }
  4023. widthsArray = null;
  4024. header.reapplyColumnWidths();
  4025. body.reapplyColumnWidths();
  4026. footer.reapplyColumnWidths();
  4027. recalculateElementSizes();
  4028. } finally {
  4029. Profiler.leave(
  4030. "Escalator.ColumnConfigurationImpl.setColumnWidths");
  4031. }
  4032. }
  4033. private void checkValidColumnIndex(int index)
  4034. throws IllegalArgumentException {
  4035. if (!Range.withLength(0, getColumnCount()).contains(index)) {
  4036. throw new IllegalArgumentException("The given column index ("
  4037. + index + ") does not exist");
  4038. }
  4039. }
  4040. @Override
  4041. public double getColumnWidth(int index)
  4042. throws IllegalArgumentException {
  4043. checkValidColumnIndex(index);
  4044. return columns.get(index).getDefinedWidth();
  4045. }
  4046. @Override
  4047. public double getColumnWidthActual(int index) {
  4048. return columns.get(index).getCalculatedWidth();
  4049. }
  4050. private double getMaxCellWidth(int colIndex)
  4051. throws IllegalArgumentException {
  4052. double headerWidth = header.measureMinCellWidth(colIndex, true);
  4053. double bodyWidth = body.measureMinCellWidth(colIndex, true);
  4054. double footerWidth = footer.measureMinCellWidth(colIndex, true);
  4055. double maxWidth = Math.max(headerWidth,
  4056. Math.max(bodyWidth, footerWidth));
  4057. if (maxWidth < 0 && header.getRowCount() == 0
  4058. && body.getRowCount() == 0 && footer.getRowCount() == 0) {
  4059. maxWidth = 0;
  4060. }
  4061. assert maxWidth >= 0 : "Got a negative max width for a column, which should be impossible.";
  4062. return maxWidth;
  4063. }
  4064. private double getMinCellWidth(int colIndex)
  4065. throws IllegalArgumentException {
  4066. double headerWidth = header.measureMinCellWidth(colIndex, false);
  4067. double bodyWidth = body.measureMinCellWidth(colIndex, false);
  4068. double footerWidth = footer.measureMinCellWidth(colIndex, false);
  4069. double minWidth = Math.max(headerWidth,
  4070. Math.max(bodyWidth, footerWidth));
  4071. if (minWidth < 0 && header.getRowCount() == 0
  4072. && body.getRowCount() == 0 && footer.getRowCount() == 0) {
  4073. minWidth = 0;
  4074. }
  4075. assert minWidth >= 0 : "Got a negative min width for a column, which should be impossible.";
  4076. return minWidth;
  4077. }
  4078. /**
  4079. * Calculates the width of the columns in a given range.
  4080. *
  4081. * @param columns
  4082. * the columns to calculate
  4083. * @return the total width of the columns in the given
  4084. * <code>columns</code>
  4085. */
  4086. double getCalculatedColumnsWidth(final Range columns) {
  4087. /*
  4088. * This is an assert instead of an exception, since this is an
  4089. * internal method.
  4090. */
  4091. assert columns
  4092. .isSubsetOf(Range.between(0, getColumnCount())) : "Range "
  4093. + "was outside of current column range (i.e.: "
  4094. + Range.between(0, getColumnCount())
  4095. + ", but was given :" + columns;
  4096. double sum = 0;
  4097. for (int i = columns.getStart(); i < columns.getEnd(); i++) {
  4098. double columnWidthActual = getColumnWidthActual(i);
  4099. sum += columnWidthActual;
  4100. }
  4101. return sum;
  4102. }
  4103. double[] getCalculatedColumnWidths() {
  4104. if (widthsArray == null || widthsArray.length != getColumnCount()) {
  4105. widthsArray = new double[getColumnCount()];
  4106. for (int i = 0; i < columns.size(); i++) {
  4107. widthsArray[i] = columns.get(i).getCalculatedWidth();
  4108. }
  4109. }
  4110. return widthsArray;
  4111. }
  4112. @Override
  4113. public void refreshColumns(int index, int numberOfColumns)
  4114. throws IndexOutOfBoundsException, IllegalArgumentException {
  4115. if (numberOfColumns < 1) {
  4116. throw new IllegalArgumentException(
  4117. "Number of columns must be 1 or greater (was "
  4118. + numberOfColumns + ")");
  4119. }
  4120. if (index < 0 || index + numberOfColumns > getColumnCount()) {
  4121. throw new IndexOutOfBoundsException("The given "
  4122. + "column range (" + index + ".."
  4123. + (index + numberOfColumns)
  4124. + ") was outside of the current number of columns ("
  4125. + getColumnCount() + ")");
  4126. }
  4127. header.refreshColumns(index, numberOfColumns);
  4128. body.refreshColumns(index, numberOfColumns);
  4129. footer.refreshColumns(index, numberOfColumns);
  4130. }
  4131. }
  4132. /**
  4133. * A decision on how to measure a spacer when it is partially within a
  4134. * designated range.
  4135. * <p>
  4136. * The meaning of each value may differ depending on the context it is being
  4137. * used in. Check that particular method's JavaDoc.
  4138. */
  4139. private enum SpacerInclusionStrategy {
  4140. /** A representation of "the entire spacer". */
  4141. COMPLETE,
  4142. /** A representation of "a partial spacer". */
  4143. PARTIAL,
  4144. /** A representation of "no spacer at all". */
  4145. NONE
  4146. }
  4147. private class SpacerContainer {
  4148. /** This is used mainly for testing purposes */
  4149. private static final String SPACER_LOGICAL_ROW_PROPERTY = "vLogicalRow";
  4150. private final class SpacerImpl implements Spacer {
  4151. private TableCellElement spacerElement;
  4152. private TableRowElement root;
  4153. private DivElement deco;
  4154. private int rowIndex;
  4155. private double height = -1;
  4156. private boolean domHasBeenSetup = false;
  4157. private double decoHeight;
  4158. private double defaultCellBorderBottomSize = -1;
  4159. public SpacerImpl(int rowIndex) {
  4160. this.rowIndex = rowIndex;
  4161. root = TableRowElement.as(DOM.createTR());
  4162. spacerElement = TableCellElement.as(DOM.createTD());
  4163. root.appendChild(spacerElement);
  4164. root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex);
  4165. deco = DivElement.as(DOM.createDiv());
  4166. }
  4167. public void setPositionDiff(double x, double y) {
  4168. setPosition(getLeft() + x, getTop() + y);
  4169. }
  4170. public void setupDom(double height) {
  4171. assert !domHasBeenSetup : "DOM can't be set up twice.";
  4172. assert RootPanel.get().getElement().isOrHasChild(
  4173. root) : "Root element should've been attached to the DOM by now.";
  4174. domHasBeenSetup = true;
  4175. getRootElement().getStyle().setWidth(getInnerWidth(), Unit.PX);
  4176. setHeight(height);
  4177. spacerElement
  4178. .setColSpan(getColumnConfiguration().getColumnCount());
  4179. setStylePrimaryName(getStylePrimaryName());
  4180. }
  4181. public TableRowElement getRootElement() {
  4182. return root;
  4183. }
  4184. @Override
  4185. public Element getDecoElement() {
  4186. return deco;
  4187. }
  4188. public void setPosition(double x, double y) {
  4189. positions.set(getRootElement(), x, y);
  4190. positions.set(getDecoElement(), 0,
  4191. y - getSpacerDecoTopOffset());
  4192. }
  4193. private double getSpacerDecoTopOffset() {
  4194. return getBody().getDefaultRowHeight();
  4195. }
  4196. public void setStylePrimaryName(String style) {
  4197. UIObject.setStylePrimaryName(root, style + "-spacer");
  4198. UIObject.setStylePrimaryName(deco, style + "-spacer-deco");
  4199. }
  4200. public void setHeight(double height) {
  4201. assert height >= 0 : "Height must be more >= 0 (was " + height
  4202. + ")";
  4203. final double heightDiff = height - Math.max(0, this.height);
  4204. final double oldHeight = this.height;
  4205. this.height = height;
  4206. // since the spacer might be rendered on top of the previous
  4207. // rows border (done with css), need to increase height the
  4208. // amount of the border thickness
  4209. if (defaultCellBorderBottomSize < 0) {
  4210. defaultCellBorderBottomSize = WidgetUtil
  4211. .getBorderBottomThickness(body
  4212. .getRowElement(
  4213. getVisibleRowRange().getStart())
  4214. .getFirstChildElement());
  4215. }
  4216. root.getStyle().setHeight(height + defaultCellBorderBottomSize,
  4217. Unit.PX);
  4218. // move the visible spacers getRow row onwards.
  4219. shiftSpacerPositionsAfterRow(getRow(), heightDiff);
  4220. /*
  4221. * If we're growing, we'll adjust the scroll size first, then
  4222. * adjust scrolling. If we're shrinking, we do it after the
  4223. * second if-clause.
  4224. */
  4225. boolean spacerIsGrowing = heightDiff > 0;
  4226. if (spacerIsGrowing) {
  4227. verticalScrollbar.setScrollSize(
  4228. verticalScrollbar.getScrollSize() + heightDiff);
  4229. }
  4230. /*
  4231. * Don't modify the scrollbars if we're expanding the -1 spacer
  4232. * while we're scrolled to the top.
  4233. */
  4234. boolean minusOneSpacerException = spacerIsGrowing
  4235. && getRow() == -1 && body.getTopRowLogicalIndex() == 0;
  4236. boolean viewportNeedsScrolling = getRow() < body
  4237. .getTopRowLogicalIndex() && !minusOneSpacerException;
  4238. if (viewportNeedsScrolling) {
  4239. /*
  4240. * We can't use adjustScrollPos here, probably because of a
  4241. * bookkeeping-related race condition.
  4242. *
  4243. * This particular situation is easier, however, since we
  4244. * know exactly how many pixels we need to move (heightDiff)
  4245. * and all elements below the spacer always need to move
  4246. * that pixel amount.
  4247. */
  4248. for (TableRowElement row : body.visualRowOrder) {
  4249. body.setRowPosition(row, 0,
  4250. body.getRowTop(row) + heightDiff);
  4251. }
  4252. double top = getTop();
  4253. double bottom = top + oldHeight;
  4254. double scrollTop = verticalScrollbar.getScrollPos();
  4255. boolean viewportTopIsAtMidSpacer = top < scrollTop
  4256. && scrollTop < bottom;
  4257. final double moveDiff;
  4258. if (viewportTopIsAtMidSpacer && !spacerIsGrowing) {
  4259. /*
  4260. * If the scroll top is in the middle of the modified
  4261. * spacer, we want to scroll the viewport up as usual,
  4262. * but we don't want to scroll past the top of it.
  4263. *
  4264. * Math.max ensures this (remember: the result is going
  4265. * to be negative).
  4266. */
  4267. moveDiff = Math.max(heightDiff, top - scrollTop);
  4268. } else {
  4269. moveDiff = heightDiff;
  4270. }
  4271. body.setBodyScrollPosition(tBodyScrollLeft,
  4272. tBodyScrollTop + moveDiff);
  4273. verticalScrollbar.setScrollPosByDelta(moveDiff);
  4274. } else {
  4275. body.shiftRowPositions(getRow(), heightDiff);
  4276. }
  4277. if (!spacerIsGrowing) {
  4278. verticalScrollbar.setScrollSize(
  4279. verticalScrollbar.getScrollSize() + heightDiff);
  4280. }
  4281. updateDecoratorGeometry(height);
  4282. }
  4283. /** Resizes and places the decorator. */
  4284. private void updateDecoratorGeometry(double detailsHeight) {
  4285. Style style = deco.getStyle();
  4286. decoHeight = detailsHeight + getBody().getDefaultRowHeight();
  4287. style.setHeight(decoHeight, Unit.PX);
  4288. }
  4289. @Override
  4290. public Element getElement() {
  4291. return spacerElement;
  4292. }
  4293. @Override
  4294. public int getRow() {
  4295. return rowIndex;
  4296. }
  4297. public double getHeight() {
  4298. assert height >= 0 : "Height was not previously set by setHeight.";
  4299. return height;
  4300. }
  4301. public double getTop() {
  4302. return positions.getTop(getRootElement());
  4303. }
  4304. public double getLeft() {
  4305. return positions.getLeft(getRootElement());
  4306. }
  4307. /**
  4308. * Sets a new row index for this spacer. Also updates the
  4309. * bookkeeping at {@link SpacerContainer#rowIndexToSpacer}.
  4310. */
  4311. @SuppressWarnings("boxing")
  4312. public void setRowIndex(int rowIndex) {
  4313. SpacerImpl spacer = rowIndexToSpacer.remove(this.rowIndex);
  4314. assert this == spacer : "trying to move an unexpected spacer.";
  4315. int oldIndex = this.rowIndex;
  4316. this.rowIndex = rowIndex;
  4317. root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex);
  4318. rowIndexToSpacer.put(this.rowIndex, this);
  4319. fireEvent(new SpacerIndexChangedEvent(oldIndex, this.rowIndex));
  4320. }
  4321. /**
  4322. * Updates the spacer's visibility parameters, based on whether it
  4323. * is being currently visible or not.
  4324. */
  4325. public void updateVisibility() {
  4326. if (isInViewport()) {
  4327. show();
  4328. } else {
  4329. hide();
  4330. }
  4331. }
  4332. private boolean isInViewport() {
  4333. int top = (int) Math.ceil(getTop());
  4334. int height = (int) Math.floor(getHeight());
  4335. Range location = Range.withLength(top, height);
  4336. return getViewportPixels().intersects(location);
  4337. }
  4338. public void show() {
  4339. getRootElement().getStyle().clearDisplay();
  4340. getDecoElement().getStyle().clearDisplay();
  4341. Escalator.this.fireEvent(
  4342. new SpacerVisibilityChangedEvent(getRow(), true));
  4343. }
  4344. public void hide() {
  4345. getRootElement().getStyle().setDisplay(Display.NONE);
  4346. getDecoElement().getStyle().setDisplay(Display.NONE);
  4347. Escalator.this.fireEvent(
  4348. new SpacerVisibilityChangedEvent(getRow(), false));
  4349. }
  4350. /**
  4351. * Crop the decorator element so that it doesn't overlap the header
  4352. * and footer sections.
  4353. *
  4354. * @param bodyTop
  4355. * the top cordinate of the escalator body
  4356. * @param bodyBottom
  4357. * the bottom cordinate of the escalator body
  4358. * @param decoWidth
  4359. * width of the deco
  4360. */
  4361. private void updateDecoClip(final double bodyTop,
  4362. final double bodyBottom, final double decoWidth) {
  4363. final int top = deco.getAbsoluteTop();
  4364. final int bottom = deco.getAbsoluteBottom();
  4365. /*
  4366. * FIXME
  4367. *
  4368. * Height and its use is a workaround for the issue where
  4369. * coordinates of the deco are not calculated yet. This will
  4370. * prevent a deco from being displayed when it's added to DOM
  4371. */
  4372. final int height = bottom - top;
  4373. if (top < bodyTop || bottom > bodyBottom) {
  4374. final double topClip = Math.max(0.0D, bodyTop - top);
  4375. final double bottomClip = height
  4376. - Math.max(0.0D, bottom - bodyBottom);
  4377. // TODO [optimize] not sure how GWT compiles this
  4378. final String clip = new StringBuilder("rect(")
  4379. .append(topClip).append("px,").append(decoWidth)
  4380. .append("px,").append(bottomClip).append("px,0)")
  4381. .toString();
  4382. deco.getStyle().setProperty("clip", clip);
  4383. } else {
  4384. deco.getStyle().setProperty("clip", "auto");
  4385. }
  4386. }
  4387. }
  4388. private final TreeMap<Integer, SpacerImpl> rowIndexToSpacer = new TreeMap<>();
  4389. private SpacerUpdater spacerUpdater = SpacerUpdater.NULL;
  4390. private final ScrollHandler spacerScroller = new ScrollHandler() {
  4391. private double prevScrollX = 0;
  4392. @Override
  4393. public void onScroll(ScrollEvent event) {
  4394. if (WidgetUtil.pixelValuesEqual(getScrollLeft(), prevScrollX)) {
  4395. return;
  4396. }
  4397. prevScrollX = getScrollLeft();
  4398. for (SpacerImpl spacer : rowIndexToSpacer.values()) {
  4399. spacer.setPosition(prevScrollX, spacer.getTop());
  4400. }
  4401. }
  4402. };
  4403. private HandlerRegistration spacerScrollerRegistration;
  4404. /** Width of the spacers' decos. Calculated once then cached. */
  4405. private double spacerDecoWidth = 0.0D;
  4406. public void setSpacer(int rowIndex, double height)
  4407. throws IllegalArgumentException {
  4408. if (rowIndex < -1 || rowIndex >= getBody().getRowCount()) {
  4409. throw new IllegalArgumentException("invalid row index: "
  4410. + rowIndex + ", while the body only has "
  4411. + getBody().getRowCount() + " rows.");
  4412. }
  4413. if (height >= 0) {
  4414. if (!spacerExists(rowIndex)) {
  4415. insertNewSpacer(rowIndex, height);
  4416. } else {
  4417. updateExistingSpacer(rowIndex, height);
  4418. }
  4419. } else if (spacerExists(rowIndex)) {
  4420. removeSpacer(rowIndex);
  4421. }
  4422. updateSpacerDecosVisibility();
  4423. }
  4424. /** Checks if a given element is a spacer element */
  4425. public boolean isSpacer(Element row) {
  4426. /*
  4427. * If this needs optimization, we could do a more heuristic check
  4428. * based on stylenames and stuff, instead of iterating through the
  4429. * map.
  4430. */
  4431. for (SpacerImpl spacer : rowIndexToSpacer.values()) {
  4432. if (spacer.getRootElement().equals(row)) {
  4433. return true;
  4434. }
  4435. }
  4436. return false;
  4437. }
  4438. @SuppressWarnings("boxing")
  4439. void scrollToSpacer(int spacerIndex, ScrollDestination destination,
  4440. int padding) {
  4441. assert !destination.equals(ScrollDestination.MIDDLE)
  4442. || padding != 0 : "destination/padding check should be done before this method";
  4443. if (!rowIndexToSpacer.containsKey(spacerIndex)) {
  4444. throw new IllegalArgumentException(
  4445. "No spacer open at index " + spacerIndex);
  4446. }
  4447. SpacerImpl spacer = rowIndexToSpacer.get(spacerIndex);
  4448. double targetStartPx = spacer.getTop();
  4449. double targetEndPx = targetStartPx + spacer.getHeight();
  4450. Range viewportPixels = getViewportPixels();
  4451. double viewportStartPx = viewportPixels.getStart();
  4452. double viewportEndPx = viewportPixels.getEnd();
  4453. double scrollTop = getScrollPos(destination, targetStartPx,
  4454. targetEndPx, viewportStartPx, viewportEndPx, padding);
  4455. setScrollTop(scrollTop);
  4456. }
  4457. public void reapplySpacerWidths() {
  4458. // FIXME #16266 , spacers get couple pixels too much because borders
  4459. final double width = getInnerWidth() - spacerDecoWidth;
  4460. for (SpacerImpl spacer : rowIndexToSpacer.values()) {
  4461. spacer.getRootElement().getStyle().setWidth(width, Unit.PX);
  4462. }
  4463. }
  4464. public void paintRemoveSpacers(Range removedRowsRange) {
  4465. removeSpacers(removedRowsRange);
  4466. shiftSpacersByRows(removedRowsRange.getStart(),
  4467. -removedRowsRange.length());
  4468. }
  4469. @SuppressWarnings("boxing")
  4470. public void removeSpacers(Range removedRange) {
  4471. Map<Integer, SpacerImpl> removedSpacers = rowIndexToSpacer.subMap(
  4472. removedRange.getStart(), true, removedRange.getEnd(),
  4473. false);
  4474. if (removedSpacers.isEmpty()) {
  4475. return;
  4476. }
  4477. for (SpacerImpl spacer : removedSpacers.values()) {
  4478. /*
  4479. * [[optimization]] TODO: Each invocation of the setHeight
  4480. * method has a cascading effect in the DOM. if this proves to
  4481. * be slow, the DOM offset could be updated as a batch.
  4482. */
  4483. destroySpacerContent(spacer);
  4484. spacer.setHeight(0); // resets row offsets
  4485. spacer.getRootElement().removeFromParent();
  4486. spacer.getDecoElement().removeFromParent();
  4487. }
  4488. removedSpacers.clear();
  4489. if (rowIndexToSpacer.isEmpty()) {
  4490. assert spacerScrollerRegistration != null : "Spacer scroller registration was null";
  4491. spacerScrollerRegistration.removeHandler();
  4492. spacerScrollerRegistration = null;
  4493. }
  4494. }
  4495. public Map<Integer, SpacerImpl> getSpacers() {
  4496. return new HashMap<>(rowIndexToSpacer);
  4497. }
  4498. /**
  4499. * Calculates the sum of all spacers.
  4500. *
  4501. * @return sum of all spacers, or 0 if no spacers present
  4502. */
  4503. public double getSpacerHeightsSum() {
  4504. return getHeights(rowIndexToSpacer.values());
  4505. }
  4506. /**
  4507. * Calculates the sum of all spacers from one row index onwards.
  4508. *
  4509. * @param logicalRowIndex
  4510. * the spacer to include as the first calculated spacer
  4511. * @return the sum of all spacers from {@code logicalRowIndex} and
  4512. * onwards, or 0 if no suitable spacers were found
  4513. */
  4514. @SuppressWarnings("boxing")
  4515. public Collection<SpacerImpl> getSpacersForRowAndAfter(
  4516. int logicalRowIndex) {
  4517. return new ArrayList<>(
  4518. rowIndexToSpacer.tailMap(logicalRowIndex, true).values());
  4519. }
  4520. /**
  4521. * Get all spacers from one pixel point onwards.
  4522. * <p>
  4523. *
  4524. * In this method, the {@link SpacerInclusionStrategy} has the following
  4525. * meaning when a spacer lies in the middle of either pixel argument:
  4526. * <dl>
  4527. * <dt>{@link SpacerInclusionStrategy#COMPLETE COMPLETE}
  4528. * <dd>include the spacer
  4529. * <dt>{@link SpacerInclusionStrategy#PARTIAL PARTIAL}
  4530. * <dd>include the spacer
  4531. * <dt>{@link SpacerInclusionStrategy#NONE NONE}
  4532. * <dd>ignore the spacer
  4533. * </dl>
  4534. *
  4535. * @param px
  4536. * the pixel point after which to return all spacers
  4537. * @param strategy
  4538. * the inclusion strategy regarding the {@code px}
  4539. * @return a collection of the spacers that exist after {@code px}
  4540. */
  4541. public Collection<SpacerImpl> getSpacersAfterPx(final double px,
  4542. final SpacerInclusionStrategy strategy) {
  4543. List<SpacerImpl> spacers = new ArrayList<>(
  4544. rowIndexToSpacer.values());
  4545. for (int i = 0; i < spacers.size(); i++) {
  4546. SpacerImpl spacer = spacers.get(i);
  4547. double top = spacer.getTop();
  4548. double bottom = top + spacer.getHeight();
  4549. if (top > px) {
  4550. return spacers.subList(i, spacers.size());
  4551. } else if (bottom > px) {
  4552. if (strategy == SpacerInclusionStrategy.NONE) {
  4553. return spacers.subList(i + 1, spacers.size());
  4554. } else {
  4555. return spacers.subList(i, spacers.size());
  4556. }
  4557. }
  4558. }
  4559. return Collections.emptySet();
  4560. }
  4561. /**
  4562. * Gets the spacers currently rendered in the DOM.
  4563. *
  4564. * @return an unmodifiable (but live) collection of the spacers
  4565. * currently in the DOM
  4566. */
  4567. public Collection<SpacerImpl> getSpacersInDom() {
  4568. return Collections
  4569. .unmodifiableCollection(rowIndexToSpacer.values());
  4570. }
  4571. /**
  4572. * Gets the amount of pixels occupied by spacers between two pixel
  4573. * points.
  4574. * <p>
  4575. * In this method, the {@link SpacerInclusionStrategy} has the following
  4576. * meaning when a spacer lies in the middle of either pixel argument:
  4577. * <dl>
  4578. * <dt>{@link SpacerInclusionStrategy#COMPLETE COMPLETE}
  4579. * <dd>take the entire spacer into account
  4580. * <dt>{@link SpacerInclusionStrategy#PARTIAL PARTIAL}
  4581. * <dd>take only the visible area into account
  4582. * <dt>{@link SpacerInclusionStrategy#NONE NONE}
  4583. * <dd>ignore that spacer
  4584. * </dl>
  4585. *
  4586. * @param rangeTop
  4587. * the top pixel point
  4588. * @param topInclusion
  4589. * the inclusion strategy regarding {@code rangeTop}.
  4590. * @param rangeBottom
  4591. * the bottom pixel point
  4592. * @param bottomInclusion
  4593. * the inclusion strategy regarding {@code rangeBottom}.
  4594. * @return the pixels occupied by spacers between {@code rangeTop} and
  4595. * {@code rangeBottom}
  4596. */
  4597. public double getSpacerHeightsSumBetweenPx(double rangeTop,
  4598. SpacerInclusionStrategy topInclusion, double rangeBottom,
  4599. SpacerInclusionStrategy bottomInclusion) {
  4600. assert rangeTop <= rangeBottom : "rangeTop must be less than rangeBottom";
  4601. double heights = 0;
  4602. /*
  4603. * TODO [[optimize]]: this might be somewhat inefficient (due to
  4604. * iterator-based scanning, instead of using the treemap's search
  4605. * functionalities). But it should be easy to write, read, verify
  4606. * and maintain.
  4607. */
  4608. for (SpacerImpl spacer : rowIndexToSpacer.values()) {
  4609. double top = spacer.getTop();
  4610. double height = spacer.getHeight();
  4611. double bottom = top + height;
  4612. /*
  4613. * If we happen to implement a DoubleRange (in addition to the
  4614. * int-based Range) at some point, the following logic should
  4615. * probably be converted into using the
  4616. * Range.partitionWith-equivalent.
  4617. */
  4618. boolean topIsAboveRange = top < rangeTop;
  4619. boolean topIsInRange = rangeTop <= top && top <= rangeBottom;
  4620. boolean topIsBelowRange = rangeBottom < top;
  4621. boolean bottomIsAboveRange = bottom < rangeTop;
  4622. boolean bottomIsInRange = rangeTop <= bottom
  4623. && bottom <= rangeBottom;
  4624. boolean bottomIsBelowRange = rangeBottom < bottom;
  4625. assert topIsAboveRange ^ topIsBelowRange
  4626. ^ topIsInRange : "Bad top logic";
  4627. assert bottomIsAboveRange ^ bottomIsBelowRange
  4628. ^ bottomIsInRange : "Bad bottom logic";
  4629. if (bottomIsAboveRange) {
  4630. continue;
  4631. } else if (topIsBelowRange) {
  4632. return heights;
  4633. } else if (topIsAboveRange && bottomIsInRange) {
  4634. switch (topInclusion) {
  4635. case PARTIAL:
  4636. heights += bottom - rangeTop;
  4637. break;
  4638. case COMPLETE:
  4639. heights += height;
  4640. break;
  4641. default:
  4642. break;
  4643. }
  4644. } else if (topIsAboveRange && bottomIsBelowRange) {
  4645. /*
  4646. * Here we arbitrarily decide that the top inclusion will
  4647. * have the honor of overriding the bottom inclusion if
  4648. * happens to be a conflict of interests.
  4649. */
  4650. switch (topInclusion) {
  4651. case NONE:
  4652. return 0;
  4653. case COMPLETE:
  4654. return height;
  4655. case PARTIAL:
  4656. return rangeBottom - rangeTop;
  4657. default:
  4658. throw new IllegalArgumentException(
  4659. "Unexpected inclusion state :" + topInclusion);
  4660. }
  4661. } else if (topIsInRange && bottomIsInRange) {
  4662. heights += height;
  4663. } else if (topIsInRange && bottomIsBelowRange) {
  4664. switch (bottomInclusion) {
  4665. case PARTIAL:
  4666. heights += rangeBottom - top;
  4667. break;
  4668. case COMPLETE:
  4669. heights += height;
  4670. break;
  4671. default:
  4672. break;
  4673. }
  4674. return heights;
  4675. } else {
  4676. assert false : "Unnaccounted-for situation";
  4677. }
  4678. }
  4679. return heights;
  4680. }
  4681. /**
  4682. * Gets the amount of pixels occupied by spacers from the top until a
  4683. * certain spot from the top of the body.
  4684. *
  4685. * @param px
  4686. * pixels counted from the top
  4687. * @return the pixels occupied by spacers up until {@code px}
  4688. */
  4689. public double getSpacerHeightsSumUntilPx(double px) {
  4690. return getSpacerHeightsSumBetweenPx(0,
  4691. SpacerInclusionStrategy.PARTIAL, px,
  4692. SpacerInclusionStrategy.PARTIAL);
  4693. }
  4694. /**
  4695. * Gets the amount of pixels occupied by spacers until a logical row
  4696. * index.
  4697. *
  4698. * @param logicalIndex
  4699. * a logical row index
  4700. * @return the pixels occupied by spacers up until {@code logicalIndex}
  4701. */
  4702. @SuppressWarnings("boxing")
  4703. public double getSpacerHeightsSumUntilIndex(int logicalIndex) {
  4704. return getHeights(
  4705. rowIndexToSpacer.headMap(logicalIndex, false).values());
  4706. }
  4707. private double getHeights(Collection<SpacerImpl> spacers) {
  4708. double heights = 0;
  4709. for (SpacerImpl spacer : spacers) {
  4710. heights += spacer.getHeight();
  4711. }
  4712. return heights;
  4713. }
  4714. /**
  4715. * Gets the height of the spacer for a row index.
  4716. *
  4717. * @param rowIndex
  4718. * the index of the row where the spacer should be
  4719. * @return the height of the spacer at index {@code rowIndex}, or 0 if
  4720. * there is no spacer there
  4721. */
  4722. public double getSpacerHeight(int rowIndex) {
  4723. SpacerImpl spacer = getSpacer(rowIndex);
  4724. if (spacer != null) {
  4725. return spacer.getHeight();
  4726. } else {
  4727. return 0;
  4728. }
  4729. }
  4730. private boolean spacerExists(int rowIndex) {
  4731. return rowIndexToSpacer.containsKey(Integer.valueOf(rowIndex));
  4732. }
  4733. @SuppressWarnings("boxing")
  4734. private void insertNewSpacer(int rowIndex, double height) {
  4735. if (spacerScrollerRegistration == null) {
  4736. spacerScrollerRegistration = addScrollHandler(spacerScroller);
  4737. }
  4738. final SpacerImpl spacer = new SpacerImpl(rowIndex);
  4739. rowIndexToSpacer.put(rowIndex, spacer);
  4740. // set the position before adding it to DOM
  4741. positions.set(spacer.getRootElement(), getScrollLeft(),
  4742. calculateSpacerTop(rowIndex));
  4743. TableRowElement spacerRoot = spacer.getRootElement();
  4744. spacerRoot.getStyle()
  4745. .setWidth(columnConfiguration.calculateRowWidth(), Unit.PX);
  4746. body.getElement().appendChild(spacerRoot);
  4747. spacer.setupDom(height);
  4748. // set the deco position, requires that spacer is in the DOM
  4749. positions.set(spacer.getDecoElement(), 0,
  4750. spacer.getTop() - spacer.getSpacerDecoTopOffset());
  4751. spacerDecoContainer.appendChild(spacer.getDecoElement());
  4752. if (spacerDecoContainer.getParentElement() == null) {
  4753. getElement().appendChild(spacerDecoContainer);
  4754. // calculate the spacer deco width, it won't change
  4755. spacerDecoWidth = getBoundingWidth(spacer.getDecoElement());
  4756. }
  4757. initSpacerContent(spacer);
  4758. body.sortDomElements();
  4759. }
  4760. private void updateExistingSpacer(int rowIndex, double newHeight) {
  4761. getSpacer(rowIndex).setHeight(newHeight);
  4762. }
  4763. public SpacerImpl getSpacer(int rowIndex) {
  4764. return rowIndexToSpacer.get(Integer.valueOf(rowIndex));
  4765. }
  4766. private void removeSpacer(int rowIndex) {
  4767. removeSpacers(Range.withOnly(rowIndex));
  4768. }
  4769. public void setStylePrimaryName(String style) {
  4770. for (SpacerImpl spacer : rowIndexToSpacer.values()) {
  4771. spacer.setStylePrimaryName(style);
  4772. }
  4773. }
  4774. public void setSpacerUpdater(SpacerUpdater spacerUpdater)
  4775. throws IllegalArgumentException {
  4776. if (spacerUpdater == null) {
  4777. throw new IllegalArgumentException(
  4778. "spacer updater cannot be null");
  4779. }
  4780. destroySpacerContent(rowIndexToSpacer.values());
  4781. this.spacerUpdater = spacerUpdater;
  4782. initSpacerContent(rowIndexToSpacer.values());
  4783. }
  4784. public SpacerUpdater getSpacerUpdater() {
  4785. return spacerUpdater;
  4786. }
  4787. private void destroySpacerContent(Iterable<SpacerImpl> spacers) {
  4788. for (SpacerImpl spacer : spacers) {
  4789. destroySpacerContent(spacer);
  4790. }
  4791. }
  4792. private void destroySpacerContent(SpacerImpl spacer) {
  4793. assert getElement().isOrHasChild(spacer
  4794. .getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching";
  4795. assert getElement().isOrHasChild(spacer
  4796. .getElement()) : "Spacer element somehow got detached from Escalator before detaching";
  4797. spacerUpdater.destroy(spacer);
  4798. assert getElement().isOrHasChild(spacer
  4799. .getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching";
  4800. assert getElement().isOrHasChild(spacer
  4801. .getElement()) : "Spacer element somehow got detached from Escalator before detaching";
  4802. }
  4803. private void initSpacerContent(Iterable<SpacerImpl> spacers) {
  4804. for (SpacerImpl spacer : spacers) {
  4805. initSpacerContent(spacer);
  4806. }
  4807. }
  4808. private void initSpacerContent(SpacerImpl spacer) {
  4809. assert getElement().isOrHasChild(spacer
  4810. .getRootElement()) : "Spacer's root element somehow got detached from Escalator before attaching";
  4811. assert getElement().isOrHasChild(spacer
  4812. .getElement()) : "Spacer element somehow got detached from Escalator before attaching";
  4813. spacerUpdater.init(spacer);
  4814. assert getElement().isOrHasChild(spacer
  4815. .getRootElement()) : "Spacer's root element somehow got detached from Escalator during attaching";
  4816. assert getElement().isOrHasChild(spacer
  4817. .getElement()) : "Spacer element somehow got detached from Escalator during attaching";
  4818. spacer.updateVisibility();
  4819. }
  4820. public String getSubPartName(Element subElement) {
  4821. for (SpacerImpl spacer : rowIndexToSpacer.values()) {
  4822. if (spacer.getRootElement().isOrHasChild(subElement)) {
  4823. return "spacer[" + spacer.getRow() + "]";
  4824. }
  4825. }
  4826. return null;
  4827. }
  4828. public Element getSubPartElement(int index) {
  4829. SpacerImpl spacer = rowIndexToSpacer.get(Integer.valueOf(index));
  4830. if (spacer != null) {
  4831. return spacer.getElement();
  4832. } else {
  4833. return null;
  4834. }
  4835. }
  4836. private double calculateSpacerTop(int logicalIndex) {
  4837. return body.getRowTop(logicalIndex) + body.getDefaultRowHeight();
  4838. }
  4839. @SuppressWarnings("boxing")
  4840. private void shiftSpacerPositionsAfterRow(int changedRowIndex,
  4841. double diffPx) {
  4842. for (SpacerImpl spacer : rowIndexToSpacer
  4843. .tailMap(changedRowIndex, false).values()) {
  4844. spacer.setPositionDiff(0, diffPx);
  4845. }
  4846. }
  4847. /**
  4848. * Shifts spacers at and after a specific row by an amount of rows.
  4849. * <p>
  4850. * This moves both their associated row index and also their visual
  4851. * placement.
  4852. * <p>
  4853. * <em>Note:</em> This method does not check for the validity of any
  4854. * arguments.
  4855. *
  4856. * @param index
  4857. * the index of first row to move
  4858. * @param numberOfRows
  4859. * the number of rows to shift the spacers with. A positive
  4860. * value is downwards, a negative value is upwards.
  4861. */
  4862. public void shiftSpacersByRows(int index, int numberOfRows) {
  4863. final double pxDiff = numberOfRows * body.getDefaultRowHeight();
  4864. List<SpacerContainer.SpacerImpl> spacers = new ArrayList<>(
  4865. getSpacersForRowAndAfter(index));
  4866. if (numberOfRows < 0) {
  4867. for (SpacerContainer.SpacerImpl spacer : spacers) {
  4868. spacer.setPositionDiff(0, pxDiff);
  4869. spacer.setRowIndex(spacer.getRow() + numberOfRows);
  4870. }
  4871. } else {
  4872. for (int i = spacers.size() - 1; i >= 0; --i) {
  4873. SpacerContainer.SpacerImpl spacer = spacers.get(i);
  4874. spacer.setPositionDiff(0, pxDiff);
  4875. spacer.setRowIndex(spacer.getRow() + numberOfRows);
  4876. }
  4877. }
  4878. }
  4879. private void updateSpacerDecosVisibility() {
  4880. final Range visibleRowRange = getVisibleRowRange();
  4881. Collection<SpacerImpl> visibleSpacers = rowIndexToSpacer
  4882. .subMap(visibleRowRange.getStart() - 1,
  4883. visibleRowRange.getEnd() + 1)
  4884. .values();
  4885. if (!visibleSpacers.isEmpty()) {
  4886. final double top = tableWrapper.getAbsoluteTop()
  4887. + header.getHeightOfSection();
  4888. final double bottom = tableWrapper.getAbsoluteBottom()
  4889. - footer.getHeightOfSection();
  4890. for (SpacerImpl spacer : visibleSpacers) {
  4891. spacer.updateDecoClip(top, bottom, spacerDecoWidth);
  4892. }
  4893. }
  4894. }
  4895. }
  4896. private class ElementPositionBookkeeper {
  4897. /**
  4898. * A map containing cached values of an element's current top position.
  4899. */
  4900. private final Map<Element, Double> elementTopPositionMap = new HashMap<>();
  4901. private final Map<Element, Double> elementLeftPositionMap = new HashMap<>();
  4902. public void set(final Element e, final double x, final double y) {
  4903. assert e != null : "Element was null";
  4904. position.set(e, x, y);
  4905. elementTopPositionMap.put(e, Double.valueOf(y));
  4906. elementLeftPositionMap.put(e, Double.valueOf(x));
  4907. }
  4908. public double getTop(final Element e) {
  4909. Double top = elementTopPositionMap.get(e);
  4910. if (top == null) {
  4911. throw new IllegalArgumentException("Element " + e
  4912. + " was not found in the position bookkeeping");
  4913. }
  4914. return top.doubleValue();
  4915. }
  4916. public double getLeft(final Element e) {
  4917. Double left = elementLeftPositionMap.get(e);
  4918. if (left == null) {
  4919. throw new IllegalArgumentException("Element " + e
  4920. + " was not found in the position bookkeeping");
  4921. }
  4922. return left.doubleValue();
  4923. }
  4924. public void remove(Element e) {
  4925. elementTopPositionMap.remove(e);
  4926. elementLeftPositionMap.remove(e);
  4927. }
  4928. }
  4929. /**
  4930. * Utility class for parsing and storing SubPart request string attributes
  4931. * for Grid and Escalator.
  4932. *
  4933. * @since 7.5.0
  4934. */
  4935. public static class SubPartArguments {
  4936. private String type;
  4937. private int[] indices;
  4938. private SubPartArguments(String type, int[] indices) {
  4939. /*
  4940. * The constructor is private so that no third party would by
  4941. * mistake start using this parsing scheme, since it's not official
  4942. * by TestBench (yet?).
  4943. */
  4944. this.type = type;
  4945. this.indices = indices;
  4946. }
  4947. public String getType() {
  4948. return type;
  4949. }
  4950. public int getIndicesLength() {
  4951. return indices.length;
  4952. }
  4953. public int getIndex(int i) {
  4954. return indices[i];
  4955. }
  4956. public int[] getIndices() {
  4957. return Arrays.copyOf(indices, indices.length);
  4958. }
  4959. static SubPartArguments create(String subPart) {
  4960. String[] splitArgs = subPart.split("\\[");
  4961. String type = splitArgs[0];
  4962. int[] indices = new int[splitArgs.length - 1];
  4963. for (int i = 0; i < indices.length; ++i) {
  4964. String tmp = splitArgs[i + 1];
  4965. indices[i] = Integer
  4966. .parseInt(tmp.substring(0, tmp.indexOf("]", 1)));
  4967. }
  4968. return new SubPartArguments(type, indices);
  4969. }
  4970. }
  4971. // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y
  4972. /**
  4973. * The solution to
  4974. * <code>|tan<sup>-1</sup>(<i>x</i>)|&times;(180/&pi;)&nbsp;=&nbsp;30</code>
  4975. * .
  4976. * <p>
  4977. * This constant is placed in the Escalator class, instead of an inner
  4978. * class, since even mathematical expressions aren't allowed in non-static
  4979. * inner classes for constants.
  4980. */
  4981. private static final double RATIO_OF_30_DEGREES = 1 / Math.sqrt(3);
  4982. /**
  4983. * The solution to
  4984. * <code>|tan<sup>-1</sup>(<i>x</i>)|&times;(180/&pi;)&nbsp;=&nbsp;40</code>
  4985. * .
  4986. * <p>
  4987. * This constant is placed in the Escalator class, instead of an inner
  4988. * class, since even mathematical expressions aren't allowed in non-static
  4989. * inner classes for constants.
  4990. */
  4991. private static final double RATIO_OF_40_DEGREES = Math.tan(2 * Math.PI / 9);
  4992. private static final String DEFAULT_WIDTH = "500.0px";
  4993. private static final String DEFAULT_HEIGHT = "400.0px";
  4994. private FlyweightRow flyweightRow = new FlyweightRow();
  4995. /** The {@code <thead/>} tag. */
  4996. private final TableSectionElement headElem = TableSectionElement
  4997. .as(DOM.createTHead());
  4998. /** The {@code <tbody/>} tag. */
  4999. private final TableSectionElement bodyElem = TableSectionElement
  5000. .as(DOM.createTBody());
  5001. /** The {@code <tfoot/>} tag. */
  5002. private final TableSectionElement footElem = TableSectionElement
  5003. .as(DOM.createTFoot());
  5004. /**
  5005. * TODO: investigate whether this field is now unnecessary, as
  5006. * {@link ScrollbarBundle} now caches its values.
  5007. *
  5008. * @deprecated maybe...
  5009. */
  5010. @Deprecated
  5011. private double tBodyScrollTop = 0;
  5012. /**
  5013. * TODO: investigate whether this field is now unnecessary, as
  5014. * {@link ScrollbarBundle} now caches its values.
  5015. *
  5016. * @deprecated maybe...
  5017. */
  5018. @Deprecated
  5019. private double tBodyScrollLeft = 0;
  5020. private final VerticalScrollbarBundle verticalScrollbar = new VerticalScrollbarBundle();
  5021. private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle();
  5022. private final AriaGridHelper ariaGridHelper = new AriaGridHelper();
  5023. private final HeaderRowContainer header = new HeaderRowContainer(headElem);
  5024. private final BodyRowContainerImpl body = new BodyRowContainerImpl(
  5025. bodyElem);
  5026. private final FooterRowContainer footer = new FooterRowContainer(footElem);
  5027. /**
  5028. * Flag for keeping track of {@link RowHeightChangedEvent}s
  5029. */
  5030. private boolean rowHeightChangedEventFired = false;
  5031. private final Scroller scroller = new Scroller();
  5032. private final ColumnConfigurationImpl columnConfiguration = new ColumnConfigurationImpl();
  5033. private final DivElement tableWrapper;
  5034. private final Element table;
  5035. private final DivElement horizontalScrollbarDeco = DivElement
  5036. .as(DOM.createDiv());
  5037. private final DivElement headerDeco = DivElement.as(DOM.createDiv());
  5038. private final DivElement footerDeco = DivElement.as(DOM.createDiv());
  5039. private final DivElement spacerDecoContainer = DivElement
  5040. .as(DOM.createDiv());
  5041. private PositionFunction position;
  5042. /** The cached width of the escalator, in pixels. */
  5043. private double widthOfEscalator = 0;
  5044. /** The cached height of the escalator, in pixels. */
  5045. private double heightOfEscalator = 0;
  5046. /** The height of Escalator in terms of body rows. */
  5047. private double heightByRows = 10.0d;
  5048. /** The height of Escalator, as defined by {@link #setHeight(String)} */
  5049. private String heightByCss = "";
  5050. private HeightMode heightMode = HeightMode.CSS;
  5051. private double delayToCancelTouchScroll = -1;
  5052. private boolean layoutIsScheduled = false;
  5053. private ScheduledCommand layoutCommand = () -> {
  5054. recalculateElementSizes();
  5055. layoutIsScheduled = false;
  5056. };
  5057. private final ElementPositionBookkeeper positions = new ElementPositionBookkeeper();
  5058. /**
  5059. * Creates a new Escalator widget instance.
  5060. */
  5061. public Escalator() {
  5062. detectAndApplyPositionFunction();
  5063. getLogger().info("Using " + position.getClass().getSimpleName()
  5064. + " for position");
  5065. final Element root = DOM.createDiv();
  5066. setElement(root);
  5067. setupScrollbars(root);
  5068. tableWrapper = DivElement.as(DOM.createDiv());
  5069. Event.sinkEvents(tableWrapper, Event.ONSCROLL | Event.KEYEVENTS);
  5070. Event.setEventListener(tableWrapper, event -> {
  5071. if (event.getKeyCode() != KeyCodes.KEY_TAB) {
  5072. return;
  5073. }
  5074. boolean browserScroll = tableWrapper.getScrollLeft() != 0
  5075. || tableWrapper.getScrollTop() != 0;
  5076. boolean keyEvent = event.getType().startsWith("key");
  5077. if (browserScroll || keyEvent) {
  5078. // Browser is scrolling our div automatically, reset
  5079. tableWrapper.setScrollLeft(0);
  5080. tableWrapper.setScrollTop(0);
  5081. Element focused = WidgetUtil.getFocusedElement();
  5082. Stream.of(header, body, footer).forEach(container -> {
  5083. Cell cell = container.getCell(focused);
  5084. if (cell == null) {
  5085. return;
  5086. }
  5087. scrollToColumn(cell.getColumn(), ScrollDestination.ANY, 0);
  5088. if (container == body) {
  5089. scrollToRow(cell.getRow(), ScrollDestination.ANY, 0);
  5090. }
  5091. });
  5092. }
  5093. });
  5094. root.appendChild(tableWrapper);
  5095. table = DOM.createTable();
  5096. tableWrapper.appendChild(table);
  5097. table.appendChild(headElem);
  5098. table.appendChild(bodyElem);
  5099. table.appendChild(footElem);
  5100. Style hCornerStyle = headerDeco.getStyle();
  5101. hCornerStyle.setWidth(verticalScrollbar.getScrollbarThickness(),
  5102. Unit.PX);
  5103. hCornerStyle.setDisplay(Display.NONE);
  5104. root.appendChild(headerDeco);
  5105. Style fCornerStyle = footerDeco.getStyle();
  5106. fCornerStyle.setWidth(verticalScrollbar.getScrollbarThickness(),
  5107. Unit.PX);
  5108. fCornerStyle.setDisplay(Display.NONE);
  5109. root.appendChild(footerDeco);
  5110. Style hWrapperStyle = horizontalScrollbarDeco.getStyle();
  5111. hWrapperStyle.setDisplay(Display.NONE);
  5112. hWrapperStyle.setHeight(horizontalScrollbar.getScrollbarThickness(),
  5113. Unit.PX);
  5114. root.appendChild(horizontalScrollbarDeco);
  5115. setStylePrimaryName("v-escalator");
  5116. spacerDecoContainer.setAttribute("aria-hidden", "true");
  5117. // init default dimensions
  5118. setHeight(null);
  5119. setWidth(null);
  5120. publishJSHelpers(root);
  5121. }
  5122. private double getBoundingWidth(Element element) {
  5123. // Gets the current width, including border and padding, for the element
  5124. // while ignoring any transforms applied to the element (e.g. scale)
  5125. return new ComputedStyle(element).getWidthIncludingBorderPadding();
  5126. }
  5127. private double getBoundingHeight(Element element) {
  5128. // Gets the current height, including border and padding, for the
  5129. // element while ignoring any transforms applied to the element (e.g.
  5130. // scale)
  5131. return new ComputedStyle(element).getHeightIncludingBorderPadding();
  5132. }
  5133. private int getBodyRowCount() {
  5134. return getBody().getRowCount();
  5135. }
  5136. private native void publishJSHelpers(Element root)
  5137. /*-{
  5138. var self = this;
  5139. root.getBodyRowCount = $entry(function () {
  5140. return self.@Escalator::getBodyRowCount()();
  5141. });
  5142. }-*/;
  5143. private void setupScrollbars(final Element root) {
  5144. ScrollHandler scrollHandler = event -> {
  5145. scroller.onScroll();
  5146. fireEvent(new ScrollEvent());
  5147. };
  5148. int scrollbarThickness = WidgetUtil.getNativeScrollbarSize();
  5149. if (BrowserInfo.get().isIE()) {
  5150. /*
  5151. * IE refuses to scroll properly if the DIV isn't at least one pixel
  5152. * larger than the scrollbar controls themselves.
  5153. */
  5154. scrollbarThickness += 1;
  5155. }
  5156. root.appendChild(verticalScrollbar.getElement());
  5157. verticalScrollbar.addScrollHandler(scrollHandler);
  5158. verticalScrollbar.setScrollbarThickness(scrollbarThickness);
  5159. root.appendChild(horizontalScrollbar.getElement());
  5160. horizontalScrollbar.addScrollHandler(scrollHandler);
  5161. horizontalScrollbar.setScrollbarThickness(scrollbarThickness);
  5162. horizontalScrollbar
  5163. .addVisibilityHandler(new ScrollbarBundle.VisibilityHandler() {
  5164. private boolean queued = false;
  5165. @Override
  5166. public void visibilityChanged(
  5167. ScrollbarBundle.VisibilityChangeEvent event) {
  5168. if (queued) {
  5169. return;
  5170. }
  5171. queued = true;
  5172. /*
  5173. * We either lost or gained a scrollbar. In any case, we
  5174. * need to change the height, if it's defined by rows.
  5175. */
  5176. Scheduler.get().scheduleFinally(() -> {
  5177. applyHeightByRows();
  5178. queued = false;
  5179. });
  5180. }
  5181. });
  5182. /*
  5183. * Because of all the IE hacks we've done above, we now have scrollbars
  5184. * hiding underneath a lot of DOM elements.
  5185. *
  5186. * This leads to problems with OSX (and many touch-only devices) when
  5187. * scrollbars are only shown when scrolling, as the scrollbar elements
  5188. * are hidden underneath everything. We trust that the scrollbars behave
  5189. * properly in these situations and simply pop them out with a bit of
  5190. * z-indexing.
  5191. */
  5192. if (WidgetUtil.getNativeScrollbarSize() == 0) {
  5193. verticalScrollbar.getElement().getStyle().setZIndex(90);
  5194. horizontalScrollbar.getElement().getStyle().setZIndex(90);
  5195. }
  5196. }
  5197. @Override
  5198. protected void onLoad() {
  5199. super.onLoad();
  5200. header.autodetectRowHeightLater();
  5201. body.autodetectRowHeightLater();
  5202. footer.autodetectRowHeightLater();
  5203. header.paintInsertRows(0, header.getRowCount());
  5204. footer.paintInsertRows(0, footer.getRowCount());
  5205. boolean columnsChanged = false;
  5206. for (ColumnConfigurationImpl.Column column : columnConfiguration.columns) {
  5207. boolean columnChanged = column.measureAndSetWidthIfNeeded();
  5208. if (columnChanged) {
  5209. columnsChanged = true;
  5210. }
  5211. }
  5212. if (columnsChanged) {
  5213. header.reapplyColumnWidths();
  5214. body.reapplyColumnWidths();
  5215. footer.reapplyColumnWidths();
  5216. }
  5217. verticalScrollbar.onLoad();
  5218. horizontalScrollbar.onLoad();
  5219. scroller.attachScrollListener(verticalScrollbar.getElement());
  5220. scroller.attachScrollListener(horizontalScrollbar.getElement());
  5221. scroller.attachMousewheelListener(getElement());
  5222. if (isCurrentBrowserIE11OrEdge()) {
  5223. // Touch listeners doesn't work for IE11 and Edge (#18737)
  5224. scroller.attachPointerEventListeners(getElement());
  5225. } else {
  5226. scroller.attachTouchListeners(getElement());
  5227. }
  5228. /*
  5229. * Note: There's no need to explicitly insert rows into the body.
  5230. *
  5231. * recalculateElementSizes will recalculate the height of the body. This
  5232. * has the side-effect that as the body's size grows bigger (i.e. from 0
  5233. * to its actual height), more escalator rows are populated. Those
  5234. * escalator rows are then immediately rendered. This, in effect, is the
  5235. * same thing as inserting those rows.
  5236. *
  5237. * In fact, having an extra paintInsertRows here would lead to duplicate
  5238. * rows.
  5239. */
  5240. recalculateElementSizes();
  5241. }
  5242. @Override
  5243. protected void onUnload() {
  5244. scroller.detachScrollListener(verticalScrollbar.getElement());
  5245. scroller.detachScrollListener(horizontalScrollbar.getElement());
  5246. scroller.detachMousewheelListener(getElement());
  5247. if (isCurrentBrowserIE11OrEdge()) {
  5248. // Touch listeners doesn't work for IE11 and Edge (#18737)
  5249. scroller.detachPointerEventListeners(getElement());
  5250. } else {
  5251. scroller.detachTouchListeners(getElement());
  5252. }
  5253. /*
  5254. * We can call paintRemoveRows here, because static ranges are simple to
  5255. * remove.
  5256. */
  5257. header.paintRemoveRows(0, header.getRowCount());
  5258. footer.paintRemoveRows(0, footer.getRowCount());
  5259. /*
  5260. * We can't call body.paintRemoveRows since it relies on rowCount to be
  5261. * updated correctly. Since it isn't, we'll simply and brutally rip out
  5262. * the DOM elements (in an elegant way, of course).
  5263. */
  5264. int rowsToRemove = body.getDomRowCount();
  5265. for (int i = 0; i < rowsToRemove; i++) {
  5266. int index = rowsToRemove - i - 1;
  5267. TableRowElement tr = bodyElem.getRows().getItem(index);
  5268. body.paintRemoveRow(tr, index);
  5269. positions.remove(tr);
  5270. }
  5271. body.visualRowOrder.clear();
  5272. body.setTopRowLogicalIndex(0);
  5273. super.onUnload();
  5274. }
  5275. private void detectAndApplyPositionFunction() {
  5276. final Style docStyle = Document.get().getBody().getStyle();
  5277. if (hasProperty(docStyle, "transform")) {
  5278. if (hasProperty(docStyle, "transformStyle")) {
  5279. position = new Translate3DPosition();
  5280. } else {
  5281. position = new TranslatePosition();
  5282. }
  5283. } else if (hasProperty(docStyle, "webkitTransform")) {
  5284. position = new WebkitTranslate3DPosition();
  5285. }
  5286. }
  5287. private Logger getLogger() {
  5288. return Logger.getLogger(getClass().getName());
  5289. }
  5290. private static native boolean hasProperty(Style style, String name)
  5291. /*-{
  5292. return style[name] !== undefined;
  5293. }-*/;
  5294. /**
  5295. * Check whether there are both columns and any row data (for either
  5296. * headers, body or footer).
  5297. *
  5298. * @return <code>true</code> if header, body or footer has rows and there
  5299. * are columns
  5300. */
  5301. private boolean hasColumnAndRowData() {
  5302. return (header.getRowCount() > 0 || body.getRowCount() > 0
  5303. || footer.getRowCount() > 0)
  5304. && columnConfiguration.getColumnCount() > 0;
  5305. }
  5306. /**
  5307. * Check whether there are any cells in the DOM.
  5308. *
  5309. * @return <code>true</code> if header, body or footer has any child
  5310. * elements
  5311. */
  5312. private boolean hasSomethingInDom() {
  5313. return headElem.hasChildNodes() || bodyElem.hasChildNodes()
  5314. || footElem.hasChildNodes();
  5315. }
  5316. /**
  5317. * Returns the row container for the header in this Escalator.
  5318. *
  5319. * @return the header. Never <code>null</code>
  5320. */
  5321. public RowContainer getHeader() {
  5322. return header;
  5323. }
  5324. /**
  5325. * Returns the row container for the body in this Escalator.
  5326. *
  5327. * @return the body. Never <code>null</code>
  5328. */
  5329. public BodyRowContainer getBody() {
  5330. return body;
  5331. }
  5332. /**
  5333. * Returns the row container for the footer in this Escalator.
  5334. *
  5335. * @return the footer. Never <code>null</code>
  5336. */
  5337. public RowContainer getFooter() {
  5338. return footer;
  5339. }
  5340. /**
  5341. * Returns the configuration object for the columns in this Escalator.
  5342. *
  5343. * @return the configuration object for the columns in this Escalator. Never
  5344. * <code>null</code>
  5345. */
  5346. public ColumnConfiguration getColumnConfiguration() {
  5347. return columnConfiguration;
  5348. }
  5349. @Override
  5350. public void setWidth(final String width) {
  5351. if (width != null && !width.isEmpty()) {
  5352. super.setWidth(width);
  5353. } else {
  5354. super.setWidth(DEFAULT_WIDTH);
  5355. }
  5356. recalculateElementSizes();
  5357. }
  5358. /**
  5359. * {@inheritDoc}
  5360. * <p>
  5361. * If Escalator is currently not in {@link HeightMode#CSS}, the given value
  5362. * is remembered, and applied once the mode is applied.
  5363. *
  5364. * @see #setHeightMode(HeightMode)
  5365. */
  5366. @Override
  5367. public void setHeight(String height) {
  5368. /*
  5369. * TODO remove method once RequiresResize and the Vaadin layoutmanager
  5370. * listening mechanisms are implemented
  5371. */
  5372. if (height != null && !height.isEmpty()) {
  5373. heightByCss = height;
  5374. } else {
  5375. if (getHeightMode() == HeightMode.UNDEFINED) {
  5376. heightByRows = body.getRowCount();
  5377. applyHeightByRows();
  5378. return;
  5379. } else {
  5380. heightByCss = DEFAULT_HEIGHT;
  5381. }
  5382. }
  5383. if (getHeightMode() == HeightMode.CSS) {
  5384. setHeightInternal(height);
  5385. }
  5386. }
  5387. private void setHeightInternal(final String height) {
  5388. final int escalatorRowsBefore = body.visualRowOrder.size();
  5389. if (height != null && !height.isEmpty()) {
  5390. super.setHeight(height);
  5391. } else {
  5392. if (getHeightMode() == HeightMode.UNDEFINED) {
  5393. int newHeightByRows = body.getRowCount();
  5394. if (heightByRows != newHeightByRows) {
  5395. heightByRows = newHeightByRows;
  5396. applyHeightByRows();
  5397. }
  5398. return;
  5399. } else {
  5400. super.setHeight(DEFAULT_HEIGHT);
  5401. }
  5402. }
  5403. recalculateElementSizes();
  5404. if (escalatorRowsBefore != body.visualRowOrder.size()) {
  5405. fireRowVisibilityChangeEvent();
  5406. }
  5407. }
  5408. /**
  5409. * Returns the vertical scroll offset. Note that this is not necessarily the
  5410. * same as the {@code scrollTop} attribute in the DOM.
  5411. *
  5412. * @return the logical vertical scroll offset
  5413. */
  5414. public double getScrollTop() {
  5415. return verticalScrollbar.getScrollPos();
  5416. }
  5417. /**
  5418. * Sets the vertical scroll offset. Note that this will not necessarily
  5419. * become the same as the {@code scrollTop} attribute in the DOM.
  5420. *
  5421. * @param scrollTop
  5422. * the number of pixels to scroll vertically
  5423. */
  5424. public void setScrollTop(final double scrollTop) {
  5425. verticalScrollbar.setScrollPos(scrollTop);
  5426. }
  5427. /**
  5428. * Returns the logical horizontal scroll offset. Note that this is not
  5429. * necessarily the same as the {@code scrollLeft} attribute in the DOM.
  5430. *
  5431. * @return the logical horizontal scroll offset
  5432. */
  5433. public double getScrollLeft() {
  5434. return horizontalScrollbar.getScrollPos();
  5435. }
  5436. /**
  5437. * Sets the logical horizontal scroll offset. Note that will not necessarily
  5438. * become the same as the {@code scrollLeft} attribute in the DOM.
  5439. *
  5440. * @param scrollLeft
  5441. * the number of pixels to scroll horizontally
  5442. */
  5443. public void setScrollLeft(final double scrollLeft) {
  5444. horizontalScrollbar.setScrollPos(scrollLeft);
  5445. }
  5446. /**
  5447. * Returns the scroll width for the escalator. Note that this is not
  5448. * necessary the same as {@code Element.scrollWidth} in the DOM.
  5449. *
  5450. * @since 7.5.0
  5451. * @return the scroll width in pixels
  5452. */
  5453. public double getScrollWidth() {
  5454. return horizontalScrollbar.getScrollSize();
  5455. }
  5456. /**
  5457. * Returns the scroll height for the escalator. Note that this is not
  5458. * necessary the same as {@code Element.scrollHeight} in the DOM.
  5459. *
  5460. * @since 7.5.0
  5461. * @return the scroll height in pixels
  5462. */
  5463. public double getScrollHeight() {
  5464. return verticalScrollbar.getScrollSize();
  5465. }
  5466. /**
  5467. * Scrolls the body horizontally so that the column at the given index is
  5468. * visible and there is at least {@code padding} pixels in the direction of
  5469. * the given scroll destination.
  5470. *
  5471. * @param columnIndex
  5472. * the index of the column to scroll to
  5473. * @param destination
  5474. * where the column should be aligned visually after scrolling
  5475. * @param padding
  5476. * the number pixels to place between the scrolled-to column and
  5477. * the viewport edge.
  5478. * @throws IndexOutOfBoundsException
  5479. * if {@code columnIndex} is not a valid index for an existing
  5480. * column
  5481. * @throws IllegalArgumentException
  5482. * if {@code destination} is {@link ScrollDestination#MIDDLE}
  5483. * and padding is nonzero; or if the indicated column is frozen;
  5484. * or if {@code destination == null}
  5485. */
  5486. public void scrollToColumn(final int columnIndex,
  5487. final ScrollDestination destination, final int padding)
  5488. throws IndexOutOfBoundsException, IllegalArgumentException {
  5489. validateScrollDestination(destination, padding);
  5490. verifyValidColumnIndex(columnIndex);
  5491. if (columnIndex < columnConfiguration.frozenColumns) {
  5492. throw new IllegalArgumentException(
  5493. "The given column index " + columnIndex + " is frozen.");
  5494. }
  5495. scroller.scrollToColumn(columnIndex, destination, padding);
  5496. }
  5497. private void verifyValidColumnIndex(final int columnIndex)
  5498. throws IndexOutOfBoundsException {
  5499. if (columnIndex < 0
  5500. || columnIndex >= columnConfiguration.getColumnCount()) {
  5501. throw new IndexOutOfBoundsException("The given column index "
  5502. + columnIndex + " does not exist.");
  5503. }
  5504. }
  5505. /**
  5506. * Scrolls the body vertically so that the row at the given index is visible
  5507. * and there is at least {@literal padding} pixels to the given scroll
  5508. * destination.
  5509. *
  5510. * @param rowIndex
  5511. * the index of the logical row to scroll to
  5512. * @param destination
  5513. * where the row should be aligned visually after scrolling
  5514. * @param padding
  5515. * the number pixels to place between the scrolled-to row and the
  5516. * viewport edge.
  5517. * @throws IndexOutOfBoundsException
  5518. * if {@code rowIndex} is not a valid index for an existing row
  5519. * @throws IllegalArgumentException
  5520. * if {@code destination} is {@link ScrollDestination#MIDDLE}
  5521. * and padding is nonzero; or if {@code destination == null}
  5522. * @see #scrollToRowAndSpacer(int, ScrollDestination, int)
  5523. * @see #scrollToSpacer(int, ScrollDestination, int)
  5524. */
  5525. public void scrollToRow(final int rowIndex,
  5526. final ScrollDestination destination, final int padding)
  5527. throws IndexOutOfBoundsException, IllegalArgumentException {
  5528. Scheduler.get().scheduleFinally(() -> {
  5529. validateScrollDestination(destination, padding);
  5530. verifyValidRowIndex(rowIndex);
  5531. scroller.scrollToRow(rowIndex, destination, padding);
  5532. });
  5533. }
  5534. private void verifyValidRowIndex(final int rowIndex) {
  5535. if (rowIndex < 0 || rowIndex >= body.getRowCount()) {
  5536. throw new IndexOutOfBoundsException(
  5537. "The given row index " + rowIndex + " does not exist.");
  5538. }
  5539. }
  5540. /**
  5541. * Scrolls the body vertically so that the spacer at the given row index is
  5542. * visible and there is at least {@literal padding} pixesl to the given
  5543. * scroll destination.
  5544. *
  5545. * @since 7.5.0
  5546. * @param spacerIndex
  5547. * the row index of the spacer to scroll to
  5548. * @param destination
  5549. * where the spacer should be aligned visually after scrolling
  5550. * @param padding
  5551. * the number of pixels to place between the scrolled-to spacer
  5552. * and the viewport edge
  5553. * @throws IllegalArgumentException
  5554. * if {@code spacerIndex} is not an opened spacer; or if
  5555. * {@code destination} is {@link ScrollDestination#MIDDLE} and
  5556. * padding is nonzero; or if {@code destination == null}
  5557. * @see #scrollToRow(int, ScrollDestination, int)
  5558. * @see #scrollToRowAndSpacer(int, ScrollDestination, int)
  5559. */
  5560. public void scrollToSpacer(final int spacerIndex,
  5561. ScrollDestination destination, final int padding)
  5562. throws IllegalArgumentException {
  5563. validateScrollDestination(destination, padding);
  5564. body.scrollToSpacer(spacerIndex, destination, padding);
  5565. }
  5566. /**
  5567. * Scrolls vertically to a row and the spacer below it.
  5568. * <p>
  5569. * If a spacer is not open at that index, this method behaves like
  5570. * {@link #scrollToRow(int, ScrollDestination, int)}
  5571. *
  5572. * @since 7.5.0
  5573. * @param rowIndex
  5574. * the index of the logical row to scroll to. -1 takes the
  5575. * topmost spacer into account as well.
  5576. * @param destination
  5577. * where the row should be aligned visually after scrolling
  5578. * @param padding
  5579. * the number pixels to place between the scrolled-to row and the
  5580. * viewport edge.
  5581. * @see #scrollToRow(int, ScrollDestination, int)
  5582. * @see #scrollToSpacer(int, ScrollDestination, int)
  5583. * @throws IllegalArgumentException
  5584. * if {@code destination} is {@link ScrollDestination#MIDDLE}
  5585. * and {@code padding} is not zero; or if {@code rowIndex} is
  5586. * not a valid row index, or -1; or if
  5587. * {@code destination == null}; or if {@code rowIndex == -1} and
  5588. * there is no spacer open at that index.
  5589. */
  5590. public void scrollToRowAndSpacer(final int rowIndex,
  5591. final ScrollDestination destination, final int padding)
  5592. throws IllegalArgumentException {
  5593. Scheduler.get().scheduleFinally(() -> {
  5594. validateScrollDestination(destination, padding);
  5595. if (rowIndex != -1) {
  5596. verifyValidRowIndex(rowIndex);
  5597. }
  5598. // row range
  5599. final Range rowRange;
  5600. if (rowIndex != -1) {
  5601. int rowTop = (int) Math.floor(body.getRowTop(rowIndex));
  5602. int rowHeight = (int) Math.ceil(body.getDefaultRowHeight());
  5603. rowRange = Range.withLength(rowTop, rowHeight);
  5604. } else {
  5605. rowRange = Range.withLength(0, 0);
  5606. }
  5607. // get spacer
  5608. final SpacerContainer.SpacerImpl spacer = body.spacerContainer
  5609. .getSpacer(rowIndex);
  5610. if (rowIndex == -1 && spacer == null) {
  5611. throw new IllegalArgumentException("Cannot scroll to row index "
  5612. + "-1, as there is no spacer open at that index.");
  5613. }
  5614. // make into target range
  5615. final Range targetRange;
  5616. if (spacer != null) {
  5617. final int spacerTop = (int) Math.floor(spacer.getTop());
  5618. final int spacerHeight = (int) Math.ceil(spacer.getHeight());
  5619. Range spacerRange = Range.withLength(spacerTop, spacerHeight);
  5620. targetRange = rowRange.combineWith(spacerRange);
  5621. } else {
  5622. targetRange = rowRange;
  5623. }
  5624. // get params
  5625. int targetStart = targetRange.getStart();
  5626. int targetEnd = targetRange.getEnd();
  5627. double viewportStart = getScrollTop();
  5628. double viewportEnd = viewportStart + body.getHeightOfSection();
  5629. double scrollPos = getScrollPos(destination, targetStart, targetEnd,
  5630. viewportStart, viewportEnd, padding);
  5631. setScrollTop(scrollPos);
  5632. });
  5633. }
  5634. private static void validateScrollDestination(
  5635. final ScrollDestination destination, final int padding) {
  5636. if (destination == null) {
  5637. throw new IllegalArgumentException("Destination cannot be null");
  5638. }
  5639. if (destination == ScrollDestination.MIDDLE && padding != 0) {
  5640. throw new IllegalArgumentException(
  5641. "You cannot have a padding with a MIDDLE destination");
  5642. }
  5643. }
  5644. /**
  5645. * Recalculates the dimensions for all elements that require manual
  5646. * calculations. Also updates the dimension caches.
  5647. * <p>
  5648. * <em>Note:</em> This method has the <strong>side-effect</strong>
  5649. * automatically makes sure that an appropriate amount of escalator rows are
  5650. * present. So, if the body area grows, more <strong>escalator rows might be
  5651. * inserted</strong>. Conversely, if the body area shrinks,
  5652. * <strong>escalator rows might be removed</strong>.
  5653. */
  5654. private void recalculateElementSizes() {
  5655. if (!isAttached()) {
  5656. return;
  5657. }
  5658. Profiler.enter("Escalator.recalculateElementSizes");
  5659. widthOfEscalator = Math.max(0, getBoundingWidth(getElement()));
  5660. heightOfEscalator = Math.max(0, getBoundingHeight(getElement()));
  5661. header.recalculateSectionHeight();
  5662. body.recalculateSectionHeight();
  5663. footer.recalculateSectionHeight();
  5664. scroller.recalculateScrollbarsForVirtualViewport();
  5665. body.verifyEscalatorCount();
  5666. body.reapplySpacerWidths();
  5667. Profiler.leave("Escalator.recalculateElementSizes");
  5668. }
  5669. /**
  5670. * Snap deltas of x and y to the major four axes (up, down, left, right)
  5671. * with a threshold of a number of degrees from those axes.
  5672. *
  5673. * @param deltaX
  5674. * the delta in the x axis
  5675. * @param deltaY
  5676. * the delta in the y axis
  5677. * @param thresholdRatio
  5678. * the threshold in ratio (0..1) between x and y for when to snap
  5679. * @return a two-element array: <code>[snappedX, snappedY]</code>
  5680. */
  5681. private static double[] snapDeltas(final double deltaX, final double deltaY,
  5682. final double thresholdRatio) {
  5683. final double[] array = new double[2];
  5684. if (deltaX != 0 && deltaY != 0) {
  5685. final double aDeltaX = Math.abs(deltaX);
  5686. final double aDeltaY = Math.abs(deltaY);
  5687. final double yRatio = aDeltaY / aDeltaX;
  5688. final double xRatio = aDeltaX / aDeltaY;
  5689. array[0] = (xRatio < thresholdRatio) ? 0 : deltaX;
  5690. array[1] = (yRatio < thresholdRatio) ? 0 : deltaY;
  5691. } else {
  5692. array[0] = deltaX;
  5693. array[1] = deltaY;
  5694. }
  5695. return array;
  5696. }
  5697. /**
  5698. * Adds an event handler that gets notified when the range of visible rows
  5699. * changes e.g. because of scrolling, row resizing or spacers
  5700. * appearing/disappearing.
  5701. *
  5702. * @param rowVisibilityChangeHandler
  5703. * the event handler
  5704. * @return a handler registration for the added handler
  5705. */
  5706. public HandlerRegistration addRowVisibilityChangeHandler(
  5707. RowVisibilityChangeHandler rowVisibilityChangeHandler) {
  5708. return addHandler(rowVisibilityChangeHandler,
  5709. RowVisibilityChangeEvent.TYPE);
  5710. }
  5711. private void fireRowVisibilityChangeEvent() {
  5712. if (!body.visualRowOrder.isEmpty()) {
  5713. int visibleRangeStart = body
  5714. .getLogicalRowIndex(body.visualRowOrder.getFirst());
  5715. int visibleRangeEnd = body
  5716. .getLogicalRowIndex(body.visualRowOrder.getLast()) + 1;
  5717. int visibleRowCount = visibleRangeEnd - visibleRangeStart;
  5718. fireEvent(new RowVisibilityChangeEvent(visibleRangeStart,
  5719. visibleRowCount));
  5720. } else {
  5721. fireEvent(new RowVisibilityChangeEvent(0, 0));
  5722. }
  5723. }
  5724. /**
  5725. * Gets the logical index range of currently visible rows.
  5726. *
  5727. * @return logical index range of visible rows
  5728. */
  5729. public Range getVisibleRowRange() {
  5730. if (!body.visualRowOrder.isEmpty()) {
  5731. return Range.withLength(body.getTopRowLogicalIndex(),
  5732. body.visualRowOrder.size());
  5733. } else {
  5734. return Range.withLength(0, 0);
  5735. }
  5736. }
  5737. /**
  5738. * Returns the widget from a cell node or <code>null</code> if there is no
  5739. * widget in the cell
  5740. *
  5741. * @param cellNode
  5742. * The cell node
  5743. */
  5744. static Widget getWidgetFromCell(Node cellNode) {
  5745. Node possibleWidgetNode = cellNode.getFirstChild();
  5746. if (possibleWidgetNode != null
  5747. && possibleWidgetNode.getNodeType() == Node.ELEMENT_NODE) {
  5748. @SuppressWarnings("deprecation")
  5749. com.google.gwt.user.client.Element castElement = (com.google.gwt.user.client.Element) possibleWidgetNode
  5750. .cast();
  5751. Widget w = WidgetUtil.findWidget(castElement);
  5752. // Ensure findWidget did not traverse past the cell element in the
  5753. // DOM hierarchy
  5754. if (cellNode.isOrHasChild(w.getElement())) {
  5755. return w;
  5756. }
  5757. }
  5758. return null;
  5759. }
  5760. @Override
  5761. public void setStylePrimaryName(String style) {
  5762. super.setStylePrimaryName(style);
  5763. verticalScrollbar.setStylePrimaryName(style);
  5764. horizontalScrollbar.setStylePrimaryName(style);
  5765. UIObject.setStylePrimaryName(tableWrapper, style + "-tablewrapper");
  5766. UIObject.setStylePrimaryName(headerDeco, style + "-header-deco");
  5767. UIObject.setStylePrimaryName(footerDeco, style + "-footer-deco");
  5768. UIObject.setStylePrimaryName(horizontalScrollbarDeco,
  5769. style + "-horizontal-scrollbar-deco");
  5770. UIObject.setStylePrimaryName(spacerDecoContainer,
  5771. style + "-spacer-deco-container");
  5772. header.setStylePrimaryName(style);
  5773. body.setStylePrimaryName(style);
  5774. footer.setStylePrimaryName(style);
  5775. }
  5776. /**
  5777. * Sets the number of rows that should be visible in Escalator's body, while
  5778. * {@link #getHeightMode()} is {@link HeightMode#ROW}.
  5779. * <p>
  5780. * If Escalator is currently not in {@link HeightMode#ROW}, the given value
  5781. * is remembered, and applied once the mode is applied.
  5782. *
  5783. * @param rows
  5784. * the number of rows that should be visible in Escalator's body
  5785. * @throws IllegalArgumentException
  5786. * if {@code rows} is &leq; 0, {@link Double#isInfinite(double)
  5787. * infinite} or {@link Double#isNaN(double) NaN}.
  5788. * @see #setHeightMode(HeightMode)
  5789. */
  5790. public void setHeightByRows(double rows) throws IllegalArgumentException {
  5791. if (rows < 0) {
  5792. throw new IllegalArgumentException(
  5793. "The number of rows must be a positive number.");
  5794. } else if (Double.isInfinite(rows)) {
  5795. throw new IllegalArgumentException(
  5796. "The number of rows must be finite.");
  5797. } else if (Double.isNaN(rows)) {
  5798. throw new IllegalArgumentException("The number must not be NaN.");
  5799. }
  5800. heightByRows = rows;
  5801. applyHeightByRows();
  5802. }
  5803. /**
  5804. * Gets the amount of rows in Escalator's body that are shown, while
  5805. * {@link #getHeightMode()} is {@link HeightMode#ROW}.
  5806. * <p>
  5807. * By default, it is 10.
  5808. *
  5809. * @return the amount of rows that are being shown in Escalator's body
  5810. * @see #setHeightByRows(double)
  5811. */
  5812. public double getHeightByRows() {
  5813. return heightByRows;
  5814. }
  5815. /**
  5816. * Reapplies the row-based height of the Grid, if Grid currently should
  5817. * define its height that way.
  5818. */
  5819. private void applyHeightByRows() {
  5820. if (heightMode != HeightMode.ROW
  5821. && heightMode != HeightMode.UNDEFINED) {
  5822. return;
  5823. }
  5824. double headerHeight = header.getHeightOfSection();
  5825. double footerHeight = footer.getHeightOfSection();
  5826. double bodyHeight = body.getDefaultRowHeight() * heightByRows;
  5827. double scrollbar = horizontalScrollbar.showsScrollHandle()
  5828. ? horizontalScrollbar.getScrollbarThickness()
  5829. : 0;
  5830. double spacerHeight = 0; // ignored if HeightMode.ROW
  5831. if (heightMode == HeightMode.UNDEFINED) {
  5832. spacerHeight = body.spacerContainer.getSpacerHeightsSum();
  5833. }
  5834. double totalHeight = headerHeight + bodyHeight + spacerHeight
  5835. + scrollbar + footerHeight;
  5836. setHeightInternal(totalHeight + "px");
  5837. }
  5838. /**
  5839. * Defines the mode in which the Escalator widget's height is calculated.
  5840. * <p>
  5841. * If {@link HeightMode#CSS} is given, Escalator will respect the values
  5842. * given via {@link #setHeight(String)}, and behave as a traditional Widget.
  5843. * <p>
  5844. * If {@link HeightMode#ROW} is given, Escalator will make sure that the
  5845. * {@link #getBody() body} will display as many rows as
  5846. * {@link #getHeightByRows()} defines. <em>Note:</em> If headers/footers are
  5847. * inserted or removed, the widget will resize itself to still display the
  5848. * required amount of rows in its body. It also takes the horizontal
  5849. * scrollbar into account.
  5850. *
  5851. * @param heightMode
  5852. * the mode in to which Escalator should be set
  5853. */
  5854. public void setHeightMode(HeightMode heightMode) {
  5855. /*
  5856. * This method is a workaround for the fact that Vaadin re-applies
  5857. * widget dimensions (height/width) on each state change event. The
  5858. * original design was to have setHeight an setHeightByRow be equals,
  5859. * and whichever was called the latest was considered in effect.
  5860. *
  5861. * But, because of Vaadin always calling setHeight on the widget, this
  5862. * approach doesn't work.
  5863. */
  5864. if (heightMode != this.heightMode) {
  5865. this.heightMode = heightMode;
  5866. switch (this.heightMode) {
  5867. case CSS:
  5868. setHeight(heightByCss);
  5869. break;
  5870. case ROW:
  5871. setHeightByRows(heightByRows);
  5872. break;
  5873. case UNDEFINED:
  5874. setHeightByRows(body.getRowCount());
  5875. break;
  5876. default:
  5877. throw new IllegalStateException("Unimplemented feature "
  5878. + "- unknown HeightMode: " + this.heightMode);
  5879. }
  5880. }
  5881. }
  5882. /**
  5883. * Returns the current {@link HeightMode} the Escalator is in.
  5884. * <p>
  5885. * Defaults to {@link HeightMode#CSS}.
  5886. *
  5887. * @return the current HeightMode
  5888. */
  5889. public HeightMode getHeightMode() {
  5890. return heightMode;
  5891. }
  5892. /**
  5893. * Returns the {@link RowContainer} which contains the element.
  5894. *
  5895. * @param element
  5896. * the element to check for
  5897. * @return the container the element is in or <code>null</code> if element
  5898. * is not present in any container.
  5899. */
  5900. public RowContainer findRowContainer(Element element) {
  5901. if (getHeader().getElement() != element
  5902. && getHeader().getElement().isOrHasChild(element)) {
  5903. return getHeader();
  5904. } else if (getBody().getElement() != element
  5905. && getBody().getElement().isOrHasChild(element)) {
  5906. return getBody();
  5907. } else if (getFooter().getElement() != element
  5908. && getFooter().getElement().isOrHasChild(element)) {
  5909. return getFooter();
  5910. }
  5911. return null;
  5912. }
  5913. /**
  5914. * Sets whether a scroll direction is locked or not.
  5915. * <p>
  5916. * If a direction is locked, the escalator will refuse to scroll in that
  5917. * direction.
  5918. *
  5919. * @param direction
  5920. * the orientation of the scroll to set the lock status
  5921. * @param locked
  5922. * <code>true</code> to lock, <code>false</code> to unlock
  5923. */
  5924. public void setScrollLocked(ScrollbarBundle.Direction direction,
  5925. boolean locked) {
  5926. switch (direction) {
  5927. case HORIZONTAL:
  5928. horizontalScrollbar.setLocked(locked);
  5929. break;
  5930. case VERTICAL:
  5931. verticalScrollbar.setLocked(locked);
  5932. break;
  5933. default:
  5934. throw new UnsupportedOperationException(
  5935. "Unexpected value: " + direction);
  5936. }
  5937. }
  5938. /**
  5939. * Checks whether or not an direction is locked for scrolling.
  5940. *
  5941. * @param direction
  5942. * the direction of the scroll of which to check the lock status
  5943. * @return <code>true</code> if the direction is locked
  5944. */
  5945. public boolean isScrollLocked(ScrollbarBundle.Direction direction) {
  5946. switch (direction) {
  5947. case HORIZONTAL:
  5948. return horizontalScrollbar.isLocked();
  5949. case VERTICAL:
  5950. return verticalScrollbar.isLocked();
  5951. default:
  5952. throw new UnsupportedOperationException(
  5953. "Unexpected value: " + direction);
  5954. }
  5955. }
  5956. /**
  5957. * Adds a scroll handler to this escalator.
  5958. *
  5959. * @param handler
  5960. * the scroll handler to add
  5961. * @return a handler registration for the registered scroll handler
  5962. */
  5963. public HandlerRegistration addScrollHandler(ScrollHandler handler) {
  5964. return addHandler(handler, ScrollEvent.TYPE);
  5965. }
  5966. /**
  5967. * Returns true if the Escalator is currently scrolling by touch, or has not
  5968. * made the decision yet whether to accept touch actions as scrolling or
  5969. * not.
  5970. *
  5971. * @see #setDelayToCancelTouchScroll(double)
  5972. *
  5973. * @return true when the component is touch scrolling at the moment
  5974. * @since 8.1
  5975. */
  5976. public boolean isTouchScrolling() {
  5977. return scroller.touchHandlerBundle.touching;
  5978. }
  5979. /**
  5980. * Returns the time after which to not consider a touch event a scroll event
  5981. * if the user has not moved the touch. This can be used to differentiate
  5982. * between quick touch move (scrolling) and long tap (e.g. context menu or
  5983. * drag and drop operation).
  5984. *
  5985. * @return delay in milliseconds after which to cancel touch scrolling if
  5986. * there is no movement, -1 means scrolling is always allowed
  5987. * @since 8.1
  5988. */
  5989. public double getDelayToCancelTouchScroll() {
  5990. return delayToCancelTouchScroll;
  5991. }
  5992. /**
  5993. * Sets the time after which to not consider a touch event a scroll event if
  5994. * the user has not moved the touch. This can be used to differentiate
  5995. * between quick touch move (scrolling) and long tap (e.g. context menu or
  5996. * drag and drop operation).
  5997. *
  5998. * @param delayToCancelTouchScroll
  5999. * delay in milliseconds after which to cancel touch scrolling if
  6000. * there is no movement, -1 to always allow scrolling
  6001. * @since 8.1
  6002. */
  6003. public void setDelayToCancelTouchScroll(double delayToCancelTouchScroll) {
  6004. this.delayToCancelTouchScroll = delayToCancelTouchScroll;
  6005. }
  6006. @Override
  6007. public boolean isWorkPending() {
  6008. return body.domSorter.waiting || verticalScrollbar.isWorkPending()
  6009. || horizontalScrollbar.isWorkPending() || layoutIsScheduled;
  6010. }
  6011. @Override
  6012. public void onResize() {
  6013. if (isAttached() && !layoutIsScheduled) {
  6014. layoutIsScheduled = true;
  6015. Scheduler.get().scheduleFinally(layoutCommand);
  6016. }
  6017. }
  6018. /**
  6019. * Gets the maximum number of body rows that can be visible on the screen at
  6020. * once.
  6021. *
  6022. * @return the maximum capacity
  6023. */
  6024. public int getMaxVisibleRowCount() {
  6025. return body.getMaxVisibleRowCount();
  6026. }
  6027. /**
  6028. * Gets the escalator's inner width. This is the entire width in pixels,
  6029. * without the vertical scrollbar.
  6030. *
  6031. * @return escalator's inner width
  6032. */
  6033. public double getInnerWidth() {
  6034. return getBoundingWidth(tableWrapper);
  6035. }
  6036. /**
  6037. * Resets all cached pixel sizes and reads new values from the DOM. This
  6038. * methods should be used e.g. when styles affecting the dimensions of
  6039. * elements in this escalator have been changed.
  6040. */
  6041. public void resetSizesFromDom() {
  6042. header.autodetectRowHeightNow();
  6043. body.autodetectRowHeightNow();
  6044. footer.autodetectRowHeightNow();
  6045. for (int i = 0; i < columnConfiguration.getColumnCount(); i++) {
  6046. columnConfiguration.setColumnWidth(i,
  6047. columnConfiguration.getColumnWidth(i));
  6048. }
  6049. }
  6050. private Range getViewportPixels() {
  6051. int from = (int) Math.floor(verticalScrollbar.getScrollPos());
  6052. int to = (int) body.getHeightOfSection();
  6053. return Range.withLength(from, to);
  6054. }
  6055. @Override
  6056. @SuppressWarnings("deprecation")
  6057. public com.google.gwt.user.client.Element getSubPartElement(
  6058. String subPart) {
  6059. SubPartArguments args = SubPartArguments.create(subPart);
  6060. Element tableStructureElement = getSubPartElementTableStructure(args);
  6061. if (tableStructureElement != null) {
  6062. return DOM.asOld(tableStructureElement);
  6063. }
  6064. Element spacerElement = getSubPartElementSpacer(args);
  6065. if (spacerElement != null) {
  6066. return DOM.asOld(spacerElement);
  6067. }
  6068. return null;
  6069. }
  6070. /**
  6071. * Returns the {@code <div class="{primary-stylename}-tablewrapper" />}
  6072. * element which has the table inside it. {primary-stylename} is .e.g
  6073. * {@code v-grid}.
  6074. * <p>
  6075. * <em>NOTE: you should not do any modifications to the returned element.
  6076. * This API is only available for querying data from the element.</em>
  6077. *
  6078. * @return the table wrapper element
  6079. * @since 8.1
  6080. */
  6081. public Element getTableWrapper() {
  6082. return tableWrapper;
  6083. }
  6084. /**
  6085. * Returns the <code>&lt;table&gt;</code> element of the grid.
  6086. *
  6087. * @return the table element
  6088. * @since 8.2
  6089. */
  6090. public Element getTable() {
  6091. return table;
  6092. }
  6093. private Element getSubPartElementTableStructure(SubPartArguments args) {
  6094. String type = args.getType();
  6095. int[] indices = args.getIndices();
  6096. // Get correct RowContainer for type from Escalator
  6097. RowContainer container = null;
  6098. if (type.equalsIgnoreCase("header")) {
  6099. container = getHeader();
  6100. } else if (type.equalsIgnoreCase("cell")) {
  6101. // If wanted row is not visible, we need to scroll there.
  6102. Range visibleRowRange = getVisibleRowRange();
  6103. if (indices.length > 0) {
  6104. // Contains a row number, ensure it is available and visible
  6105. boolean rowInCache = visibleRowRange.contains(indices[0]);
  6106. // Scrolling might be a no-op if row is already in the viewport
  6107. scrollToRow(indices[0], ScrollDestination.ANY, 0);
  6108. if (!rowInCache) {
  6109. // Row was not in cache, scrolling caused lazy loading and
  6110. // the caller needs to wait and call this method again to be
  6111. // able to get the requested element
  6112. return null;
  6113. }
  6114. }
  6115. container = getBody();
  6116. } else if (type.equalsIgnoreCase("footer")) {
  6117. container = getFooter();
  6118. }
  6119. if (null != container) {
  6120. if (indices.length == 0) {
  6121. // No indexing. Just return the wanted container element
  6122. return container.getElement();
  6123. } else {
  6124. try {
  6125. return getSubPart(container, indices);
  6126. } catch (Exception e) {
  6127. getLogger().log(Level.SEVERE, e.getMessage());
  6128. }
  6129. }
  6130. }
  6131. return null;
  6132. }
  6133. private Element getSubPart(RowContainer container, int[] indices) {
  6134. Element targetElement = container.getRowElement(indices[0]);
  6135. // Scroll wanted column to view if able
  6136. if (indices.length > 1 && targetElement != null) {
  6137. if (getColumnConfiguration().getFrozenColumnCount() <= indices[1]) {
  6138. scrollToColumn(indices[1], ScrollDestination.ANY, 0);
  6139. }
  6140. targetElement = getCellFromRow(TableRowElement.as(targetElement),
  6141. indices[1]);
  6142. for (int i = 2; i < indices.length && targetElement != null; ++i) {
  6143. targetElement = (Element) targetElement.getChild(indices[i]);
  6144. }
  6145. }
  6146. return targetElement;
  6147. }
  6148. private static Element getCellFromRow(TableRowElement rowElement,
  6149. int index) {
  6150. int childCount = rowElement.getCells().getLength();
  6151. if (index < 0 || index >= childCount) {
  6152. return null;
  6153. }
  6154. TableCellElement currentCell = null;
  6155. boolean indexInColspan = false;
  6156. int i = 0;
  6157. while (!indexInColspan) {
  6158. currentCell = rowElement.getCells().getItem(i);
  6159. // Calculate if this is the cell we are looking for
  6160. int colSpan = currentCell.getColSpan();
  6161. indexInColspan = index < colSpan + i;
  6162. // Increment by colspan to skip over hidden cells
  6163. i += colSpan;
  6164. }
  6165. return currentCell;
  6166. }
  6167. private Element getSubPartElementSpacer(SubPartArguments args) {
  6168. if ("spacer".equals(args.getType()) && args.getIndicesLength() == 1) {
  6169. return body.spacerContainer.getSubPartElement(args.getIndex(0));
  6170. } else {
  6171. return null;
  6172. }
  6173. }
  6174. @Override
  6175. @SuppressWarnings("deprecation")
  6176. public String getSubPartName(
  6177. com.google.gwt.user.client.Element subElement) {
  6178. /*
  6179. * The spacer check needs to be before table structure check, because
  6180. * (for now) the table structure will take spacer elements into account
  6181. * as well, when it shouldn't.
  6182. */
  6183. String spacer = getSubPartNameSpacer(subElement);
  6184. if (spacer != null) {
  6185. return spacer;
  6186. }
  6187. String tableStructure = getSubPartNameTableStructure(subElement);
  6188. if (tableStructure != null) {
  6189. return tableStructure;
  6190. }
  6191. return null;
  6192. }
  6193. private String getSubPartNameTableStructure(Element subElement) {
  6194. List<RowContainer> containers = Arrays.asList(getHeader(), getBody(),
  6195. getFooter());
  6196. List<String> containerType = Arrays.asList("header", "cell", "footer");
  6197. for (int i = 0; i < containers.size(); ++i) {
  6198. RowContainer container = containers.get(i);
  6199. boolean containerRow = (subElement.getTagName()
  6200. .equalsIgnoreCase("tr")
  6201. && subElement.getParentElement() == container.getElement());
  6202. if (containerRow) {
  6203. /*
  6204. * Wanted SubPart is row that is a child of containers root to
  6205. * get indices, we use a cell that is a child of this row
  6206. */
  6207. subElement = subElement.getFirstChildElement();
  6208. }
  6209. Cell cell = container.getCell(subElement);
  6210. if (cell != null) {
  6211. // Skip the column index if subElement was a child of root
  6212. return containerType.get(i) + "[" + cell.getRow()
  6213. + (containerRow ? "]" : "][" + cell.getColumn() + "]");
  6214. }
  6215. }
  6216. return null;
  6217. }
  6218. private String getSubPartNameSpacer(Element subElement) {
  6219. return body.spacerContainer.getSubPartName(subElement);
  6220. }
  6221. private void logWarning(String message) {
  6222. getLogger().warning(message);
  6223. }
  6224. /**
  6225. * This is an internal method for calculating minimum width for Column
  6226. * resize.
  6227. *
  6228. * @return minimum width for column
  6229. */
  6230. double getMinCellWidth(int colIndex) {
  6231. return columnConfiguration.getMinCellWidth(colIndex);
  6232. }
  6233. /**
  6234. * Internal method for checking whether the browser is IE11 or Edge
  6235. *
  6236. * @return true only if the current browser is IE11, or Edge
  6237. */
  6238. private static boolean isCurrentBrowserIE11OrEdge() {
  6239. return BrowserInfo.get().isIE11() || BrowserInfo.get().isEdge();
  6240. }
  6241. }