tooltipster.bundle.js 117 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273
  1. /**
  2. * tooltipster http://iamceege.github.io/tooltipster/
  3. * A rockin' custom tooltip jQuery plugin
  4. * Developed by Caleb Jacob and Louis Ameline
  5. * MIT license
  6. */
  7. (function (root, factory) {
  8. if (typeof define === 'function' && define.amd) {
  9. // AMD. Register as an anonymous module unless amdModuleId is set
  10. define(["jquery"], function (a0) {
  11. return (factory(a0));
  12. });
  13. } else if (typeof exports === 'object') {
  14. // Node. Does not work with strict CommonJS, but
  15. // only CommonJS-like environments that support module.exports,
  16. // like Node.
  17. module.exports = factory(require("jquery"));
  18. } else {
  19. factory(jQuery);
  20. }
  21. }(this, function ($) {
  22. // This file will be UMDified by a build task.
  23. var defaults = {
  24. animation: 'fade',
  25. animationDuration: 350,
  26. content: null,
  27. contentAsHTML: false,
  28. contentCloning: false,
  29. debug: true,
  30. delay: 300,
  31. delayTouch: [300, 500],
  32. functionInit: null,
  33. functionBefore: null,
  34. functionReady: null,
  35. functionAfter: null,
  36. functionFormat: null,
  37. IEmin: 6,
  38. interactive: false,
  39. multiple: false,
  40. // will default to document.body, or must be an element positioned at (0, 0)
  41. // in the document, typically like the very top views of an app.
  42. parent: null,
  43. plugins: ['sideTip'],
  44. repositionOnScroll: false,
  45. restoration: 'none',
  46. selfDestruction: true,
  47. theme: [],
  48. timer: 0,
  49. trackerInterval: 500,
  50. trackOrigin: false,
  51. trackTooltip: false,
  52. trigger: 'hover',
  53. triggerClose: {
  54. click: false,
  55. mouseleave: false,
  56. originClick: false,
  57. scroll: false,
  58. tap: false,
  59. touchleave: false
  60. },
  61. triggerOpen: {
  62. click: false,
  63. mouseenter: false,
  64. tap: false,
  65. touchstart: false
  66. },
  67. updateAnimation: 'rotate',
  68. zIndex: 9999999
  69. },
  70. // we'll avoid using the 'window' global as a good practice but npm's
  71. // jquery@<2.1.0 package actually requires a 'window' global, so not sure
  72. // it's useful at all
  73. win = (typeof window != 'undefined') ? window : null,
  74. // env will be proxied by the core for plugins to have access its properties
  75. env = {
  76. // detect if this device can trigger touch events. Better have a false
  77. // positive (unused listeners, that's ok) than a false negative.
  78. // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/touchevents.js
  79. // http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript
  80. hasTouchCapability: !!(
  81. win
  82. && ( 'ontouchstart' in win
  83. || (win.DocumentTouch && win.document instanceof win.DocumentTouch)
  84. || win.navigator.maxTouchPoints
  85. )
  86. ),
  87. hasTransitions: transitionSupport(),
  88. IE: false,
  89. // don't set manually, it will be updated by a build task after the manifest
  90. semVer: '4.2.6',
  91. window: win
  92. },
  93. core = function() {
  94. // core variables
  95. // the core emitters
  96. this.__$emitterPrivate = $({});
  97. this.__$emitterPublic = $({});
  98. this.__instancesLatestArr = [];
  99. // collects plugin constructors
  100. this.__plugins = {};
  101. // proxy env variables for plugins who might use them
  102. this._env = env;
  103. };
  104. // core methods
  105. core.prototype = {
  106. /**
  107. * A function to proxy the public methods of an object onto another
  108. *
  109. * @param {object} constructor The constructor to bridge
  110. * @param {object} obj The object that will get new methods (an instance or the core)
  111. * @param {string} pluginName A plugin name for the console log message
  112. * @return {core}
  113. * @private
  114. */
  115. __bridge: function(constructor, obj, pluginName) {
  116. // if it's not already bridged
  117. if (!obj[pluginName]) {
  118. var fn = function() {};
  119. fn.prototype = constructor;
  120. var pluginInstance = new fn();
  121. // the _init method has to exist in instance constructors but might be missing
  122. // in core constructors
  123. if (pluginInstance.__init) {
  124. pluginInstance.__init(obj);
  125. }
  126. $.each(constructor, function(methodName, fn) {
  127. // don't proxy "private" methods, only "protected" and public ones
  128. if (methodName.indexOf('__') != 0) {
  129. // if the method does not exist yet
  130. if (!obj[methodName]) {
  131. obj[methodName] = function() {
  132. return pluginInstance[methodName].apply(pluginInstance, Array.prototype.slice.apply(arguments));
  133. };
  134. // remember to which plugin this method corresponds (several plugins may
  135. // have methods of the same name, we need to be sure)
  136. obj[methodName].bridged = pluginInstance;
  137. }
  138. else if (defaults.debug) {
  139. console.log('The '+ methodName +' method of the '+ pluginName
  140. +' plugin conflicts with another plugin or native methods');
  141. }
  142. }
  143. });
  144. obj[pluginName] = pluginInstance;
  145. }
  146. return this;
  147. },
  148. /**
  149. * For mockup in Node env if need be, for testing purposes
  150. *
  151. * @return {core}
  152. * @private
  153. */
  154. __setWindow: function(window) {
  155. env.window = window;
  156. return this;
  157. },
  158. /**
  159. * Returns a ruler, a tool to help measure the size of a tooltip under
  160. * various settings. Meant for plugins
  161. *
  162. * @see Ruler
  163. * @return {object} A Ruler instance
  164. * @protected
  165. */
  166. _getRuler: function($tooltip) {
  167. return new Ruler($tooltip);
  168. },
  169. /**
  170. * For internal use by plugins, if needed
  171. *
  172. * @return {core}
  173. * @protected
  174. */
  175. _off: function() {
  176. this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
  177. return this;
  178. },
  179. /**
  180. * For internal use by plugins, if needed
  181. *
  182. * @return {core}
  183. * @protected
  184. */
  185. _on: function() {
  186. this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
  187. return this;
  188. },
  189. /**
  190. * For internal use by plugins, if needed
  191. *
  192. * @return {core}
  193. * @protected
  194. */
  195. _one: function() {
  196. this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
  197. return this;
  198. },
  199. /**
  200. * Returns (getter) or adds (setter) a plugin
  201. *
  202. * @param {string|object} plugin Provide a string (in the full form
  203. * "namespace.name") to use as as getter, an object to use as a setter
  204. * @return {object|core}
  205. * @protected
  206. */
  207. _plugin: function(plugin) {
  208. var self = this;
  209. // getter
  210. if (typeof plugin == 'string') {
  211. var pluginName = plugin,
  212. p = null;
  213. // if the namespace is provided, it's easy to search
  214. if (pluginName.indexOf('.') > 0) {
  215. p = self.__plugins[pluginName];
  216. }
  217. // otherwise, return the first name that matches
  218. else {
  219. $.each(self.__plugins, function(i, plugin) {
  220. if (plugin.name.substring(plugin.name.length - pluginName.length - 1) == '.'+ pluginName) {
  221. p = plugin;
  222. return false;
  223. }
  224. });
  225. }
  226. return p;
  227. }
  228. // setter
  229. else {
  230. // force namespaces
  231. if (plugin.name.indexOf('.') < 0) {
  232. throw new Error('Plugins must be namespaced');
  233. }
  234. self.__plugins[plugin.name] = plugin;
  235. // if the plugin has core features
  236. if (plugin.core) {
  237. // bridge non-private methods onto the core to allow new core methods
  238. self.__bridge(plugin.core, self, plugin.name);
  239. }
  240. return this;
  241. }
  242. },
  243. /**
  244. * Trigger events on the core emitters
  245. *
  246. * @returns {core}
  247. * @protected
  248. */
  249. _trigger: function() {
  250. var args = Array.prototype.slice.apply(arguments);
  251. if (typeof args[0] == 'string') {
  252. args[0] = { type: args[0] };
  253. }
  254. // note: the order of emitters matters
  255. this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args);
  256. this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args);
  257. return this;
  258. },
  259. /**
  260. * Returns instances of all tooltips in the page or an a given element
  261. *
  262. * @param {string|HTML object collection} selector optional Use this
  263. * parameter to restrict the set of objects that will be inspected
  264. * for the retrieval of instances. By default, all instances in the
  265. * page are returned.
  266. * @return {array} An array of instance objects
  267. * @public
  268. */
  269. instances: function(selector) {
  270. var instances = [],
  271. sel = selector || '.tooltipstered';
  272. $(sel).each(function() {
  273. var $this = $(this),
  274. ns = $this.data('tooltipster-ns');
  275. if (ns) {
  276. $.each(ns, function(i, namespace) {
  277. instances.push($this.data(namespace));
  278. });
  279. }
  280. });
  281. return instances;
  282. },
  283. /**
  284. * Returns the Tooltipster objects generated by the last initializing call
  285. *
  286. * @return {array} An array of instance objects
  287. * @public
  288. */
  289. instancesLatest: function() {
  290. return this.__instancesLatestArr;
  291. },
  292. /**
  293. * For public use only, not to be used by plugins (use ::_off() instead)
  294. *
  295. * @return {core}
  296. * @public
  297. */
  298. off: function() {
  299. this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
  300. return this;
  301. },
  302. /**
  303. * For public use only, not to be used by plugins (use ::_on() instead)
  304. *
  305. * @return {core}
  306. * @public
  307. */
  308. on: function() {
  309. this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
  310. return this;
  311. },
  312. /**
  313. * For public use only, not to be used by plugins (use ::_one() instead)
  314. *
  315. * @return {core}
  316. * @public
  317. */
  318. one: function() {
  319. this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
  320. return this;
  321. },
  322. /**
  323. * Returns all HTML elements which have one or more tooltips
  324. *
  325. * @param {string} selector optional Use this to restrict the results
  326. * to the descendants of an element
  327. * @return {array} An array of HTML elements
  328. * @public
  329. */
  330. origins: function(selector) {
  331. var sel = selector ?
  332. selector +' ' :
  333. '';
  334. return $(sel +'.tooltipstered').toArray();
  335. },
  336. /**
  337. * Change default options for all future instances
  338. *
  339. * @param {object} d The options that should be made defaults
  340. * @return {core}
  341. * @public
  342. */
  343. setDefaults: function(d) {
  344. $.extend(defaults, d);
  345. return this;
  346. },
  347. /**
  348. * For users to trigger their handlers on the public emitter
  349. *
  350. * @returns {core}
  351. * @public
  352. */
  353. triggerHandler: function() {
  354. this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
  355. return this;
  356. }
  357. };
  358. // $.tooltipster will be used to call core methods
  359. $.tooltipster = new core();
  360. // the Tooltipster instance class (mind the capital T)
  361. $.Tooltipster = function(element, options) {
  362. // list of instance variables
  363. // stack of custom callbacks provided as parameters to API methods
  364. this.__callbacks = {
  365. close: [],
  366. open: []
  367. };
  368. // the schedule time of DOM removal
  369. this.__closingTime;
  370. // this will be the user content shown in the tooltip. A capital "C" is used
  371. // because there is also a method called content()
  372. this.__Content;
  373. // for the size tracker
  374. this.__contentBcr;
  375. // to disable the tooltip after destruction
  376. this.__destroyed = false;
  377. // we can't emit directly on the instance because if a method with the same
  378. // name as the event exists, it will be called by jQuery. Se we use a plain
  379. // object as emitter. This emitter is for internal use by plugins,
  380. // if needed.
  381. this.__$emitterPrivate = $({});
  382. // this emitter is for the user to listen to events without risking to mess
  383. // with our internal listeners
  384. this.__$emitterPublic = $({});
  385. this.__enabled = true;
  386. // the reference to the gc interval
  387. this.__garbageCollector;
  388. // various position and size data recomputed before each repositioning
  389. this.__Geometry;
  390. // the tooltip position, saved after each repositioning by a plugin
  391. this.__lastPosition;
  392. // a unique namespace per instance
  393. this.__namespace = 'tooltipster-'+ Math.round(Math.random()*1000000);
  394. this.__options;
  395. // will be used to support origins in scrollable areas
  396. this.__$originParents;
  397. this.__pointerIsOverOrigin = false;
  398. // to remove themes if needed
  399. this.__previousThemes = [];
  400. // the state can be either: appearing, stable, disappearing, closed
  401. this.__state = 'closed';
  402. // timeout references
  403. this.__timeouts = {
  404. close: [],
  405. open: null
  406. };
  407. // store touch events to be able to detect emulated mouse events
  408. this.__touchEvents = [];
  409. // the reference to the tracker interval
  410. this.__tracker = null;
  411. // the element to which this tooltip is associated
  412. this._$origin;
  413. // this will be the tooltip element (jQuery wrapped HTML element).
  414. // It's the job of a plugin to create it and append it to the DOM
  415. this._$tooltip;
  416. // launch
  417. this.__init(element, options);
  418. };
  419. $.Tooltipster.prototype = {
  420. /**
  421. * @param origin
  422. * @param options
  423. * @private
  424. */
  425. __init: function(origin, options) {
  426. var self = this;
  427. self._$origin = $(origin);
  428. self.__options = $.extend(true, {}, defaults, options);
  429. // some options may need to be reformatted
  430. self.__optionsFormat();
  431. // don't run on old IE if asked no to
  432. if ( !env.IE
  433. || env.IE >= self.__options.IEmin
  434. ) {
  435. // note: the content is null (empty) by default and can stay that
  436. // way if the plugin remains initialized but not fed any content. The
  437. // tooltip will just not appear.
  438. // let's save the initial value of the title attribute for later
  439. // restoration if need be.
  440. var initialTitle = null;
  441. // it will already have been saved in case of multiple tooltips
  442. if (self._$origin.data('tooltipster-initialTitle') === undefined) {
  443. initialTitle = self._$origin.attr('title');
  444. // we do not want initialTitle to be "undefined" because
  445. // of how jQuery's .data() method works
  446. if (initialTitle === undefined) initialTitle = null;
  447. self._$origin.data('tooltipster-initialTitle', initialTitle);
  448. }
  449. // If content is provided in the options, it has precedence over the
  450. // title attribute.
  451. // Note: an empty string is considered content, only 'null' represents
  452. // the absence of content.
  453. // Also, an existing title="" attribute will result in an empty string
  454. // content
  455. if (self.__options.content !== null) {
  456. self.__contentSet(self.__options.content);
  457. }
  458. else {
  459. var selector = self._$origin.attr('data-tooltip-content'),
  460. $el;
  461. if (selector){
  462. $el = $(selector);
  463. }
  464. if ($el && $el[0]) {
  465. self.__contentSet($el.first());
  466. }
  467. else {
  468. self.__contentSet(initialTitle);
  469. }
  470. }
  471. self._$origin
  472. // strip the title off of the element to prevent the default tooltips
  473. // from popping up
  474. .removeAttr('title')
  475. // to be able to find all instances on the page later (upon window
  476. // events in particular)
  477. .addClass('tooltipstered');
  478. // set listeners on the origin
  479. self.__prepareOrigin();
  480. // set the garbage collector
  481. self.__prepareGC();
  482. // init plugins
  483. $.each(self.__options.plugins, function(i, pluginName) {
  484. self._plug(pluginName);
  485. });
  486. // to detect swiping
  487. if (env.hasTouchCapability) {
  488. $(env.window.document.body).on('touchmove.'+ self.__namespace +'-triggerOpen', function(event) {
  489. self._touchRecordEvent(event);
  490. });
  491. }
  492. self
  493. // prepare the tooltip when it gets created. This event must
  494. // be fired by a plugin
  495. ._on('created', function() {
  496. self.__prepareTooltip();
  497. })
  498. // save position information when it's sent by a plugin
  499. ._on('repositioned', function(e) {
  500. self.__lastPosition = e.position;
  501. });
  502. }
  503. else {
  504. self.__options.disabled = true;
  505. }
  506. },
  507. /**
  508. * Insert the content into the appropriate HTML element of the tooltip
  509. *
  510. * @returns {self}
  511. * @private
  512. */
  513. __contentInsert: function() {
  514. var self = this,
  515. $el = self._$tooltip.find('.tooltipster-content'),
  516. formattedContent = self.__Content,
  517. format = function(content) {
  518. formattedContent = content;
  519. };
  520. self._trigger({
  521. type: 'format',
  522. content: self.__Content,
  523. format: format
  524. });
  525. if (self.__options.functionFormat) {
  526. formattedContent = self.__options.functionFormat.call(
  527. self,
  528. self,
  529. { origin: self._$origin[0] },
  530. self.__Content
  531. );
  532. }
  533. if (typeof formattedContent === 'string' && !self.__options.contentAsHTML) {
  534. $el.text(formattedContent);
  535. }
  536. else {
  537. $el
  538. .empty()
  539. .append(formattedContent);
  540. }
  541. return self;
  542. },
  543. /**
  544. * Save the content, cloning it beforehand if need be
  545. *
  546. * @param content
  547. * @returns {self}
  548. * @private
  549. */
  550. __contentSet: function(content) {
  551. // clone if asked. Cloning the object makes sure that each instance has its
  552. // own version of the content (in case a same object were provided for several
  553. // instances)
  554. // reminder: typeof null === object
  555. if (content instanceof $ && this.__options.contentCloning) {
  556. content = content.clone(true);
  557. }
  558. this.__Content = content;
  559. this._trigger({
  560. type: 'updated',
  561. content: content
  562. });
  563. return this;
  564. },
  565. /**
  566. * Error message about a method call made after destruction
  567. *
  568. * @private
  569. */
  570. __destroyError: function() {
  571. throw new Error('This tooltip has been destroyed and cannot execute your method call.');
  572. },
  573. /**
  574. * Gather all information about dimensions and available space,
  575. * called before every repositioning
  576. *
  577. * @private
  578. * @returns {object}
  579. */
  580. __geometry: function() {
  581. var self = this,
  582. $target = self._$origin,
  583. originIsArea = self._$origin.is('area');
  584. // if this._$origin is a map area, the target we'll need
  585. // the dimensions of is actually the image using the map,
  586. // not the area itself
  587. if (originIsArea) {
  588. var mapName = self._$origin.parent().attr('name');
  589. $target = $('img[usemap="#'+ mapName +'"]');
  590. }
  591. var bcr = $target[0].getBoundingClientRect(),
  592. $document = $(env.window.document),
  593. $window = $(env.window),
  594. $parent = $target,
  595. // some useful properties of important elements
  596. geo = {
  597. // available space for the tooltip, see down below
  598. available: {
  599. document: null,
  600. window: null
  601. },
  602. document: {
  603. size: {
  604. height: $document.height(),
  605. width: $document.width()
  606. }
  607. },
  608. window: {
  609. scroll: {
  610. // the second ones are for IE compatibility
  611. left: env.window.scrollX || env.window.document.documentElement.scrollLeft,
  612. top: env.window.scrollY || env.window.document.documentElement.scrollTop
  613. },
  614. size: {
  615. height: $window.height(),
  616. width: $window.width()
  617. }
  618. },
  619. origin: {
  620. // the origin has a fixed lineage if itself or one of its
  621. // ancestors has a fixed position
  622. fixedLineage: false,
  623. // relative to the document
  624. offset: {},
  625. size: {
  626. height: bcr.bottom - bcr.top,
  627. width: bcr.right - bcr.left
  628. },
  629. usemapImage: originIsArea ? $target[0] : null,
  630. // relative to the window
  631. windowOffset: {
  632. bottom: bcr.bottom,
  633. left: bcr.left,
  634. right: bcr.right,
  635. top: bcr.top
  636. }
  637. }
  638. },
  639. geoFixed = false;
  640. // if the element is a map area, some properties may need
  641. // to be recalculated
  642. if (originIsArea) {
  643. var shape = self._$origin.attr('shape'),
  644. coords = self._$origin.attr('coords');
  645. if (coords) {
  646. coords = coords.split(',');
  647. $.map(coords, function(val, i) {
  648. coords[i] = parseInt(val);
  649. });
  650. }
  651. // if the image itself is the area, nothing more to do
  652. if (shape != 'default') {
  653. switch(shape) {
  654. case 'circle':
  655. var circleCenterLeft = coords[0],
  656. circleCenterTop = coords[1],
  657. circleRadius = coords[2],
  658. areaTopOffset = circleCenterTop - circleRadius,
  659. areaLeftOffset = circleCenterLeft - circleRadius;
  660. geo.origin.size.height = circleRadius * 2;
  661. geo.origin.size.width = geo.origin.size.height;
  662. geo.origin.windowOffset.left += areaLeftOffset;
  663. geo.origin.windowOffset.top += areaTopOffset;
  664. break;
  665. case 'rect':
  666. var areaLeft = coords[0],
  667. areaTop = coords[1],
  668. areaRight = coords[2],
  669. areaBottom = coords[3];
  670. geo.origin.size.height = areaBottom - areaTop;
  671. geo.origin.size.width = areaRight - areaLeft;
  672. geo.origin.windowOffset.left += areaLeft;
  673. geo.origin.windowOffset.top += areaTop;
  674. break;
  675. case 'poly':
  676. var areaSmallestX = 0,
  677. areaSmallestY = 0,
  678. areaGreatestX = 0,
  679. areaGreatestY = 0,
  680. arrayAlternate = 'even';
  681. for (var i = 0; i < coords.length; i++) {
  682. var areaNumber = coords[i];
  683. if (arrayAlternate == 'even') {
  684. if (areaNumber > areaGreatestX) {
  685. areaGreatestX = areaNumber;
  686. if (i === 0) {
  687. areaSmallestX = areaGreatestX;
  688. }
  689. }
  690. if (areaNumber < areaSmallestX) {
  691. areaSmallestX = areaNumber;
  692. }
  693. arrayAlternate = 'odd';
  694. }
  695. else {
  696. if (areaNumber > areaGreatestY) {
  697. areaGreatestY = areaNumber;
  698. if (i == 1) {
  699. areaSmallestY = areaGreatestY;
  700. }
  701. }
  702. if (areaNumber < areaSmallestY) {
  703. areaSmallestY = areaNumber;
  704. }
  705. arrayAlternate = 'even';
  706. }
  707. }
  708. geo.origin.size.height = areaGreatestY - areaSmallestY;
  709. geo.origin.size.width = areaGreatestX - areaSmallestX;
  710. geo.origin.windowOffset.left += areaSmallestX;
  711. geo.origin.windowOffset.top += areaSmallestY;
  712. break;
  713. }
  714. }
  715. }
  716. // user callback through an event
  717. var edit = function(r) {
  718. geo.origin.size.height = r.height,
  719. geo.origin.windowOffset.left = r.left,
  720. geo.origin.windowOffset.top = r.top,
  721. geo.origin.size.width = r.width
  722. };
  723. self._trigger({
  724. type: 'geometry',
  725. edit: edit,
  726. geometry: {
  727. height: geo.origin.size.height,
  728. left: geo.origin.windowOffset.left,
  729. top: geo.origin.windowOffset.top,
  730. width: geo.origin.size.width
  731. }
  732. });
  733. // calculate the remaining properties with what we got
  734. geo.origin.windowOffset.right = geo.origin.windowOffset.left + geo.origin.size.width;
  735. geo.origin.windowOffset.bottom = geo.origin.windowOffset.top + geo.origin.size.height;
  736. geo.origin.offset.left = geo.origin.windowOffset.left + geo.window.scroll.left;
  737. geo.origin.offset.top = geo.origin.windowOffset.top + geo.window.scroll.top;
  738. geo.origin.offset.bottom = geo.origin.offset.top + geo.origin.size.height;
  739. geo.origin.offset.right = geo.origin.offset.left + geo.origin.size.width;
  740. // the space that is available to display the tooltip relatively to the document
  741. geo.available.document = {
  742. bottom: {
  743. height: geo.document.size.height - geo.origin.offset.bottom,
  744. width: geo.document.size.width
  745. },
  746. left: {
  747. height: geo.document.size.height,
  748. width: geo.origin.offset.left
  749. },
  750. right: {
  751. height: geo.document.size.height,
  752. width: geo.document.size.width - geo.origin.offset.right
  753. },
  754. top: {
  755. height: geo.origin.offset.top,
  756. width: geo.document.size.width
  757. }
  758. };
  759. // the space that is available to display the tooltip relatively to the viewport
  760. // (the resulting values may be negative if the origin overflows the viewport)
  761. geo.available.window = {
  762. bottom: {
  763. // the inner max is here to make sure the available height is no bigger
  764. // than the viewport height (when the origin is off screen at the top).
  765. // The outer max just makes sure that the height is not negative (when
  766. // the origin overflows at the bottom).
  767. height: Math.max(geo.window.size.height - Math.max(geo.origin.windowOffset.bottom, 0), 0),
  768. width: geo.window.size.width
  769. },
  770. left: {
  771. height: geo.window.size.height,
  772. width: Math.max(geo.origin.windowOffset.left, 0)
  773. },
  774. right: {
  775. height: geo.window.size.height,
  776. width: Math.max(geo.window.size.width - Math.max(geo.origin.windowOffset.right, 0), 0)
  777. },
  778. top: {
  779. height: Math.max(geo.origin.windowOffset.top, 0),
  780. width: geo.window.size.width
  781. }
  782. };
  783. while ($parent[0].tagName.toLowerCase() != 'html') {
  784. if ($parent.css('position') == 'fixed') {
  785. geo.origin.fixedLineage = true;
  786. break;
  787. }
  788. $parent = $parent.parent();
  789. }
  790. return geo;
  791. },
  792. /**
  793. * Some options may need to be formated before being used
  794. *
  795. * @returns {self}
  796. * @private
  797. */
  798. __optionsFormat: function() {
  799. if (typeof this.__options.animationDuration == 'number') {
  800. this.__options.animationDuration = [this.__options.animationDuration, this.__options.animationDuration];
  801. }
  802. if (typeof this.__options.delay == 'number') {
  803. this.__options.delay = [this.__options.delay, this.__options.delay];
  804. }
  805. if (typeof this.__options.delayTouch == 'number') {
  806. this.__options.delayTouch = [this.__options.delayTouch, this.__options.delayTouch];
  807. }
  808. if (typeof this.__options.theme == 'string') {
  809. this.__options.theme = [this.__options.theme];
  810. }
  811. // determine the future parent
  812. if (this.__options.parent === null) {
  813. this.__options.parent = $(env.window.document.body);
  814. }
  815. else if (typeof this.__options.parent == 'string') {
  816. this.__options.parent = $(this.__options.parent);
  817. }
  818. if (this.__options.trigger == 'hover') {
  819. this.__options.triggerOpen = {
  820. mouseenter: true,
  821. touchstart: true
  822. };
  823. this.__options.triggerClose = {
  824. mouseleave: true,
  825. originClick: true,
  826. touchleave: true
  827. };
  828. }
  829. else if (this.__options.trigger == 'click') {
  830. this.__options.triggerOpen = {
  831. click: true,
  832. tap: true
  833. };
  834. this.__options.triggerClose = {
  835. click: true,
  836. tap: true
  837. };
  838. }
  839. // for the plugins
  840. this._trigger('options');
  841. return this;
  842. },
  843. /**
  844. * Schedules or cancels the garbage collector task
  845. *
  846. * @returns {self}
  847. * @private
  848. */
  849. __prepareGC: function() {
  850. var self = this;
  851. // in case the selfDestruction option has been changed by a method call
  852. if (self.__options.selfDestruction) {
  853. // the GC task
  854. self.__garbageCollector = setInterval(function() {
  855. var now = new Date().getTime();
  856. // forget the old events
  857. self.__touchEvents = $.grep(self.__touchEvents, function(event, i) {
  858. // 1 minute
  859. return now - event.time > 60000;
  860. });
  861. // auto-destruct if the origin is gone
  862. if (!bodyContains(self._$origin)) {
  863. self.close(function(){
  864. self.destroy();
  865. });
  866. }
  867. }, 20000);
  868. }
  869. else {
  870. clearInterval(self.__garbageCollector);
  871. }
  872. return self;
  873. },
  874. /**
  875. * Sets listeners on the origin if the open triggers require them.
  876. * Unlike the listeners set at opening time, these ones
  877. * remain even when the tooltip is closed. It has been made a
  878. * separate method so it can be called when the triggers are
  879. * changed in the options. Closing is handled in _open()
  880. * because of the bindings that may be needed on the tooltip
  881. * itself
  882. *
  883. * @returns {self}
  884. * @private
  885. */
  886. __prepareOrigin: function() {
  887. var self = this;
  888. // in case we're resetting the triggers
  889. self._$origin.off('.'+ self.__namespace +'-triggerOpen');
  890. // if the device is touch capable, even if only mouse triggers
  891. // are asked, we need to listen to touch events to know if the mouse
  892. // events are actually emulated (so we can ignore them)
  893. if (env.hasTouchCapability) {
  894. self._$origin.on(
  895. 'touchstart.'+ self.__namespace +'-triggerOpen ' +
  896. 'touchend.'+ self.__namespace +'-triggerOpen ' +
  897. 'touchcancel.'+ self.__namespace +'-triggerOpen',
  898. function(event){
  899. self._touchRecordEvent(event);
  900. }
  901. );
  902. }
  903. // mouse click and touch tap work the same way
  904. if ( self.__options.triggerOpen.click
  905. || (self.__options.triggerOpen.tap && env.hasTouchCapability)
  906. ) {
  907. var eventNames = '';
  908. if (self.__options.triggerOpen.click) {
  909. eventNames += 'click.'+ self.__namespace +'-triggerOpen ';
  910. }
  911. if (self.__options.triggerOpen.tap && env.hasTouchCapability) {
  912. eventNames += 'touchend.'+ self.__namespace +'-triggerOpen';
  913. }
  914. self._$origin.on(eventNames, function(event) {
  915. if (self._touchIsMeaningfulEvent(event)) {
  916. self._open(event);
  917. }
  918. });
  919. }
  920. // mouseenter and touch start work the same way
  921. if ( self.__options.triggerOpen.mouseenter
  922. || (self.__options.triggerOpen.touchstart && env.hasTouchCapability)
  923. ) {
  924. var eventNames = '';
  925. if (self.__options.triggerOpen.mouseenter) {
  926. eventNames += 'mouseenter.'+ self.__namespace +'-triggerOpen ';
  927. }
  928. if (self.__options.triggerOpen.touchstart && env.hasTouchCapability) {
  929. eventNames += 'touchstart.'+ self.__namespace +'-triggerOpen';
  930. }
  931. self._$origin.on(eventNames, function(event) {
  932. if ( self._touchIsTouchEvent(event)
  933. || !self._touchIsEmulatedEvent(event)
  934. ) {
  935. self.__pointerIsOverOrigin = true;
  936. self._openShortly(event);
  937. }
  938. });
  939. }
  940. // info for the mouseleave/touchleave close triggers when they use a delay
  941. if ( self.__options.triggerClose.mouseleave
  942. || (self.__options.triggerClose.touchleave && env.hasTouchCapability)
  943. ) {
  944. var eventNames = '';
  945. if (self.__options.triggerClose.mouseleave) {
  946. eventNames += 'mouseleave.'+ self.__namespace +'-triggerOpen ';
  947. }
  948. if (self.__options.triggerClose.touchleave && env.hasTouchCapability) {
  949. eventNames += 'touchend.'+ self.__namespace +'-triggerOpen touchcancel.'+ self.__namespace +'-triggerOpen';
  950. }
  951. self._$origin.on(eventNames, function(event) {
  952. if (self._touchIsMeaningfulEvent(event)) {
  953. self.__pointerIsOverOrigin = false;
  954. }
  955. });
  956. }
  957. return self;
  958. },
  959. /**
  960. * Do the things that need to be done only once after the tooltip
  961. * HTML element it has been created. It has been made a separate
  962. * method so it can be called when options are changed. Remember
  963. * that the tooltip may actually exist in the DOM before it is
  964. * opened, and present after it has been closed: it's the display
  965. * plugin that takes care of handling it.
  966. *
  967. * @returns {self}
  968. * @private
  969. */
  970. __prepareTooltip: function() {
  971. var self = this,
  972. p = self.__options.interactive ? 'auto' : '';
  973. // this will be useful to know quickly if the tooltip is in
  974. // the DOM or not
  975. self._$tooltip
  976. .attr('id', self.__namespace)
  977. .css({
  978. // pointer events
  979. 'pointer-events': p,
  980. zIndex: self.__options.zIndex
  981. });
  982. // themes
  983. // remove the old ones and add the new ones
  984. $.each(self.__previousThemes, function(i, theme) {
  985. self._$tooltip.removeClass(theme);
  986. });
  987. $.each(self.__options.theme, function(i, theme) {
  988. self._$tooltip.addClass(theme);
  989. });
  990. self.__previousThemes = $.merge([], self.__options.theme);
  991. return self;
  992. },
  993. /**
  994. * Handles the scroll on any of the parents of the origin (when the
  995. * tooltip is open)
  996. *
  997. * @param {object} event
  998. * @returns {self}
  999. * @private
  1000. */
  1001. __scrollHandler: function(event) {
  1002. var self = this;
  1003. if (self.__options.triggerClose.scroll) {
  1004. self._close(event);
  1005. }
  1006. else {
  1007. // if the origin or tooltip have been removed: do nothing, the tracker will
  1008. // take care of it later
  1009. if (bodyContains(self._$origin) && bodyContains(self._$tooltip)) {
  1010. var geo = null;
  1011. // if the scroll happened on the window
  1012. if (event.target === env.window.document) {
  1013. // if the origin has a fixed lineage, window scroll will have no
  1014. // effect on its position nor on the position of the tooltip
  1015. if (!self.__Geometry.origin.fixedLineage) {
  1016. // we don't need to do anything unless repositionOnScroll is true
  1017. // because the tooltip will already have moved with the window
  1018. // (and of course with the origin)
  1019. if (self.__options.repositionOnScroll) {
  1020. self.reposition(event);
  1021. }
  1022. }
  1023. }
  1024. // if the scroll happened on another parent of the tooltip, it means
  1025. // that it's in a scrollable area and now needs to have its position
  1026. // adjusted or recomputed, depending ont the repositionOnScroll
  1027. // option. Also, if the origin is partly hidden due to a parent that
  1028. // hides its overflow, we'll just hide (not close) the tooltip.
  1029. else {
  1030. geo = self.__geometry();
  1031. var overflows = false;
  1032. // a fixed position origin is not affected by the overflow hiding
  1033. // of a parent
  1034. if (self._$origin.css('position') != 'fixed') {
  1035. self.__$originParents.each(function(i, el) {
  1036. var $el = $(el),
  1037. overflowX = $el.css('overflow-x'),
  1038. overflowY = $el.css('overflow-y');
  1039. if (overflowX != 'visible' || overflowY != 'visible') {
  1040. var bcr = el.getBoundingClientRect();
  1041. if (overflowX != 'visible') {
  1042. if ( geo.origin.windowOffset.left < bcr.left
  1043. || geo.origin.windowOffset.right > bcr.right
  1044. ) {
  1045. overflows = true;
  1046. return false;
  1047. }
  1048. }
  1049. if (overflowY != 'visible') {
  1050. if ( geo.origin.windowOffset.top < bcr.top
  1051. || geo.origin.windowOffset.bottom > bcr.bottom
  1052. ) {
  1053. overflows = true;
  1054. return false;
  1055. }
  1056. }
  1057. }
  1058. // no need to go further if fixed, for the same reason as above
  1059. if ($el.css('position') == 'fixed') {
  1060. return false;
  1061. }
  1062. });
  1063. }
  1064. if (overflows) {
  1065. self._$tooltip.css('visibility', 'hidden');
  1066. }
  1067. else {
  1068. self._$tooltip.css('visibility', 'visible');
  1069. // reposition
  1070. if (self.__options.repositionOnScroll) {
  1071. self.reposition(event);
  1072. }
  1073. // or just adjust offset
  1074. else {
  1075. // we have to use offset and not windowOffset because this way,
  1076. // only the scroll distance of the scrollable areas are taken into
  1077. // account (the scrolltop value of the main window must be
  1078. // ignored since the tooltip already moves with it)
  1079. var offsetLeft = geo.origin.offset.left - self.__Geometry.origin.offset.left,
  1080. offsetTop = geo.origin.offset.top - self.__Geometry.origin.offset.top;
  1081. // add the offset to the position initially computed by the display plugin
  1082. self._$tooltip.css({
  1083. left: self.__lastPosition.coord.left + offsetLeft,
  1084. top: self.__lastPosition.coord.top + offsetTop
  1085. });
  1086. }
  1087. }
  1088. }
  1089. self._trigger({
  1090. type: 'scroll',
  1091. event: event,
  1092. geo: geo
  1093. });
  1094. }
  1095. }
  1096. return self;
  1097. },
  1098. /**
  1099. * Changes the state of the tooltip
  1100. *
  1101. * @param {string} state
  1102. * @returns {self}
  1103. * @private
  1104. */
  1105. __stateSet: function(state) {
  1106. this.__state = state;
  1107. this._trigger({
  1108. type: 'state',
  1109. state: state
  1110. });
  1111. return this;
  1112. },
  1113. /**
  1114. * Clear appearance timeouts
  1115. *
  1116. * @returns {self}
  1117. * @private
  1118. */
  1119. __timeoutsClear: function() {
  1120. // there is only one possible open timeout: the delayed opening
  1121. // when the mouseenter/touchstart open triggers are used
  1122. clearTimeout(this.__timeouts.open);
  1123. this.__timeouts.open = null;
  1124. // ... but several close timeouts: the delayed closing when the
  1125. // mouseleave close trigger is used and the timer option
  1126. $.each(this.__timeouts.close, function(i, timeout) {
  1127. clearTimeout(timeout);
  1128. });
  1129. this.__timeouts.close = [];
  1130. return this;
  1131. },
  1132. /**
  1133. * Start the tracker that will make checks at regular intervals
  1134. *
  1135. * @returns {self}
  1136. * @private
  1137. */
  1138. __trackerStart: function() {
  1139. var self = this,
  1140. $content = self._$tooltip.find('.tooltipster-content');
  1141. // get the initial content size
  1142. if (self.__options.trackTooltip) {
  1143. self.__contentBcr = $content[0].getBoundingClientRect();
  1144. }
  1145. self.__tracker = setInterval(function() {
  1146. // if the origin or tooltip elements have been removed.
  1147. // Note: we could destroy the instance now if the origin has
  1148. // been removed but we'll leave that task to our garbage collector
  1149. if (!bodyContains(self._$origin) || !bodyContains(self._$tooltip)) {
  1150. self._close();
  1151. }
  1152. // if everything is alright
  1153. else {
  1154. // compare the former and current positions of the origin to reposition
  1155. // the tooltip if need be
  1156. if (self.__options.trackOrigin) {
  1157. var g = self.__geometry(),
  1158. identical = false;
  1159. // compare size first (a change requires repositioning too)
  1160. if (areEqual(g.origin.size, self.__Geometry.origin.size)) {
  1161. // for elements that have a fixed lineage (see __geometry()), we track the
  1162. // top and left properties (relative to window)
  1163. if (self.__Geometry.origin.fixedLineage) {
  1164. if (areEqual(g.origin.windowOffset, self.__Geometry.origin.windowOffset)) {
  1165. identical = true;
  1166. }
  1167. }
  1168. // otherwise, track total offset (relative to document)
  1169. else {
  1170. if (areEqual(g.origin.offset, self.__Geometry.origin.offset)) {
  1171. identical = true;
  1172. }
  1173. }
  1174. }
  1175. if (!identical) {
  1176. // close the tooltip when using the mouseleave close trigger
  1177. // (see https://github.com/iamceege/tooltipster/pull/253)
  1178. if (self.__options.triggerClose.mouseleave) {
  1179. self._close();
  1180. }
  1181. else {
  1182. self.reposition();
  1183. }
  1184. }
  1185. }
  1186. if (self.__options.trackTooltip) {
  1187. var currentBcr = $content[0].getBoundingClientRect();
  1188. if ( currentBcr.height !== self.__contentBcr.height
  1189. || currentBcr.width !== self.__contentBcr.width
  1190. ) {
  1191. self.reposition();
  1192. self.__contentBcr = currentBcr;
  1193. }
  1194. }
  1195. }
  1196. }, self.__options.trackerInterval);
  1197. return self;
  1198. },
  1199. /**
  1200. * Closes the tooltip (after the closing delay)
  1201. *
  1202. * @param event
  1203. * @param callback
  1204. * @param force Set to true to override a potential refusal of the user's function
  1205. * @returns {self}
  1206. * @protected
  1207. */
  1208. _close: function(event, callback, force) {
  1209. var self = this,
  1210. ok = true;
  1211. self._trigger({
  1212. type: 'close',
  1213. event: event,
  1214. stop: function() {
  1215. ok = false;
  1216. }
  1217. });
  1218. // a destroying tooltip (force == true) may not refuse to close
  1219. if (ok || force) {
  1220. // save the method custom callback and cancel any open method custom callbacks
  1221. if (callback) self.__callbacks.close.push(callback);
  1222. self.__callbacks.open = [];
  1223. // clear open/close timeouts
  1224. self.__timeoutsClear();
  1225. var finishCallbacks = function() {
  1226. // trigger any close method custom callbacks and reset them
  1227. $.each(self.__callbacks.close, function(i,c) {
  1228. c.call(self, self, {
  1229. event: event,
  1230. origin: self._$origin[0]
  1231. });
  1232. });
  1233. self.__callbacks.close = [];
  1234. };
  1235. if (self.__state != 'closed') {
  1236. var necessary = true,
  1237. d = new Date(),
  1238. now = d.getTime(),
  1239. newClosingTime = now + self.__options.animationDuration[1];
  1240. // the tooltip may already already be disappearing, but if a new
  1241. // call to close() is made after the animationDuration was changed
  1242. // to 0 (for example), we ought to actually close it sooner than
  1243. // previously scheduled. In that case it should be noted that the
  1244. // browser will not adapt the animation duration to the new
  1245. // animationDuration that was set after the start of the closing
  1246. // animation.
  1247. // Note: the same thing could be considered at opening, but is not
  1248. // really useful since the tooltip is actually opened immediately
  1249. // upon a call to _open(). Since it would not make the opening
  1250. // animation finish sooner, its sole impact would be to trigger the
  1251. // state event and the open callbacks sooner than the actual end of
  1252. // the opening animation, which is not great.
  1253. if (self.__state == 'disappearing') {
  1254. if ( newClosingTime > self.__closingTime
  1255. // in case closing is actually overdue because the script
  1256. // execution was suspended. See #679
  1257. && self.__options.animationDuration[1] > 0
  1258. ) {
  1259. necessary = false;
  1260. }
  1261. }
  1262. if (necessary) {
  1263. self.__closingTime = newClosingTime;
  1264. if (self.__state != 'disappearing') {
  1265. self.__stateSet('disappearing');
  1266. }
  1267. var finish = function() {
  1268. // stop the tracker
  1269. clearInterval(self.__tracker);
  1270. // a "beforeClose" option has been asked several times but would
  1271. // probably useless since the content element is still accessible
  1272. // via ::content(), and because people can always use listeners
  1273. // inside their content to track what's going on. For the sake of
  1274. // simplicity, this has been denied. Bur for the rare people who
  1275. // really need the option (for old browsers or for the case where
  1276. // detaching the content is actually destructive, for file or
  1277. // password inputs for example), this event will do the work.
  1278. self._trigger({
  1279. type: 'closing',
  1280. event: event
  1281. });
  1282. // unbind listeners which are no longer needed
  1283. self._$tooltip
  1284. .off('.'+ self.__namespace +'-triggerClose')
  1285. .removeClass('tooltipster-dying');
  1286. // orientationchange, scroll and resize listeners
  1287. $(env.window).off('.'+ self.__namespace +'-triggerClose');
  1288. // scroll listeners
  1289. self.__$originParents.each(function(i, el) {
  1290. $(el).off('scroll.'+ self.__namespace +'-triggerClose');
  1291. });
  1292. // clear the array to prevent memory leaks
  1293. self.__$originParents = null;
  1294. $(env.window.document.body).off('.'+ self.__namespace +'-triggerClose');
  1295. self._$origin.off('.'+ self.__namespace +'-triggerClose');
  1296. self._off('dismissable');
  1297. // a plugin that would like to remove the tooltip from the
  1298. // DOM when closed should bind on this
  1299. self.__stateSet('closed');
  1300. // trigger event
  1301. self._trigger({
  1302. type: 'after',
  1303. event: event
  1304. });
  1305. // call our constructor custom callback function
  1306. if (self.__options.functionAfter) {
  1307. self.__options.functionAfter.call(self, self, {
  1308. event: event,
  1309. origin: self._$origin[0]
  1310. });
  1311. }
  1312. // call our method custom callbacks functions
  1313. finishCallbacks();
  1314. };
  1315. if (env.hasTransitions) {
  1316. self._$tooltip.css({
  1317. '-moz-animation-duration': self.__options.animationDuration[1] + 'ms',
  1318. '-ms-animation-duration': self.__options.animationDuration[1] + 'ms',
  1319. '-o-animation-duration': self.__options.animationDuration[1] + 'ms',
  1320. '-webkit-animation-duration': self.__options.animationDuration[1] + 'ms',
  1321. 'animation-duration': self.__options.animationDuration[1] + 'ms',
  1322. 'transition-duration': self.__options.animationDuration[1] + 'ms'
  1323. });
  1324. self._$tooltip
  1325. // clear both potential open and close tasks
  1326. .clearQueue()
  1327. .removeClass('tooltipster-show')
  1328. // for transitions only
  1329. .addClass('tooltipster-dying');
  1330. if (self.__options.animationDuration[1] > 0) {
  1331. self._$tooltip.delay(self.__options.animationDuration[1]);
  1332. }
  1333. self._$tooltip.queue(finish);
  1334. }
  1335. else {
  1336. self._$tooltip
  1337. .stop()
  1338. .fadeOut(self.__options.animationDuration[1], finish);
  1339. }
  1340. }
  1341. }
  1342. // if the tooltip is already closed, we still need to trigger
  1343. // the method custom callbacks
  1344. else {
  1345. finishCallbacks();
  1346. }
  1347. }
  1348. return self;
  1349. },
  1350. /**
  1351. * For internal use by plugins, if needed
  1352. *
  1353. * @returns {self}
  1354. * @protected
  1355. */
  1356. _off: function() {
  1357. this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
  1358. return this;
  1359. },
  1360. /**
  1361. * For internal use by plugins, if needed
  1362. *
  1363. * @returns {self}
  1364. * @protected
  1365. */
  1366. _on: function() {
  1367. this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
  1368. return this;
  1369. },
  1370. /**
  1371. * For internal use by plugins, if needed
  1372. *
  1373. * @returns {self}
  1374. * @protected
  1375. */
  1376. _one: function() {
  1377. this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
  1378. return this;
  1379. },
  1380. /**
  1381. * Opens the tooltip right away.
  1382. *
  1383. * @param event
  1384. * @param callback Will be called when the opening animation is over
  1385. * @returns {self}
  1386. * @protected
  1387. */
  1388. _open: function(event, callback) {
  1389. var self = this;
  1390. // if the destruction process has not begun and if this was not
  1391. // triggered by an unwanted emulated click event
  1392. if (!self.__destroying) {
  1393. // check that the origin is still in the DOM
  1394. if ( bodyContains(self._$origin)
  1395. // if the tooltip is enabled
  1396. && self.__enabled
  1397. ) {
  1398. var ok = true;
  1399. // if the tooltip is not open yet, we need to call functionBefore.
  1400. // otherwise we can jst go on
  1401. if (self.__state == 'closed') {
  1402. // trigger an event. The event.stop function allows the callback
  1403. // to prevent the opening of the tooltip
  1404. self._trigger({
  1405. type: 'before',
  1406. event: event,
  1407. stop: function() {
  1408. ok = false;
  1409. }
  1410. });
  1411. if (ok && self.__options.functionBefore) {
  1412. // call our custom function before continuing
  1413. ok = self.__options.functionBefore.call(self, self, {
  1414. event: event,
  1415. origin: self._$origin[0]
  1416. });
  1417. }
  1418. }
  1419. if (ok !== false) {
  1420. // if there is some content
  1421. if (self.__Content !== null) {
  1422. // save the method callback and cancel close method callbacks
  1423. if (callback) {
  1424. self.__callbacks.open.push(callback);
  1425. }
  1426. self.__callbacks.close = [];
  1427. // get rid of any appearance timeouts
  1428. self.__timeoutsClear();
  1429. var extraTime,
  1430. finish = function() {
  1431. if (self.__state != 'stable') {
  1432. self.__stateSet('stable');
  1433. }
  1434. // trigger any open method custom callbacks and reset them
  1435. $.each(self.__callbacks.open, function(i,c) {
  1436. c.call(self, self, {
  1437. origin: self._$origin[0],
  1438. tooltip: self._$tooltip[0]
  1439. });
  1440. });
  1441. self.__callbacks.open = [];
  1442. };
  1443. // if the tooltip is already open
  1444. if (self.__state !== 'closed') {
  1445. // the timer (if any) will start (or restart) right now
  1446. extraTime = 0;
  1447. // if it was disappearing, cancel that
  1448. if (self.__state === 'disappearing') {
  1449. self.__stateSet('appearing');
  1450. if (env.hasTransitions) {
  1451. self._$tooltip
  1452. .clearQueue()
  1453. .removeClass('tooltipster-dying')
  1454. .addClass('tooltipster-show');
  1455. if (self.__options.animationDuration[0] > 0) {
  1456. self._$tooltip.delay(self.__options.animationDuration[0]);
  1457. }
  1458. self._$tooltip.queue(finish);
  1459. }
  1460. else {
  1461. // in case the tooltip was currently fading out, bring it back
  1462. // to life
  1463. self._$tooltip
  1464. .stop()
  1465. .fadeIn(finish);
  1466. }
  1467. }
  1468. // if the tooltip is already open, we still need to trigger the method
  1469. // custom callback
  1470. else if (self.__state == 'stable') {
  1471. finish();
  1472. }
  1473. }
  1474. // if the tooltip isn't already open, open it
  1475. else {
  1476. // a plugin must bind on this and store the tooltip in this._$tooltip
  1477. self.__stateSet('appearing');
  1478. // the timer (if any) will start when the tooltip has fully appeared
  1479. // after its transition
  1480. extraTime = self.__options.animationDuration[0];
  1481. // insert the content inside the tooltip
  1482. self.__contentInsert();
  1483. // reposition the tooltip and attach to the DOM
  1484. self.reposition(event, true);
  1485. // animate in the tooltip. If the display plugin wants no css
  1486. // animations, it may override the animation option with a
  1487. // dummy value that will produce no effect
  1488. if (env.hasTransitions) {
  1489. // note: there seems to be an issue with start animations which
  1490. // are randomly not played on fast devices in both Chrome and FF,
  1491. // couldn't find a way to solve it yet. It seems that applying
  1492. // the classes before appending to the DOM helps a little, but
  1493. // it messes up some CSS transitions. The issue almost never
  1494. // happens when delay[0]==0 though
  1495. self._$tooltip
  1496. .addClass('tooltipster-'+ self.__options.animation)
  1497. .addClass('tooltipster-initial')
  1498. .css({
  1499. '-moz-animation-duration': self.__options.animationDuration[0] + 'ms',
  1500. '-ms-animation-duration': self.__options.animationDuration[0] + 'ms',
  1501. '-o-animation-duration': self.__options.animationDuration[0] + 'ms',
  1502. '-webkit-animation-duration': self.__options.animationDuration[0] + 'ms',
  1503. 'animation-duration': self.__options.animationDuration[0] + 'ms',
  1504. 'transition-duration': self.__options.animationDuration[0] + 'ms'
  1505. });
  1506. setTimeout(
  1507. function() {
  1508. // a quick hover may have already triggered a mouseleave
  1509. if (self.__state != 'closed') {
  1510. self._$tooltip
  1511. .addClass('tooltipster-show')
  1512. .removeClass('tooltipster-initial');
  1513. if (self.__options.animationDuration[0] > 0) {
  1514. self._$tooltip.delay(self.__options.animationDuration[0]);
  1515. }
  1516. self._$tooltip.queue(finish);
  1517. }
  1518. },
  1519. 0
  1520. );
  1521. }
  1522. else {
  1523. // old browsers will have to live with this
  1524. self._$tooltip
  1525. .css('display', 'none')
  1526. .fadeIn(self.__options.animationDuration[0], finish);
  1527. }
  1528. // checks if the origin is removed while the tooltip is open
  1529. self.__trackerStart();
  1530. // NOTE: the listeners below have a '-triggerClose' namespace
  1531. // because we'll remove them when the tooltip closes (unlike
  1532. // the '-triggerOpen' listeners). So some of them are actually
  1533. // not about close triggers, rather about positioning.
  1534. $(env.window)
  1535. // reposition on resize
  1536. .on('resize.'+ self.__namespace +'-triggerClose', function(e) {
  1537. var $ae = $(document.activeElement);
  1538. // reposition only if the resize event was not triggered upon the opening
  1539. // of a virtual keyboard due to an input field being focused within the tooltip
  1540. // (otherwise the repositioning would lose the focus)
  1541. if ( (!$ae.is('input') && !$ae.is('textarea'))
  1542. || !$.contains(self._$tooltip[0], $ae[0])
  1543. ) {
  1544. self.reposition(e);
  1545. }
  1546. })
  1547. // same as below for parents
  1548. .on('scroll.'+ self.__namespace +'-triggerClose', function(e) {
  1549. self.__scrollHandler(e);
  1550. });
  1551. self.__$originParents = self._$origin.parents();
  1552. // scrolling may require the tooltip to be moved or even
  1553. // repositioned in some cases
  1554. self.__$originParents.each(function(i, parent) {
  1555. $(parent).on('scroll.'+ self.__namespace +'-triggerClose', function(e) {
  1556. self.__scrollHandler(e);
  1557. });
  1558. });
  1559. if ( self.__options.triggerClose.mouseleave
  1560. || (self.__options.triggerClose.touchleave && env.hasTouchCapability)
  1561. ) {
  1562. // we use an event to allow users/plugins to control when the mouseleave/touchleave
  1563. // close triggers will come to action. It allows to have more triggering elements
  1564. // than just the origin and the tooltip for example, or to cancel/delay the closing,
  1565. // or to make the tooltip interactive even if it wasn't when it was open, etc.
  1566. self._on('dismissable', function(event) {
  1567. if (event.dismissable) {
  1568. if (event.delay) {
  1569. timeout = setTimeout(function() {
  1570. // event.event may be undefined
  1571. self._close(event.event);
  1572. }, event.delay);
  1573. self.__timeouts.close.push(timeout);
  1574. }
  1575. else {
  1576. self._close(event);
  1577. }
  1578. }
  1579. else {
  1580. clearTimeout(timeout);
  1581. }
  1582. });
  1583. // now set the listeners that will trigger 'dismissable' events
  1584. var $elements = self._$origin,
  1585. eventNamesIn = '',
  1586. eventNamesOut = '',
  1587. timeout = null;
  1588. // if we have to allow interaction, bind on the tooltip too
  1589. if (self.__options.interactive) {
  1590. $elements = $elements.add(self._$tooltip);
  1591. }
  1592. if (self.__options.triggerClose.mouseleave) {
  1593. eventNamesIn += 'mouseenter.'+ self.__namespace +'-triggerClose ';
  1594. eventNamesOut += 'mouseleave.'+ self.__namespace +'-triggerClose ';
  1595. }
  1596. if (self.__options.triggerClose.touchleave && env.hasTouchCapability) {
  1597. eventNamesIn += 'touchstart.'+ self.__namespace +'-triggerClose';
  1598. eventNamesOut += 'touchend.'+ self.__namespace +'-triggerClose touchcancel.'+ self.__namespace +'-triggerClose';
  1599. }
  1600. $elements
  1601. // close after some time spent outside of the elements
  1602. .on(eventNamesOut, function(event) {
  1603. // it's ok if the touch gesture ended up to be a swipe,
  1604. // it's still a "touch leave" situation
  1605. if ( self._touchIsTouchEvent(event)
  1606. || !self._touchIsEmulatedEvent(event)
  1607. ) {
  1608. var delay = (event.type == 'mouseleave') ?
  1609. self.__options.delay :
  1610. self.__options.delayTouch;
  1611. self._trigger({
  1612. delay: delay[1],
  1613. dismissable: true,
  1614. event: event,
  1615. type: 'dismissable'
  1616. });
  1617. }
  1618. })
  1619. // suspend the mouseleave timeout when the pointer comes back
  1620. // over the elements
  1621. .on(eventNamesIn, function(event) {
  1622. // it's also ok if the touch event is a swipe gesture
  1623. if ( self._touchIsTouchEvent(event)
  1624. || !self._touchIsEmulatedEvent(event)
  1625. ) {
  1626. self._trigger({
  1627. dismissable: false,
  1628. event: event,
  1629. type: 'dismissable'
  1630. });
  1631. }
  1632. });
  1633. }
  1634. // close the tooltip when the origin gets a mouse click (common behavior of
  1635. // native tooltips)
  1636. if (self.__options.triggerClose.originClick) {
  1637. self._$origin.on('click.'+ self.__namespace + '-triggerClose', function(event) {
  1638. // we could actually let a tap trigger this but this feature just
  1639. // does not make sense on touch devices
  1640. if ( !self._touchIsTouchEvent(event)
  1641. && !self._touchIsEmulatedEvent(event)
  1642. ) {
  1643. self._close(event);
  1644. }
  1645. });
  1646. }
  1647. // set the same bindings for click and touch on the body to close the tooltip
  1648. if ( self.__options.triggerClose.click
  1649. || (self.__options.triggerClose.tap && env.hasTouchCapability)
  1650. ) {
  1651. // don't set right away since the click/tap event which triggered this method
  1652. // (if it was a click/tap) is going to bubble up to the body, we don't want it
  1653. // to close the tooltip immediately after it opened
  1654. setTimeout(function() {
  1655. if (self.__state != 'closed') {
  1656. var eventNames = '',
  1657. $body = $(env.window.document.body);
  1658. if (self.__options.triggerClose.click) {
  1659. eventNames += 'click.'+ self.__namespace +'-triggerClose ';
  1660. }
  1661. if (self.__options.triggerClose.tap && env.hasTouchCapability) {
  1662. eventNames += 'touchend.'+ self.__namespace +'-triggerClose';
  1663. }
  1664. $body.on(eventNames, function(event) {
  1665. if (self._touchIsMeaningfulEvent(event)) {
  1666. self._touchRecordEvent(event);
  1667. if (!self.__options.interactive || !$.contains(self._$tooltip[0], event.target)) {
  1668. self._close(event);
  1669. }
  1670. }
  1671. });
  1672. // needed to detect and ignore swiping
  1673. if (self.__options.triggerClose.tap && env.hasTouchCapability) {
  1674. $body.on('touchstart.'+ self.__namespace +'-triggerClose', function(event) {
  1675. self._touchRecordEvent(event);
  1676. });
  1677. }
  1678. }
  1679. }, 0);
  1680. }
  1681. self._trigger('ready');
  1682. // call our custom callback
  1683. if (self.__options.functionReady) {
  1684. self.__options.functionReady.call(self, self, {
  1685. origin: self._$origin[0],
  1686. tooltip: self._$tooltip[0]
  1687. });
  1688. }
  1689. }
  1690. // if we have a timer set, let the countdown begin
  1691. if (self.__options.timer > 0) {
  1692. var timeout = setTimeout(function() {
  1693. self._close();
  1694. }, self.__options.timer + extraTime);
  1695. self.__timeouts.close.push(timeout);
  1696. }
  1697. }
  1698. }
  1699. }
  1700. }
  1701. return self;
  1702. },
  1703. /**
  1704. * When using the mouseenter/touchstart open triggers, this function will
  1705. * schedule the opening of the tooltip after the delay, if there is one
  1706. *
  1707. * @param event
  1708. * @returns {self}
  1709. * @protected
  1710. */
  1711. _openShortly: function(event) {
  1712. var self = this,
  1713. ok = true;
  1714. if (self.__state != 'stable' && self.__state != 'appearing') {
  1715. // if a timeout is not already running
  1716. if (!self.__timeouts.open) {
  1717. self._trigger({
  1718. type: 'start',
  1719. event: event,
  1720. stop: function() {
  1721. ok = false;
  1722. }
  1723. });
  1724. if (ok) {
  1725. var delay = (event.type.indexOf('touch') == 0) ?
  1726. self.__options.delayTouch :
  1727. self.__options.delay;
  1728. if (delay[0]) {
  1729. self.__timeouts.open = setTimeout(function() {
  1730. self.__timeouts.open = null;
  1731. // open only if the pointer (mouse or touch) is still over the origin.
  1732. // The check on the "meaningful event" can only be made here, after some
  1733. // time has passed (to know if the touch was a swipe or not)
  1734. if (self.__pointerIsOverOrigin && self._touchIsMeaningfulEvent(event)) {
  1735. // signal that we go on
  1736. self._trigger('startend');
  1737. self._open(event);
  1738. }
  1739. else {
  1740. // signal that we cancel
  1741. self._trigger('startcancel');
  1742. }
  1743. }, delay[0]);
  1744. }
  1745. else {
  1746. // signal that we go on
  1747. self._trigger('startend');
  1748. self._open(event);
  1749. }
  1750. }
  1751. }
  1752. }
  1753. return self;
  1754. },
  1755. /**
  1756. * Meant for plugins to get their options
  1757. *
  1758. * @param {string} pluginName The name of the plugin that asks for its options
  1759. * @param {object} defaultOptions The default options of the plugin
  1760. * @returns {object} The options
  1761. * @protected
  1762. */
  1763. _optionsExtract: function(pluginName, defaultOptions) {
  1764. var self = this,
  1765. options = $.extend(true, {}, defaultOptions);
  1766. // if the plugin options were isolated in a property named after the
  1767. // plugin, use them (prevents conflicts with other plugins)
  1768. var pluginOptions = self.__options[pluginName];
  1769. // if not, try to get them as regular options
  1770. if (!pluginOptions){
  1771. pluginOptions = {};
  1772. $.each(defaultOptions, function(optionName, value) {
  1773. var o = self.__options[optionName];
  1774. if (o !== undefined) {
  1775. pluginOptions[optionName] = o;
  1776. }
  1777. });
  1778. }
  1779. // let's merge the default options and the ones that were provided. We'd want
  1780. // to do a deep copy but not let jQuery merge arrays, so we'll do a shallow
  1781. // extend on two levels, that will be enough if options are not more than 1
  1782. // level deep
  1783. $.each(options, function(optionName, value) {
  1784. if (pluginOptions[optionName] !== undefined) {
  1785. if (( typeof value == 'object'
  1786. && !(value instanceof Array)
  1787. && value != null
  1788. )
  1789. &&
  1790. ( typeof pluginOptions[optionName] == 'object'
  1791. && !(pluginOptions[optionName] instanceof Array)
  1792. && pluginOptions[optionName] != null
  1793. )
  1794. ) {
  1795. $.extend(options[optionName], pluginOptions[optionName]);
  1796. }
  1797. else {
  1798. options[optionName] = pluginOptions[optionName];
  1799. }
  1800. }
  1801. });
  1802. return options;
  1803. },
  1804. /**
  1805. * Used at instantiation of the plugin, or afterwards by plugins that activate themselves
  1806. * on existing instances
  1807. *
  1808. * @param {object} pluginName
  1809. * @returns {self}
  1810. * @protected
  1811. */
  1812. _plug: function(pluginName) {
  1813. var plugin = $.tooltipster._plugin(pluginName);
  1814. if (plugin) {
  1815. // if there is a constructor for instances
  1816. if (plugin.instance) {
  1817. // proxy non-private methods on the instance to allow new instance methods
  1818. $.tooltipster.__bridge(plugin.instance, this, plugin.name);
  1819. }
  1820. }
  1821. else {
  1822. throw new Error('The "'+ pluginName +'" plugin is not defined');
  1823. }
  1824. return this;
  1825. },
  1826. /**
  1827. * This will return true if the event is a mouse event which was
  1828. * emulated by the browser after a touch event. This allows us to
  1829. * really dissociate mouse and touch triggers.
  1830. *
  1831. * There is a margin of error if a real mouse event is fired right
  1832. * after (within the delay shown below) a touch event on the same
  1833. * element, but hopefully it should not happen often.
  1834. *
  1835. * @returns {boolean}
  1836. * @protected
  1837. */
  1838. _touchIsEmulatedEvent: function(event) {
  1839. var isEmulated = false,
  1840. now = new Date().getTime();
  1841. for (var i = this.__touchEvents.length - 1; i >= 0; i--) {
  1842. var e = this.__touchEvents[i];
  1843. // delay, in milliseconds. It's supposed to be 300ms in
  1844. // most browsers (350ms on iOS) to allow a double tap but
  1845. // can be less (check out FastClick for more info)
  1846. if (now - e.time < 500) {
  1847. if (e.target === event.target) {
  1848. isEmulated = true;
  1849. }
  1850. }
  1851. else {
  1852. break;
  1853. }
  1854. }
  1855. return isEmulated;
  1856. },
  1857. /**
  1858. * Returns false if the event was an emulated mouse event or
  1859. * a touch event involved in a swipe gesture.
  1860. *
  1861. * @param {object} event
  1862. * @returns {boolean}
  1863. * @protected
  1864. */
  1865. _touchIsMeaningfulEvent: function(event) {
  1866. return (
  1867. (this._touchIsTouchEvent(event) && !this._touchSwiped(event.target))
  1868. || (!this._touchIsTouchEvent(event) && !this._touchIsEmulatedEvent(event))
  1869. );
  1870. },
  1871. /**
  1872. * Checks if an event is a touch event
  1873. *
  1874. * @param {object} event
  1875. * @returns {boolean}
  1876. * @protected
  1877. */
  1878. _touchIsTouchEvent: function(event){
  1879. return event.type.indexOf('touch') == 0;
  1880. },
  1881. /**
  1882. * Store touch events for a while to detect swiping and emulated mouse events
  1883. *
  1884. * @param {object} event
  1885. * @returns {self}
  1886. * @protected
  1887. */
  1888. _touchRecordEvent: function(event) {
  1889. if (this._touchIsTouchEvent(event)) {
  1890. event.time = new Date().getTime();
  1891. this.__touchEvents.push(event);
  1892. }
  1893. return this;
  1894. },
  1895. /**
  1896. * Returns true if a swipe happened after the last touchstart event fired on
  1897. * event.target.
  1898. *
  1899. * We need to differentiate a swipe from a tap before we let the event open
  1900. * or close the tooltip. A swipe is when a touchmove (scroll) event happens
  1901. * on the body between the touchstart and the touchend events of an element.
  1902. *
  1903. * @param {object} target The HTML element that may have triggered the swipe
  1904. * @returns {boolean}
  1905. * @protected
  1906. */
  1907. _touchSwiped: function(target) {
  1908. var swiped = false;
  1909. for (var i = this.__touchEvents.length - 1; i >= 0; i--) {
  1910. var e = this.__touchEvents[i];
  1911. if (e.type == 'touchmove') {
  1912. swiped = true;
  1913. break;
  1914. }
  1915. else if (
  1916. e.type == 'touchstart'
  1917. && target === e.target
  1918. ) {
  1919. break;
  1920. }
  1921. }
  1922. return swiped;
  1923. },
  1924. /**
  1925. * Triggers an event on the instance emitters
  1926. *
  1927. * @returns {self}
  1928. * @protected
  1929. */
  1930. _trigger: function() {
  1931. var args = Array.prototype.slice.apply(arguments);
  1932. if (typeof args[0] == 'string') {
  1933. args[0] = { type: args[0] };
  1934. }
  1935. // add properties to the event
  1936. args[0].instance = this;
  1937. args[0].origin = this._$origin ? this._$origin[0] : null;
  1938. args[0].tooltip = this._$tooltip ? this._$tooltip[0] : null;
  1939. // note: the order of emitters matters
  1940. this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args);
  1941. $.tooltipster._trigger.apply($.tooltipster, args);
  1942. this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args);
  1943. return this;
  1944. },
  1945. /**
  1946. * Deactivate a plugin on this instance
  1947. *
  1948. * @returns {self}
  1949. * @protected
  1950. */
  1951. _unplug: function(pluginName) {
  1952. var self = this;
  1953. // if the plugin has been activated on this instance
  1954. if (self[pluginName]) {
  1955. var plugin = $.tooltipster._plugin(pluginName);
  1956. // if there is a constructor for instances
  1957. if (plugin.instance) {
  1958. // unbridge
  1959. $.each(plugin.instance, function(methodName, fn) {
  1960. // if the method exists (privates methods do not) and comes indeed from
  1961. // this plugin (may be missing or come from a conflicting plugin).
  1962. if ( self[methodName]
  1963. && self[methodName].bridged === self[pluginName]
  1964. ) {
  1965. delete self[methodName];
  1966. }
  1967. });
  1968. }
  1969. // destroy the plugin
  1970. if (self[pluginName].__destroy) {
  1971. self[pluginName].__destroy();
  1972. }
  1973. // remove the reference to the plugin instance
  1974. delete self[pluginName];
  1975. }
  1976. return self;
  1977. },
  1978. /**
  1979. * @see self::_close
  1980. * @returns {self}
  1981. * @public
  1982. */
  1983. close: function(callback) {
  1984. if (!this.__destroyed) {
  1985. this._close(null, callback);
  1986. }
  1987. else {
  1988. this.__destroyError();
  1989. }
  1990. return this;
  1991. },
  1992. /**
  1993. * Sets or gets the content of the tooltip
  1994. *
  1995. * @returns {mixed|self}
  1996. * @public
  1997. */
  1998. content: function(content) {
  1999. var self = this;
  2000. // getter method
  2001. if (content === undefined) {
  2002. return self.__Content;
  2003. }
  2004. // setter method
  2005. else {
  2006. if (!self.__destroyed) {
  2007. // change the content
  2008. self.__contentSet(content);
  2009. if (self.__Content !== null) {
  2010. // update the tooltip if it is open
  2011. if (self.__state !== 'closed') {
  2012. // reset the content in the tooltip
  2013. self.__contentInsert();
  2014. // reposition and resize the tooltip
  2015. self.reposition();
  2016. // if we want to play a little animation showing the content changed
  2017. if (self.__options.updateAnimation) {
  2018. if (env.hasTransitions) {
  2019. // keep the reference in the local scope
  2020. var animation = self.__options.updateAnimation;
  2021. self._$tooltip.addClass('tooltipster-update-'+ animation);
  2022. // remove the class after a while. The actual duration of the
  2023. // update animation may be shorter, it's set in the CSS rules
  2024. setTimeout(function() {
  2025. if (self.__state != 'closed') {
  2026. self._$tooltip.removeClass('tooltipster-update-'+ animation);
  2027. }
  2028. }, 1000);
  2029. }
  2030. else {
  2031. self._$tooltip.fadeTo(200, 0.5, function() {
  2032. if (self.__state != 'closed') {
  2033. self._$tooltip.fadeTo(200, 1);
  2034. }
  2035. });
  2036. }
  2037. }
  2038. }
  2039. }
  2040. else {
  2041. self._close();
  2042. }
  2043. }
  2044. else {
  2045. self.__destroyError();
  2046. }
  2047. return self;
  2048. }
  2049. },
  2050. /**
  2051. * Destroys the tooltip
  2052. *
  2053. * @returns {self}
  2054. * @public
  2055. */
  2056. destroy: function() {
  2057. var self = this;
  2058. if (!self.__destroyed) {
  2059. if(self.__state != 'closed'){
  2060. // no closing delay
  2061. self.option('animationDuration', 0)
  2062. // force closing
  2063. ._close(null, null, true);
  2064. }
  2065. else {
  2066. // there might be an open timeout still running
  2067. self.__timeoutsClear();
  2068. }
  2069. // send event
  2070. self._trigger('destroy');
  2071. self.__destroyed = true;
  2072. self._$origin
  2073. .removeData(self.__namespace)
  2074. // remove the open trigger listeners
  2075. .off('.'+ self.__namespace +'-triggerOpen');
  2076. // remove the touch listener
  2077. $(env.window.document.body).off('.' + self.__namespace +'-triggerOpen');
  2078. var ns = self._$origin.data('tooltipster-ns');
  2079. // if the origin has been removed from DOM, its data may
  2080. // well have been destroyed in the process and there would
  2081. // be nothing to clean up or restore
  2082. if (ns) {
  2083. // if there are no more tooltips on this element
  2084. if (ns.length === 1) {
  2085. // optional restoration of a title attribute
  2086. var title = null;
  2087. if (self.__options.restoration == 'previous') {
  2088. title = self._$origin.data('tooltipster-initialTitle');
  2089. }
  2090. else if (self.__options.restoration == 'current') {
  2091. // old school technique to stringify when outerHTML is not supported
  2092. title = (typeof self.__Content == 'string') ?
  2093. self.__Content :
  2094. $('<div></div>').append(self.__Content).html();
  2095. }
  2096. if (title) {
  2097. self._$origin.attr('title', title);
  2098. }
  2099. // final cleaning
  2100. self._$origin.removeClass('tooltipstered');
  2101. self._$origin
  2102. .removeData('tooltipster-ns')
  2103. .removeData('tooltipster-initialTitle');
  2104. }
  2105. else {
  2106. // remove the instance namespace from the list of namespaces of
  2107. // tooltips present on the element
  2108. ns = $.grep(ns, function(el, i) {
  2109. return el !== self.__namespace;
  2110. });
  2111. self._$origin.data('tooltipster-ns', ns);
  2112. }
  2113. }
  2114. // last event
  2115. self._trigger('destroyed');
  2116. // unbind private and public event listeners
  2117. self._off();
  2118. self.off();
  2119. // remove external references, just in case
  2120. self.__Content = null;
  2121. self.__$emitterPrivate = null;
  2122. self.__$emitterPublic = null;
  2123. self.__options.parent = null;
  2124. self._$origin = null;
  2125. self._$tooltip = null;
  2126. // make sure the object is no longer referenced in there to prevent
  2127. // memory leaks
  2128. $.tooltipster.__instancesLatestArr = $.grep($.tooltipster.__instancesLatestArr, function(el, i) {
  2129. return self !== el;
  2130. });
  2131. clearInterval(self.__garbageCollector);
  2132. }
  2133. else {
  2134. self.__destroyError();
  2135. }
  2136. // we return the scope rather than true so that the call to
  2137. // .tooltipster('destroy') actually returns the matched elements
  2138. // and applies to all of them
  2139. return self;
  2140. },
  2141. /**
  2142. * Disables the tooltip
  2143. *
  2144. * @returns {self}
  2145. * @public
  2146. */
  2147. disable: function() {
  2148. if (!this.__destroyed) {
  2149. // close first, in case the tooltip would not disappear on
  2150. // its own (no close trigger)
  2151. this._close();
  2152. this.__enabled = false;
  2153. return this;
  2154. }
  2155. else {
  2156. this.__destroyError();
  2157. }
  2158. return this;
  2159. },
  2160. /**
  2161. * Returns the HTML element of the origin
  2162. *
  2163. * @returns {self}
  2164. * @public
  2165. */
  2166. elementOrigin: function() {
  2167. if (!this.__destroyed) {
  2168. return this._$origin[0];
  2169. }
  2170. else {
  2171. this.__destroyError();
  2172. }
  2173. },
  2174. /**
  2175. * Returns the HTML element of the tooltip
  2176. *
  2177. * @returns {self}
  2178. * @public
  2179. */
  2180. elementTooltip: function() {
  2181. return this._$tooltip ? this._$tooltip[0] : null;
  2182. },
  2183. /**
  2184. * Enables the tooltip
  2185. *
  2186. * @returns {self}
  2187. * @public
  2188. */
  2189. enable: function() {
  2190. this.__enabled = true;
  2191. return this;
  2192. },
  2193. /**
  2194. * Alias, deprecated in 4.0.0
  2195. *
  2196. * @param {function} callback
  2197. * @returns {self}
  2198. * @public
  2199. */
  2200. hide: function(callback) {
  2201. return this.close(callback);
  2202. },
  2203. /**
  2204. * Returns the instance
  2205. *
  2206. * @returns {self}
  2207. * @public
  2208. */
  2209. instance: function() {
  2210. return this;
  2211. },
  2212. /**
  2213. * For public use only, not to be used by plugins (use ::_off() instead)
  2214. *
  2215. * @returns {self}
  2216. * @public
  2217. */
  2218. off: function() {
  2219. if (!this.__destroyed) {
  2220. this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
  2221. }
  2222. return this;
  2223. },
  2224. /**
  2225. * For public use only, not to be used by plugins (use ::_on() instead)
  2226. *
  2227. * @returns {self}
  2228. * @public
  2229. */
  2230. on: function() {
  2231. if (!this.__destroyed) {
  2232. this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
  2233. }
  2234. else {
  2235. this.__destroyError();
  2236. }
  2237. return this;
  2238. },
  2239. /**
  2240. * For public use only, not to be used by plugins
  2241. *
  2242. * @returns {self}
  2243. * @public
  2244. */
  2245. one: function() {
  2246. if (!this.__destroyed) {
  2247. this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
  2248. }
  2249. else {
  2250. this.__destroyError();
  2251. }
  2252. return this;
  2253. },
  2254. /**
  2255. * @see self::_open
  2256. * @returns {self}
  2257. * @public
  2258. */
  2259. open: function(callback) {
  2260. if (!this.__destroyed) {
  2261. this._open(null, callback);
  2262. }
  2263. else {
  2264. this.__destroyError();
  2265. }
  2266. return this;
  2267. },
  2268. /**
  2269. * Get or set options. For internal use and advanced users only.
  2270. *
  2271. * @param {string} o Option name
  2272. * @param {mixed} val optional A new value for the option
  2273. * @return {mixed|self} If val is omitted, the value of the option
  2274. * is returned, otherwise the instance itself is returned
  2275. * @public
  2276. */
  2277. option: function(o, val) {
  2278. // getter
  2279. if (val === undefined) {
  2280. return this.__options[o];
  2281. }
  2282. // setter
  2283. else {
  2284. if (!this.__destroyed) {
  2285. // change value
  2286. this.__options[o] = val;
  2287. // format
  2288. this.__optionsFormat();
  2289. // re-prepare the triggers if needed
  2290. if ($.inArray(o, ['trigger', 'triggerClose', 'triggerOpen']) >= 0) {
  2291. this.__prepareOrigin();
  2292. }
  2293. if (o === 'selfDestruction') {
  2294. this.__prepareGC();
  2295. }
  2296. }
  2297. else {
  2298. this.__destroyError();
  2299. }
  2300. return this;
  2301. }
  2302. },
  2303. /**
  2304. * This method is in charge of setting the position and size properties of the tooltip.
  2305. * All the hard work is delegated to the display plugin.
  2306. * Note: The tooltip may be detached from the DOM at the moment the method is called
  2307. * but must be attached by the end of the method call.
  2308. *
  2309. * @param {object} event For internal use only. Defined if an event such as
  2310. * window resizing triggered the repositioning
  2311. * @param {boolean} tooltipIsDetached For internal use only. Set this to true if you
  2312. * know that the tooltip not being in the DOM is not an issue (typically when the
  2313. * tooltip element has just been created but has not been added to the DOM yet).
  2314. * @returns {self}
  2315. * @public
  2316. */
  2317. reposition: function(event, tooltipIsDetached) {
  2318. var self = this;
  2319. if (!self.__destroyed) {
  2320. // if the tooltip is still open and the origin is still in the DOM
  2321. if (self.__state != 'closed' && bodyContains(self._$origin)) {
  2322. // if the tooltip has not been removed from DOM manually (or if it
  2323. // has been detached on purpose)
  2324. if (tooltipIsDetached || bodyContains(self._$tooltip)) {
  2325. if (!tooltipIsDetached) {
  2326. // detach in case the tooltip overflows the window and adds
  2327. // scrollbars to it, so __geometry can be accurate
  2328. self._$tooltip.detach();
  2329. }
  2330. // refresh the geometry object before passing it as a helper
  2331. self.__Geometry = self.__geometry();
  2332. // let a plugin fo the rest
  2333. self._trigger({
  2334. type: 'reposition',
  2335. event: event,
  2336. helper: {
  2337. geo: self.__Geometry
  2338. }
  2339. });
  2340. }
  2341. }
  2342. }
  2343. else {
  2344. self.__destroyError();
  2345. }
  2346. return self;
  2347. },
  2348. /**
  2349. * Alias, deprecated in 4.0.0
  2350. *
  2351. * @param callback
  2352. * @returns {self}
  2353. * @public
  2354. */
  2355. show: function(callback) {
  2356. return this.open(callback);
  2357. },
  2358. /**
  2359. * Returns some properties about the instance
  2360. *
  2361. * @returns {object}
  2362. * @public
  2363. */
  2364. status: function() {
  2365. return {
  2366. destroyed: this.__destroyed,
  2367. enabled: this.__enabled,
  2368. open: this.__state !== 'closed',
  2369. state: this.__state
  2370. };
  2371. },
  2372. /**
  2373. * For public use only, not to be used by plugins
  2374. *
  2375. * @returns {self}
  2376. * @public
  2377. */
  2378. triggerHandler: function() {
  2379. if (!this.__destroyed) {
  2380. this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
  2381. }
  2382. else {
  2383. this.__destroyError();
  2384. }
  2385. return this;
  2386. }
  2387. };
  2388. $.fn.tooltipster = function() {
  2389. // for using in closures
  2390. var args = Array.prototype.slice.apply(arguments),
  2391. // common mistake: an HTML element can't be in several tooltips at the same time
  2392. contentCloningWarning = 'You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.';
  2393. // this happens with $(sel).tooltipster(...) when $(sel) does not match anything
  2394. if (this.length === 0) {
  2395. // still chainable
  2396. return this;
  2397. }
  2398. // this happens when calling $(sel).tooltipster('methodName or options')
  2399. // where $(sel) matches one or more elements
  2400. else {
  2401. // method calls
  2402. if (typeof args[0] === 'string') {
  2403. var v = '#*$~&';
  2404. this.each(function() {
  2405. // retrieve the namepaces of the tooltip(s) that exist on that element.
  2406. // We will interact with the first tooltip only.
  2407. var ns = $(this).data('tooltipster-ns'),
  2408. // self represents the instance of the first tooltipster plugin
  2409. // associated to the current HTML object of the loop
  2410. self = ns ? $(this).data(ns[0]) : null;
  2411. // if the current element holds a tooltipster instance
  2412. if (self) {
  2413. if (typeof self[args[0]] === 'function') {
  2414. if ( this.length > 1
  2415. && args[0] == 'content'
  2416. && ( args[1] instanceof $
  2417. || (typeof args[1] == 'object' && args[1] != null && args[1].tagName)
  2418. )
  2419. && !self.__options.contentCloning
  2420. && self.__options.debug
  2421. ) {
  2422. console.log(contentCloningWarning);
  2423. }
  2424. // note : args[1] and args[2] may not be defined
  2425. var resp = self[args[0]](args[1], args[2]);
  2426. }
  2427. else {
  2428. throw new Error('Unknown method "'+ args[0] +'"');
  2429. }
  2430. // if the function returned anything other than the instance
  2431. // itself (which implies chaining, except for the `instance` method)
  2432. if (resp !== self || args[0] === 'instance') {
  2433. v = resp;
  2434. // return false to stop .each iteration on the first element
  2435. // matched by the selector
  2436. return false;
  2437. }
  2438. }
  2439. else {
  2440. throw new Error('You called Tooltipster\'s "'+ args[0] +'" method on an uninitialized element');
  2441. }
  2442. });
  2443. return (v !== '#*$~&') ? v : this;
  2444. }
  2445. // first argument is undefined or an object: the tooltip is initializing
  2446. else {
  2447. // reset the array of last initialized objects
  2448. $.tooltipster.__instancesLatestArr = [];
  2449. // is there a defined value for the multiple option in the options object ?
  2450. var multipleIsSet = args[0] && args[0].multiple !== undefined,
  2451. // if the multiple option is set to true, or if it's not defined but
  2452. // set to true in the defaults
  2453. multiple = (multipleIsSet && args[0].multiple) || (!multipleIsSet && defaults.multiple),
  2454. // same for content
  2455. contentIsSet = args[0] && args[0].content !== undefined,
  2456. content = (contentIsSet && args[0].content) || (!contentIsSet && defaults.content),
  2457. // same for contentCloning
  2458. contentCloningIsSet = args[0] && args[0].contentCloning !== undefined,
  2459. contentCloning =
  2460. (contentCloningIsSet && args[0].contentCloning)
  2461. || (!contentCloningIsSet && defaults.contentCloning),
  2462. // same for debug
  2463. debugIsSet = args[0] && args[0].debug !== undefined,
  2464. debug = (debugIsSet && args[0].debug) || (!debugIsSet && defaults.debug);
  2465. if ( this.length > 1
  2466. && ( content instanceof $
  2467. || (typeof content == 'object' && content != null && content.tagName)
  2468. )
  2469. && !contentCloning
  2470. && debug
  2471. ) {
  2472. console.log(contentCloningWarning);
  2473. }
  2474. // create a tooltipster instance for each element if it doesn't
  2475. // already have one or if the multiple option is set, and attach the
  2476. // object to it
  2477. this.each(function() {
  2478. var go = false,
  2479. $this = $(this),
  2480. ns = $this.data('tooltipster-ns'),
  2481. obj = null;
  2482. if (!ns) {
  2483. go = true;
  2484. }
  2485. else if (multiple) {
  2486. go = true;
  2487. }
  2488. else if (debug) {
  2489. console.log('Tooltipster: one or more tooltips are already attached to the element below. Ignoring.');
  2490. console.log(this);
  2491. }
  2492. if (go) {
  2493. obj = new $.Tooltipster(this, args[0]);
  2494. // save the reference of the new instance
  2495. if (!ns) ns = [];
  2496. ns.push(obj.__namespace);
  2497. $this.data('tooltipster-ns', ns);
  2498. // save the instance itself
  2499. $this.data(obj.__namespace, obj);
  2500. // call our constructor custom function.
  2501. // we do this here and not in ::init() because we wanted
  2502. // the object to be saved in $this.data before triggering
  2503. // it
  2504. if (obj.__options.functionInit) {
  2505. obj.__options.functionInit.call(obj, obj, {
  2506. origin: this
  2507. });
  2508. }
  2509. // and now the event, for the plugins and core emitter
  2510. obj._trigger('init');
  2511. }
  2512. $.tooltipster.__instancesLatestArr.push(obj);
  2513. });
  2514. return this;
  2515. }
  2516. }
  2517. };
  2518. // Utilities
  2519. /**
  2520. * A class to check if a tooltip can fit in given dimensions
  2521. *
  2522. * @param {object} $tooltip The jQuery wrapped tooltip element, or a clone of it
  2523. */
  2524. function Ruler($tooltip) {
  2525. // list of instance variables
  2526. this.$container;
  2527. this.constraints = null;
  2528. this.__$tooltip;
  2529. this.__init($tooltip);
  2530. }
  2531. Ruler.prototype = {
  2532. /**
  2533. * Move the tooltip into an invisible div that does not allow overflow to make
  2534. * size tests. Note: the tooltip may or may not be attached to the DOM at the
  2535. * moment this method is called, it does not matter.
  2536. *
  2537. * @param {object} $tooltip The object to test. May be just a clone of the
  2538. * actual tooltip.
  2539. * @private
  2540. */
  2541. __init: function($tooltip) {
  2542. this.__$tooltip = $tooltip;
  2543. this.__$tooltip
  2544. .css({
  2545. // for some reason we have to specify top and left 0
  2546. left: 0,
  2547. // any overflow will be ignored while measuring
  2548. overflow: 'hidden',
  2549. // positions at (0,0) without the div using 100% of the available width
  2550. position: 'absolute',
  2551. top: 0
  2552. })
  2553. // overflow must be auto during the test. We re-set this in case
  2554. // it were modified by the user
  2555. .find('.tooltipster-content')
  2556. .css('overflow', 'auto');
  2557. this.$container = $('<div class="tooltipster-ruler"></div>')
  2558. .append(this.__$tooltip)
  2559. .appendTo(env.window.document.body);
  2560. },
  2561. /**
  2562. * Force the browser to redraw (re-render) the tooltip immediately. This is required
  2563. * when you changed some CSS properties and need to make something with it
  2564. * immediately, without waiting for the browser to redraw at the end of instructions.
  2565. *
  2566. * @see http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes
  2567. * @private
  2568. */
  2569. __forceRedraw: function() {
  2570. // note: this would work but for Webkit only
  2571. //this.__$tooltip.close();
  2572. //this.__$tooltip[0].offsetHeight;
  2573. //this.__$tooltip.open();
  2574. // works in FF too
  2575. var $p = this.__$tooltip.parent();
  2576. this.__$tooltip.detach();
  2577. this.__$tooltip.appendTo($p);
  2578. },
  2579. /**
  2580. * Set maximum dimensions for the tooltip. A call to ::measure afterwards
  2581. * will tell us if the content overflows or if it's ok
  2582. *
  2583. * @param {int} width
  2584. * @param {int} height
  2585. * @return {Ruler}
  2586. * @public
  2587. */
  2588. constrain: function(width, height) {
  2589. this.constraints = {
  2590. width: width,
  2591. height: height
  2592. };
  2593. this.__$tooltip.css({
  2594. // we disable display:flex, otherwise the content would overflow without
  2595. // creating horizontal scrolling (which we need to detect).
  2596. display: 'block',
  2597. // reset any previous height
  2598. height: '',
  2599. // we'll check if horizontal scrolling occurs
  2600. overflow: 'auto',
  2601. // we'll set the width and see what height is generated and if there
  2602. // is horizontal overflow
  2603. width: width
  2604. });
  2605. return this;
  2606. },
  2607. /**
  2608. * Reset the tooltip content overflow and remove the test container
  2609. *
  2610. * @returns {Ruler}
  2611. * @public
  2612. */
  2613. destroy: function() {
  2614. // in case the element was not a clone
  2615. this.__$tooltip
  2616. .detach()
  2617. .find('.tooltipster-content')
  2618. .css({
  2619. // reset to CSS value
  2620. display: '',
  2621. overflow: ''
  2622. });
  2623. this.$container.remove();
  2624. },
  2625. /**
  2626. * Removes any constraints
  2627. *
  2628. * @returns {Ruler}
  2629. * @public
  2630. */
  2631. free: function() {
  2632. this.constraints = null;
  2633. // reset to natural size
  2634. this.__$tooltip.css({
  2635. display: '',
  2636. height: '',
  2637. overflow: 'visible',
  2638. width: ''
  2639. });
  2640. return this;
  2641. },
  2642. /**
  2643. * Returns the size of the tooltip. When constraints are applied, also returns
  2644. * whether the tooltip fits in the provided dimensions.
  2645. * The idea is to see if the new height is small enough and if the content does
  2646. * not overflow horizontally.
  2647. *
  2648. * @param {int} width
  2649. * @param {int} height
  2650. * @returns {object} An object with a bool `fits` property and a `size` property
  2651. * @public
  2652. */
  2653. measure: function() {
  2654. this.__forceRedraw();
  2655. var tooltipBcr = this.__$tooltip[0].getBoundingClientRect(),
  2656. result = { size: {
  2657. // bcr.width/height are not defined in IE8- but in this
  2658. // case, bcr.right/bottom will have the same value
  2659. // except in iOS 8+ where tooltipBcr.bottom/right are wrong
  2660. // after scrolling for reasons yet to be determined.
  2661. // tooltipBcr.top/left might not be 0, see issue #514
  2662. height: tooltipBcr.height || (tooltipBcr.bottom - tooltipBcr.top),
  2663. width: tooltipBcr.width || (tooltipBcr.right - tooltipBcr.left)
  2664. }};
  2665. if (this.constraints) {
  2666. // note: we used to use offsetWidth instead of boundingRectClient but
  2667. // it returned rounded values, causing issues with sub-pixel layouts.
  2668. // note2: noticed that the bcrWidth of text content of a div was once
  2669. // greater than the bcrWidth of its container by 1px, causing the final
  2670. // tooltip box to be too small for its content. However, evaluating
  2671. // their widths one against the other (below) surprisingly returned
  2672. // equality. Happened only once in Chrome 48, was not able to reproduce
  2673. // => just having fun with float position values...
  2674. var $content = this.__$tooltip.find('.tooltipster-content'),
  2675. height = this.__$tooltip.outerHeight(),
  2676. contentBcr = $content[0].getBoundingClientRect(),
  2677. fits = {
  2678. height: height <= this.constraints.height,
  2679. width: (
  2680. // this condition accounts for min-width property that
  2681. // may apply
  2682. tooltipBcr.width <= this.constraints.width
  2683. // the -1 is here because scrollWidth actually returns
  2684. // a rounded value, and may be greater than bcr.width if
  2685. // it was rounded up. This may cause an issue for contents
  2686. // which actually really overflow by 1px or so, but that
  2687. // should be rare. Not sure how to solve this efficiently.
  2688. // See http://blogs.msdn.com/b/ie/archive/2012/02/17/sub-pixel-rendering-and-the-css-object-model.aspx
  2689. && contentBcr.width >= $content[0].scrollWidth - 1
  2690. )
  2691. };
  2692. result.fits = fits.height && fits.width;
  2693. }
  2694. // old versions of IE get the width wrong for some reason and it causes
  2695. // the text to be broken to a new line, so we round it up. If the width
  2696. // is the width of the screen though, we can assume it is accurate.
  2697. if ( env.IE
  2698. && env.IE <= 11
  2699. && result.size.width !== env.window.document.documentElement.clientWidth
  2700. ) {
  2701. result.size.width = Math.ceil(result.size.width) + 1;
  2702. }
  2703. return result;
  2704. }
  2705. };
  2706. // quick & dirty compare function, not bijective nor multidimensional
  2707. function areEqual(a,b) {
  2708. var same = true;
  2709. $.each(a, function(i, _) {
  2710. if (b[i] === undefined || a[i] !== b[i]) {
  2711. same = false;
  2712. return false;
  2713. }
  2714. });
  2715. return same;
  2716. }
  2717. /**
  2718. * A fast function to check if an element is still in the DOM. It
  2719. * tries to use an id as ids are indexed by the browser, or falls
  2720. * back to jQuery's `contains` method. May fail if two elements
  2721. * have the same id, but so be it
  2722. *
  2723. * @param {object} $obj A jQuery-wrapped HTML element
  2724. * @return {boolean}
  2725. */
  2726. function bodyContains($obj) {
  2727. var id = $obj.attr('id'),
  2728. el = id ? env.window.document.getElementById(id) : null;
  2729. // must also check that the element with the id is the one we want
  2730. return el ? el === $obj[0] : $.contains(env.window.document.body, $obj[0]);
  2731. }
  2732. // detect IE versions for dirty fixes
  2733. var uA = navigator.userAgent.toLowerCase();
  2734. if (uA.indexOf('msie') != -1) env.IE = parseInt(uA.split('msie')[1]);
  2735. else if (uA.toLowerCase().indexOf('trident') !== -1 && uA.indexOf(' rv:11') !== -1) env.IE = 11;
  2736. else if (uA.toLowerCase().indexOf('edge/') != -1) env.IE = parseInt(uA.toLowerCase().split('edge/')[1]);
  2737. // detecting support for CSS transitions
  2738. function transitionSupport() {
  2739. // env.window is not defined yet when this is called
  2740. if (!win) return false;
  2741. var b = win.document.body || win.document.documentElement,
  2742. s = b.style,
  2743. p = 'transition',
  2744. v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms'];
  2745. if (typeof s[p] == 'string') { return true; }
  2746. p = p.charAt(0).toUpperCase() + p.substr(1);
  2747. for (var i=0; i<v.length; i++) {
  2748. if (typeof s[v[i] + p] == 'string') { return true; }
  2749. }
  2750. return false;
  2751. }
  2752. // we'll return jQuery for plugins not to have to declare it as a dependency,
  2753. // but it's done by a build task since it should be included only once at the
  2754. // end when we concatenate the main file with a plugin
  2755. // sideTip is Tooltipster's default plugin.
  2756. // This file will be UMDified by a build task.
  2757. var pluginName = 'tooltipster.sideTip';
  2758. $.tooltipster._plugin({
  2759. name: pluginName,
  2760. instance: {
  2761. /**
  2762. * Defaults are provided as a function for an easy override by inheritance
  2763. *
  2764. * @return {object} An object with the defaults options
  2765. * @private
  2766. */
  2767. __defaults: function() {
  2768. return {
  2769. // if the tooltip should display an arrow that points to the origin
  2770. arrow: true,
  2771. // the distance in pixels between the tooltip and the origin
  2772. distance: 6,
  2773. // allows to easily change the position of the tooltip
  2774. functionPosition: null,
  2775. maxWidth: null,
  2776. // used to accomodate the arrow of tooltip if there is one.
  2777. // First to make sure that the arrow target is not too close
  2778. // to the edge of the tooltip, so the arrow does not overflow
  2779. // the tooltip. Secondly when we reposition the tooltip to
  2780. // make sure that it's positioned in such a way that the arrow is
  2781. // still pointing at the target (and not a few pixels beyond it).
  2782. // It should be equal to or greater than half the width of
  2783. // the arrow (by width we mean the size of the side which touches
  2784. // the side of the tooltip).
  2785. minIntersection: 16,
  2786. minWidth: 0,
  2787. // deprecated in 4.0.0. Listed for _optionsExtract to pick it up
  2788. position: null,
  2789. side: 'top',
  2790. // set to false to position the tooltip relatively to the document rather
  2791. // than the window when we open it
  2792. viewportAware: true
  2793. };
  2794. },
  2795. /**
  2796. * Run once: at instantiation of the plugin
  2797. *
  2798. * @param {object} instance The tooltipster object that instantiated this plugin
  2799. * @private
  2800. */
  2801. __init: function(instance) {
  2802. var self = this;
  2803. // list of instance variables
  2804. self.__instance = instance;
  2805. self.__namespace = 'tooltipster-sideTip-'+ Math.round(Math.random()*1000000);
  2806. self.__previousState = 'closed';
  2807. self.__options;
  2808. // initial formatting
  2809. self.__optionsFormat();
  2810. self.__instance._on('state.'+ self.__namespace, function(event) {
  2811. if (event.state == 'closed') {
  2812. self.__close();
  2813. }
  2814. else if (event.state == 'appearing' && self.__previousState == 'closed') {
  2815. self.__create();
  2816. }
  2817. self.__previousState = event.state;
  2818. });
  2819. // reformat every time the options are changed
  2820. self.__instance._on('options.'+ self.__namespace, function() {
  2821. self.__optionsFormat();
  2822. });
  2823. self.__instance._on('reposition.'+ self.__namespace, function(e) {
  2824. self.__reposition(e.event, e.helper);
  2825. });
  2826. },
  2827. /**
  2828. * Called when the tooltip has closed
  2829. *
  2830. * @private
  2831. */
  2832. __close: function() {
  2833. // detach our content object first, so the next jQuery's remove()
  2834. // call does not unbind its event handlers
  2835. if (this.__instance.content() instanceof $) {
  2836. this.__instance.content().detach();
  2837. }
  2838. // remove the tooltip from the DOM
  2839. this.__instance._$tooltip.remove();
  2840. this.__instance._$tooltip = null;
  2841. },
  2842. /**
  2843. * Creates the HTML element of the tooltip.
  2844. *
  2845. * @private
  2846. */
  2847. __create: function() {
  2848. // note: we wrap with a .tooltipster-box div to be able to set a margin on it
  2849. // (.tooltipster-base must not have one)
  2850. var $html = $(
  2851. '<div class="tooltipster-base tooltipster-sidetip">' +
  2852. '<div class="tooltipster-box">' +
  2853. '<div class="tooltipster-content"></div>' +
  2854. '</div>' +
  2855. '<div class="tooltipster-arrow">' +
  2856. '<div class="tooltipster-arrow-uncropped">' +
  2857. '<div class="tooltipster-arrow-border"></div>' +
  2858. '<div class="tooltipster-arrow-background"></div>' +
  2859. '</div>' +
  2860. '</div>' +
  2861. '</div>'
  2862. );
  2863. // hide arrow if asked
  2864. if (!this.__options.arrow) {
  2865. $html
  2866. .find('.tooltipster-box')
  2867. .css('margin', 0)
  2868. .end()
  2869. .find('.tooltipster-arrow')
  2870. .hide();
  2871. }
  2872. // apply min/max width if asked
  2873. if (this.__options.minWidth) {
  2874. $html.css('min-width', this.__options.minWidth + 'px');
  2875. }
  2876. if (this.__options.maxWidth) {
  2877. $html.css('max-width', this.__options.maxWidth + 'px');
  2878. }
  2879. this.__instance._$tooltip = $html;
  2880. // tell the instance that the tooltip element has been created
  2881. this.__instance._trigger('created');
  2882. },
  2883. /**
  2884. * Used when the plugin is to be unplugged
  2885. *
  2886. * @private
  2887. */
  2888. __destroy: function() {
  2889. this.__instance._off('.'+ self.__namespace);
  2890. },
  2891. /**
  2892. * (Re)compute this.__options from the options declared to the instance
  2893. *
  2894. * @private
  2895. */
  2896. __optionsFormat: function() {
  2897. var self = this;
  2898. // get the options
  2899. self.__options = self.__instance._optionsExtract(pluginName, self.__defaults());
  2900. // for backward compatibility, deprecated in v4.0.0
  2901. if (self.__options.position) {
  2902. self.__options.side = self.__options.position;
  2903. }
  2904. // options formatting
  2905. // format distance as a four-cell array if it ain't one yet and then make
  2906. // it an object with top/bottom/left/right properties
  2907. if (typeof self.__options.distance != 'object') {
  2908. self.__options.distance = [self.__options.distance];
  2909. }
  2910. if (self.__options.distance.length < 4) {
  2911. if (self.__options.distance[1] === undefined) self.__options.distance[1] = self.__options.distance[0];
  2912. if (self.__options.distance[2] === undefined) self.__options.distance[2] = self.__options.distance[0];
  2913. if (self.__options.distance[3] === undefined) self.__options.distance[3] = self.__options.distance[1];
  2914. self.__options.distance = {
  2915. top: self.__options.distance[0],
  2916. right: self.__options.distance[1],
  2917. bottom: self.__options.distance[2],
  2918. left: self.__options.distance[3]
  2919. };
  2920. }
  2921. // let's transform:
  2922. // 'top' into ['top', 'bottom', 'right', 'left']
  2923. // 'right' into ['right', 'left', 'top', 'bottom']
  2924. // 'bottom' into ['bottom', 'top', 'right', 'left']
  2925. // 'left' into ['left', 'right', 'top', 'bottom']
  2926. if (typeof self.__options.side == 'string') {
  2927. var opposites = {
  2928. 'top': 'bottom',
  2929. 'right': 'left',
  2930. 'bottom': 'top',
  2931. 'left': 'right'
  2932. };
  2933. self.__options.side = [self.__options.side, opposites[self.__options.side]];
  2934. if (self.__options.side[0] == 'left' || self.__options.side[0] == 'right') {
  2935. self.__options.side.push('top', 'bottom');
  2936. }
  2937. else {
  2938. self.__options.side.push('right', 'left');
  2939. }
  2940. }
  2941. // misc
  2942. // disable the arrow in IE6 unless the arrow option was explicitly set to true
  2943. if ( $.tooltipster._env.IE === 6
  2944. && self.__options.arrow !== true
  2945. ) {
  2946. self.__options.arrow = false;
  2947. }
  2948. },
  2949. /**
  2950. * This method must compute and set the positioning properties of the
  2951. * tooltip (left, top, width, height, etc.). It must also make sure the
  2952. * tooltip is eventually appended to its parent (since the element may be
  2953. * detached from the DOM at the moment the method is called).
  2954. *
  2955. * We'll evaluate positioning scenarios to find which side can contain the
  2956. * tooltip in the best way. We'll consider things relatively to the window
  2957. * (unless the user asks not to), then to the document (if need be, or if the
  2958. * user explicitly requires the tests to run on the document). For each
  2959. * scenario, measures are taken, allowing us to know how well the tooltip
  2960. * is going to fit. After that, a sorting function will let us know what
  2961. * the best scenario is (we also allow the user to choose his favorite
  2962. * scenario by using an event).
  2963. *
  2964. * @param {object} helper An object that contains variables that plugin
  2965. * creators may find useful (see below)
  2966. * @param {object} helper.geo An object with many layout properties
  2967. * about objects of interest (window, document, origin). This should help
  2968. * plugin users compute the optimal position of the tooltip
  2969. * @private
  2970. */
  2971. __reposition: function(event, helper) {
  2972. var self = this,
  2973. finalResult,
  2974. // to know where to put the tooltip, we need to know on which point
  2975. // of the x or y axis we should center it. That coordinate is the target
  2976. targets = self.__targetFind(helper),
  2977. testResults = [];
  2978. // make sure the tooltip is detached while we make tests on a clone
  2979. self.__instance._$tooltip.detach();
  2980. // we could actually provide the original element to the Ruler and
  2981. // not a clone, but it just feels right to keep it out of the
  2982. // machinery.
  2983. var $clone = self.__instance._$tooltip.clone(),
  2984. // start position tests session
  2985. ruler = $.tooltipster._getRuler($clone),
  2986. satisfied = false,
  2987. animation = self.__instance.option('animation');
  2988. // an animation class could contain properties that distort the size
  2989. if (animation) {
  2990. $clone.removeClass('tooltipster-'+ animation);
  2991. }
  2992. // start evaluating scenarios
  2993. $.each(['window', 'document'], function(i, container) {
  2994. var takeTest = null;
  2995. // let the user decide to keep on testing or not
  2996. self.__instance._trigger({
  2997. container: container,
  2998. helper: helper,
  2999. satisfied: satisfied,
  3000. takeTest: function(bool) {
  3001. takeTest = bool;
  3002. },
  3003. results: testResults,
  3004. type: 'positionTest'
  3005. });
  3006. if ( takeTest == true
  3007. || ( takeTest != false
  3008. && satisfied == false
  3009. // skip the window scenarios if asked. If they are reintegrated by
  3010. // the callback of the positionTest event, they will have to be
  3011. // excluded using the callback of positionTested
  3012. && (container != 'window' || self.__options.viewportAware)
  3013. )
  3014. ) {
  3015. // for each allowed side
  3016. for (var i=0; i < self.__options.side.length; i++) {
  3017. var distance = {
  3018. horizontal: 0,
  3019. vertical: 0
  3020. },
  3021. side = self.__options.side[i];
  3022. if (side == 'top' || side == 'bottom') {
  3023. distance.vertical = self.__options.distance[side];
  3024. }
  3025. else {
  3026. distance.horizontal = self.__options.distance[side];
  3027. }
  3028. // this may have an effect on the size of the tooltip if there are css
  3029. // rules for the arrow or something else
  3030. self.__sideChange($clone, side);
  3031. $.each(['natural', 'constrained'], function(i, mode) {
  3032. takeTest = null;
  3033. // emit an event on the instance
  3034. self.__instance._trigger({
  3035. container: container,
  3036. event: event,
  3037. helper: helper,
  3038. mode: mode,
  3039. results: testResults,
  3040. satisfied: satisfied,
  3041. side: side,
  3042. takeTest: function(bool) {
  3043. takeTest = bool;
  3044. },
  3045. type: 'positionTest'
  3046. });
  3047. if ( takeTest == true
  3048. || ( takeTest != false
  3049. && satisfied == false
  3050. )
  3051. ) {
  3052. var testResult = {
  3053. container: container,
  3054. // we let the distance as an object here, it can make things a little easier
  3055. // during the user's calculations at positionTest/positionTested
  3056. distance: distance,
  3057. // whether the tooltip can fit in the size of the viewport (does not mean
  3058. // that we'll be able to make it initially entirely visible, see 'whole')
  3059. fits: null,
  3060. mode: mode,
  3061. outerSize: null,
  3062. side: side,
  3063. size: null,
  3064. target: targets[side],
  3065. // check if the origin has enough surface on screen for the tooltip to
  3066. // aim at it without overflowing the viewport (this is due to the thickness
  3067. // of the arrow represented by the minIntersection length).
  3068. // If not, the tooltip will have to be partly or entirely off screen in
  3069. // order to stay docked to the origin. This value will stay null when the
  3070. // container is the document, as it is not relevant
  3071. whole: null
  3072. };
  3073. // get the size of the tooltip with or without size constraints
  3074. var rulerConfigured = (mode == 'natural') ?
  3075. ruler.free() :
  3076. ruler.constrain(
  3077. helper.geo.available[container][side].width - distance.horizontal,
  3078. helper.geo.available[container][side].height - distance.vertical
  3079. ),
  3080. rulerResults = rulerConfigured.measure();
  3081. testResult.size = rulerResults.size;
  3082. testResult.outerSize = {
  3083. height: rulerResults.size.height + distance.vertical,
  3084. width: rulerResults.size.width + distance.horizontal
  3085. };
  3086. if (mode == 'natural') {
  3087. if( helper.geo.available[container][side].width >= testResult.outerSize.width
  3088. && helper.geo.available[container][side].height >= testResult.outerSize.height
  3089. ) {
  3090. testResult.fits = true;
  3091. }
  3092. else {
  3093. testResult.fits = false;
  3094. }
  3095. }
  3096. else {
  3097. testResult.fits = rulerResults.fits;
  3098. }
  3099. if (container == 'window') {
  3100. if (!testResult.fits) {
  3101. testResult.whole = false;
  3102. }
  3103. else {
  3104. if (side == 'top' || side == 'bottom') {
  3105. testResult.whole = (
  3106. helper.geo.origin.windowOffset.right >= self.__options.minIntersection
  3107. && helper.geo.window.size.width - helper.geo.origin.windowOffset.left >= self.__options.minIntersection
  3108. );
  3109. }
  3110. else {
  3111. testResult.whole = (
  3112. helper.geo.origin.windowOffset.bottom >= self.__options.minIntersection
  3113. && helper.geo.window.size.height - helper.geo.origin.windowOffset.top >= self.__options.minIntersection
  3114. );
  3115. }
  3116. }
  3117. }
  3118. testResults.push(testResult);
  3119. // we don't need to compute more positions if we have one fully on screen
  3120. if (testResult.whole) {
  3121. satisfied = true;
  3122. }
  3123. else {
  3124. // don't run the constrained test unless the natural width was greater
  3125. // than the available width, otherwise it's pointless as we know it
  3126. // wouldn't fit either
  3127. if ( testResult.mode == 'natural'
  3128. && ( testResult.fits
  3129. || testResult.size.width <= helper.geo.available[container][side].width
  3130. )
  3131. ) {
  3132. return false;
  3133. }
  3134. }
  3135. }
  3136. });
  3137. }
  3138. }
  3139. });
  3140. // the user may eliminate the unwanted scenarios from testResults, but he's
  3141. // not supposed to alter them at this point. functionPosition and the
  3142. // position event serve that purpose.
  3143. self.__instance._trigger({
  3144. edit: function(r) {
  3145. testResults = r;
  3146. },
  3147. event: event,
  3148. helper: helper,
  3149. results: testResults,
  3150. type: 'positionTested'
  3151. });
  3152. /**
  3153. * Sort the scenarios to find the favorite one.
  3154. *
  3155. * The favorite scenario is when we can fully display the tooltip on screen,
  3156. * even if it means that the middle of the tooltip is no longer centered on
  3157. * the middle of the origin (when the origin is near the edge of the screen
  3158. * or even partly off screen). We want the tooltip on the preferred side,
  3159. * even if it means that we have to use a constrained size rather than a
  3160. * natural one (as long as it fits). When the origin is off screen at the top
  3161. * the tooltip will be positioned at the bottom (if allowed), if the origin
  3162. * is off screen on the right, it will be positioned on the left, etc.
  3163. * If there are no scenarios where the tooltip can fit on screen, or if the
  3164. * user does not want the tooltip to fit on screen (viewportAware == false),
  3165. * we fall back to the scenarios relative to the document.
  3166. *
  3167. * When the tooltip is bigger than the viewport in either dimension, we stop
  3168. * looking at the window scenarios and consider the document scenarios only,
  3169. * with the same logic to find on which side it would fit best.
  3170. *
  3171. * If the tooltip cannot fit the document on any side, we force it at the
  3172. * bottom, so at least the user can scroll to see it.
  3173. */
  3174. testResults.sort(function(a, b) {
  3175. // best if it's whole (the tooltip fits and adapts to the viewport)
  3176. if (a.whole && !b.whole) {
  3177. return -1;
  3178. }
  3179. else if (!a.whole && b.whole) {
  3180. return 1;
  3181. }
  3182. else if (a.whole && b.whole) {
  3183. var ai = self.__options.side.indexOf(a.side),
  3184. bi = self.__options.side.indexOf(b.side);
  3185. // use the user's sides fallback array
  3186. if (ai < bi) {
  3187. return -1;
  3188. }
  3189. else if (ai > bi) {
  3190. return 1;
  3191. }
  3192. else {
  3193. // will be used if the user forced the tests to continue
  3194. return a.mode == 'natural' ? -1 : 1;
  3195. }
  3196. }
  3197. else {
  3198. // better if it fits
  3199. if (a.fits && !b.fits) {
  3200. return -1;
  3201. }
  3202. else if (!a.fits && b.fits) {
  3203. return 1;
  3204. }
  3205. else if (a.fits && b.fits) {
  3206. var ai = self.__options.side.indexOf(a.side),
  3207. bi = self.__options.side.indexOf(b.side);
  3208. // use the user's sides fallback array
  3209. if (ai < bi) {
  3210. return -1;
  3211. }
  3212. else if (ai > bi) {
  3213. return 1;
  3214. }
  3215. else {
  3216. // will be used if the user forced the tests to continue
  3217. return a.mode == 'natural' ? -1 : 1;
  3218. }
  3219. }
  3220. else {
  3221. // if everything failed, this will give a preference to the case where
  3222. // the tooltip overflows the document at the bottom
  3223. if ( a.container == 'document'
  3224. && a.side == 'bottom'
  3225. && a.mode == 'natural'
  3226. ) {
  3227. return -1;
  3228. }
  3229. else {
  3230. return 1;
  3231. }
  3232. }
  3233. }
  3234. });
  3235. finalResult = testResults[0];
  3236. // now let's find the coordinates of the tooltip relatively to the window
  3237. finalResult.coord = {};
  3238. switch (finalResult.side) {
  3239. case 'left':
  3240. case 'right':
  3241. finalResult.coord.top = Math.floor(finalResult.target - finalResult.size.height / 2);
  3242. break;
  3243. case 'bottom':
  3244. case 'top':
  3245. finalResult.coord.left = Math.floor(finalResult.target - finalResult.size.width / 2);
  3246. break;
  3247. }
  3248. switch (finalResult.side) {
  3249. case 'left':
  3250. finalResult.coord.left = helper.geo.origin.windowOffset.left - finalResult.outerSize.width;
  3251. break;
  3252. case 'right':
  3253. finalResult.coord.left = helper.geo.origin.windowOffset.right + finalResult.distance.horizontal;
  3254. break;
  3255. case 'top':
  3256. finalResult.coord.top = helper.geo.origin.windowOffset.top - finalResult.outerSize.height;
  3257. break;
  3258. case 'bottom':
  3259. finalResult.coord.top = helper.geo.origin.windowOffset.bottom + finalResult.distance.vertical;
  3260. break;
  3261. }
  3262. // if the tooltip can potentially be contained within the viewport dimensions
  3263. // and that we are asked to make it fit on screen
  3264. if (finalResult.container == 'window') {
  3265. // if the tooltip overflows the viewport, we'll move it accordingly (then it will
  3266. // not be centered on the middle of the origin anymore). We only move horizontally
  3267. // for top and bottom tooltips and vice versa.
  3268. if (finalResult.side == 'top' || finalResult.side == 'bottom') {
  3269. // if there is an overflow on the left
  3270. if (finalResult.coord.left < 0) {
  3271. // prevent the overflow unless the origin itself gets off screen (minus the
  3272. // margin needed to keep the arrow pointing at the target)
  3273. if (helper.geo.origin.windowOffset.right - this.__options.minIntersection >= 0) {
  3274. finalResult.coord.left = 0;
  3275. }
  3276. else {
  3277. finalResult.coord.left = helper.geo.origin.windowOffset.right - this.__options.minIntersection - 1;
  3278. }
  3279. }
  3280. // or an overflow on the right
  3281. else if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) {
  3282. if (helper.geo.origin.windowOffset.left + this.__options.minIntersection <= helper.geo.window.size.width) {
  3283. finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width;
  3284. }
  3285. else {
  3286. finalResult.coord.left = helper.geo.origin.windowOffset.left + this.__options.minIntersection + 1 - finalResult.size.width;
  3287. }
  3288. }
  3289. }
  3290. else {
  3291. // overflow at the top
  3292. if (finalResult.coord.top < 0) {
  3293. if (helper.geo.origin.windowOffset.bottom - this.__options.minIntersection >= 0) {
  3294. finalResult.coord.top = 0;
  3295. }
  3296. else {
  3297. finalResult.coord.top = helper.geo.origin.windowOffset.bottom - this.__options.minIntersection - 1;
  3298. }
  3299. }
  3300. // or at the bottom
  3301. else if (finalResult.coord.top > helper.geo.window.size.height - finalResult.size.height) {
  3302. if (helper.geo.origin.windowOffset.top + this.__options.minIntersection <= helper.geo.window.size.height) {
  3303. finalResult.coord.top = helper.geo.window.size.height - finalResult.size.height;
  3304. }
  3305. else {
  3306. finalResult.coord.top = helper.geo.origin.windowOffset.top + this.__options.minIntersection + 1 - finalResult.size.height;
  3307. }
  3308. }
  3309. }
  3310. }
  3311. else {
  3312. // there might be overflow here too but it's easier to handle. If there has
  3313. // to be an overflow, we'll make sure it's on the right side of the screen
  3314. // (because the browser will extend the document size if there is an overflow
  3315. // on the right, but not on the left). The sort function above has already
  3316. // made sure that a bottom document overflow is preferred to a top overflow,
  3317. // so we don't have to care about it.
  3318. // if there is an overflow on the right
  3319. if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) {
  3320. // this may actually create on overflow on the left but we'll fix it in a sec
  3321. finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width;
  3322. }
  3323. // if there is an overflow on the left
  3324. if (finalResult.coord.left < 0) {
  3325. // don't care if it overflows the right after that, we made our best
  3326. finalResult.coord.left = 0;
  3327. }
  3328. }
  3329. // submit the positioning proposal to the user function which may choose to change
  3330. // the side, size and/or the coordinates
  3331. // first, set the rules that corresponds to the proposed side: it may change
  3332. // the size of the tooltip, and the custom functionPosition may want to detect the
  3333. // size of something before making a decision. So let's make things easier for the
  3334. // implementor
  3335. self.__sideChange($clone, finalResult.side);
  3336. // add some variables to the helper
  3337. helper.tooltipClone = $clone[0];
  3338. helper.tooltipParent = self.__instance.option('parent').parent[0];
  3339. // move informative values to the helper
  3340. helper.mode = finalResult.mode;
  3341. helper.whole = finalResult.whole;
  3342. // add some variables to the helper for the functionPosition callback (these
  3343. // will also be added to the event fired by self.__instance._trigger but that's
  3344. // ok, we're just being consistent)
  3345. helper.origin = self.__instance._$origin[0];
  3346. helper.tooltip = self.__instance._$tooltip[0];
  3347. // leave only the actionable values in there for functionPosition
  3348. delete finalResult.container;
  3349. delete finalResult.fits;
  3350. delete finalResult.mode;
  3351. delete finalResult.outerSize;
  3352. delete finalResult.whole;
  3353. // keep only the distance on the relevant side, for clarity
  3354. finalResult.distance = finalResult.distance.horizontal || finalResult.distance.vertical;
  3355. // beginners may not be comfortable with the concept of editing the object
  3356. // passed by reference, so we provide an edit function and pass a clone
  3357. var finalResultClone = $.extend(true, {}, finalResult);
  3358. // emit an event on the instance
  3359. self.__instance._trigger({
  3360. edit: function(result) {
  3361. finalResult = result;
  3362. },
  3363. event: event,
  3364. helper: helper,
  3365. position: finalResultClone,
  3366. type: 'position'
  3367. });
  3368. if (self.__options.functionPosition) {
  3369. var result = self.__options.functionPosition.call(self, self.__instance, helper, finalResultClone);
  3370. if (result) finalResult = result;
  3371. }
  3372. // end the positioning tests session (the user might have had a
  3373. // use for it during the position event, now it's over)
  3374. ruler.destroy();
  3375. // compute the position of the target relatively to the tooltip root
  3376. // element so we can place the arrow and make the needed adjustments
  3377. var arrowCoord,
  3378. maxVal;
  3379. if (finalResult.side == 'top' || finalResult.side == 'bottom') {
  3380. arrowCoord = {
  3381. prop: 'left',
  3382. val: finalResult.target - finalResult.coord.left
  3383. };
  3384. maxVal = finalResult.size.width - this.__options.minIntersection;
  3385. }
  3386. else {
  3387. arrowCoord = {
  3388. prop: 'top',
  3389. val: finalResult.target - finalResult.coord.top
  3390. };
  3391. maxVal = finalResult.size.height - this.__options.minIntersection;
  3392. }
  3393. // cannot lie beyond the boundaries of the tooltip, minus the
  3394. // arrow margin
  3395. if (arrowCoord.val < this.__options.minIntersection) {
  3396. arrowCoord.val = this.__options.minIntersection;
  3397. }
  3398. else if (arrowCoord.val > maxVal) {
  3399. arrowCoord.val = maxVal;
  3400. }
  3401. var originParentOffset;
  3402. // let's convert the window-relative coordinates into coordinates relative to the
  3403. // future positioned parent that the tooltip will be appended to
  3404. if (helper.geo.origin.fixedLineage) {
  3405. // same as windowOffset when the position is fixed
  3406. originParentOffset = helper.geo.origin.windowOffset;
  3407. }
  3408. else {
  3409. // this assumes that the parent of the tooltip is located at
  3410. // (0, 0) in the document, typically like when the parent is
  3411. // <body>.
  3412. // If we ever allow other types of parent, .tooltipster-ruler
  3413. // will have to be appended to the parent to inherit css style
  3414. // values that affect the display of the text and such.
  3415. originParentOffset = {
  3416. left: helper.geo.origin.windowOffset.left + helper.geo.window.scroll.left,
  3417. top: helper.geo.origin.windowOffset.top + helper.geo.window.scroll.top
  3418. };
  3419. }
  3420. finalResult.coord = {
  3421. left: originParentOffset.left + (finalResult.coord.left - helper.geo.origin.windowOffset.left),
  3422. top: originParentOffset.top + (finalResult.coord.top - helper.geo.origin.windowOffset.top)
  3423. };
  3424. // set position values on the original tooltip element
  3425. self.__sideChange(self.__instance._$tooltip, finalResult.side);
  3426. if (helper.geo.origin.fixedLineage) {
  3427. self.__instance._$tooltip
  3428. .css('position', 'fixed');
  3429. }
  3430. else {
  3431. // CSS default
  3432. self.__instance._$tooltip
  3433. .css('position', '');
  3434. }
  3435. self.__instance._$tooltip
  3436. .css({
  3437. left: finalResult.coord.left,
  3438. top: finalResult.coord.top,
  3439. // we need to set a size even if the tooltip is in its natural size
  3440. // because when the tooltip is positioned beyond the width of the body
  3441. // (which is by default the width of the window; it will happen when
  3442. // you scroll the window horizontally to get to the origin), its text
  3443. // content will otherwise break lines at each word to keep up with the
  3444. // body overflow strategy.
  3445. height: finalResult.size.height,
  3446. width: finalResult.size.width
  3447. })
  3448. .find('.tooltipster-arrow')
  3449. .css({
  3450. 'left': '',
  3451. 'top': ''
  3452. })
  3453. .css(arrowCoord.prop, arrowCoord.val);
  3454. // append the tooltip HTML element to its parent
  3455. self.__instance._$tooltip.appendTo(self.__instance.option('parent'));
  3456. self.__instance._trigger({
  3457. type: 'repositioned',
  3458. event: event,
  3459. position: finalResult
  3460. });
  3461. },
  3462. /**
  3463. * Make whatever modifications are needed when the side is changed. This has
  3464. * been made an independant method for easy inheritance in custom plugins based
  3465. * on this default plugin.
  3466. *
  3467. * @param {object} $obj
  3468. * @param {string} side
  3469. * @private
  3470. */
  3471. __sideChange: function($obj, side) {
  3472. $obj
  3473. .removeClass('tooltipster-bottom')
  3474. .removeClass('tooltipster-left')
  3475. .removeClass('tooltipster-right')
  3476. .removeClass('tooltipster-top')
  3477. .addClass('tooltipster-'+ side);
  3478. },
  3479. /**
  3480. * Returns the target that the tooltip should aim at for a given side.
  3481. * The calculated value is a distance from the edge of the window
  3482. * (left edge for top/bottom sides, top edge for left/right side). The
  3483. * tooltip will be centered on that position and the arrow will be
  3484. * positioned there (as much as possible).
  3485. *
  3486. * @param {object} helper
  3487. * @return {integer}
  3488. * @private
  3489. */
  3490. __targetFind: function(helper) {
  3491. var target = {},
  3492. rects = this.__instance._$origin[0].getClientRects();
  3493. // these lines fix a Chrome bug (issue #491)
  3494. if (rects.length > 1) {
  3495. var opacity = this.__instance._$origin.css('opacity');
  3496. if(opacity == 1) {
  3497. this.__instance._$origin.css('opacity', 0.99);
  3498. rects = this.__instance._$origin[0].getClientRects();
  3499. this.__instance._$origin.css('opacity', 1);
  3500. }
  3501. }
  3502. // by default, the target will be the middle of the origin
  3503. if (rects.length < 2) {
  3504. target.top = Math.floor(helper.geo.origin.windowOffset.left + (helper.geo.origin.size.width / 2));
  3505. target.bottom = target.top;
  3506. target.left = Math.floor(helper.geo.origin.windowOffset.top + (helper.geo.origin.size.height / 2));
  3507. target.right = target.left;
  3508. }
  3509. // if multiple client rects exist, the element may be text split
  3510. // up into multiple lines and the middle of the origin may not be
  3511. // best option anymore. We need to choose the best target client rect
  3512. else {
  3513. // top: the first
  3514. var targetRect = rects[0];
  3515. target.top = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2);
  3516. // right: the middle line, rounded down in case there is an even
  3517. // number of lines (looks more centered => check out the
  3518. // demo with 4 split lines)
  3519. if (rects.length > 2) {
  3520. targetRect = rects[Math.ceil(rects.length / 2) - 1];
  3521. }
  3522. else {
  3523. targetRect = rects[0];
  3524. }
  3525. target.right = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2);
  3526. // bottom: the last
  3527. targetRect = rects[rects.length - 1];
  3528. target.bottom = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2);
  3529. // left: the middle line, rounded up
  3530. if (rects.length > 2) {
  3531. targetRect = rects[Math.ceil((rects.length + 1) / 2) - 1];
  3532. }
  3533. else {
  3534. targetRect = rects[rects.length - 1];
  3535. }
  3536. target.left = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2);
  3537. }
  3538. return target;
  3539. }
  3540. }
  3541. });
  3542. /* a build task will add "return $;" here */
  3543. return $;
  3544. }));