Search code examples
javascriptfullcalendar-3tooltipster

Tooltipster: [data-tooltip-content] as pointer to dynamic HTML


THE ISSUE: Using Tooltipster.JS with FullCalendar and / or dynamically rendered content

I've been scratching my head and chewing over this for quite a while now and I'm no close to where I was when I started. I've read and re-read the Tooltipster docs but I simply can't seem to use 'data-tooltip-content' as a pointer to the #idTooltipsterElement. It simply displays the value of 'data-tooltip-content' even though the docs clearly state:

Tooltipster Documentation states:

5. Use HTML inside your tooltips

Tooltipster allows you to use any HTML markup inside your tooltips. It means that you can insert things like images and text formatting tags.

Instead of a title attribute, use a data-tooltip-content attribute to provide a selector that corresponds to the HTML element of your page that should be used as content. This is your HTML:

<span class="tooltip" data-tooltip-content="#tooltip_content">This span has a tooltip with HTML when you hover over it!</span>

<div class="tooltip_templates">
    <span id="tooltip_content">
        <img src="myimage.png" /> <strong>This is the content of my tooltip!</strong>
    </span>
</div>

In your CSS file, add .tooltip_templates { display: none; } so that the content does not get displayed outside of the tooltip.

When we moved from Tippy.js to Tooltipster.js I was having other issues which thankfully are not present in Tootipster, but I found the former was far easier to set up even though it has nowhere near the amount of functionality nor documentation as the later.

However: I previously just used 'data-tooltip-content' to put my entire HTML elements inside it and oddly enough I noticed that this also works with Tooltipster as well even though their documentations seems to prefer the above approach. Now normally I would simply keep the old approach which I'm already familiar with, but unfortunately this bring with it a whole host of potential challenges relating to post event render changes (we are likely to implement) in FullCalendar which we use extensively on our pages, as well as formatting / styling difficulties which I've experienced already.

I am reasonably certain the preferred solution described on the Tooltipster documented would be ideal for our implementation, but I simply can't work out how to get it to work in our fullCalendar environment which is broken down in the following program flow:

  1. eventRender: pull events from your event source and render each entry for the chosen month / period. This is where the initial Toolstipster gets created and assigned a unique id

  2. eventAfterAllRender: this basically runs after the eventRender and this is where we do our post render tidy up e.g. check for invalid image links etc and replace remove any image icons/references as well as change or disable/remove the tooltip if no longer necessary. If required we replace the tooltip content here otherwise we set the Tooltipster content to whatever data was passed along form the eventRender process.

  3. eventMouseover: this section holds the event hover method which is responsible for displaying the Tooltipster tooltip.

I created a sample fiddle using a striped down version of our code and a couple of test events instead of using an external data source, but the result is the same when I use this code in our own environment. I've included the following to reflect our environment: Bootstrap 4.4.0 - FullCalendar 3.10.1 - Tooltipster 4.2.7

If you hover over one of the events the tooltip just displays the value of the section pointer e.g. #tt_event1 (the id of the element it points to) instead of what is set in the content section

