'use strict'; /*@ Ox.AnnotationPanel Video Annotation Panel ([options[, self]]) -> AnnotationPanel Object options Options object calendarSize calendar size clickLink click link callback editable: if true, annotations can be edited font small, medium, large highlight highlight given string in annotations layers array with annotation objects mapSize map size range all, position, selection selected selected annotation showCalendar if true, calendar is shown showFonts if true, option to select font size is show showLayers object with layers to show showMap if true, show map showUsers if true show user sort position, start, text width panel width self shared private variable @*/ Ox.AnnotationPanel = function(options, self) { self = self || {}; var that = Ox.Element({}, self) .defaults({ calendarSize: 256, clickLink: null, editable: false, font: 'small', highlight: '', layers: [], mapSize: 256, range: 'all', selected: '', showCalendar: false, showFonts: false, showLayers: {}, showMap: false, showUsers: false, sort: 'position', width: 256 }) .options(options || {}) .addClass('OxAnnotationPanel'); self.editing = false; if (self.options.showUsers) { self.users = getUsers(); self.enabledUsers = self.users; } self.$menubar = Ox.Bar({ size: 16 }) .addClass('OxVideoPlayer'); self.$folders = Ox.Element().css({overflowY: 'auto'}); self.$folder = []; self.options.layers.forEach(function(layer, i) { var item = Ox.getObjectById(layer.items, self.options.selected), selected = item ? item.id : ''; self.$folder[i] = Ox.AnnotationFolder( Ox.extend({ clickLink: self.options.clickLink, collapsed: !self.options.showLayers[layer.id], editable: self.options.editable, font: self.options.font, highlight: self.options.highlight, 'in': self.options['in'], out: self.options.out, position: self.options.position, range: self.options.range, selected: selected, sort: self.options.sort, width: self.options.width - Ox.UI.SCROLLBAR_SIZE }, layer, layer.type == 'event' ? { showWidget: self.options.showCalendar, widgetSize: self.options.calendarSize } : layer.type == 'place' ? { showWidget: self.options.showMap, widgetSize: self.options.mapSize } : {}) ) .bindEvent({ add: function(data) { that.triggerEvent('add', Ox.extend({layer: layer.id}, data)); }, blur: function() { that.triggerEvent('blur'); }, change: function(data) { that.triggerEvent('change', Ox.extend({layer: layer.id}, data)); }, edit: function() { self.editing = true; renderEditMenu(); that.triggerEvent('edit'); }, info: function(data) { that.triggerEvent('info', {layer: layer.id}); }, insert: insert, open: function() { that.triggerEvent('open'); }, remove: function(data) { that.triggerEvent('remove', Ox.extend({layer: layer.id}, data)); }, resizewidget: function(data) { that.triggerEvent('resize' + ( layer.type == 'event' ? 'calendar' : 'map' ), data); }, select: function(data) { selectAnnotation(data, i); }, selectafter: function() { selectNext(layer.id, 1); }, selectbefore: function() { selectNext(layer.id, -1); }, selectnone: selectNone, submit: function(data) { that.triggerEvent('submit', Ox.extend({layer: layer.id}, data)); }, togglelayer: function(data) { self.options.showLayers[layer.id] = !data.collapsed; that.triggerEvent('togglelayer', Ox.extend({layer: layer.id}, data)); }, togglewidget: function(data) { that.triggerEvent('toggle' + ( layer.type == 'event' ? 'calendar' : 'map' ), data); }, }) .appendTo(self.$folders); [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'b', 'backslash', 'closebracket', 'comma', 'dot', 'equal', 'f', 'g', 'h', 'i', 'minus', 'n', 'o', 'openbracket', 'p', 'shift_0', 'shift_equal', 'shift_g', 'shift_i', 'shift_minus', 'shift_o', 'slash', 'space' ].forEach(function(key) { key = 'key_' + key; self.$folder[i].bindEvent(key, function() { that.triggerEvent(key); }); }); }); renderOptionsMenu(); self.options.editable && renderEditMenu(); that.setElement( Ox.SplitPanel({ elements: [ { element: self.$menubar, size: 16 }, { element: self.$folders } ], orientation: 'vertical' }) ); self.options.selected && getFolder(self.options.selected) && scrollToSelected( getFolder(self.options.selected).options('type') ); function getAnnotation(annotationId) { var found = false, annotation; Ox.forEach(self.options.layers, function(layer, i) { Ox.forEach(layer.items, function(item) { if (item.id == annotationId) { annotation = item; found = true; Ox.break(); } }); found && Ox.break(); }); return annotation; } function getFolder(annotationId) { var found = false, folder; Ox.forEach(self.options.layers, function(layer, i) { Ox.forEach(layer.items, function(item) { if (item.id == annotationId) { folder = self.$folder[i]; found = true; Ox.break(); } }); found && Ox.break(); }); return folder; } function getUsers() { return Ox.sort(Ox.unique(Ox.flatten( self.options.layers.map(function(layer) { return layer.items.map(function(item) { return item.user; }); }) ))); } function insert(data) { var id = data.id; Ox.InsertHTMLDialog(Ox.extend({ callback: function(data) { Ox.UI.elements[id] .value(data.value) .focusInput(data.position) .triggerEvent('change', data.value); } }, data)).open(); } function renderEditMenu() { var annotation, annotationTitle, folder, isDefined, isEditable, isEvent, isEventOrPlace, isPlace, isString, key, manageTitle, type, value; if (self.options.selected) { annotation = getAnnotation(self.options.selected); folder = getFolder(self.options.selected); if(annotation && folder) { key = folder.options('id'); type = folder.options('type'); value = annotation.value; isEditable = annotation.editable; isEvent = type == 'event'; isPlace = type == 'place'; isEventOrPlace = isEvent || isPlace; isString = type != 'text'; // fixme: absence of annotation[type] may be an error isDefined = isEventOrPlace && !!annotation[type] && !!annotation[type].type; annotationTitle = folder.options('item') + ': "' + value + '"'; } } manageTitle = (isDefined ? 'Edit' : 'Define') + ' ' + (isPlace ? 'Place' : isEvent ? 'Event' : 'Place or Event') + '...' self.$editMenuButton && self.$editMenuButton.remove(); self.$editMenuButton = Ox.MenuButton({ items: Ox.merge( [ {id: 'deselect', title: 'Deselect Annotation', disabled: !self.options.selected || self.editing, keyboard: 'escape'}, {id: 'edit', title: 'Edit Annotation', disabled: !self.options.selected || !isEditable || self.editing, keyboard: 'return'}, {id: 'delete', title: 'Delete Annotation', disabled: !self.options.selected || !isEditable, keyboard: 'delete'}, {}, {id: 'insert', title: 'Insert...', disabled: isString || !self.editing, keyboard: 'control i'}, {id: 'undo', title: 'Undo Changes', disabled: !self.editing, keyboard: 'escape'}, {id: 'save', title: 'Save Changes', disabled: !self.editing, keyboard: isString ? 'return' : 'shift return'}, ], pandora.site.map == 'manual' ? [ {}, {id: 'manage', title: manageTitle, disabled: !self.options.selected || !isEventOrPlace}, ] : [], isString ? [ {}, {id: 'annotation', title: annotationTitle, disabled: true}, {id: 'find', title: 'Find in This ' + pandora.site.itemName.singular}, {id: 'findannotations', title: 'Find in All ' + pandora.site.itemName.plural} ] : [] ), maxWidth: 256, style: 'square', title: 'edit', tooltip: 'Editing Options', type: 'image' }) .css({float: 'right'}) .bindEvent({ click: function(data) { if (data.id == 'delete') { getFolder(self.options.selected).removeItem(); } else if (data.id == 'deselect') { getFolder(self.options.selected).options({selected: ''}); } else if (data.id == 'edit') { getFolder(self.options.selected).editItem(); } else if (data.id == 'find') { that.triggerEvent('find', {value: value}); } else if (data.id == 'findannotations') { that.triggerEvent('findannotations', {key: key, value: value}); } else if (data.id == 'insert') { var id = $('.OxEditableElement div.OxInput').data('oxid'), element = $('.OxEditableElement textarea.OxInput')[0]; insert({ end: element.selectionEnd, id: id, selection: element.value .substr(element.selectionStart, element.selectionEnd), start: element.selectionStart, value: element.value }); } else if (data.id == 'manage') { that.triggerEvent('define', { id: getAnnotation(self.options.selected)[type].id, type: type }); } else if (data.id == 'save') { // ... } else if (data.id == 'undo') { // ... } }, hide: function() { self.options.selected ? getFolder(self.options.selected).gainFocus() : that.triggerEvent('focus'); } }) .appendTo(self.$menubar); } function renderOptionsMenu() { self.$optionsMenuButton && self.$optionsMenuButton.remove(); self.$optionsMenuButton = Ox.MenuButton({ items: Ox.merge( [ {id: 'showannotations', title: 'Show Annotations', disabled: true}, {group: 'range', min: 1, max: 1, items: [ {id: 'all', title: 'All', checked: self.options.range == 'all'}, {id: 'selection', title: 'In Current Selection', checked: self.options.range == 'selection'}, {id: 'position', title: 'At Current Position', checked: self.options.range == 'position'} ]}, {}, {id: 'sortannotations', title: 'Sort Annotations', disabled: true}, {group: 'sort', min: 1, max: 1, items: [ {id: 'position', title: 'By Position', checked: self.options.sort == 'position'}, {id: 'duration', title: 'By Duration', checked: self.options.sort == 'duration'}, {id: 'text', title: 'By Text', checked: self.options.sort == 'text'} ]} ], self.options.showFonts ? [ {}, {id: 'fontsize', title: 'Font Size', disabled: true}, {group: 'font', min: 1, max: 1, items: [ {id: 'small', title: 'Small', checked: self.options.font == 'small'}, {id: 'medium', title: 'Medium', checked: self.options.font == 'medium'}, {id: 'large', title: 'Large', checked: self.options.font == 'large'} ]} ] : [], self.options.showUsers && self.users.length ? [ {}, {id: 'users', title: 'Show Users', disabled: true}, {group: 'users', min: 1, max: -1, items: self.users.map(function(user) { return {id: user, title: Ox.encodeHTMLEntities(user), checked: self.enabledUsers == 'all' || self.enabledUsers.indexOf(user) > -1 }; })} ] : [] ), style: 'square', title: 'set', tooltip: 'Options', type: 'image' }) .css({float: 'left'}) .bindEvent({ change: function(data) { var set = {}; if (data.id == 'users') { self.enabledUsers = data.checked.map(function(checked) { return checked.id; }); self.$folder.forEach(function($folder) { $folder.options({users: self.enabledUsers}); }); } else { set[data.id] = data.checked[0].id; self.$folder.forEach(function($folder) { $folder.options(set); }); that.triggerEvent('annotations' + data.id, set); } }, hide: function() { self.options.selected ? getFolder(self.options.selected).gainFocus() : that.triggerEvent('focus'); } }) .appendTo(self.$menubar); } function scrollToSelected(type) { var $item = that.find('.OxEditableElement.OxSelected'), itemHeight = $item.height() + (type == 'text' ? 8 : 0), itemTop = ($item.offset() || {}).top, itemBottom = itemTop + itemHeight, height = self.$folders.height(), scrollTop = self.$folders.scrollTop(), top = self.$folders.offset().top; if (itemTop < top || itemBottom > top + height) { if (itemTop < top) { scrollTop += itemTop - top; } else { scrollTop += itemBottom - top - height; } self.$folders.animate({ scrollTop: scrollTop + 'px' }, 0); } } function selectAnnotation(data, index) { if (data.id) { Ox.forEach(self.$folder, function($folder, i) { if (i != index && $folder.options('selected')) { self.deselecting = true; $folder.options({selected: ''}); self.deselecting = false; Ox.break(); } }); scrollToSelected(self.options.layers[index].type); } if (!self.deselecting) { self.options.selected = data.id; self.options.editable && renderEditMenu(); that.triggerEvent('select', data); } } function selectNone() { if (self.options.selected) { getFolder(self.options.selected).options({selected: ''}); } } function selectNext(layer, direction) { var index = Ox.mod( Ox.getIndexById(self.options.layers, layer) + direction, self.options.layers.length ); self.$folder[index].selectItem(direction == 1 ? 0 : -1); } function updateEditMenu() { var action = self.options.selected ? 'enableItem' : 'disableItem'; self.$editMenuButton[action]('edit'); self.$editMenuButton[action]('delete'); } self.setOption = function(key, value) { if (key == 'highlight') { self.$folder.forEach(function($folder) { $folder.options({highlight: value}); }); } else if (['in', 'out', 'position'].indexOf(key) > -1) { self.$folder.forEach(function($folder) { $folder.options(key, value); }); } else if (key == 'selected') { self.options.editable && updateEditMenu(); if (value) { getFolder(value).options({selected: value}); } else { self.$folder.forEach(function($folder) { $folder.options({selected: ''}); }); } } else if (key == 'width') { self.$folder.forEach(function($folder) { $folder.options({width: self.options.width - Ox.UI.SCROLLBAR_SIZE}); }); } }; /*@ addItem add item (layer, item) -> add item to layer @*/ that.addItem = function(layer, item) { // called from addannotation callback var i = Ox.getIndexById(self.options.layers, layer); self.$folder[i].addItem(item); self.users = getUsers(); if (self.enabledUsers != 'all' && self.enabledUsers.indexOf(item.user) == -1) { self.enabledUsers.push(item.user); self.$folder[i].options({users: self.enabledUsers}); } renderOptionsMenu(); renderEditMenu(); }; /*@ blurItem blur item () -> blur selected item @*/ that.blurItem = function() { self.editing = false; getFolder(self.options.selected).blurItem(); renderEditMenu(); }; /*@ editItem editItem () -> put selected item into edit mode @*/ that.editItem = function() { self.editing = true; getFolder(self.options.selected).editItem(); renderEditMenu(); }; /*@ removeItem removeItem (remove) -> remove item @*/ that.removeItem = function(remove) { if (remove) { // remove initiated by video editor getFolder(self.options.selected).removeItem(); } else { // called from removeannotation callback self.options.selected = ''; self.users = getUsers(); renderOptionsMenu(); renderEditMenu(); } }; /*@ updateItem updateItem (id, item) -> update item @*/ that.updateItem = function(id, item) { // called from editannotation callback // on the first update of a new annotation, the id will change self.options.selected = item.id; getFolder(id).updateItem(id, item); renderEditMenu(); }; return that; };