web/res/metadataplayer/LdtPlayer-core.js
changeset 1304 10974bff4dae
parent 1198 ff4b567d51f2
child 1308 ef42d4f12cfc
--- a/web/res/metadataplayer/LdtPlayer-core.js	Fri Dec 11 18:11:13 2015 +0100
+++ b/web/res/metadataplayer/LdtPlayer-core.js	Tue Dec 29 13:25:14 2015 +0100
@@ -13,7 +13,7 @@
                                            |_|            |___/         
 
  *  Copyright 2010-2012 Institut de recherche et d'innovation 
- *	contributor(s) : Karim Hamidou, Samuel Huron, Raphael Velt, Thibaut Cavalie
+ *	contributor(s) : Karim Hamidou, Samuel Huron, Raphael Velt, Thibaut Cavalie, Yves-Marie Haussonne, Nicolas Durand, Olivier Aubert
  *	 
  *	contact@iri.centrepompidou.fr
  *	http://www.iri.centrepompidou.fr 
@@ -28,6 +28,7 @@
  *	The fact that you are presently reading this means that you have had
  *	knowledge of the CeCILL-C license and that you accept its terms.
 */
+// Metadataplayer - version 0.2
 /* Initialization of the namespace */
 
 if (typeof window.IriSP === "undefined") {
@@ -82,7 +83,7 @@
     var list = [],
         positions = [],
         text = _text.replace(/(^\s+|\s+$)/g,'');
-    
+
     function addToList(_rx, _startHtml, _endHtml) {
         while(true) {
             var result = _rx.exec(text);
@@ -101,11 +102,11 @@
             positions.push(end);
         }
     }
-    
+
     if (_regexp) {
         addToList(_regexp, '<span class="Ldt-Highlight">', '</span>');
     }
-    
+
     addToList(/(https?:\/\/)?[\w\d\-]+\.[\w\d\-]+\S+/gm, function(matches) {
         return '<a href="' + (matches[1] ? '' : 'http://') + matches[0] + '" target="_blank">';
     }, '</a>');
@@ -114,19 +115,19 @@
     }, '</a>');
     addToList(/\*[^*]+\*/gm, '<b>', '</b>');
     addToList(/[\n\r]+/gm, '', '<br />');
-    
+
     IriSP._(_extend).each(function(x) {
         addToList.apply(null, x);
     });
-    
+
     positions = IriSP._(positions)
         .chain()
         .uniq()
         .sortBy(function(p) { return parseInt(p); })
         .value();
-    
+
     var res = "", lastIndex = 0;
-    
+
     for (var i = 0; i < positions.length; i++) {
         var pos = positions[i];
         res += text.substring(lastIndex, pos);
@@ -144,11 +145,11 @@
         }
         lastIndex = pos;
     }
-    
+
     res += text.substring(lastIndex);
-    
+
     return res;