e.g. <div class="tooltip_templates"><span id="tt_event1" class="tooltip_content"><img src="https://hackernoon.com/hn-images/1*cmqZiJz1TuUedRoeI3g_Iw.jpeg" width="450" height="auto"><p style="text-align:left;"><span class="flag-icon flag-icon-uk"></span><strong class="title">Tips for Writing Cleaner Code</strong><br>optional desctiptive text can go here</p></span></div>

  $(document).ready(function() {

    $('#calendar').fullCalendar({
      defaultView: 'month',
      header: '',
      defaultDate: '2020-03-01',
      events: [{
        id: 'event1',
        className: 'UK',
        title: 'Tips for Writing Cleaner Code',
        description: 'I decided to write an article that will be useful for beginners to understand their mistakes and to put together some practices. /n source: hackernoon.com/tips-for-writing-cleaner-better-code-e36ffeb55526',
        start: '2020-03-02', end: '2020-03-02'
      }, {
        id: 'event2',
        className: 'NL',
        title: 'Modern Style of Javascript with Arrow Functions',
        description: 'The complete explanation of Arrow functions in Javascript, and how it helps developers to write flexible and consistent code. /n source: hackernoon.com/modern-style-of-javascript-with-arrow-functions-lg1x3474',
        start: '2020-03-04', end: '2020-03-11'
      }
              ],
      eventRender: function(event, element, view) {

        window.dataE = window.dataE || [];
        element.attr( 'id', event.id );

        var /* Vars */
        desc = (event.description), url='',
            urlEvent1 = "https://hackernoon.com/hn-images/1*cmqZiJz1TuUedRoeI3g_Iw.jpeg",
            urlEvent2 = "https://images.unsplash.com/photo-1527427337751-fdca2f128ce5?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEwMDk2Mn0",
            tipRef="tip_content_"+event.id, idTip="#"+tipRef
        /* End Vars */;

        // passing 2 extra image params manually - usually from seperate data routine
        if (event.title == 'Tips for Writing Cleaner Code') { url = urlEvent1; } else { url = urlEvent2; }

        var tt_ref = '#tt_'+event.id;
        var className = ''+event.className;
        var cc = className.toLowerCase();
        var tt = '<div class="tooltip_templates"><span id="tt_' + event.id + '" class="tooltip_content"><img src="' + url + '" width="450" height="auto"><p style="text-align:left;"><span class="flag-icon flag-icon-' + cc + '"></span><strong class="title">' + event.title + '</strong><br>optional desctiptive text can go here</p></span></div>';

        element.attr( 'data-tooltip-content', tt_ref ); // the tooltip pointer
        element.attr( 'data-tt-tooltipser' ); element.attr( 'data-tt-tooltipser', tt );
        element.attr( 'tt_title' ); element.attr( 'tt_title', event.title );
        element.addClass('tt_tooltip tt_group');

        var eID = '#'+event.id;

        // Tooltipster
        if (!element.hasClass('tt_added')) {
          $(eID).tooltipster();
          $(eID).tooltipster({
            //content: tipContent,
            //contentCloning: true,
            trigger: 'hover',
            //multiple: true,
            animation: 'fade',
            arrow: true,
            delay: 300,
            maxWidth: 600,
            contentAsHTML: true,
            debug: true
          });
          element.addClass('tt_added');
        }

        // create array of event.IDs for use in [eventAfterAllRender]
        if (Array.isArray(dataE)){
          var json = JSON.stringify(event.id),
              item = dataE.find(el => JSON.stringify(el) === json);
          if (typeof item !== 'undefined'){
            return false;
          } else {
            dataE.push(event.id);
          }
        }

      },

      eventAfterAllRender: function(event, element){

        // get events from dataE array created during [eventRender]
        var count = 0;
        for (var i=0; i<dataE.length; i++) {

          var id = dataE[i], eID = '#'+id ;

          // now obtain the tooltip & tooltipster variables for each event
          var tipTooltipRef = $(eID).attr('data-tooltip-content');
          var tipTooltipsterContent = $(eID).attr('data-tt-tooltipser');

          console.log("tipTooltipRef:", tipTooltipRef);
          console.log("tipTooltipsterContent:", tipTooltipsterContent);
          console.log("$(eID)", $(eID));

          // append the tooltipster content aquired via tooltipster var
          $(eID).append(tipTooltipsterContent);
          $(eID).tooltipster();
          // set the content pointer
          $(eID).tooltipster('content', tipTooltipRef );


          //TOOLTIPSTER: update any necessary Tooltip content
          $('.fc-event').mouseenter(function() {

            if (tipTooltipRef == '' || tipTooltipRef == 'undefined'){
              $(eID).tooltipster('content', 'Invalid image link 😔');
              // OR simply: $(this).tooltipster('disable');
            } else {
              var t = $(eID).attr('tt_title'), tt_Title = '<div class="ttTitle">'+t+'</div>' ;
              var tt_element = $(eID).find('.tooltipster-content');
              tt_element.append(tt_Title);
              $(eID).tooltipster('option','contentAsHTML','true');
              $(eID).tooltipster('content', tipTooltipRef);

              if (!$(eID).hasClass('tt_added')) {
                $(eID).tooltipster();
                $(eID).tooltipster({
                  content: tipTooltipRef,
                  //contentCloning: true,
                  trigger: 'hover',
                  //multiple: true,
                  animation: 'fade',
                  arrow: true,
                  delay: 300,
                  maxWidth: 600,
                  contentAsHTML: true,
                  //debug: true
                });
              // bind on start events (triggered on mouseenter)
              $(eID).on('start', function(event) {
               if ($(event.instance.elementOrigin()).hasClass('tt_group')){
                    var instances = $.tooltipster.instances('.tt_group'),
                        open = false,
                        duration;
                    $.each(instances, function (i, instance) {
                      if (instance !== event.instance) {
                        // if another instance is already open
                        if (instance.status().open){
                          open = true;
                          // get the current animationDuration
                          duration = instance.option('animationDuration');
                          // close the tooltip without animation
                          instance.option('animationDuration', 0);
                          instance.close();
                       // restore the animationDuration to its normal value
                          instance.option('animationDuration', duration);
                        }
                      }
                    });
                  // if another instance was open
                  if (open) {
                     duration = event.instance.option('animationDuration');
                      // open the tooltip without animation
                      event.instance.option('animationDuration', 0);
                      event.instance.open();
                      // restore the animationDuration to its normal value
                      event.instance.option('animationDuration', duration);
                      // now that we have opened the tooltip,
                      //the hover trigger must be stopped
                      event.stop();
                    }
                  }
                });
                $(eID).addClass('tt_added');
              }
            }
          });

        }

      },

      eventMouseover: function(view, event, element){

        //TOOLTIPSTER: update any necessary Tooltip content
        var tipContent = $(this).attr('data-ttipster');
        var id = event.id //$(this).attr('id');
        var eID = '#'+id;
        var tipID = '#tt_'+id;

        if (tipContent == '' || tipContent == 'undefined'){
          $(eID).tooltipster('content', 'Invalid image 🔗 detected: unable to display at present 😔');
          $(eID).tooltipster('disable');
          // or $(this).tooltipster('destroy');
        } else {
  // TOOLTIPSTER: Not really req now as tipContent is set @ evenRender
          //$(eID).tooltipster('option','contentAsHTML','true');
          $(eID).tooltipster('option','multiple','true');
          $(eID).tooltipster({
            functionInit: function(instance, helper){
              var content = $(helper.origin).find(tipID).detach();
              instance.content(content);
            }
          });
          if (!$(this).hasClass('tt_added')) {
            $(eID).tooltipster();
            $(eID).tooltipster({
              content: tipContent,
              //contentCloning: true,
              trigger: 'hover',
              //multiple: true,
              animation: 'fade',
              arrow: true,
              delay: 300, //[300, 100]
              maxWidth: 600,
              contentAsHTML: true,
              debug: true
            });
            // bind on start events (triggered on mouseenter)
            $(this).on('start', function(event) {
              if ($(event.instance.elementOrigin()).hasClass('tt_group')) {
                var instances = $.tooltipster.instances('.tt_group'),
                    open = false,
                    duration;
                $.each(instances, function (i, instance) {
                  if (instance !== event.instance) {
                    // if another instance is already open
                    if (instance.status().open){
                      open = true;
                      // get the current animationDuration
                      duration = instance.option('animationDuration');
                      // close the tooltip without animation
                      instance.option('animationDuration', 0);
                      instance.close();
                      // restore the animationDuration to its normal value
                      instance.option('animationDuration', duration);
                    }
                  }
                });
                // if another instance was open
                if (open) {
                  duration = event.instance.option('animationDuration');
                  // open the tooltip without animation
                  event.instance.option('animationDuration', 0);
                  event.instance.open();
                  // restore the animationDuration to its normal value
                  event.instance.option('animationDuration', duration);
                  // now that we have opened the tooltip,
                  //the hover trigger must be stopped
                  event.stop();
                }
              }
            });
            $(this).addClass('tt_added');
          }
        }

      },

      eventClick:  function(event, element, view) {
        var e = (event.description);
        if (e != null){
          var chr = e.length;
          // event click coded goes here
          alert(e);
        }
      }

    });
  });
