timeline/js/timeline.js
author veltr
Fri, 11 Oct 2013 11:57:20 +0200
changeset 105 fe4b70b9991d
parent 104 3fef455d214a
child 106 574bb047a940
permissions -rw-r--r--
Backoffice timeline as default

/*
 * Main Timeline code
 */

window.Tlns = {
    Utils : {},
    Defaults : {},
    Templates : {},
    Classes : {}
};

/* Utility Functions */

Tlns.Utils.zeroPad = function(_n) {
    return (_n < 10 ? "0" : "") + _n;
};

Tlns.Utils.SetDefaults = function(_object, _defaults, _options) {
    var _options = _options || {};
    _(_defaults).each(function(_v, _k) {
        if(/^m(in|ax)_/.test(_k)) {
            var _tab = _k.split('_');
            if( typeof _object[_tab[1]] !== "undefined") {
                var _fn = Math[_tab[0] === "max" ? "min":"max"];
                _object[_tab[1]] = _fn(_object[_tab[1]], _v);
            }
        } else {
            if( typeof _options[_k] !== "undefined") {
                _object[_k] = _options[_k];
            } else {
                _object[_k] = _v;
            }
        }
    });
};

Tlns.Utils.dateFormat = function(_date, _template) {
    if (typeof _date !== "object") {
        _date = new Date(parseInt(_date));
    }
    var _params = {
        hours: _date.getHours(),
        isDayStart: !_date.getHours(),
        "0hours": Tlns.Utils.zeroPad(_date.getHours()),
        minutes: _date.getMinutes(),
        "0minutes": Tlns.Utils.zeroPad(_date.getMinutes()),
        seconds: _date.getSeconds(),
        "0seconds": Tlns.Utils.zeroPad(_date.getSeconds()),
        dayOfWeek: ["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"][_date.getDay()],
        shortDayOfWeek: ["Dim","Lun","Mar","Mer","Jeu","Ven","Sam"][_date.getDay()],
        dayOfMonth: _date.getDate(),
        "0dayOfMonth": Tlns.Utils.zeroPad(_date.getDate()),
        monthNumber: 1+_date.getMonth(),
        "0monthNumber": Tlns.Utils.zeroPad(1+_date.getMonth()),
        monthName: ["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"][_date.getMonth()],
        shortMonthName: ["jan.","fev.","mar.","avr.","mai","jun.","jul.","aou.","sep.","oct.","nov.","dec."][_date.getMonth()],
        year: _date.getFullYear()
    };
    return Mustache.to_html(_template, _params);
};

Tlns.Utils.guid = function() {
    return 'xxxx-xxxx-xxxx-xxxx'.replace(/x/g,function() {
        return Math.floor(Math.random()*16).toString(16);
    });
};

Tlns.Utils.timeFieldProcess = function(_val) {
    var _h = 0,
        _m = 0,
        _matches = _val.match(/(\d+)/g);
    if (_matches && _matches.length) {
        _h = Math.min(23, +(_matches[0]));
        if (_matches.length > 1) {
            _m = Math.min(59, +(_matches[1]));
        }
    }
    return {
        hours: _h,
        minutes: _m,
        text: Tlns.Utils.zeroPad(_h) + ':' + Tlns.Utils.zeroPad(_m)
    };
};

Tlns.Utils.dateFieldProcess = function(_val) {
    var _now = new Date(),
        _y = _now.getFullYear(),
        _m = 1 + _now.getMonth(),
        _d = _now.getDate(),
        _matches = _val.match(/(\d+)/g);
    if (_matches && _matches.length) {
        _d = Math.min(31, +(_matches[0]));
        if (_matches.length > 1) {
            _m = Math.min(12, +(_matches[1]));
        }
        if (_matches.length > 2) {
            _y = parseInt(_matches[2]);
            if (_y < 2000) {
                _y += 2000;
            }
            _y = Math.min(2020, Math.max(2000, _y));
        }
    }
    return {
        year: _y,
        month: _m,
        date: _d,
        text: Tlns.Utils.zeroPad(_d) + '/' + Tlns.Utils.zeroPad(_m) + '/' + _y
    };
};

