1/** 2 * EGroupware - SmallParT - videobar widget 3 * 4 * @link https://www.egroupware.org 5 * @package smallpart 6 * @subpackage Ui 7 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 8 */ 9 10import {et2_video} from "../../api/js/etemplate/et2_widget_video"; 11import {et2_createWidget, et2_register_widget, WidgetConfig} from "../../api/js/etemplate/et2_core_widget"; 12import {ClassWithAttributes} from '../../api/js/etemplate/et2_core_inheritance'; 13import {CommentType} from './app'; 14 15type CommentMarked = Array<{x: number; y: number; c: string}>; 16export class et2_smallpart_videobar extends et2_video implements et2_IResizeable 17{ 18 19 static readonly _attributes : any = { 20 "marking_enabled": { 21 "name": "Marking", 22 "type": "boolean", 23 "description": "", 24 "default": false 25 }, 26 27 "marking_readonly": { 28 "name": "Marking readonly", 29 "type": "boolean", 30 "description": "", 31 "default": true 32 }, 33 34 "marking_color": { 35 "name": "Marking color", 36 "type": "string", 37 "description": "", 38 "default": "ffffff" 39 }, 40 41 "marking_callback": { 42 43 }, 44 45 "slider_callback": { 46 "name": "Slider on click callback", 47 "type":"js", 48 "default": et2_no_init, 49 "description": "Callback function to get executed after clicking om slider bar" 50 }, 51 52 "slider_tags": { 53 "name": "slider tags", 54 "type": "any", 55 "description": "comment tags on slider", 56 "default": {} 57 }, 58 59 "stop_contextmenu": { 60 "name": "stop contextmenu", 61 "type": "boolean", 62 "description": "This would prevent the browser native contextmenu on video tag", 63 "default": true 64 }, 65 "ontimeupdate_callback": { 66 "name": "ontimeupdate callback", 67 "type":"js", 68 "default": et2_no_init, 69 "description": "Callback function to get executed while video is playing" 70 }, 71 "onresize_callback": { 72 "name": "onresize callback", 73 'type': "js", 74 "default": et2_no_init, 75 "description": "Callback function called when video gets resized" 76 } 77 }; 78 79 private container: JQuery = null; 80 81 private wrapper: JQuery = null; 82 83 private slider: JQuery = null; 84 85 private marking: JQuery = null; 86 87 private timer = null; 88 89 private slider_progressbar: JQuery = null; 90 91 private comments: Array<CommentType> = null; 92 93 private mark_ratio: number = 0; 94 95 private marking_color: string = 'ffffff'; 96 97 private marks: CommentMarked = []; 98 99 private marking_readonly: boolean = true; 100 101 private _scrolled: Array = []; 102 103 /** 104 * 105 * @memberOf et2_DOMWidget 106 */ 107 constructor(_parent, _attrs? : WidgetConfig, _child? : object) 108 { 109 // Call the inherited constructor 110 super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_smallpart_videobar._attributes, _child || {})); 111 112 // wrapper DIV container for video tag and marking selector 113 this.wrapper = jQuery(document.createElement('div')) 114 .append(this.video) 115 .addClass('videobar_wrapper'); 116 117 // widget container 118 this.container = jQuery(document.createElement('div')) 119 .append(this.wrapper) 120 .addClass('et2_smallpart_videobar videobar_container'); 121 122 // slider div 123 this.slider = jQuery(document.createElement('div')) 124 .appendTo(this.container) 125 .addClass('videobar_slider'); 126 127 // marking div 128 this.marking = jQuery(document.createElement('div')) 129 .addClass('videobar_marking container'); 130 this.marking.append(jQuery(document.createElement('div')) 131 .addClass('markingMask maskOn')); 132 this.marking.append(jQuery(document.createElement('div')) 133 .addClass('marksContainer')); 134 135 // slider progressbar span 136 this.slider_progressbar = jQuery(document.createElement('span')) 137 .addClass('videobar_slider_progressbar') 138 .appendTo(this.slider); 139 140 this.wrapper.append(this.marking); 141 142 this._buildHandlers(); 143 144 // timer span 145 this.timer = et2_createWidget('smallpart-videotime', {}, this); 146 147 //@TODO: this should not be necessary but for some reason attach to the dom 148 // not working on et2_creatWidget there manully attach it here. 149 jQuery(this.timer.getDOMNode()).attr('id', this.id+"[timer]") 150 this.container.append(this.timer.getDOMNode()); 151 152 if (this.options.stop_contextmenu) this.video.on('contextmenu', function(){return false;}); 153 154 this.setDOMNode(this.container[0]); 155 } 156 157 private _buildHandlers() 158 { 159 var self = this; 160 this.slider.on('click', function(e){ 161 self._slider_onclick.call(self ,e); 162 }); 163 164 } 165 166 private _slider_onclick(e:JQueryMouseEventObject) 167 { 168 this.slider_progressbar.css({width:e.offsetX}); 169 this._scrolled = []; 170 this.video[0]['previousTime'] = this.video[0]['currentTime']; 171 this.video[0]['currentTime'] = e.offsetX * this.video[0].duration / this.slider.width(); 172 this.timer.set_value(this.video[0]['currentTime']); 173 if (typeof this.slider_callback == "function") this.slider_callback(this.video[0], this); 174 } 175 176 doLoadingFinished(): boolean 177 { 178 super.doLoadingFinished(); 179 let self = this; 180 181 this.video[0].addEventListener("loadedmetadata", function(){ 182 self._videoLoadnigIsFinished(); 183 }); 184 return false; 185 } 186 187 public _vtimeToSliderPosition(_vtime: string | number): number 188 { 189 return this.slider.width() / this.video[0]['duration'] * parseFloat(<string>_vtime); 190 } 191 192 public set_slider_tags(_comments: Array<CommentType>) 193 { 194 this.comments = _comments; 195 // need to wait video is loaded before setting tags 196 if (this.video.width() == 0) return; 197 198 this.slider.empty(); 199 this.slider.append(this.slider_progressbar); 200 for (let i in this.comments) 201 { 202 if (!this.comments[i]) continue; 203 this.slider.append(jQuery(document.createElement('span')) 204 .offset({left: this._vtimeToSliderPosition(this.comments[i]['comment_starttime'])}) 205 .css({'background-color': '#'+this.comments[i]['comment_color']}) 206 .attr('data-id', this.comments[i]['comment_id']) 207 .addClass('commentOnSlider commentColor'+this.comments[i]['comment_color'])); 208 } 209 } 210 211 public set_marking_readonly(_state) 212 { 213 this.marking_readonly = _state; 214 } 215 216 public set_marking_color (_color) 217 { 218 this.marking_color = _color; 219 } 220 221 public set_marking_enabled(_state: boolean, _callback) 222 { 223 let self= this; 224 let isDrawing = false; 225 this.marking.toggle(_state); 226 let drawing = function(e) 227 { 228 if (e.target.nodeName !== "SPAN" && !self.marking_readonly) 229 { 230 let pixelX = Math.floor(e.originalEvent.offsetX / self.mark_ratio) * self.mark_ratio; 231 let pixelY = Math.floor(e.originalEvent.offsetY / self.mark_ratio) * self.mark_ratio; 232 let mark = { 233 x: self._convertMarkedPixelX2Percent(pixelX), 234 y: self._convertMarkedPixelY2Percent(pixelY), 235 c: self.marking_color 236 }; 237 self._addMark(mark); 238 _callback(mark); 239 } 240 }; 241 if (_state) 242 { 243 this.marking.find('.marksContainer') 244 .off() 245 .on('mousedown', function(e){ 246 console.log('mousedown') 247 isDrawing = true; 248 }) 249 .on('mouseup', function(e){ 250 isDrawing = false; 251 drawing(e); 252 }) 253 .on('mousemove', function(e){ 254 if (isDrawing === true) { 255 drawing(e); 256 } 257 }); 258 } 259 } 260 261 public setMarkingMask(_state: boolean) 262 { 263 if (_state) 264 { 265 this.marking.find('.markingMask').addClass('maskOn'); 266 } 267 else 268 { 269 this.marking.find('.markingMask').removeClass('maskOn'); 270 } 271 } 272 273 public setMarksState(_state: boolean) 274 { 275 this.marking.find('.marksContainer').toggle(_state); 276 } 277 278 public setMarks(_marks: CommentMarked) 279 { 280 let self = this; 281 // clone the array to avoid missing its original content 282 let $marksContainer = this.marking.find('.marksContainer').empty(); 283 this.marks = _marks?.slice(0) || []; 284 this.mark_ratio = parseFloat((this.video.width() / 80).toPrecision(4)); 285 for(let i in _marks) 286 { 287 $marksContainer.append(jQuery(document.createElement('span')) 288 .offset({left: this._convertMarkPercentX2Pixel(_marks[i]['x']), top: this._convertMarkPercentY2Pixel(_marks[i]['y'])}) 289 .css({ 290 "background-color":"#"+_marks[i]['c'], 291 "width": this.mark_ratio, 292 "height": this.mark_ratio 293 }) 294 .attr('data-color', _marks[i]['c']) 295 .click(function(){ 296 if (!self.marking_readonly) self._removeMark(self._getMark(this), this); 297 }) 298 .addClass('marks')); 299 } 300 } 301 302 public getMarks(): CommentMarked 303 { 304 if (this.marks) return this.marks; 305 let $marks = this.marking.find('.marksContainer').find('span.marks'); 306 let marks = []; 307 let self =this; 308 $marks.each(function(){ 309 marks.push({ 310 x: self._convertMarkedPixelX2Percent(parseFloat(this.style.left)), 311 y: self._convertMarkedPixelY2Percent(parseFloat(this.style.top)), 312 c: this.dataset['color'] 313 }) 314 }); 315 this.marks = marks; 316 return marks; 317 } 318 319 private _getMark(_node: HTMLElement): CommentMarked 320 { 321 return [{ 322 x: this._convertMarkedPixelX2Percent(parseFloat(_node.style.left)), 323 y: this._convertMarkedPixelY2Percent(parseFloat(_node.style.top)), 324 c: _node.dataset['color'] 325 }]; 326 } 327 328 private _addMark(_mark) 329 { 330 this.marks.push(_mark); 331 this.setMarks(this.marks); 332 } 333 334 public removeMarks() 335 { 336 this.marks = []; 337 this.marking.find('.marksContainer').find('span.marks').remove(); 338 } 339 340 private _removeMark(_mark: CommentMarked, _node: HTMLElement) 341 { 342 for (let i in this.marks) 343 { 344 if (this.marks[i]['x'] == _mark[0]['x'] && this.marks[i]['y'] == _mark[0]['y']) this.marks.splice(<number><unknown>i, 1); 345 } 346 if (_node) jQuery(_node).remove(); 347 } 348 349 private _convertMarkedPixelX2Percent(_x: number): number 350 { 351 return parseFloat((_x / this.video.width() / 0.01).toPrecision(4)); 352 } 353 354 private _convertMarkedPixelY2Percent(_y: number): number 355 { 356 return parseFloat((_y / this.video.height() / 0.01).toPrecision(4)); 357 } 358 359 private _convertMarkPercentX2Pixel(_x: number): number 360 { 361 return _x * this.video.width() * 0.01; 362 } 363 364 private _convertMarkPercentY2Pixel(_y: number): number 365 { 366 return _y * this.video.height() * 0.01; 367 } 368 369 /** 370 * Seek to a time / position 371 * 372 * @param _vtime in seconds 373 */ 374 public seek_video(_vtime : number) 375 { 376 super.seek_video(_vtime); 377 this._scrolled = []; 378 this.timer.set_value(this.video[0]['currentTime']); 379 this.slider_progressbar.css({width: this._vtimeToSliderPosition(_vtime)}); 380 } 381 382 /** 383 * Play video 384 */ 385 public play_video(_ended_callback, _onTagCallback) : Promise<void> 386 { 387 let self = this; 388 let ended_callback = _ended_callback; 389 this._scrolled = []; 390 return super.play_video().then(function(){ 391 self.video[0].ontimeupdate = function(_event){ 392 self.slider_progressbar.css({width: self._vtimeToSliderPosition(self.video[0].currentTime)}); 393 self.timer.set_value(self.video[0]['currentTime']); 394 if (typeof ended_callback == "function" && self.video[0].ended) 395 { 396 ended_callback.call(); 397 self.pause_video(); 398 } 399 if (typeof _onTagCallback == "function") { 400 for (let i in self.comments) 401 { 402 if (Math.floor(self.video[0].currentTime) == parseInt(self.comments[i]['comment_starttime']) 403 && (self._scrolled.length == 0 || self._scrolled.indexOf(parseInt(self.comments[i]['comment_id'])) == -1 )) 404 { 405 _onTagCallback.call(this, self.comments[i]['comment_id']); 406 self._scrolled.push(parseInt(self.comments[i]['comment_id'])); 407 } 408 } 409 } 410 if (typeof self.ontimeupdate_callback == "function") { 411 self.ontimeupdate_callback.call(this, Math.floor(self.video[0].currentTime)); 412 } 413 }; 414 }); 415 } 416 417 /** 418 * Pause video 419 */ 420 public pause_video() 421 { 422 super.pause_video(); 423 } 424 425 private _videoLoadnigIsFinished() 426 { 427 // this will make sure that slider and video are synced 428 this.slider.width(this.video.width()); 429 this.set_slider_tags(this.comments); 430 this.marking.css({width: this.video.width(), height: this.video.height()}); 431 } 432 433 resize (_height) 434 { 435 this.slider.width('auto'); 436 this.marking.width('auto'); 437 this.slider.width(this.video.width()); 438 this.marking.css({width: this.video.width(), height: this.video.height()}); 439 this.slider_progressbar.css({width: this._vtimeToSliderPosition(this.video[0].currentTime)}); 440 //redraw marks and tags to get the right ratio 441 this.setMarks(this.getMarks()); 442 this.set_slider_tags(this.comments); 443 if (typeof this.onresize_callback == 'function') this.onresize_callback.call(this, this.video.width(), this.video.height(), this._vtimeToSliderPosition(this.video[0].currentTime)) 444 } 445 446 /** 447 * return slider dom node as jquery object 448 */ 449 getSliderDOMNode() 450 { 451 return this.slider; 452 } 453} 454et2_register_widget(et2_smallpart_videobar, ["smallpart-videobar"]);