diff --git a/source/UI/css/UI.css b/source/UI/css/UI.css index c13be065..2b29ba4e 100644 --- a/source/UI/css/UI.css +++ b/source/UI/css/UI.css @@ -16,6 +16,9 @@ a:hover, .OxLink:hover { blockquote { margin: 0 1.5em 0 1.5em; } +html, body { + overscroll-behavior: none; +} body { margin: 0; cursor: default; diff --git a/source/UI/js/Form/ArrayEditable.js b/source/UI/js/Form/ArrayEditable.js index f361569d..11f62ff9 100644 --- a/source/UI/js/Form/ArrayEditable.js +++ b/source/UI/js/Form/ArrayEditable.js @@ -25,6 +25,7 @@ Ox.ArrayEditable = function(options, self) { var that = Ox.Element({}, self) .defaults({ clickLink: null, + confirmDeleteDialog: null, editable: true, format: null, getSortValue: null, @@ -116,23 +117,31 @@ Ox.ArrayEditable = function(options, self) { self.selected = getSelectedPosition(); - function deleteItem(id) { + function deleteItem(id, empty) { // it is possible to delete an (empty-value) item // by selecting another one id = id || self.options.selected; - var position = Ox.getIndexById(self.options.items, id) - if (self.options.editable) { - self.options.items.splice(position, 1); - renderItems(); - that.triggerEvent('delete', {id: id}); - self.editing = false; - if (self.options.selected == id) { - self.selected = -1; - self.options.selected = ''; - } else if (self.options.selected) { - self.selected = Ox.getIndexById(self.options.item, self.options.selected) + + (self.options.confirmDeleteDialog && !empty ? self.options.confirmDeleteDialog: Ox.noop)({ + items: Ox.isArray(id) ? id : [id], + }, function(result) { + if (self.options.editable) { + (Ox.isArray(id) ? id : [id]).forEach(id => { + var position = Ox.getIndexById(self.options.items, id) + self.options.items.splice(position, 1); + that.triggerEvent('delete', {id: id}); + }) + renderItems(); + self.editing = false; + if (self.options.selected == id) { + self.selected = -1; + self.options.selected = ''; + } else if (self.options.selected) { + self.selected = Ox.getIndexById(self.options.item, self.options.selected) + } } - } + }) + } function doubleclick(e) { @@ -146,12 +155,20 @@ Ox.ArrayEditable = function(options, self) { } } + function getId(position) { + return position > -1 ? self.options.items[position].id : ''; + } + function getSelectedId() { - return self.selected > -1 ? self.options.items[self.selected].id : ''; + return getId(self.selected); + } + + function getPosition(id) { + return Ox.getIndexById(self.options.items, id); } function getSelectedPosition() { - return Ox.getIndexById(self.options.items, self.options.selected); + return getPosition(self.options.selected); } function renderItems(blur) { @@ -267,6 +284,50 @@ Ox.ArrayEditable = function(options, self) { : that.triggerEvent('selectprevious'); } } + function toggleItem(idOrPosition) { + var id, position; + if (Ox.isString(idOrPosition)) { + id = idOrPosition + position = getPosition(id); + } else { + position = idOrPosition + id = getId(position); + } + if (!Ox.isArray(self.options.selected)) { + self.options.selected = [self.options.selected] + } + if (!Ox.isArray(self.selected)) { + self.selected = [self.selected] + } + if (!self.options.selected.includes(id)) { + var minPos, maxPos; + if (Ox.min(self.selected) > position) { + minPos = position + maxPos = Ox.min(self.selected) + } else { + minPos = Ox.max(self.selected) + 1 + maxPos = position + 1 + } + Ox.range(minPos, maxPos).forEach(pos => { + self.selected.push(pos) + self.options.selected.push(getId(pos)) + }) + + } + that.find('.OxSelected').removeClass('OxSelected').removeClass('OxMultiple'); + that.find('.OxGroup').removeClass('OxGroup') + if (self.options.selected.length == 1) { + self.options.selected = self.options.selected[0] + self.selected = self.selected[0] + self.$items[self.selected].addClass('OxSelected'); + self.options.highlightGroup && that.updateItemGroup() + } else { + Ox.forEach(self.selected, function(selected) { + self.$items[selected].addClass('OxSelected').addClass('OxMultiple'); + }) + } + triggerSelectEvent(); + } function selectItem(idOrPosition) { if (Ox.isString(idOrPosition)) { @@ -327,8 +388,14 @@ Ox.ArrayEditable = function(options, self) { position = $element.data('position'); // if clicked on an element if (position != self.selected) { - // select another item - selectItem(position); + if (e.shiftKey && self.selected != -1) { + // add/remove item from multiple selection + document.getSelection().removeAllRanges(); + toggleItem(position); + } else { + // select another item + selectItem(position); + } } else if (e.metaKey) { // or deselect current item selectNone(); @@ -370,7 +437,7 @@ Ox.ArrayEditable = function(options, self) { var item = Ox.getObjectById(self.options.items, id); Ox.Log('AE', 'submitItem', id, item) if (value === '') { - deleteItem(item.id); + deleteItem(item.id, true); } else { that.triggerEvent(item.value === value ? 'blur' : 'submit', { id: item.id, diff --git a/source/UI/js/Video/AnnotationFolder.js b/source/UI/js/Video/AnnotationFolder.js index 4b04dcd4..2284f36d 100644 --- a/source/UI/js/Video/AnnotationFolder.js +++ b/source/UI/js/Video/AnnotationFolder.js @@ -35,6 +35,7 @@ Ox.AnnotationFolder = function(options, self) { var that = Ox.Element({}, self) .defaults({ clickLink: null, + confirmDeleteDialog: null, collapsed: false, editable: false, highlight: '', @@ -281,6 +282,7 @@ Ox.AnnotationFolder = function(options, self) { } self.$annotations = Ox.ArrayEditable(Ox.extend({ clickLink: self.options.clickLink, + confirmDeleteDialog: self.options.confirmDeleteDialog, editable: self.options.editable, getSortValue: self.options.type == 'text' ? function(value) { @@ -579,26 +581,40 @@ Ox.AnnotationFolder = function(options, self) { function selectAnnotation(data) { if (self.loaded) { - var item = Ox.getObjectById(self.options.items, data.id); - self.options.selected = item ? data.id : ''; - if (self.widget) { - if (self.options.type == 'event') { - self.$calendar.options({ - selected: item && isDefined(item) ? item.event.id : '' - }) - .panToEvent(); - } else { - self.$map.options({ - selected: item && isDefined(item) ? item.place.id : '' - }) - .panToPlace(); + var extendedData; + if (Ox.isArray(data.id) && data.id.length) { + var items = data.id.map(id => { + return Ox.getObjectById(self.options.items, id); + }) + self.options.selected = data.id; + extendedData = Ox.extend(data, { + 'in': Ox.min(items.map(item => { return item["in"]; })), + out: Ox.max(items.map(item => { return item["out"]; })), + layer: self.options.id + }); + } else { + var item = Ox.getObjectById(self.options.items, data.id) + self.options.selected = item ? data.id : ''; + if (self.widget) { + if (self.options.type == 'event') { + self.$calendar.options({ + selected: item && isDefined(item) ? item.event.id : '' + }) + .panToEvent(); + } else { + self.$map.options({ + selected: item && isDefined(item) ? item.place.id : '' + }) + .panToPlace(); + } } + extendedData = Ox.extend(data, item ? { + 'in': item['in'], + out: item.out, + layer: self.options.id + } : {}); } - that.triggerEvent('select', Ox.extend(data, item ? { - 'in': item['in'], - out: item.out, - layer: self.options.id - } : {})); + that.triggerEvent('select', extendedData) } } diff --git a/source/UI/js/Video/AnnotationPanel.js b/source/UI/js/Video/AnnotationPanel.js index 7e03f779..658dc9f4 100644 --- a/source/UI/js/Video/AnnotationPanel.js +++ b/source/UI/js/Video/AnnotationPanel.js @@ -48,9 +48,11 @@ Ox.AnnotationPanel = function(options, self) { .defaults({ calendarSize: 256, clickLink: null, + confirmDeleteDialog: null, editable: false, enableExport: false, enableImport: false, + enableTranscribe: false, highlight: '', highlightAnnotations: 'none', highlightLayer: '*', @@ -303,7 +305,11 @@ Ox.AnnotationPanel = function(options, self) { {}, {id: 'import', title: Ox._('Import Annotations...'), disabled: !self.options.enableImport}, {id: 'export', title: Ox._('Export Annotations...'), disabled: !self.options.enableExport}, - ] + ].concat( + self.options.enableTranscribe ? [ + {id: 'transcribeaudio', title: Ox._('Transcribe Audio...')}, + ]: [] + ) ), maxWidth: 256, style: 'square', @@ -351,6 +357,8 @@ Ox.AnnotationPanel = function(options, self) { // ... } else if (data.id == 'showentityinfo') { that.triggerEvent('showentityinfo', annotation.entity); + } else if (data.id == 'transcribeaudio') { + that.triggerEvent('transcribeaudio'); } else if (data.id == 'undo') { // ... } @@ -371,6 +379,7 @@ Ox.AnnotationPanel = function(options, self) { self.$folder[index] = Ox.AnnotationFolder( Ox.extend({ clickLink: self.options.clickLink, + confirmDeleteDialog: self.options.confirmDeleteDialog, collapsed: !self.options.showLayers[layer.id], editable: self.options.editable, highlight: getHighlight(layer.id), diff --git a/source/UI/js/Video/VideoAnnotationPanel.js b/source/UI/js/Video/VideoAnnotationPanel.js index 5ce19326..3d52533a 100644 --- a/source/UI/js/Video/VideoAnnotationPanel.js +++ b/source/UI/js/Video/VideoAnnotationPanel.js @@ -61,11 +61,13 @@ Ox.VideoAnnotationPanel = function(options, self) { censoredIcon: '', censoredTooltip: '', clickLink: null, + confirmDeleteDialog: null, cuts: [], duration: 0, enableDownload: false, enableImport: false, enableExport: false, + enableTranscribe: false, enableSetPosterFrame: false, enableSubtitles: false, find: '', @@ -844,9 +846,11 @@ Ox.VideoAnnotationPanel = function(options, self) { autocomplete: self.options.autocomplete, calendarSize: self.options.annotationsCalendarSize, clickLink: self.options.clickLink, + confirmDeleteDialog: self.options.confirmDeleteDialog, editable: true, enableExport: self.options.enableExport, enableImport: self.options.enableImport, + enableTranscribe: self.options.enableTranscribe, highlight: self.options.find, highlightAnnotations: self.options.annotationsHighlight, highlightLayer: self.options.findLayer, @@ -949,6 +953,9 @@ Ox.VideoAnnotationPanel = function(options, self) { that.triggerEvent('showentityinfo', data); }, submit: submitAnnotation, + transcribeaudio: function() { + that.triggerEvent('transcribeaudio'); + }, toggle: toggleAnnotations, togglecalendar: function(data) { self.options.showAnnotationsCalendar = !data.collapsed;