/* Defaults */

/* TOUTE LA CONFIGURATION DE L'APPLI SE FAIT ICI */

Tlns.Defaults.Timeline = {
    email: "",
    token: "",
    container : "timeline",
    width : 790,
    height : 225,
    min_width : 400,
    min_height : 100,
    main_width : 726,
    linelabels : [
        "RDV",
        "Actu",
        "Appels",
        "Persos"
    ],
    picto_url: "img/",
    timescales : [{
        label : "Semaine",
        span : 7 * 86400 * 1000,
        grid_interval : 86400 * 1000,
        grid_date_format : '{{dayOfMonth}} {{monthName}}',
        max_importance : 0
    }, {
        label : "3 jours",
        span : 3 * 86400 * 1000,
        grid_interval : 6 * 3600 * 1000,
        grid_date_format : '{{^isDayStart}}{{0hours}}h{{0minutes}}{{/isDayStart}}{{#isDayStart}}{{dayOfMonth}} {{shortMonthName}}{{/isDayStart}}',
        max_importance : 1
    }, {
        label : "Journée",
        span : 86400 * 1000,
        grid_interval : 2 * 3600 * 1000,
        grid_date_format : '{{^isDayStart}}{{0hours}}h{{0minutes}}{{/isDayStart}}{{#isDayStart}}{{dayOfMonth}} {{shortMonthName}}{{/isDayStart}}',
        max_importance : 2
    }, {
        label : "Demi-Journée",
        span : 6 * 3600 * 1000,
        grid_interval : 3600 * 1000,
        grid_date_format : '{{^isDayStart}}{{0hours}}h{{0minutes}}{{/isDayStart}}{{#isDayStart}}{{dayOfMonth}} {{shortMonthName}}{{/isDayStart}}',
        max_importance : 3
    }],
    level: 0,
    central_time: 0,
    sync_now: true,
    contents_endpoint: "http://anarchy2.solicis.fr/api/cms/content?method=searchForTimelineEdito",
    get_favorite_endpoint: "http://anarchy2.solicis.fr/stream/timeline/favorites",
    set_favorite_endpoint: "http://anarchy2.solicis.fr/stream/timeline/favorite",
    contribution_endpoint: "http://anarchy2.solicis.fr/stream/timeline/contribs",
    use_auth: true,
    occurrences: [],
    grid_spacing: 12,
    tooltip_date_format: '{{dayOfMonth}} {{monthName}} {{year}} à {{0hours}}:{{0minutes}}',
    class_info: {
        "Cms\\Previously": {
            label: "Précédemment",
            univers_id: 0,
            picto: "previously.png",
            show: true
        },
        "Cms\\Chapter": {
            label: "Roman",
            univers_id: 0,
            picto: "roman.png",
            show: true
        },
        "Cms\\FlashTrash": {
            label: "JT",
            univers_id: 0,
            picto: "flash.png",
            show: true
        },
        "Cms\\Article": {
            label: "Article",
            univers_id: 1,
            show: true
        },
        "Cms\\Poll": {
            label: "Sondage",
            univers_id: 2,
            picto: "sondage.png",
            show: true
        },
        "Cms\\CallWitness": {
            label: "Appel à Témoins",
            univers_id: 2,
            picto: "temoignage.png",
            show: true
        },
        "Cms\\Rule": {
            label: "Règle du jour",
            univers_id: 2,
            picto: "regle.png",
            show: true
        },
        "Cms\\CallCharacter": {
            label: "Appel à Personnage",
            univers_id: 3,
            show: true
        },
        "Cms\\SweetCadaver": {
            label: "Cadavre exquis",
            univers_id: 3,
            show: true
        }
    },
    maxtime: false,
    url_base: "http://anarchy2.solicis.fr",
    user_id: false,
    use_jsonp: false
};