-    
+
 };
 
 IriSP.log = function() {
@@ -161,11 +162,27 @@
 	jqSel.attr("draggable", "true").on("dragstart", function(_event) {
 		var d = (typeof data === "function" ? data.call(this) : data);
 		try {
+            if (d.html === undefined && d.uri && d.text) {
+                d.html = '<a href="' + d.uri + '">' + d.text + '</a>';
+            }
 			IriSP._(d).each(function(v, k) {
-				if (v) {
+                if (v && k != 'text' && k != 'html') {
 					_event.originalEvent.dataTransfer.setData("text/x-iri-" + k, v);
 				}
 			});
+            if (d.uri && d.text) {
+                _event.originalEvent.dataTransfer.setData("text/x-moz-url", d.uri + "\n" + d.text.replace("\n", " "));
+                _event.originalEvent.dataTransfer.setData("text/plain", d.text + " " + d.uri);
+            }
+            // Define generic text/html and text/plain last (least
+            // specific types, see
+            // https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Drag_operations#Drag_Data)
+            if (d.html !== undefined) {
+                _event.originalEvent.dataTransfer.setData("text/html", d.html);
+            }
+            if (d.text !== undefined && ! d.uri) {
+                _event.originalEvent.dataTransfer.setData("text/plain", d.text);
+            }
 		} catch(err) {
 			_event.originalEvent.dataTransfer.setData("Text", JSON.stringify(d));
 		}
@@ -180,6 +197,62 @@
     });
 };
 
+IriSP.timestamp2ms = function(t) {
+    // Convert timestamp to numeric value
+    // It accepts the following forms:
+    // [h:mm:ss] [mm:ss] [ss]
+    var s = t.split(":").reverse();
+    while (s.length < 3) {
+        s.push("0");
+    }
+    return 1000 * (3600 * parseInt(s[2], 10) + 60 * parseInt(s[1], 10) + parseInt(s[0], 10));
+};
+
+IriSP.setFullScreen= function(elem, value) {
+    // Set fullscreen on or off
+    if (value) {
+		if (elem.requestFullscreen) {
+			elem.requestFullscreen();
+		} else if (elem.mozRequestFullScreen) {
+			elem.mozRequestFullScreen();
+		} else if (elem.webkitRequestFullscreen) {
+			elem.webkitRequestFullscreen();
+		} else if (elem.msRequestFullscreen) {
+			elem.msRequestFullscreen();
+		}
+	} else {
+        if (document.exitFullscreen) {
+            document.exitFullscreen();
+        } else if (document.msExitFullscreen) {
+            document.msExitFullscreen();
+        } else if (document.mozCancelFullScreen) {
+            document.mozCancelFullScreen();
+        } else if (document.webkitExitFullscreen) {
+            document.webkitExitFullscreen();
+        }
+    }
+};
+
+IriSP.isFullscreen = function() {
+	return (document.fullscreenElement ||  document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement);
+};
+
+IriSP.getFullscreenElement = function () {
+    return (document.fullscreenElement
+            || document.webkitFullscreenElement
+            || document.mozFullScreenElement
+            || document.msFullscreenElement
+            || undefined);
+};
+
+IriSP.getFullscreenEventname = function () {
+    return ((document.exitFullscreen && "fullscreenchange")
+            || (document.webkitExitFullscreen && "webkitfullscreenchange")
+            || (document.mozExitFullScreen && "mozfullscreenchange")
+            || (document.msExitFullscreen && "msfullscreenchange")
+            || "");
+};
+
 /* js is where data is stored in a standard form, whatever the serializer */
 
 //TODO: Separate Project-specific data from Source
@@ -757,6 +830,7 @@
     this.volume = .5;
     this.paused = true;
     this.muted = false;
+    this.timeRange = false;
     this.loadedMetadata = false;
     var _this = this;
     this.on("play", function() {
@@ -781,6 +855,18 @@
             _a.trigger("enter");
             _this.trigger("enter-annotation",_a);
         });
+        
+        if (_this.getTimeRange()){
+            if (_this.getTimeRange()[0] > _time) {
+                _this.pause();
+                _this.setCurrentTime(_this.getTimeRange()[0]);
+            }
+            if (_this.getTimeRange()[1] < _time){
+                _this.pause();
+                _this.setCurrentTime(_this.getTimeRange()[1]);
+            }
+        }
+        
     });
     this.on("loadedmetadata", function() {
         _this.loadedMetadata = true;
@@ -805,6 +891,10 @@
     return this.muted;
 };
 
+Playable.prototype.getTimeRange = function() {
+    return this.timeRange;
+}
+
 Playable.prototype.setCurrentTime = function(_time) {
     this.trigger("setcurrenttime",_time);
 };
@@ -817,6 +907,16 @@
     this.trigger("setmuted",_muted);
 };
 
