/**
* @module Player
*/
/**
* I am the AnnotationsController who mediates between the data model of
* all {{#crossLink "Annotation"}}annotations{{/crossLink}} (stored in {{#crossLink "HypervideoModel"}}HypervideoModel{{/crossLink}})
* and their various User Interface elements (e.g. in {{#crossLink "ViewVideo"}}ViewVideo{{/crossLink}})
*
* @class AnnotationsController
* @static
*/
FrameTrail.defineModule('AnnotationsController', function(){
var HypervideoModel = FrameTrail.module('HypervideoModel'),
ViewVideo = FrameTrail.module('ViewVideo'),
annotationInFocus = null,
openedAnnotation = null,
annotations;
/**
* I initialize the AnnotationsController.
* My init process has two tasks: connect the annotation menu in the Sidebar
* with the data model (select current annotation set) and initialize
* the annotations (instances of type {{#crossLink "Annotation"}}Annotation{{/crossLink}})
*
* @method initController
*/
function initController() {
annotations = HypervideoModel.annotations;
initSidebarSelectmenu();
if ( FrameTrail.getState('hv_config_annotationsVisible') ) {
refreshSidebarSelectmenu(true);
} else {
refreshSidebarSelectmenu(false);
}
initAnnotations();
}
/**
* I update the AnnotationsController during runtime.
* My update process has two tasks: refresh the annotation menu in the Sidebar
* with the data model (select current annotation set) and initialize
* the annotations (instances of type {{#crossLink "Annotation"}}Annotation{{/crossLink}})
*
* @method updateController
*/
function updateController() {
// update references
annotations = FrameTrail.module('HypervideoModel').annotations;
ViewVideo = FrameTrail.module('ViewVideo');
if ( FrameTrail.getState('hv_config_annotationsVisible') ) {
refreshSidebarSelectmenu(true);
} else {
refreshSidebarSelectmenu(false);
}
initAnnotations();
}
/**
* I connect the select menu (jquery-ui on select element)
* with the data model.
*
* @method initSidebarSelectmenu
* @private
*/
function initSidebarSelectmenu() {
var SelectAnnotationContainer = FrameTrail.module('Sidebar').SelectAnnotationContainer,
SelectAnnotation = FrameTrail.module('Sidebar').SelectAnnotationContainer.find('#SelectAnnotation'),
HypervideoModel = FrameTrail.module('HypervideoModel');
$.widget( 'custom.stylableselectmenu', $.ui.selectmenu, {
_renderItem: function( ul, item ) {
var li = $( '<li>', {
text: item.label,
style: 'color: ' + item.element.attr( 'data-color' )
});
if ( item.disabled ) {
li.addClass( 'ui-state-disabled' );
}
$( '<span>', {
style: 'background-color: ' + item.element.attr( 'data-color' ),
'class': 'color-square'
})
.appendTo( li );
return li.appendTo( ul );
}
});
SelectAnnotation.stylableselectmenu();
SelectAnnotation.on('stylableselectmenuchange', function(evt, ui) {
FrameTrail.module('HypervideoModel').annotationSet = SelectAnnotation.val();
annotations = FrameTrail.module('HypervideoModel').annotations;
initAnnotations();
});
SelectAnnotation.on('stylableselectmenuselect', function(evt, ui) {
/*
// too much color in the interface, keep default color for now
SelectAnnotation.stylableselectmenu('widget').css({
'border-color': ui.item.element.attr( 'data-color' ),
color: ui.item.element.attr( 'data-color' )
});
*/
});
}
/**
* I refresh the view of the select menu in the sidebar.
* When my parameter is false, I hide the select menu,
* otherwise I show it an append all available annotation sets
* as option to the select box.
*
* @method refreshSidebarSelectmenu
* @param {Boolean} visible
* @private
*/
function refreshSidebarSelectmenu(visible) {
var SelectAnnotationContainer = FrameTrail.module('Sidebar').SelectAnnotationContainer,
HypervideoModel = FrameTrail.module('HypervideoModel'),
SelectAnnotation,
annotationSets;
if (visible) {
SelectAnnotation = SelectAnnotationContainer.find('#SelectAnnotation'),
SelectSingle = SelectAnnotationContainer.find('#SelectAnnotationSingle'),
annotationSets = HypervideoModel.annotationSets;
if ( annotationSets.length > 1 && !FrameTrail.getState('editMode') ) {
SelectAnnotation.empty();
for (var idx in annotationSets) {
SelectAnnotation.append(
'<option value="'
+ annotationSets[idx].id
+ '" data-color="#'
+ annotationSets[idx].color
+ '">'
+ annotationSets[idx].name
+ '</option>'
);
}
SelectAnnotation.stylableselectmenu('refresh');
SelectAnnotation.val(HypervideoModel.annotationSet).stylableselectmenu('refresh');
var uiObj = {
item: {
element: SelectAnnotationContainer.find('option').eq(0)
}
}
SelectAnnotation.trigger('stylableselectmenuselect', uiObj);
SelectAnnotation.stylableselectmenu('widget').show();
SelectSingle.text('').hide();
} else if ( annotationSets.length == 1 && !FrameTrail.getState('editMode') ) {
SelectAnnotation.stylableselectmenu('widget').hide();
SelectSingle.text(annotationSets[0].name).show();
} else if ( FrameTrail.getState('editMode') ) {
SelectAnnotation.stylableselectmenu('widget').hide();
SelectSingle.text(FrameTrail.getState('username')).show();
}
SelectAnnotationContainer.show();
} else if ( !FrameTrail.getState('editMode') ) {
SelectAnnotationContainer.hide();
}
}
/**
* I first empty all DOM elements, and then ask all
* annotations of the current data model, to append new DOM elements,
* which I the arrange and prepare for view.
*
* @method initAnnotations
* @private
*/
function initAnnotations() {
var annotationColor;
if (!FrameTrail.module('Database').users[FrameTrail.module('HypervideoModel').annotationSet]) {
annotationColor = '999999';
} else {
annotationColor = FrameTrail.module('Database').users[FrameTrail.module('HypervideoModel').annotationSet].color;
}
//console.log('current annotation color: ', annotationColor)
// update references
annotations = FrameTrail.module('HypervideoModel').annotations;
ViewVideo = FrameTrail.module('ViewVideo');
ViewVideo.AnnotationContainer.empty();
ViewVideo.AnnotationTileSlider.empty();
ViewVideo.AnnotationTimeline.empty();
ViewVideo.AnnotationPreviewContainer.empty();
for (var i = 0; i < annotations.length; i++) {
annotations[i].renderInDOM();
}
distributeTiles();
initAnnotationSlider();
}
/**
* I distribute the tileElements in the tileContainer, so that they
* match closely to the position of their related timelineElements.
* When they would start to overlap, I arrange them in groups.
* See also {{#crossLink "Annotation"}}Annotation{{/crossLink}}.
*
* @method distributeTiles
* @private
*/
function distributeTiles() {
var annotations = FrameTrail.module('HypervideoModel').annotations,
videoDuration = FrameTrail.module('HypervideoModel').duration,
sliderParent = ViewVideo.AnnotationTiles,
containerElement = ViewVideo.AnnotationTileSlider,
groupCnt = 0,
gap = 3,
thisTileElement,
previousElement,
previousElementRightPos,
startTime,
endTime,
middleTime,
desiredPosition,
finalPosition;
containerElement.children().removeAttr('data-group-id');
containerElement.children().css({
position: '',
left: ''
});
function getTotalWidth(collection, addition){
var totalWidth = 0;
collection.each(function() {
totalWidth += $(this).width()+addition;
});
return totalWidth;
}
function getNegativeOffsetRightCorrection(leftPosition, collectionWidth) {
var offsetCorrection,
mostRightPos = leftPosition + collectionWidth + (gap*2);
if ( mostRightPos >= sliderParent.width() ) {
offsetCorrection = mostRightPos - sliderParent.width();
return offsetCorrection;
}
return 0;
}
// Cancel if total width > container width
if ( getTotalWidth(containerElement.children(), 3) > sliderParent.width() ) {
containerElement.width( getTotalWidth(containerElement.children(), 3) );
return;
} else {
containerElement.width('');
}
// Distribute Items
for (var i = 0; i < annotations.length; i++) {
thisTileElement = annotations[i].tileElement;
if (i > 0) {
previousElement = annotations[i-1].tileElement;
previousElementRightPos = previousElement.position().left + previousElement.width();
}
startTime = annotations[i].data.start;
endTime = annotations[i].data.end;
middleTime = startTime + ( (endTime-startTime)/2 );
desiredPosition = ( (sliderParent.width() / videoDuration) * middleTime ) - ( thisTileElement.width()/2 );
thisTileElement.attr({
'data-in': startTime,
'data-out': endTime
});
if (desiredPosition <= 0) {
finalPosition = 0;
thisTileElement.removeAttr('data-group-id');
groupCnt++;
} else if (desiredPosition < previousElementRightPos + gap) {
finalPosition = previousElementRightPos + gap;
if (previousElement.attr('data-group-id')) {
containerElement.children('[data-group-id="'+ previousElement.attr('data-group-id') +'"]').attr('data-group-id', groupCnt);
} else {
previousElement.attr('data-group-id', groupCnt);
}
thisTileElement.attr('data-group-id', groupCnt);
groupCnt++;
} else {
finalPosition = desiredPosition;
thisTileElement.removeAttr('data-group-id');
groupCnt++;
}
thisTileElement.css({
position: "absolute",
left: finalPosition + "px"
});
}
// Re-Arrange Groups
var groupCollection,
p,
previousGroupCollection,
previousGroupCollectionRightPos,
totalWidth,
groupStartTime,
groupEndTime,
groupMiddleTime,
desiredGroupPosition,
correction,
negativeOffsetRightCorrection,
groupIDs;
function arrangeGroups() {
groupIDs = [];
containerElement.children('[data-group-id]').each(function() {
if ( groupIDs.indexOf( $(this).attr('data-group-id') ) == -1 ) {
groupIDs.push($(this).attr('data-group-id'));
}
});
for (var i=0; i < groupIDs.length; i++) {
var g = groupIDs[i];
groupCollection = containerElement.children('[data-group-id="'+ g +'"]');
if (groupCollection.length < 1) {
continue;
}
if ( groupIDs[i-1] ) {
p = groupIDs[i-1];
previousGroupCollection = containerElement.children('[data-group-id="'+ p +'"]');
previousGroupCollectionRightPos = previousGroupCollection.eq(0).position().left + getTotalWidth( previousGroupCollection, 3 );
}
totalWidth = getTotalWidth( groupCollection, 3 );
groupStartTime = parseInt(groupCollection.eq(0).attr('data-in'));
groupEndTime = parseInt(groupCollection.eq(groupCollection.length-1).attr('data-out'));
groupMiddleTime = groupStartTime + ( (groupEndTime-groupStartTime)/2 );
desiredGroupPosition = ( (sliderParent.width() / videoDuration) * groupMiddleTime ) - ( totalWidth/2 );
correction = groupCollection.eq(0).position().left - desiredGroupPosition;
if ( groupCollection.eq(0).position().left - correction >= 0 && desiredGroupPosition > previousGroupCollectionRightPos + gap ) {
groupCollection.each(function() {
$(this).css('left', '-='+ correction +'');
});
} else if ( groupCollection.eq(0).position().left - correction >= 0 && desiredGroupPosition < previousGroupCollectionRightPos + gap ) {
var attachCorrection = groupCollection.eq(0).position().left - previousGroupCollectionRightPos;
groupCollection.each(function() {
$(this).css('left', '-='+ attachCorrection +'');
});
if ( groupCollection.eq(0).prev().length ) {
var prevElem = groupCollection.eq(0).prev();
if ( prevElem.attr('data-group-id') ) {
previousGroupCollection.attr('data-group-id', g);
} else {
prevElem.attr('data-group-id', g);
}
}
}
}
}
arrangeGroups();
// Deal with edge case > tiles outside container on right side
var repeatIteration;
function solveRightEdgeOverlap() {
repeatIteration = false;
for (var i = 0; i < annotations.length; i++) {
thisTileElement = annotations[i].tileElement;
var g = undefined;
if ( thisTileElement.attr('data-group-id') ) {
g = thisTileElement.attr('data-group-id');
groupCollection = containerElement.children('[data-group-id="'+ g +'"]');
} else {
groupCollection = thisTileElement;
}
if (groupCollection.eq(0).prev().length) {
previousElement = groupCollection.eq(0).prev();
if ( previousElement.attr('data-group-id') ) {
previousGroupCollection = containerElement.children('[data-group-id="'+ previousElement.attr('data-group-id') +'"]');
previousGroupCollectionRightPos = previousGroupCollection.eq(0).position().left + getTotalWidth( previousGroupCollection, 3 );
} else {
previousGroupCollection = previousElement;
previousGroupCollectionRightPos = previousElement.position().left + previousElement.width() + gap;
}
} else {
previousGroupCollectionRightPos = 0;
}
totalWidth = getTotalWidth( groupCollection, 3 );
currentGroupCollectionLeft = groupCollection.eq(0).position().left;
currentGroupCollectionRightPos = groupCollection.eq(0).position().left + totalWidth;
negativeOffsetRightCorrection = getNegativeOffsetRightCorrection(currentGroupCollectionLeft, totalWidth);
if ( currentGroupCollectionLeft - negativeOffsetRightCorrection >= 0 && negativeOffsetRightCorrection > 1 ) {
if ( currentGroupCollectionLeft - negativeOffsetRightCorrection > previousGroupCollectionRightPos + gap ) {
groupCollection.each(function() {
$(this).css('left', '-='+ negativeOffsetRightCorrection +'');
});
} else if ( currentGroupCollectionLeft - negativeOffsetRightCorrection < previousGroupCollectionRightPos + gap ) {
var attachCorrection = currentGroupCollectionLeft - previousGroupCollectionRightPos;
groupCollection.each(function() {
$(this).css('left', '-='+ attachCorrection +'');
});
if ( !g && previousElement.length && previousElement.attr('data-group-id') ) {
thisTileElement.attr('data-group-id', previousElement.attr('data-group-id'));
}
if ( previousElement.attr('data-group-id') ) {
containerElement.children('[data-group-id="'+ previousElement.attr('data-group-id') +'"]').attr('data-group-id', g);
} else {
previousElement.attr('data-group-id', g);
}
repeatIteration = false;
}
}
}
if ( repeatIteration ) {
solveRightEdgeOverlap();
}
}
solveRightEdgeOverlap();
}
/**
* I prepare the display of the annotationElement (which contains the content of an
* annotation), which are shown in the AnnotationContainer.
* @method initAnnotationSlider
* @private
*/
function initAnnotationSlider() {
var widthOfSlider = 0,
gap = 10;
for (var idx in annotations) {
widthOfSlider += annotations[idx].annotationElement.width() + gap;
}
ViewVideo.AnnotationContainer.width(widthOfSlider);
}
/**
* When the global state viewSize changes, I re-arrange
* the annotationElements and tiles, to fit the new
* width of the browser.
*
* @method changeViewSize
* @private
*/
function changeViewSize() {
updateAnnotationSlider();
distributeTiles();
}
/**
* I react to changes in the global state viewSizeChanged.
* The state changes after a window resize event
* and is meant to be used for performance-heavy operations.
*
* @method onViewSizeChanged
* @private
*/
function onViewSizeChanged() {
}
/**
* When the state of the sidebar changes, I have to re-arrange
* the tileElements and the annotationElements, to fit the new
* width of the #mainContainer.
* @method toggleSidebarOpen
* @private
*/
function toggleSidebarOpen() {
var maxSlideDuration = 280,
interval;
interval = window.setInterval(function(){
distributeTiles();
updateAnnotationSlider();
}, 40);
window.setTimeout(function(){
window.clearInterval(interval);
}, maxSlideDuration)
}
/**
* When we are in the editMode annotations, the timeline should
* show all timeline elements stacked, which is what I do.
* @method stackTimelineView
*/
function stackTimelineView() {
ViewVideo.AnnotationTimeline.CollisionDetection({spacing:0, includeVerticalMargins:true});
ViewVideo.adjustLayout();
ViewVideo.adjustHypervideo();
}
/**
* When we are in the editMode annotations, the timeline should
* show all timeline elements stacked. After leaving this mode,
* I have to reset the timelineElements and the timeline to their normal
* layout.
* @method resetTimelineView
* @private
*/
function resetTimelineView() {
ViewVideo.AnnotationTimeline.css('height', '');
ViewVideo.AnnotationTimeline.children('.timelineElement').css({
top: '',
right: '',
bottom: '',
height: ''
});
}
/**
* I am a central method of the AnnotationsController.
* I am called from the update functions inside the HypervideoController
* and I set the activeState of the annotations according to the current time.
* @method updateStatesOfAnnotations
* @param {Number} currentTime
*/
function updateStatesOfAnnotations(currentTime) {
var annotation;
for (var idx in annotations) {
annotation = annotations[idx];
if ( annotation.data.start <= currentTime
&& annotation.data.end >= currentTime) {
if (!annotation.activeState) {
annotation.setActive();
}
} else {
if (annotation.activeState) {
annotation.setInactive();
}
}
}
if (annotationInFocus && !annotationInFocus.activeState) {
annotationInFocus.setActive();
}
}
/**
* I open the annotationElement of an annotation in the annotationContainer.
* if my parameter is null, I close the annotationContainer.
* Also, I add CSS classes to the opened annotationElement, and to its left and right
* neighbour.
* @method setOpenedAnnotation
* @param {Annotation or null} annotation
* @private
*/
function setOpenedAnnotation(annotation) {
var itemPosition, leftOffset;
openedAnnotation = annotation;
for (var idx in annotations) {
annotations[idx].annotationElement.removeClass('open previous next');
annotations[idx].timelineElement.removeClass('open');
annotations[idx].tileElement.removeClass('open');
}
if (annotation) {
annotation.annotationElement.addClass('open');
annotation.annotationElement.prev().addClass('previous');
annotation.annotationElement.next().addClass('next');
updateAnnotationSlider();
ViewVideo.shownDetails = 'annotations';
if ( annotation.data.type == 'location' && annotation.annotationElement.children('.resourceDetail').data('map') ) {
annotation.annotationElement.children('.resourceDetail').data('map').updateSize();
}
} else {
ViewVideo.shownDetails = null;
}
}
/**
* I find the annotation which is active. If there are more than one active annotations,
* I return the last one which has been activated. If there is no active annotation, I return null.
* @method findTopMostActiveAnnotation
*/
function findTopMostActiveAnnotation() {
var currentTime = FrameTrail.module('HypervideoController').currentTime,
annotations = FrameTrail.module('HypervideoModel').annotations;
return (function(){
var allActiveAnnotations = [];
for (var idx in annotations) {
if ( annotations[idx].data.start <= currentTime
&& annotations[idx].data.end >= currentTime ) {
allActiveAnnotations.push(annotations[idx]);
}
}
if (allActiveAnnotations.length === 0) {
if (annotations.length === 0) {
return null
} else {
return annotations[0]
}
} else {
return allActiveAnnotations.sort(function(a,b){
if (a.data.start > b.data.start) {
return -1
} else {
return 1
}
})[0];
}
}).call();
}
/**
* The annotationContainer is a slider element, which means that
* its left position within its container element must be
* updated according to the left position of the currently opened
* annotationElement.
* @method updateAnnotationSlider
* @private
*/
function updateAnnotationSlider() {
if (openedAnnotation) {
var itemPosition = openedAnnotation.annotationElement.position();
var leftOffset = -1 * ( itemPosition.left
- 1
- ViewVideo.AnnotationContainer.parent().innerWidth() / 2
+ openedAnnotation.annotationElement.width() / 2
);
ViewVideo.AnnotationContainer.css('left', leftOffset);
}
}
/**
* When an annotation is set into focus, I have to tell
* the old annotation in the var annotationInFocus, that it
* is no longer in focus. Then I store the Annotation (or null)
* from my parameter in the var annotationInFocus, and inform it
* about it.
* @method setAnnotationInFocus
* @param {Annotation or null} annotation
* @return Annotation or null
* @private
*/
function setAnnotationInFocus(annotation) {
if (annotationInFocus) {
annotationInFocus.permanentFocusState = false;
annotationInFocus.removedFromFocus();
}
annotationInFocus = annotation;
if (annotationInFocus) {
annotationInFocus.gotInFocus();
}
updateStatesOfAnnotations(FrameTrail.module('HypervideoController').currentTime);
return annotation;
}
/**
* Listens to global state 'editMode'.
* The AnnotationsController has to react on a change of the
* editMode.
* First it checks, wether we are entering or leaving the edit mode
* in general (editMode is false, when not the editor is not active, otherwise
* it is a String indicating the editMode).
* If the editor is active, the user's own annotation set has to be selected
* an the select menu for annotations has to be hidden.
* Secondly it checks wether the editMode we enter or leave is 'annotations'.
* If so, we activate or deactivate the editing options for annotations.
*
* @method toggleEditMode
* @param {String or false} editMode
* @param {String or false} oldEditMode
*/
function toggleEditMode(editMode, oldEditMode) {
var HypervideoModel = FrameTrail.module('HypervideoModel');
if ( editMode === false && oldEditMode !== false && FrameTrail.getState('hv_config_annotationsVisible') ) {
refreshSidebarSelectmenu(true);
} else if ( editMode && oldEditMode === false ) {
HypervideoModel.annotationSet = '#myAnnotationSet';
refreshSidebarSelectmenu(true);
initAnnotations();
} else if ( editMode === false && FrameTrail.getState('hv_config_annotationsVisible') ) {
refreshSidebarSelectmenu(true);
} else {
refreshSidebarSelectmenu(false);
}
if (editMode === 'annotations' && oldEditMode !== 'annotations') {
annotations = HypervideoModel.annotations;
for (var idx in annotations) {
annotations[idx].startEditing();
}
stackTimelineView();
initEditOptions();
makeTimelineDroppable(true);
} else if (oldEditMode === 'annotations' && editMode !== 'annotations') {
for (var idx in annotations) {
annotations[idx].stopEditing();
}
setAnnotationInFocus(null);
resetTimelineView();
makeTimelineDroppable(false);
initAnnotations();
}
}
/**
* When the editMode 'annotations' was entered, the #EditingOptions area
* should show two tabs: a ResourcePicker and a tab with the annotation timelines
* of all other users, drag new items on the annotation timeline.
* @method initEditOptions
* @private
*/
function initEditOptions() {
ViewVideo.EditingOptions.empty();
var annotationsEditingOptions = $('<div id="OverlayEditingTabs">'
+ ' <ul>'
+ ' <li><a href="#ResourceList">Choose Resource</a></li>'
+ ' <li><a href="#OtherUsers">Choose Annotations of other Users</a></li>'
+ ' </ul>'
+ ' <div id="ResourceList"></div>'
+ ' <div id="OtherUsers">'
+ ' <div class="message active">Drag Annotations from the User Timelines to your Annotation Timeline</div>'
+ ' <div id="TimelineList"></div>'
+ ' </div>'
+ '</div>')
.tabs({
heightStyle: "fill"
}),
timelineList = annotationsEditingOptions.find('#TimelineList')
annotationAllSets = FrameTrail.module('HypervideoModel').annotationAllSets;
ViewVideo.EditingOptions.append(annotationsEditingOptions);
FrameTrail.module('ResourceManager').renderResourcePicker(
annotationsEditingOptions.find('#ResourceList')
);
for (var id in annotationAllSets) {
if (id === FrameTrail.module('UserManagement').userID) {
continue;
}
var otherUsername = '',
otherUserColor = '';
for (var key in HypervideoModel.annotationSets) {
if (HypervideoModel.annotationSets[key].id === id) {
otherUsername = HypervideoModel.annotationSets[key].name;
otherUserColor = HypervideoModel.annotationSets[key].color;
}
}
var userTimelineWrapper = $( '<div class="userTimelineWrapper">'
+ ' <div class="userLabel" style="color: #'+ otherUserColor +'">'
+ ' <span class="userIcon" style="background-color: #'+ otherUserColor +'"></span>'
+ ' <span>'+ otherUsername + '</span>'
+ ' </div>'
+ ' <div class="userTimeline"></div>'
+ '</div>'),
userTimeline = userTimelineWrapper.find('.userTimeline');
for (var idx in annotationAllSets[id]) {
var compareTimelineItem = annotationAllSets[id][idx].renderCompareTimelineItem();
compareTimelineItem.css('background-color', '#' + otherUserColor);
userTimeline.append(compareTimelineItem);
}
timelineList.append(userTimelineWrapper);
}
}
/**
* When the editMode 'annotations' has been entered, the
* annotation timeline should be droppable for new items
* (from the ResourcePicker or from other users' timelines).
* A drop event should trigger the process of creating a new annotation.
* My parameter is true or false to activate or deactivate this behavior.
* @method makeTimelineDroppable
* @param {Boolean} droppable
*/
function makeTimelineDroppable(droppable) {
if (droppable) {
ViewVideo.AnnotationTimeline.droppable({
accept: '.resourceThumb, .compareTimelineElement',
activeClass: 'droppableActive',
hoverClass: 'droppableHover',
tolerance: 'touch',
over: function( event, ui ) {
ViewVideo.PlayerProgress.find('.ui-slider-handle').addClass('highlight');
},
out: function( event, ui ) {
ViewVideo.PlayerProgress.find('.ui-slider-handle').removeClass('highlight');
},
drop: function( event, ui ) {
var resourceID = ui.helper.attr('data-resourceID'),
videoDuration = FrameTrail.module('HypervideoModel').duration,
startTime,
endTime;
if (ui.helper.hasClass('compareTimelineElement')) {
startTime = parseFloat(ui.helper.attr('data-start'));
endTime = parseFloat(ui.helper.attr('data-end'));
} else {
startTime = FrameTrail.module('HypervideoController').currentTime;
endTime = (startTime + 4 > videoDuration)
? videoDuration
: startTime + 4;
}
newAnnotation = FrameTrail.module('HypervideoModel').newAnnotation({
"start": startTime,
"end": endTime,
"resourceId": resourceID
});
newAnnotation.renderInDOM();
newAnnotation.startEditing();
updateStatesOfAnnotations(FrameTrail.module('HypervideoController').currentTime);
stackTimelineView();
ViewVideo.PlayerProgress.find('.ui-slider-handle').removeClass('highlight');
}
});
} else {
ViewVideo.AnnotationTimeline.droppable('destroy');
}
}
/**
* I am the starting point for the process of deleting
* an annotation.
* @method deleteAnnotation
* @param {Annotation} annotation
*/
function deleteAnnotation(annotation) {
setAnnotationInFocus(null);
annotation.removeFromDOM();
distributeTiles();
FrameTrail.module('HypervideoModel').removeAnnotation(annotation);
stackTimelineView();
}
/**
* When we enter the viewMode 'video', we have to update the
* distribution of tiles accoring to the current browser width.
* @method toggleViewMode
* @param {String} viewMode
* @param {String} oldViewMode
* @return
*/
function toggleViewMode(viewMode, oldViewMode){
if (viewMode === 'video' && oldViewMode !== 'video') {
distributeTiles();
}
}
/**
* I react to a change in the global state "userColor"
* @method changeUserColor
* @param {String} color
*/
function changeUserColor(newColor) {
var annotationSets = HypervideoModel.annotationSets,
SelectAnnotation = FrameTrail.module('Sidebar').SelectAnnotationContainer.find('#SelectAnnotation');
for (var idx in annotationSets) {
if (annotationSets[idx].id == FrameTrail.module('UserManagement').userID && newColor.length > 1) {
annotationSets[idx].color = newColor;
}
}
if (newColor.length > 1 && SelectAnnotation.stylableselectmenu) {
refreshSidebarSelectmenu(true);
}
}
/**
* I am called when the global state "hv_config_annotationsVisible" changes.
*
* This is a configuration option (saved in the hypervideo's index.json entry).
*
* @method toggleConfig_annotationsVisible
* @param {Boolean} newState
* @param {Boolean} oldState
*/
function toggleConfig_annotationsVisible(newState, oldState) {
if (newState == true) {
refreshSidebarSelectmenu(true);
} else {
refreshSidebarSelectmenu(false);
}
}
return {
onChange: {
editMode: toggleEditMode,
viewSize: changeViewSize,
viewSizeChanged: onViewSizeChanged,
sidebarOpen: toggleSidebarOpen,
viewMode: toggleViewMode,
userColor: changeUserColor,
hv_config_annotationsVisible: toggleConfig_annotationsVisible,
},
initController: initController,
updateController: updateController,
updateStatesOfAnnotations: updateStatesOfAnnotations,
stackTimelineView: stackTimelineView,
deleteAnnotation: deleteAnnotation,
findTopMostActiveAnnotation: findTopMostActiveAnnotation,
/**
* An annotation can be selected to be
* the annotationInFocus (either by clicking or dragging/resizing).
* The annotation then displays additional controls in the #EditPropertiesControls
* element of {{#crossLink "ViewVideo"}}ViewVideo{{/crossLink}}
* @attribute annotationInFocus
* @type Annotation or null
*/
set annotationInFocus(annotation) { return setAnnotationInFocus(annotation) },
get annotationInFocus() { return annotationInFocus },
/**
* An annotation can be opened.
* This means it opens the AnnotationsConatiner, where it has
* already rendered its content (the annotationElement) into.
* @attribute openedAnnotation
* @type Annotation or null
*/
get openedAnnotation() { return openedAnnotation },
set openedAnnotation(annotation) { return setOpenedAnnotation(annotation) }
};
});