for (var _i = 0; _i < Tlns.Defaults.Timeline.timescales.length; _i++) {
    Tlns.Defaults.Timeline.timescales[_i].level = _i;
}

/* Templates */

Tlns.Templates.Timeline = '<div class="Tl-Main"><div class="Tl-Grid"></div><div class="Tl-TopBar"></div>'
    + '<div class="Tl-BottomPart"><div class="Tl-AnotherGroup"><ul class="Tl-UniversLabels"></ul>'
    + '<div class="Tl-MainPart"><div class="Tl-Occurrences"></div>'
    + '</div></div><div class="Tl-Slider-Container"><a class="Tl-Slider-Zoom-In" href="#"></a><div class="Tl-Slider"></div><a class="Tl-Slider-Zoom-Out" href="#"></a></div>'
    + '<div class="Tl-Overlay-Container"><div class="Tl-Overlay-Box"><div class="Tl-Overlay"><div class="Tl-Overlay-Main"></div></div></div></div></div></div>'
    + '<div class="Tl-Details"></div>';
    
Tlns.Templates.Univers = '<div class="Tl-UniversText">{{title}}</div>';

Tlns.Templates.Occurrence =
    '{{#occurrences}}<div class="Tl-Occurrence Tl-OccOnGrid Tl-Occ{{type}}{{#editing}} Tl-Editing{{/editing}}" occurrence-id="{{id}}" style="left: {{x}}px; top: {{y}}px;">'
    + '{{#image}}<img src="{{image}}" />{{/image}}{{#isFavorite}}<div class="Tl-Occurrence-Favorite"></div>{{/isFavorite}}'
    + '{{#participationCount}}<div class="Tl-Occurrence-Participation">{{participationCount}}</div>{{/participationCount}}</div>{{/occurrences}}';

Tlns.Templates.OccurrenceTooltip = '<h3 class="Tl-Tooltip-Title">{{title}}</h3>';

Tlns.Templates.OccurrenceDetails = 
    '<div class="Tl-Detail"><div class="Tl-Detail-X"></div><div class="Tl-Detail-Favorite{{#isFavorite}} Tl-Detail-isFavorite{{/isFavorite}}"></div>'
    + '<div class="Tl-Detail-Image-Wrapper"><img class="Tl-Detail-Image" src="{{detail_image}}" />'
    + '{{#participationCount}}<div class="Tl-Detail-Participation">{{participationCount}}<span class="Tl-Participation-Icon"></span></div>{{/participationCount}}</div>'
    + '<h2 class="Tl-Detail-Title">{{title}}</h2><p class="Tl-Detail-Description">{{detail_description}}</p>'
    + '<div class="Tl-Detail-Bottom"><span class="Tl-Detail-Date">Publié le {{formatted_date}}</span><a class="Tl-Detail-Read" href="{{url}}" target="_blank">Lire la suite</a></div></div>';

/* Classes */