+Playable.prototype.setTimeRange = function(_timeBegin, _timeEnd) {
+    if ((_timeBegin < _timeEnd)&&(_timeBegin >= 0)&&(_timeEnd>0)){
+        return this.trigger("settimerange", [_timeBegin, _timeEnd]);
+    }
+}
+
+Playable.prototype.resetTimeRange = function() {
+    return this.trigger("resettimerange");
+}
+
 Playable.prototype.play = function() {
     this.trigger("setplay");
 };
@@ -840,6 +940,17 @@
 };
 
 extendPrototype(Media, Playable);
+/* */
+
+var Media = Model.Media = function(_id, _source) {
+    Playable.call(this, _id, _source);
+    this.elementType = 'media';
+    this.duration = new Time();
+    this.video = '';
+    var _this = this;
+};
+
+extendPrototype(Media, Playable);
 
 /* Default functions to be overriden by players */
     
@@ -905,6 +1016,19 @@
 
 extendPrototype(Annotation, BaseElement);
 
+/* Set begin and end in one go, to avoid undesired side-effects in
+ * setBegin/setEnd interaction */
+Annotation.prototype.setBeginEnd = function(_beginMs, _endMs) {
+    _beginMs = Math.max(0,_beginMs);
+    _endMs = Math.max(0,_endMs);
+    if (_endMs < _beginMs)
+        _endMs = _beginMs;
+    this.begin.setMilliseconds(_beginMs);
+    this.end.setMilliseconds(_endMs);
+    this.trigger("change-begin");
+    this.trigger("change-end");
+};
+
 Annotation.prototype.setBegin = function(_beginMs) {
     this.begin.setMilliseconds(Math.max(0,_beginMs));
     this.trigger("change-begin");
@@ -1432,7 +1556,17 @@
             videoEl.append(_srcNode);
         }
     }
-    
+    if (opts.subtitle) {
+        var _trackNode = IriSP.jQuery('<track>');
+        _trackNode.attr({
+            label: "Subtitles",
+            kind: "subtitles",
+            srclang: "fr",
+            src: opts.subtitle,
+            default: ""
+        });
+        videoEl.append(_trackNode);
+    }
     jqselector.html(videoEl);
     
     var mediaEl = videoEl[0];
@@ -1464,6 +1598,20 @@
         }
     });
     
+    media.on("settimerange", function(_timeRange){
+        media.timeRange = _timeRange;
+        try {
+            if (media.getCurrentTime() > _timeRange[0] || media.getCurrentTime() < _timeRange){
+                mediaEl.currentTime = (_timeRange[0] / 1000);
+            }
+        } catch (err) {
+        }
+    })
+    
+    media.on("resettimerange", function(){
+        media.timeRange = false;
+    })
+    
     media.on("setplay", function() {
         try {
             mediaEl.play();
@@ -1517,7 +1665,13 @@
         media.trigger("seeked");
     });
     
-    
+    videoEl.on("click", function() {
+        if (mediaEl.paused) {
+            media.play();
+        } else {
+            media.pause();
+        };
+    });
 };
 /* START contentapi-serializer.js */
 
@@ -1694,7 +1848,7 @@
                 if (typeof _data.content.img !== "undefined" && _data.content.img.src !== "undefined") {
                     _res.thumbnail = _data.content.img.src;
                 }
-                _res.created = IriSP.Model.isoToDate(_data.meta["dc:created"]);
+                _res.created = IriSP.Model.isoToDate(_data.created ? _data.created : _data.meta? _data.meta["dc:created"] : "");
                 if (typeof _data.color !== "undefined") {
                     var _c = parseInt(_data.color).toString(16);
                     while (_c.length < 6) {
@@ -1906,20 +2060,26 @@
     serializeAnnotation : function(_data, _source) {
         var _annType = _data.getAnnotationType();
         return {
+            id: _data.id,
             begin: _data.begin.milliseconds,
             end: _data.end.milliseconds,
             content: {
+                data: (_data.content ? _data.content.data || {} : {}),
                 description: _data.description,
                 title: _data.title,
                 audio: _data.audio
             },
+            id: _data.id ? _data.id : "", // If annotation is new, id will be undefined
             tags: _data.getTagTexts(),
             media: _data.getMedia().id,
+            project: _data.project_id,
             type_title: _annType.title,
             type: ( typeof _annType.dont_send_id !== "undefined" && _annType.dont_send_id ? "" : _annType.id ),
             meta: {
                 created: _data.created,
-                creator: _data.creator
+                creator: _data.creator,
+                modified: _data.modified,
+                contributor: _data.contributor
             }
         };
     },
@@ -1950,11 +2110,13 @@
             return _tag.id;
         });
         _ann.setTags(_tagIds);