/* tooltipster.js */
.tooltip_templates {
  display: none;
}
.tooltipster-content{
  /*display: flex;
  flex-direction: column;*/
}
.ttTitle {
}

/*! suit-flex-embed v1.4.0 | MIT License | github.com/suitcss */
.FlexEmbed {
  display: block;
  overflow: hidden;
  position: relative;
}
.FlexEmbed:before {
  content: "";
  display: block;
  width: 100%;
}

.FlexEmbed--16by9:before {
  padding-bottom: 56.25%;
}
.FlexEmbed--4by3:before {
  padding-bottom: 75%;
}
.FlexEmbed--1by1:before {
  padding-bottom: 100%;
}
.CoverImage {
  background-position: 50%;
  background-repeat: no-repeat;
  background-size: cover;
  margin: 0 auto 1em;
  max-height: 600px;
  max-width: 600px;
}
.CoverImageX2 {
  background-color: #808080;
  background-position: 50%;
  background-repeat: no-repeat;
  background-size: 100% 100%;  /*cover; contain;*/
  margin: 0 auto 1em;
  max-height: 2400px;
  max-width: 1200px;
}
<!-- Bootstrap -->
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.0/css/bootstrap.min.css" integrity="sha384-SI27wrMjH3ZZ89r4o+fGIJtnzkAnFs3E4qz9DIYioCQ5l9Rd/7UAa8DHcaL8jkWt" crossorigin="anonymous" />
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.0/js/bootstrap.min.js" integrity="sha384-3qaqj0lc6sV/qpzrc1N5DC6i1VRn/HyX4qdPaiEFbn54VjQBEU341pvjz7Dv3n6P" crossorigin="anonymous"></script>