Tlns.Classes.Timeline = function(_options) {

    /* Setting Defaults */
    Tlns.Utils.SetDefaults(this, Tlns.Defaults.Timeline, _options);

    /* Setting container CSS */
    this.$ = $('#' + this.container).html(Mustache.to_html(Tlns.Templates.Timeline, this));
    
    this.$.find('.Tl-Main').css({
        width : this.width + "px",
        height : this.height + "px"
    });
    this.top_height = this.$.find('.Tl-TopBar').outerHeight();
    this.main_height = this.height - this.top_height;
    //this.main_height = this.height - 27;
    var labelsWidth = this.$.find('.Tl-UniversLabels').width();
    this.main_width = this.width - labelsWidth - this.$.find('.Tl-Slider-Container').width();
    this.$.find('.Tl-BottomPart').css("height", this.main_height + "px");
    this.$.find('.Tl-MainPart').css("width", this.main_width + "px");
    this.$.find('.Tl-Grid').css({
        "left": labelsWidth + "px",
        "width": this.main_width + "px"
    });
    this.$.find('.Tl-Overlay-Container').css("left", (this.$.find('.Tl-BottomPart').outerWidth() - this.main_width) + "px");
    this.$slider = this.$.find('.Tl-Slider');
    
    var $mainpart = this.$.find('.Tl-MainPart'),
        _o = $mainpart.offset();
    this.dragging_bounds = {
        left: _o.left,
        top: _o.top,
        right: _o.left + $mainpart.outerWidth(),
        bottom: _o.top + $mainpart.outerHeight(),
    };
    
    var _this = this;
    
    this.throttledDrawGrid = _.throttle(function() {
        _this.drawGrid();
    }, 150);
    
    var $scrollgroup = this.$.find('.Tl-AnotherGroup');
     
    $scrollgroup.mousedown(function(_event) {
        _this.onMouseDown(_event);
        return false;
    });
    
    $scrollgroup.mousemove(function(_event) {
        _this.onMouseMove(_event);
        return false;
    });
    
    $scrollgroup.mouseup(function(_event) {
        _this.onMouseUp(_event);
        return false;
    });
    
    $scrollgroup.mousewheel(function(_event, _delta) {
        var _newLevel = Math.max(0,Math.min(_this.timescales.length-1, (_delta < 0 ? -1 : 1) + parseInt(_this.level)));
        if (_newLevel != _this.level) {
            _this.hideTooltip();
            var _deltaX = _event.pageX - _this.dragging_bounds.left,
                _tAtMouse = _this.timeFromMouse(_event.pageX),
                _newScale = _this.main_width / (_this.timescales[_newLevel].span),
                _newStart = _tAtMouse - _deltaX / _newScale,
                _newTime = _newStart + _this.timescales[_newLevel].span / 2;
            _this.central_time = _this.maxtime ? Math.min(_newTime, _this.maxtime) : _newTime;
            _this.setLevel(_newLevel);
        }
        return false;
    });
    
    this.$.find('.Tl-Overlay-Box').mouseover(function(_event) {
        $(this).show();
    }).mouseout(function(_event) {
        $(this).hide();
    });
    
    this.$slider.slider({
        orientation: "vertical",
        min: 0,
        max: this.timescales.length - 1,
        value: this.level,
        slide: function(e, ui) {
            _this.setLevel(ui.value);
        }
    });
    this.$.find('.Tl-Slider-Container').mousewheel(function(_event, _delta) {
        var _newLevel = Math.max(0,Math.min(_this.timescales.length-1, (_delta < 0 ? -1 : 1) + parseInt(_this.level)));
        if (_newLevel != _this.level) {
            _this.hideTooltip();
            _this.setLevel(_newLevel);
        }
        return false;
    });
    
    $(".Tl-Slider-Zoom-In").click(function() {
        _this.setLevel(Math.min(_this.timescales.length-1,parseInt(_this.level)+1));
        return false;
    });
    $(".Tl-Slider-Zoom-Out").click(function() {
        _this.setLevel(Math.max(0,parseInt(_this.level)-1));
        return false;
    });
    
    this.setLevel(this.level);
       
    this.onUniversLoaded(this.linelabels);
    
    this.favoriteContents = [];
    this.participationCounts = [];
    
    $.getJSON(
        this.jsonpify(this.get_favorite_endpoint),
        {
            uid: this.user_id || undefined,
            api_key: _this.use_auth ? _this.token : undefined,
            mail: _this.use_auth ? _this.email : undefined
        },
        function(d) {
            _this.favoriteContents = _(d.data).map(function(f) {
                return f.id;
            });
            _(_this.favoriteContents).each(function(f) {
                var o = _this.getOccurrence(f);
                if (o) {
                    o.isFavorite = true;
                }
            });
        }
    );
    
    $.getJSON(
        this.jsonpify(this.contribution_endpoint),
        {
            api_key: _this.use_auth ? _this.token : undefined,
            mail: _this.use_auth ? _this.email : undefined
        },
        function(d) {
            _(d.data).each(function(c) {
                _this.participationCounts[c.id] = c.nb;
                var o = _this.getOccurrence(c.id);
                if (o) {
                    o.participationCount = c.nb;
                }
            });
        }
    );
    
};