-        _ann.setBegin(_anndata.begin);
-        _ann.setEnd(_anndata.end);
+        _ann.setBeginEnd(_anndata.begin, _anndata.end);
         if (typeof _anndata.content.audio !== "undefined" && _anndata.content.audio.href) {
             _ann.audio = _anndata.content.audio;
-        }
+        };
+        if (_anndata.content.data) {
+            _ann.content = { data: _anndata.content.data };
+        };
         _source.getAnnotations().push(_ann);
     },
     serialize : function(_source) {
@@ -1964,7 +2126,7 @@
         if (typeof _data == "string") {
             _data = JSON.parse(_data);
         }
-        
+
         _source.addList('tag', new IriSP.Model.List(_source.directory));
         _source.addList('annotationType', new IriSP.Model.List(_source.directory));
         _source.addList('annotation', new IriSP.Model.List(_source.directory));
@@ -1972,7 +2134,8 @@
     }
 };
 
-/* End ldt_annotate serializer *//* ldt_localstorage serializer: Used to store personal annotations in local storage */
+/* End ldt_annotate serializer */
+/* ldt_localstorage serializer: Used to store personal annotations in local storage */
 
 if (typeof IriSP.serializers === "undefined") {
     IriSP.serializers = {};
@@ -1982,9 +2145,11 @@
     serializeAnnotation : function(_data, _source) {
         var _annType = _data.getAnnotationType();
         return {
+            id: _data.id,
             begin: _data.begin.milliseconds,
             end: _data.end.milliseconds,
             content: {
+                data: (_data.content ? _data.content.data || {} : {}),
                 description: _data.description,
                 title: _data.title,
                 audio: _data.audio
@@ -1995,7 +2160,9 @@
             type: ( typeof _annType.dont_send_id !== "undefined" && _annType.dont_send_id ? "" : _annType.id ),
             meta: {
                 created: _data.created,
-                creator: _data.creator
+                creator: _data.creator,
+                modified: _data.modified,
+                contributor: _data.contributor
             }
         };
     },
@@ -2005,6 +2172,8 @@
         _ann.title = _anndata.content.title || "";
         _ann.creator = _anndata.meta.creator || "";
         _ann.created = new Date(_anndata.meta.created);
+        _ann.contributor = _anndata.meta.contributor || "";
+        _ann.modified = new Date(_anndata.meta.modified);
         _ann.setMedia(_anndata.media, _source);
         var _anntype = _source.getElement(_anndata.type);
         if (!_anntype) {
@@ -2026,11 +2195,13 @@
             return _tag.id;
         });
         _ann.setTags(_tagIds);
-        _ann.setBegin(_anndata.begin);
-        _ann.setEnd(_anndata.end);
+        _ann.setBeginEnd(_anndata.begin, _anndata.end);
         if (typeof _anndata.content.audio !== "undefined" && _anndata.content.audio.href) {
             _ann.audio = _anndata.content.audio;
-        }
+        };
+        if (_anndata.content.data) {
+            _ann.content = { data: _anndata.content.data };
+        };
         _source.getAnnotations().push(_ann);
     },
     serialize : function(_source) {
@@ -2115,8 +2286,8 @@
         backboneRelational: "backbone-relational.js",
         paper: "paper.js",
         jqueryMousewheel: "jquery.mousewheel.min.js",
-        splitter: "jquery.splitter.js",
-        cssSplitter: "jquery.splitter.css",
+        splitter: "jquery.touchsplitter.js",
+        cssSplitter: "jquery.touchsplitter.css",
         renkanPublish: "renkan.js",
         processing: "processing-1.3.6.min.js",
         recordMicSwf: "record_mic.swf",
@@ -2206,7 +2377,7 @@
 };
 
 IriSP.guiDefaults = {
-    width : 640,            
+    width : 640,
     container : 'LdtPlayer',
     spacer_div_height : 0,
     widgets: []
@@ -2258,46 +2429,45 @@
 Metadataplayer.prototype.loadLibs = function() {
     ns.log("IriSP.Metadataplayer.prototype.loadLibs");
     var $L = $LAB
-        .script(ns.getLib("Mustache"));
-    
+        .queueScript(ns.getLib("Mustache"));
     formerJQuery = !!window.jQuery;
     former$ = !!window.$;
     formerUnderscore = !!window._;
-    
+
     if (typeof ns.jQuery === "undefined") {
-        $L.script(ns.getLib("jQuery"));
-    }
-    
-    if (typeof ns._ === "undefined") {
-        $L.script(ns.getLib("underscore"));
+        $L.queueScript(ns.getLib("jQuery"));
     }
-    
-    if (typeof window.JSON == "undefined") {
-        $L.script(ns.getLib("json"));
+
+    if (typeof ns._ === "undefined") {
+        $L.queueScript(ns.getLib("underscore"));
     }
-    
-    $L.wait()
-        .script(ns.getLib("jQueryUI"));
+
+    if (typeof window.JSON == "undefined") {
+        $L.queueScript(ns.getLib("json"));
+    }
+    $L.queueWait().queueScript(ns.getLib("jQueryUI")).queueWait();
 
     /* widget specific requirements */
     for(var _i = 0; _i < this.config.widgets.length; _i++) {
         var _t = this.config.widgets[_i].type;
         if (typeof ns.widgetsRequirements[_t] !== "undefined" && typeof ns.widgetsRequirements[_t].requires !== "undefined" ) {
             for (var _j = 0; _j < ns.widgetsRequirements[_t].requires.length; _j++) {
-                $L.script(ns.getLib(ns.widgetsRequirements[_t].requires[_j]));
+                $L.queueScript(ns.getLib(ns.widgetsRequirements[_t].requires[_j]));
             }
         }
     }
-    
+
     var _this = this;
-    
-    $L.wait(function() {
+    $L.queueWait(function() {
         _this.onLibsLoaded();
     });
+    
+    $L.runQueue();
 };
 
 Metadataplayer.prototype.onLibsLoaded = function() {
     ns.log("IriSP.Metadataplayer.prototype.onLibsLoaded");
+
     if (typeof ns.jQuery === "undefined" && typeof window.jQuery !== "undefined") {
         ns.jQuery = window.jQuery;
         if (former$ || formerJQuery) {
@@ -2310,9 +2480,10 @@
             _.noConflict();
         }
     }
+    
     ns.loadCss(ns.getLib("cssjQueryUI"));
     ns.loadCss(this.config.css);
-    
+
     this.$ = ns.jQuery('#' + this.config.container);
     this.$.css({
         "width": this.config.width,
@@ -2321,7 +2492,7 @@
     if (typeof this.config.height !== "undefined") {
         this.$.css("height", this.config.height);
     }
-      
+
     this.widgets = [];
     var _this = this;
     ns._(this.config.widgets).each(function(widgetconf, key) {
@@ -2334,9 +2505,9 @@
         });
     });
     this.$.find('.Ldt-Loader').detach();
-    
+
     this.widgetsLoaded = false;
-    
+
     this.on("widget-loaded", function() {
         if (_this.widgetsLoaded) {
             return;
@@ -2348,7 +2519,44 @@
             _this.widgetsLoaded = true;
             _this.trigger("widgets-loaded");
         }
-    });   
+    });
+};
+
+Metadataplayer.prototype.loadLocalAnnotations = function(localsourceidentifier) {
+    if (this.localSource === undefined)
+        this.localSource = this.sourceManager.newLocalSource({serializer: IriSP.serializers['ldt_localstorage']});
+    // Load current local annotations
+    if (localsourceidentifier) {
+        // Allow to override localsourceidentifier when necessary (usually at init time)
+        this.localSource.identifier = localsourceidentifier;
+    }
+    this.localSource.deSerialize(window.localStorage[this.localSource.identifier] || "[]");
+    return this.localSource;
+};
+
+Metadataplayer.prototype.saveLocalAnnotations = function() {
+    // Save annotations back to localstorage
+    window.localStorage[this.localSource.identifier] = this.localSource.serialize();
+    // Merge modifications into widget source
+    // this.source.merge(this.localSource);
+};
+
+Metadataplayer.prototype.addLocalAnnotation = function(a) {
+    this.loadLocalAnnotations();
+    this.localSource.getAnnotations().push(a);
+    this.saveLocalAnnotations();
+};
+
+Metadataplayer.prototype.deleteLocalAnnotation = function(ident) {
+    this.localSource.getAnnotations().removeId(ident, true);
+    this.saveLocalAnnotations();
+};
+
+Metadataplayer.prototype.getLocalAnnotation = function (ident) {
+    this.loadLocalAnnotations();
+    // We cannot use .getElement since it fetches
+    // elements from the global Directory
+    return IriSP._.first(IriSP._.filter(this.localSource.getAnnotations(), function (a) { return a.id == ident; }));
 };
 
 Metadataplayer.prototype.loadMetadata = function(_metadataInfo) {
@@ -2371,9 +2579,9 @@
         var _divs = this.layoutDivs(_widgetConfig.type);
         _widgetConfig.container = _divs[0];
     }
-    
+
     var _this = this;
-    
+
     if (typeof ns.Widgets[_widgetConfig.type] !== "undefined") {
         ns._.defer(function() {
             _callback(new ns.Widgets[_widgetConfig.type](_this, _widgetConfig));
@@ -2418,7 +2626,7 @@
     if (typeof _height !== "undefined") {
         divHtml.css("height", _height);
     }
-            
+
     this.$.append(divHtml);
     this.$.append(spacerHtml);
 
@@ -2427,7 +2635,8 @@
 
 })(IriSP);
 
-/* End of widgets-container/metadataplayer.js *//* widgetsDefinition of an ancestor for the Widget classes */
+/* End of widgets-container/metadataplayer.js */
+/* widgetsDefinition of an ancestor for the Widget classes */
 
 if (typeof IriSP.Widgets === "undefined") {
     IriSP.Widgets = {};
@@ -2446,44 +2655,44 @@
 
 
 IriSP.Widgets.Widget = function(player, config) {
-    
+
     if( typeof player === "undefined") {
         /* Probably an abstract call of the class when
          * individual widgets set their prototype */
         return;
     }
-    
+
     this.__subwidgets = [];
-    
+
     /* Setting all the configuration options */
     var _type = config.type || "(unknown)",
         _config = IriSP._.defaults({}, config, (player && player.config ? player.config.default_options : {}), this.defaults),
         _this = this;
-    
+
     IriSP._(_config).forEach(function(_value, _key) {
        _this[_key] = _value;
     });
-    
+
     this.$ = IriSP.jQuery('#' + this.container);
-    
+
     if (typeof this.width === "undefined") {
         this.width = this.$.width();
     } else {
         this.$.css("width", this.width);
     }
-    
+
     if (typeof this.height !== "undefined") {
         this.$.css("height", this.height);
     }
-    
+
     /* Setting this.player at the end in case it's been overriden
      * by a configuration option of the same name :-(
      */
     this.player = player || new IriSP.FakeClass(["on","trigger","off","loadWidget","loadMetadata"]);
-    
+
     /* Adding classes and html attributes */
     this.$.addClass("Ldt-TraceMe Ldt-Widget").attr("widget-type", _type);
-    
+
     this.l10n = (
         typeof this.messages[IriSP.language] !== "undefined"
         ? this.messages[IriSP.language]
@@ -2493,10 +2702,14 @@
             : this.messages["en"]
         )
     );
-    
+
     /* Loading Metadata if required */
-   
+
     function onsourceloaded() {
+        if (_this.localannotations) {
+            _this.localsource = player.loadLocalAnnotations(_this.localannotations);
+            _this.source.merge(_this.localsource);
+        }
         if (_this.media_id) {
                 _this.media = this.getElement(_this.media_id);
             } else {
@@ -2505,15 +2718,18 @@
                 };
                 _this.media = _this.source.getCurrentMedia(_mediaopts);
             }
-            
-        _this.draw();
+        if (_this.pre_draw_callback){
+            IriSP.jQuery.when(_this.pre_draw_callback()).done(_this.draw());
+        }
+        else {
+            _this.draw();
+        }
         _this.player.trigger("widget-loaded");
     }
-    
+
     if (this.metadata) {
         /* Getting metadata */
         this.source = player.loadMetadata(this.metadata);
-        
         /* Call draw when loaded */
         this.source.onLoad(onsourceloaded);
     } else {
@@ -2521,8 +2737,8 @@
             onsourceloaded();
         }
     }
-    
-    
+
+
 };
 
 IriSP.Widgets.Widget.prototype.defaults = {};
@@ -2640,7 +2856,7 @@
     // offset is normally either -1 (previous slide) or +1 (next slide)
     var _this = this;
     var currentTime = _this.media.getCurrentTime();
-    var annotations = _this.source.getAnnotations().sortBy(function(_annotation) {
+    var annotations = _this.getWidgetAnnotations().sortBy(function(_annotation) {
         return _annotation.begin;
     });
     for (var i = 0; i < annotations.length; i++) {
@@ -2653,6 +2869,51 @@
     };
 };
 
+/*
+ * Propose an export of the widget's annotations
+ *
+ * Parameter: a list of annotations. If not specified, the widget's annotations will be exported.
+ */
+IriSP.Widgets.Widget.prototype.exportAnnotations = function(annotations) {
+    var widget = this;
+    if (annotations === undefined)
+        annotations = this.getWidgetAnnotations();
+    var $ = IriSP.jQuery;
+
+    // FIXME: this should belong to a proper serialize/deserialize component?
+    var content = Mustache.to_html("[video:{{url}}]\n", {url: widget.media.url}) + annotations.map( function(a) { return Mustache.to_html("[{{ a.begin }}]{{ a.title }} {{ a.description }}[{{ a.end }}]", { a: a }); }).join("\n");
+
+    var el = $("<pre>")
+            .addClass("exportContainer")
+            .text(content)
+            .dialog({
+                title: "Annotation export",
+                open: function( event, ui ) {
+                    // Select text
+                    var range;
+                    if (document.selection) {
+		                range = document.body.createTextRange();
+                        range.moveToElementText(this[0]);
+		                range.select();
+		            } else if (window.getSelection) {
+		                range = document.createRange();
+		                range.selectNode(this[0]);
+		                window.getSelection().addRange(range);
+		            }
+                },
+                autoOpen: true,
+                width: '80%',
+                minHeight: '400',
+                height: 400,
+                buttons: [ { text: "Close", click: function() { $( this ).dialog( "close" ); } },
+                           { text: "Download", click: function () {
+                               a = document.createElement('a');
+                               a.setAttribute('href', 'data:text/plain;base64,' + btoa(content));
+                               a.setAttribute('download', 'Annotations - ' + widget.media.title.replace(/[^ \w]/g, '') + '.txt');
+                               a.click();
+                           } } ]
+            });
+};
 
 /**
  * This method responsible of drawing a widget on screen.