<!-- FulCalendar  -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.10.1/fullcalendar.min.css" integrity="sha256-tXJP+v7nTXzBaEuLtVup1zxWFRV2jyVemY+Ir6/CTQU=" crossorigin="anonymous" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js" integrity="sha256-4iQZ6BVL4qNKlQ27TExEhBN1HFPvAvAMbFavKKosSWQ=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.10.1/fullcalendar.min.js" integrity="sha256-O04jvi1wzlLxXK6xi8spqNTjX8XuHsEOfaBRbbfUbJI=" crossorigin="anonymous"></script>

<!-- Tooltipster -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/tooltipster.main.min.css" integrity="sha256-xlmCQ8IjIIx7gqrIAb5x5kEU30jJJm0/DEmrjgLow/E=" crossorigin="anonymous" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/tooltipster.bundle.min.css" integrity="sha256-Qc4lCfqZWYaHF5hgEOFrYzSIX9Rrxk0NPHRac+08QeQ=" crossorigin="anonymous" />
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/tooltipster.main.min.js" integrity="sha256-9gPC19rdxygnD5cXHFodzczLKeucNZ/dgzLhkKvNtQM=" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/tooltipster.bundle.min.js" integrity="sha256-NOU7KrY2aTI4PxDegqYUIknk9qfxVCS0E4JfE9aMwaA=" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.min.js" integrity="sha256-b9JNfGq08bjI5FVdN3ZhjWBSRsOyF6ucACQwlvgVEU4=" crossorigin="anonymous"></script>

<div id='calendar'></div>

Alternatively if you prefer a fiddle I made earlier: https://jsfiddle.net/magicmb/3manqpho/


OPTIONAL: Additionally bonus question

Another potential issue with our Tooltipster.JS implementation and perhaps I may have to create a separate SO case for this but I will mention it on here quickly anyway as I'm struggling to create a working fiddle for it. What I was trying to do with my second event was to demonstrate another slightly odd Tooltipster behaviour:

Tooltipster seems to have difficulty with events that span several days and different rows in FullCalendar. They only seem to work if hovering on the first part of the event i.e. the bit in row1 and not when hovering over the second part in row2.

Now unfortunately I haven't been able to show this on my test fiddle. Reason being is that it may very well be necessary to create separate events for each day (elements in FullCalendar), even though my single event seems to go to the end of the first row for some reason but not onto the next one. Hence I haven't been able to show the quirky behaviour by creating an event that spans two weeks with a single start and single end date like I've done in my test case.

Our own system uses external data sources and this was never apparent. Also it just works with our previous Tooltip.js implementation and initially I also thought it worked with Tooltipster, but now I'm not convinced. Perhaps this is just an issue with the later versions of Tooltipster [EDIT: Actually due to previously using data-tooltip-content to store entire tooltip] or perhaps they work in a slightly different way from previous releases. In any case if any of you know something about this do feel free to mention it in addition to the main issue above which is getting the HTML tooltip to work using pointers inside data-tooltip-content.