Tlns.Classes.Timeline.prototype.onMouseDown = function(_event) {
    this.mouse_down = true;
    this.is_dragging = false;
    this.start_pos = {
        x: _event.pageX,
        y: _event.pageY
    };
    this.time_at_start = this.central_time;
};

Tlns.Classes.Timeline.prototype.onMouseUp = function(_event) {
    this.mouse_down = false;
    this.is_dragging = false;
};

Tlns.Classes.Timeline.prototype.timeFromX = function(_x) {
    return this.start_time + _x / this.current_scale;
};

Tlns.Classes.Timeline.prototype.timeFromMouse = function(_pageX) {
    return this.timeFromX(_pageX - this.dragging_bounds.left);
};

Tlns.Classes.Timeline.prototype.universFromY = function(_y) {
    return Math.max(0,Math.min(this.univers.length, Math.floor(_y / this.univers_height)));
};

Tlns.Classes.Timeline.prototype.universFromMouse = function(_pageY) {
    return this.universFromY(_pageY - this.dragging_bounds.top);
};

Tlns.Classes.Timeline.prototype.onMouseMove = function(_event) {
    if (this.mouse_down && !this.is_dragging) {
        var _dx = this.start_pos.x - _event.pageX,
            _dy = this.start_pos.y - _event.pageY,
            _sqd = _dx * _dx + _dy * _dy;
        if (_sqd > 16) {
            this.is_dragging = true;
        }
    }
    if (this.is_dragging) {
        this.hideTooltip();
        this.setTime(this.time_at_start + Math.floor(( this.start_pos.x - _event.pageX ) / this.current_scale));
    }
};

Tlns.Classes.Timeline.prototype.onUniversLoaded = function(_data) {
    this.univers = [];
    if(_data.length) {
        this.univers_height = Math.floor(this.main_height / _data.length);
    }
    for(var _i = 0; _i < _data.length; _i++) {
        this.univers.push(new Tlns.Classes.Univers(_data[_i], this, _i));
    }
    
    this.loadOccurrences();
};

Tlns.Classes.Timeline.prototype.offsetTime = function(_timeOffset) {
    this.setTime(this.central_time + _timeOffset);
};

Tlns.Classes.Timeline.prototype.setTime = function(_centralTime) {
    this.sync_now = false;
    this.central_time = this.maxtime ? Math.min(_centralTime, this.maxtime) : _centralTime;
    this.changeSpan();
};

Tlns.Classes.Timeline.prototype.setLevel = function(_level) {
    if (_level >= 0 && _level < this.timescales.length) {
        this.level = _level;
        this.$slider.slider("value", _level);
        this.changeSpan();
    }
};

Tlns.Classes.Timeline.prototype.changeSpan = function() {
    var _now = new Date().valueOf();
    if (this.sync_now) {
        this.central_time = _now;
    }
    var _timescale = this.timescales[this.level];
    this.current_scale = this.main_width / (_timescale.span);
    this.start_time = this.central_time - (_timescale.span / 2);
    this.end_time = this.central_time + (_timescale.span / 2);
    this.throttledDrawGrid();
};

Tlns.Classes.Timeline.prototype.drawGrid = function() {
    var _now = new Date().valueOf(),
        _timescale = this.timescales[this.level],
        _offset = new Date().getTimezoneOffset() * 60000,
        _grid_width = Math.floor(_timescale.grid_interval * this.current_scale),
        _roundstart = Math.floor((this.start_time - _offset) / _timescale.grid_interval) * _timescale.grid_interval + _offset,
        _html = '';
    for (var _t = _roundstart; _t < this.end_time; _t += _timescale.grid_interval) {
        var _x = this.current_scale * (_t - this.start_time),
            isMajor = !((_t - _offset)%(24*60*60*1000));
        if (_x > 0) {
            _html += '<div class="Tl-Grid-Column' + (isMajor ? ' Tl-Grid-Major':'') + '" style="width:' + _grid_width + 'px; left: ' + _x + 'px">'
            + '<div class="Tl-Grid-Label">' + Tlns.Utils.dateFormat(_t, _timescale.grid_date_format) + '</div></div>';
        }
    }
    if (this.start_time <= _now && this.end_time >= _now) {
        _html += '<div class="Tl-Grid-Now" style="left: ' + this.current_scale * (_now - this.start_time) + 'px"></div>';
    }
    if (this.editing_occurrence && this.editing_occurrence.date <= this.end_time && this.editing_occurrence.date >= this.start_time) {
        _html += '<div class="Tl-Grid-Editing" style="left: ' + this.editing_occurrence.x + 'px"></div>';
    }
    this.$.find('.Tl-Grid').html(_html);
    this.drawOccurrences();
};

