tooltipster.main.js 87 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345
  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 pluginreturn $;
  2755. }));