Solution

  • After quite a while contemplating and trying out various things as well as referencing Tootltipster & FullCalendar Docs and numerous Github issues etc I figured out my issues.

    Key step:

    All the tooltips need to be created as part of the FullCalendar [eventRender] process and we do not trigger anything via [eventMouseover]. Also instead of binding the Tooltipster tooltip to event.id you need to associate it with tooltipster class or in my case the .tt_tooltip class due to the "tooltip" name conflict with Bootstrap.

    Another important thing to note is that it only seems we are initialising each of the Tooltipster tooltips during [eventRender] as well as setting the options, the FullCalendar documentation states:

    eventRender
    
    Triggered while an event is being rendered. A hook for modifying its DOM.
    
        function( event, element, view ) { }
    
    event is the Event Object that is attempting to be rendered.
    
    element is a newly created jQuery element that will be used for rendering.
    

    Additional important step:

    We need to carry out a generic Tooltipster tooltip initiate towards the end of doc.ready for all previously created tooltips using the tt_tooltip class: $('.tt_tooltip').tooltipster();

    EDIT: As per my additional comment below this is just an example and in my test / live environment I had to call $('.tt_tooltip').tooltipster(); at the end of [ eventAfterAllRender: ] instead of doc.ready!

    Here's the corrected code sample:

      $(document).ready(function() {
    
        $('#calendar').fullCalendar({
          defaultView: 'month',
          header: '',
          defaultDate: '2020-03-01',
          events: [{
            id: 'event1',
            className: 'GB',
            title: 'Tips for Writing Cleaner Code',
            description: 'I decided to write an article that will be useful for beginners to understand their mistakes and to put together some practices. /n source: hackernoon.com/tips-for-writing-cleaner-better-code-e36ffeb55526',
            start: '2020-03-02', end: '2020-03-02'
          }, {
            id: 'event2',
            className: 'NL',
            title: 'Modern Style of Javascript with Arrow Functions',
            description: 'The complete explanation of Arrow functions in Javascript, and how it helps developers to write flexible and consistent code. /n source: hackernoon.com/modern-style-of-javascript-with-arrow-functions-lg1x3474',
            start: '2020-03-04', end: '2020-03-11'
          }
                  ],
          eventRender: function(event, element, view) {
    
            window.dataE = window.dataE || [];
            element.attr( 'id', event.id );
            var eExist = false;
    
            // create array of event.IDs for use in [eventAfterAllRender]
            if (Array.isArray(dataE)){
              var json = JSON.stringify(event.id),
                  item = dataE.find(el => JSON.stringify(el) === json);
              if (typeof item !== 'undefined'){
                return false;
              } else {
                dataE.push(event.id);
                eExist = true;
              }
            }
    
    
            var /* Vars */
            desc = (event.description), url='',
                urlEvent1 = "https://hackernoon.com/hn-images/1*cmqZiJz1TuUedRoeI3g_Iw.jpeg",
                urlEvent2 = "https://images.unsplash.com/photo-1527427337751-fdca2f128ce5?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEwMDk2Mn0",
                tipRef="tip_content_"+event.id, idTip="#"+tipRef
            /* End Vars */;
    
            // passing 2 extra image params manually - usually from seperate data routine
            if (event.title == 'Tips for Writing Cleaner Code') { url = urlEvent1; } else { url = urlEvent2; }
    
            var tt_ref = '#tt_'+event.id;
            //var tt_ref = 'tt_'+event.id;
            var className = ''+event.className;
            var cc = className.toLowerCase();
            var tt = '<div class="tooltip_templates"><span id="tt_' + event.id + '" class="tooltip_content"><img src="' + url + '" width="350" height="auto"><p style="text-align:left;"><span class="flag-icon flag-icon-' + cc + '"></span> <span class="tt_title"><strong>' + event.title + '</strong></span><br><span class="tt_desc">optional desctiptive text can go here</span></p></span></div>';
    
            element.attr( 'data-tooltip-content', tt_ref ); // the tooltip ref selector (pointer)
            element.attr( 'data-tt-tooltipster' ); element.attr( 'data-tt-tooltipster', tt ); // tootipster content stored for [AfterEventRender]
            element.attr( 'tt_title' ); element.attr( 'tt_title', event.title ); 
            element.addClass('tt_tooltip'); // add the tooltipster class (using 'tt_tooltip' due to Bootstrap 'tooltip' conflict )
            element.append(tt); // append tooltipster content to each fullCalendar element
    
            // Tooltipster - as per Tooltipster Doc use 'tooltip' class (in our case 'tt_tooltip' due to Bootstrap conflict)
            $('.tt_tooltip').tooltipster();
            $('.tt_tooltip').tooltipster({
              content: tt_ref,
              contentCloning: true,
              trigger: 'hover',
              multiple: true,
              animation: 'fade',
              arrow: true,
              delay: 300,
              maxWidth: 600,
              contentAsHTML: true,
              debug: true
            });
    
    
          },
    
          eventAfterAllRender: function(event, element){
    
            // get events from dataE array created during [eventRender]
            var count = 0;
            for (var i=0; i<dataE.length; i++) {
    
              var id = dataE[i], eID = '#'+id ;
              // obtain the tooltip is selector & tooltipster data each event
              var tipTooltipRef = $('.fc-body').find(eID).closest('.tt_tooltip').attr('data-tooltip-content');
              var tipTooltipsterContent = $('.fc-body').find(eID).closest('.tt_tooltip').attr('data-tt-tooltipster');
    
              console.log("tipTooltipRef:", tipTooltipRef);
              console.log("tipTooltipsterContent:", tipTooltipsterContent);
    
    
              // update / disable Tooltipster content if necessary
              if (tipTooltipRef == '' || tipTooltipRef == 'undefined'){
                $('.fc-body').find(eID).closest('.tt_tooltip').tooltipster('content', 'Invalid image link 😔');
              }
    
    
            }
          },
    
          eventMouseover: function(view, event, element){
            //Not requried for Tooltipster Tooltips - handled by Tooltipster option (trigger: 'hover') 
          },
    
          eventClick:  function(event, element, view) {
            var e = (event.description);
            if (e != null){
              var chr = e.length;
              // event click coded goes here
              alert(e);
            }
          }
    
        //end: fullCalendar
        });
    
        // Even though tooltips are created during [eventRender] the generic class gets initiated here. 
        $('.tt_tooltip').tooltipster(); //{contentCloning: true}
    
      //end: doc.ready
      });
    /* tooltipster.js */
    .tooltip_templates {
      display: none;
    }
    .tooltipster-content{
      /*display: flex;
      flex-direction: column;*/
    }
    .ttTitle {
    
    }
    
    
    
    /*! suit-flex-embed v1.4.0 | MIT License | github.com/suitcss */
    .FlexEmbed {
      display: block;
      overflow: hidden;
      position: relative;
    }
    .FlexEmbed:before {
      content: "";
      display: block;
      width: 100%;
    }
    
    .FlexEmbed--16by9:before {
      padding-bottom: 56.25%;
    }
    .FlexEmbed--4by3:before {
      padding-bottom: 75%;
    }
    .FlexEmbed--1by1:before {
      padding-bottom: 100%;
    }
    .CoverImage {
      background-position: 50%;
      background-repeat: no-repeat;
      background-size: cover;
      margin: 0 auto 1em;
      max-height: 600px;
      max-width: 600px;
    }
    .CoverImageX2 {
      background-color: #808080;
      background-position: 50%;
      background-repeat: no-repeat;
      background-size: 100% 100%;  /*cover; contain;*/
      margin: 0 auto 1em;
      max-height: 2400px;
      max-width: 1200px;
    }
    <!-- Bootstrap -->
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.0/css/bootstrap.min.css" integrity="sha384-SI27wrMjH3ZZ89r4o+fGIJtnzkAnFs3E4qz9DIYioCQ5l9Rd/7UAa8DHcaL8jkWt" crossorigin="anonymous" />
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.0/js/bootstrap.min.js" integrity="sha384-3qaqj0lc6sV/qpzrc1N5DC6i1VRn/HyX4qdPaiEFbn54VjQBEU341pvjz7Dv3n6P" crossorigin="anonymous"></script>
    
    <!-- FulCalendar  -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.10.1/fullcalendar.min.css" integrity="sha256-tXJP+v7nTXzBaEuLtVup1zxWFRV2jyVemY+Ir6/CTQU=" crossorigin="anonymous" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js" integrity="sha256-4iQZ6BVL4qNKlQ27TExEhBN1HFPvAvAMbFavKKosSWQ=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.10.1/fullcalendar.min.js" integrity="sha256-O04jvi1wzlLxXK6xi8spqNTjX8XuHsEOfaBRbbfUbJI=" crossorigin="anonymous"></script>
    
    <!-- Tooltipster -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/tooltipster.main.min.css" integrity="sha256-xlmCQ8IjIIx7gqrIAb5x5kEU30jJJm0/DEmrjgLow/E=" crossorigin="anonymous" />
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/tooltipster.bundle.min.css" integrity="sha256-Qc4lCfqZWYaHF5hgEOFrYzSIX9Rrxk0NPHRac+08QeQ=" crossorigin="anonymous" />
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/tooltipster.main.min.js" integrity="sha256-9gPC19rdxygnD5cXHFodzczLKeucNZ/dgzLhkKvNtQM=" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/tooltipster.bundle.min.js" integrity="sha256-NOU7KrY2aTI4PxDegqYUIknk9qfxVCS0E4JfE9aMwaA=" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.min.js" integrity="sha256-b9JNfGq08bjI5FVdN3ZhjWBSRsOyF6ucACQwlvgVEU4=" crossorigin="anonymous"></script>
    
    <div id='calendar'></div>