Tlns.Classes.Timeline.prototype.jsonpify = function(url) {
    if (this.use_jsonp) {
        return url + (/\?/.test(url) ? "&" : "?" ) + "callback=?";
    } else {
        return url;
    }
};

Tlns.Classes.Timeline.prototype.loadOccurrences = function() {
    var _this = this;
    
    function getData(cursor) {
        $.getJSON(_this.jsonpify(_this.contents_endpoint), {
            api_key: _this.use_auth ? _this.token : undefined,
            mail: _this.use_auth ? _this.email : undefined,
            cursor: cursor
        }, function(_data) {
            _this.onOccurrencesLoaded(_data);
            if (_data.cursor && _data.cursor.hasNext) {
                getData(_data.cursor.next);
            }
        });
    }
    
    getData();
    
};

Tlns.Classes.Timeline.prototype.onOccurrencesLoaded = function(_data) {
    for (var _i = 0; _i < _data.data.length; _i++) {
        this.createOrUpdateOccurrence(_data.data[_i]);
    }
    if (!this.mouse_down) {
        this.drawOccurrences();
    }
};

Tlns.Classes.Timeline.prototype.deleteOccurrence = function(_id) {
    this.occurrences = _(this.occurrences).reject(function(_occ) {
        return _occ.id == _id;
    });
};

Tlns.Classes.Timeline.prototype.getOccurrence = function(_id) {
    return _(this.occurrences).find(function(_occ) {
        return _occ.id == _id;
    });
};

Tlns.Classes.Timeline.prototype.createOrUpdateOccurrence = function(_data) {
    var _id = _data.id,
        _occurrence = this.getOccurrence(_id),
        typeinfo = this.class_info[_data.__CLASS__];
    if (typeinfo && typeinfo.show) {
        if (typeof _occurrence === "undefined") {
            _occurrence = new Tlns.Classes.Occurrence(this);
            this.occurrences.push(_occurrence);
        }
        _occurrence.update(_data);
    }
    return _occurrence;
};

Tlns.Classes.Timeline.prototype.showTooltip = function(_x, _y, _html) {
    this.$.find('.Tl-Overlay-Box')
        .show()
        .css({
            left: _x + "px",
            top: _y + "px"
        });
    this.$.find('.Tl-Overlay-Main').html(_html);
    
};

Tlns.Classes.Timeline.prototype.hideTooltip = function() {
    this.$.find('.Tl-Overlay-Box').hide();
};

Tlns.Classes.Timeline.prototype.drawOccurrences = function() {
    var _this = this;
    _(this.occurrences).each(function(_occ) {
        _occ.x = _this.current_scale * (_occ.date - _this.start_time);
        _occ.y = _occ.univers.y;
    });
    var minT = this.timeFromX(-32),
        maxI = this.timescales[this.level].max_importance;
    var _visible = _(this.occurrences).filter(function(_occ) {
        _occ.visible = (_occ.date >= minT && _occ.date <= _this.end_time && (_occ.importance <= maxI));
        return _occ.visible;
    });
    
    /* FILTRAGE SI TROP D'OCCURRENCES PAR UNITE DE TEMPS */
    /* Commenter la partie ci-dessous pour les tests */
    
    var _timescale = this.timescales[this.level],
        _offset = new Date().getTimezoneOffset() * 60000,
        _grid_width = Math.floor(_timescale.grid_interval * this.current_scale),
        _roundstart = Math.floor((this.start_time - _offset) / _timescale.grid_interval) * _timescale.grid_interval + _offset,
        _html = '';
    for (var _t = _roundstart; _t < this.end_time; _t += _timescale.grid_interval) {
        var items = _(_visible).filter(function(_occ) {
            return _occ.date >= _t && _occ.date < _t + _timescale.grid_interval;
        });
        if (items && items.length > 1) {
            _(items).chain().rest().each(function(_occ) {
                _occ.visible = false;
            });
        }
    }
    var _visible = _(_visible).filter(function(_occ) { return _occ.visible; });
    
    /* FIN FILTRAGE */
    
    /* REORGANISATION DES PICTOS SI TROP NOMBREUX */
    
    var _moved = true, l = 0;
    while (_moved && l < 10) {
        l++;
        _moved = false;
        for (var _i = 0; _i < _visible.length; _i++) {
            for (var _j = 0; _j < _i; _j++) {
                var delta = Math.abs(_visible[_j].x-_visible[_i].x);
                if (_visible[_j].univers_id == _visible[_i].univers_id
                    && delta < this.grid_spacing
                ) {
                    var sign = _visible[_i].x < _visible[_j].x ? 1 : -1,
                        add = sign * (this.grid_spacing - delta) / 2;
                    _moved = true;
                    _visible[_i].x -= add;
                    _visible[_j].x += add;
                }
            }
        }
    }
    
    /* FIN REORGANISATION */
    
    var _html = Mustache.to_html(Tlns.Templates.Occurrence, {
        occurrences: _visible
    });
    this.$.find('.Tl-Occurrences').html(_html);
        
    this.$.find('.Tl-Occurrence').mousedown(function() { // Clic sur un contenu
        var _el = $(this),
            _id = _el.attr("occurrence-id");
        if (typeof _id !== "undefined") {
            _this.editing_occurrence = _this.getOccurrence(_id);
            if (!_this.editing_occurrence.editing) {
                _(_this.occurrences).each(function(_occ) {
                    _occ.editing = false;
                });
                _this.editing_occurrence.editing = true;
                _this.$.find(".Tl-Details").html(Mustache.to_html(Tlns.Templates.OccurrenceDetails, _this.editing_occurrence));
                _this.$.find(".Tl-Detail-Favorite").click(function() {
                    _this.editing_occurrence.toggleFavorite();
                }).hover(function() {
                    $(this)[_this.editing_occurrence.isFavorite ? "removeClass" : "addClass" ]("Tl-Detail-isFavorite");
                }, function() {
                    $(this)[_this.editing_occurrence.isFavorite ? "addClass" : "removeClass"]("Tl-Detail-isFavorite");
                });
            }
            _this.throttledDrawGrid();
        }
    }).mouseover(function(_event) { // Hover sur un contenu
        var _el = $(this),
            _id = _el.attr("occurrence-id");
        if (typeof _id !== "undefined") {
            var _occurrence = _this.getOccurrence(_id);
            if (!_this.is_dragging) {
                var _html = Mustache.to_html(Tlns.Templates.OccurrenceTooltip, _occurrence);
                _this.showTooltip(_occurrence.x, _occurrence.y, _html);
            }
        }
    }).mouseout(function() {
        var _el = $(this),
            _id = _el.attr("occurrence-id");
        if (typeof _id !== "undefined") {
            var _occurrence = _this.getOccurrence(_id);
            _this.hideTooltip();
        }
    });
    
    if (this.editing_occurrence) {
        $(".Tl-Grid-Editing, .Tl-Detail-X").css("left", this.editing_occurrence.x);
        if (this.editing_occurrence.date > this.end_time || this.editing_occurrence.date < this.start_time) {
            $(".Tl-Detail-X").hide();
        } else {
            $(".Tl-Detail-X").show();
        }
    }
    
};

Tlns.Classes.Timeline.prototype.getUnivers = function(_id) {
    return _(this.univers).find(function(_univ) {
        return (_univ.id == _id);
    });
};

/*
 * Univers
 */

Tlns.Classes.Univers = function(_data, _timeline, _index) {
    this.id = this.index = _index;
    this.title = _data;
//    this.mainCharacter = _data.personnage;
    this.y = (_timeline.univers_height * _index);

    this.$label = $('<li>').css({
        height : _timeline.univers_height + "px"
    }).html(Mustache.to_html(Tlns.Templates.Univers, this));
    
    _timeline.$.find('.Tl-UniversLabels').append(this.$label);
    
    var txtdiv = this.$label.find(".Tl-UniversText");
    txtdiv.css("margin-top", Math.floor((_timeline.univers_height - txtdiv.height()) / 2));
};

/*
 * Occurrence
 */

Tlns.Classes.Occurrence = function(_timeline) {
    this.timeline = _timeline;
};

Tlns.Classes.Occurrence.prototype.update = function(_data) {
    
    /* Récupération des propriétés du JSON */
    
    this.original_data = _data;
    this.id = _data.id;
    this.date = new Date(1000 * (_data.dateFirstPublication || _data.dateCreate) || Date.now);
    this.formatted_date = Tlns.Utils.dateFormat(this.date,this.timeline.tooltip_date_format);
    this.title = _data.title;
    this.type = _data.__CLASS__;
    this.importance = _data.importance;
    var typeinfo = this.timeline.class_info[_data.__CLASS__];
    this.univers_id = typeinfo.univers_id;
    var media = _(_data.contentHasMedias).find(function(m) {
        return !!m.media.carre;
    });
    if (media) {
        this.image = media.media.carre.replace(/carre\/[\d]+\/[\d]+/,'carre/32/32');
        this.detail_image = media.media.carre.replace(/carre\/[\d]+\/[\d]+/,'carre/135/135');
    }
    if (typeinfo.picto) {
        this.image = this.timeline.picto_url + typeinfo.picto;
    }
    var taxonomy = _(_data.contentHasTaxonomys).find(function(t) {
        return !!t.taxonomy.taxonomyHasMedias;
    });
    if (taxonomy) {
        var taxonomyMedia = _(taxonomy.taxonomy.taxonomyHasMedias).find(function(m) {
            return !!m.media.carre;
        });
        if (taxonomyMedia) {
            this.image = taxonomyMedia.media.carre.replace(/carre\/[\d]+\/[\d]+/,'carre/32/32');
        }
    }
    this.univers = this.timeline.univers[this.univers_id];
    this.format = typeinfo.label;
    
    if (this.timeline.favoriteContents.indexOf(this.id) !== -1) {
        this.isFavorite = true;
    }
    this.participationCount = this.timeline.participationCounts[this.id];
    /* End Temporary Data */
    
    var _tmp = $('<p>').html(_data.resume || "");
    var trimmedDesc = _tmp.text().trim().replace(/(\n|\r|\r\n)/mg,' ');
    this.description = trimmedDesc.replace(/(^.{60,80})[\s].+$/m,'$1&hellip;');
    this.detail_description = trimmedDesc.replace(/(^.{360,380})[\s].+$/m,'$1&hellip;');
    this.url = this.timeline.url_base + _data.url;
};

Tlns.Classes.Occurrence.prototype.toString = function() {
    return "Occurrence " + this.type + ': "' + this.title + '"';
};

Tlns.Classes.Occurrence.prototype.toggleFavorite = function() {
    var newFavStatus = !this.isFavorite;
    
    $.ajax({
        type: "POST",
        url: this.timeline.set_favorite_endpoint,
        data: {
            type: this.type,
            conId: this.id,
            status: +newFavStatus,
            uid: this.timeline.user_id || undefined
        },
        success: function(r) {
            console.log(r);
            _this.isFavorite = newFavStatus;
            _this.timeline.throttledDrawGrid();
        }
    });
    
};