vendor: 4,587 bytes, lines 1-6
1/*! 2 * Bootstrap v3.3.7 (http://getbootstrap.com) 3 * Copyright 2011-2016 Twitter, Inc. 4 * Licensed under the MIT license 5 */ 6if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){;var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(a){;function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(jQuery),+function(a){;function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.7",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a("#"===f?[]:f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){;function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.7",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target).closest(".btn");b.call(d,"toggle"),a(c.target).is('input[type="radio"], input[type="checkbox"]')||(c.preventDefault(),d.is("input,button")?d.trigger("focus"):d.find("input:visible,button:visible").first().trigger("focus"))}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){;function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("key
6down.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));if(!(a>this.$items.length-1||a<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){if(!this.sliding)return this.slide("next")},c.prototype.prev=function(){if(!this.sliding)return this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){;function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.7",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);
vendor: 2,709 bytes, line 6
6var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){;function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.7",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".
6navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&j<i.length-1&&j++,~j||(j=0),i.eq(j).trigger("focus")}}}};var h=a.fn.dropdown;a.fn.dropdown=d,a.fn.dropdown.Constructor=g,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=h,this},a(document).on("click.bs.dropdown.data-api",c).on("click.bs.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.bs.dropdown.data-api",f,g.prototype.toggle).on("keydown.bs.dropdown.data-api",f,g.prototype.keydown).on("keydown.bs.dropdown.data-api",".dropdown-menu",g.prototype.keydown)}(jQuery),+function(a){;function b(b,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},c.DEFAULTS,e.data(),"object"==typeof b&&b);f||e.data("bs.modal",f=new c(this,g)),"string"==typeof b?f[b](d):g.show&&f.show(d)})}var c=function(b,c){this.options=c,this.$body=a(document.body),this.$element=a(b),this.$dialog=this.$element.find(".modal-dialog"),this.$backdrop=null,this.isShown=null,this.originalBodyPad=null,this.scrollbarWidth=0,this.ignoreBackdropClick=!1,this.options.remote&&this.$element.find(".modal-content").load(this.options.remote,a.proxy(function(){this.$element.trigger("loaded.bs.modal")},this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=300,c.BACKDROP_TRANSITION_DURATION=150,c.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},c.prototype.toggle=function(a){return this.isShown?this.hide():this.show(a)},c.prototype.show=function(b){var d=this,e=a.Event("show.bs.modal",{relatedTarget:b});this.$element.trigger(e),this.isShown||e.isDefaultPrevented()||(this.isShown=!0,this.checkScrollbar(),this.setScrollbar(),this.$body.addClass("modal-open"),this.escape(),this.resize(),this.$element.on("click.dismiss.bs.modal",'[data-dismiss="modal"]',a.proxy(this.hide,this)),this.$dialog.on("mousedown.dismiss.bs.modal",function(){d.$element.one("mouseup.dismiss.bs.modal",function(b){a(b.target).is(d.$element)&&(d.ignoreBackdropClick=!0)})}),this.backdrop(function(){var e=a.support.transition&&d.$element.hasClass("fade");d.$element.parent().length||d.$element.appendTo(d.$body),d.$element.show().scrollTop(0),d.adjustDialog(),e&&d.$element[0].offsetWidth,d.$element.addClass("in"),d.enforceFocus();var f=a.Event("shown.bs.modal",{relatedTarget:b});e?d.$dialog.one("bsTransitionEnd",function(){d.$element.trigger("focus").trigger(f)}).emulateTransitionEnd(c.TRANSITION_DURATION):d.$element.trigger("focus").trigger(f)}))},c.prototype.hide=function(b){b&&b.preventDefault(),b=a.Event("hide.bs.modal"),this.$element.trigger(b),this.isShown&&!b.isDefaultPrevented()&&(this.isShown=!1,this.escape(),this.resize(),a(document).off("focusin.bs.modal"),this.$element.removeClass("in").off("click.dismiss.bs.modal").off("mouseup.dismiss.bs.modal"),this.$dialog.off("mousedown.dismiss.bs.modal"),a.support.transition&&this.$element.hasClass("fade")?this.$element.one("bsTransitionEnd",a.proxy(this.hideModal,this)).emulateTransitionEnd(c.TRANSITION_DURATION):this.hideModal())},c.prototype.enforceFocus=function(){a(document).off("focusin.bs.modal").on("focusin.bs.modal",a.proxy(function(a){document===a.target||this.$element[0]===a.target||this.$element.has(a.target).length||this.$element.trigger("focus")},this))},c.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keydown.dismiss.bs.modal",a.proxy(function(a){27==a.which&&this.hide()},this)):this.isShown||this.$element.off("keydown.dismiss.bs.modal")},c.prototype.resize=function(){this.isShown?a(window).on("resize.bs.modal",a.proxy(this.handleUpdate,this)):a(window).off("resize.bs.modal")},c.prototype.hideModal=function(){var a=this;this.$element.hide(),this.backdrop(function(){a.$body.removeClass("modal-open"),a.resetAdjustments(),a.resetScrollbar(),a.$element.trigger("hidden.bs.modal")})},c.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},c.prototype.backdrop=function(b){var d=this,e=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var f=a.support.transition&&e;if(this.$backdrop=a(document.createElement("div")).addClass("modal-backdrop "+e).appendTo(this.$body),this.$element.on("click.dismiss.bs.modal",a.proxy(function(a){return this.ignoreBackdropClick?void(this.ignoreBackdropClick=!1):void(a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus():this.hide()))},this)),f&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;f?this.$backdrop.one("bsTransitionEnd",b).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):b()}else if(!this.isShown&&this.$backdrop){this.$backdrop.removeClass("in");var g=function(){d.removeBackdrop(),b&&b()};a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one("bsTransitionEnd",g).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):g()}else b&&b()},c.prototype.handleUpdate=function(){this.adjustDialog()},c.prototype.adjustDialog=function(){var a=this.$element[0].scrollHeight>document.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth<a,this.scrollbarWidth=this.measureScrollbar()},c.prototype.setScrollbar=function(){var a=parseInt(this.$body.css("padding-right")||0,10);this.originalBodyPad=document.body.style.paddingRight||"",this.bodyIsOverflowing&&this.$body.css("padding-right",a+this.scrollbarWidth)},c.prototype.resetScrollbar=function(){this.$body.css("padding-right",this.originalBodyPad)},c.prototype.measureScrollbar=function(){var a=document.createElement("div");a.className="modal-scrollbar-measure",this.$body.append(a);var b=a.offsetWidth-a.clientWidth;return this.$body[0].removeChild(a),b};var d=a.fn.modal;a.fn.modal=b,a.fn.modal.Constructor=c,a.fn.modal.noConflict=function(){return a.fn.modal=d,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(c){var d=a(this),e=d.attr("href"),f=a(d.attr("data-target")||e&&e.replace(/.*(?=#[^\s]+$)/,"")),g=f.data("bs.modal")?"toggle":a.extend({remote:!/#/.test(e)&&e},f.data(),d.data());d.is("a")&&c.preventDefault(),f.one("show.bs.modal",function(a){a.isDefaultPrevented()||f.one("hidden.bs.modal",function(){d.is(":visible")&&d.trigger("focus")})}),b.call(f,g,this)})}(jQuery),+function(a){
vendor: 8,931 bytes, line 6
6;function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.tooltip",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.type=null,this.options=null,this.enabled=null,this.timeout=null,this.hoverState=null,this.$element=null,this.inState=null,this.init("tooltip",a,b)};c.VERSION="3.3.7",c.TRANSITION_DURATION=150,c.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-m<o.top?"bottom":"right"==h&&k.right+l>o.width?"left":"left"==h&&k.left-l<o.left?"right":h,f.removeClass(n).addClass(h)}var p=this.getCalculatedOffset(h,k,l,m);this.applyPlacement(p,h);var q=function(){var a=e.hoverState;e.$element.trigger("shown.bs."+e.type),e.hoverState=null,"out"==a&&e.leave(e)};a.support.transition&&this.$tip.hasClass("fade")?f.one("bsTransitionEnd",q).emulateTransitionEnd(c.TRANSITION_DURATION):q()}},c.prototype.applyPlacement=function(b,c){var d=this.tip(),e=d[0].offsetWidth,f=d[0].offsetHeight,g=parseInt(d.css("margin-top"),10),h=parseInt(d.css("margin-left"),10);isNaN(g)&&(g=0),isNaN(h)&&(h=0),b.top+=g,b.left+=h,a.offset.setOffset(d[0],a.extend({using:function(a){d.css({top:Math.round(a.top),left:Math.round(a.left)})}},b),0),d.addClass("in");var i=d[0].offsetWidth,j=d[0].offsetHeight;"top"==c&&j!=f&&(b.top=b.top+f-j);var k=this.getViewportAdjustedDelta(c,b,i,j);k.left?b.left+=k.left:b.top+=k.top;var l=/top|bottom/.test(c),m=l?2*k.left-e+i:2*k.top-f+j,n=l?"offsetWidth":"offsetHeight";d.offset(b),this.replaceArrow(m,d[0][n],l)},c.prototype.replaceArrow=function(a,b,c){this.arrow().css(c?"left":"top",50*(1-a/b)+"%").css(c?"top":"left","")},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},c.prototype.hide=function(b){function d(){"in"!=e.hoverState&&f.detach(),e.$element&&e.$element.removeAttr("aria-describedby").trigger("hidden.bs."+e.type),b&&b()}var e=this,f=a(this.$tip),g=a.Event("hide.bs."+this.type);if(this.$element.trigger(g),!g.isDefaultPrevented())return f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one("bsTransitionEnd",d).emulateTransitionEnd(c.TRANSITION_DURATION):d(),this.hoverState=null,this},c.prototype.fixTitle=function(){var a=this.$element;(a.attr("title")||"string"!=typeof a.attr("data-original-title"))&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},c.prototype.hasContent=function(){return this.getTitle()},c.prototype.getPosition=function(b){b=b||this.$element;var c=b[0],d="BODY"==c.tagName,e=c.getBoundingClientRect();null==e.width&&(e=a.extend({},e,{width:e.right-e.left,height:e.bottom-e.top}));var f=window.SVGElement&&c instanceof window.SVGElement,g=d?{top:0,left:0}:f?null:b.offset(),h={scroll:d?document.documentElement.scrollTop||document.body.scrollTop:b.scrollTop()},i=d?{width:a(window).width(),height:a(window).height()}:null;return a.extend({},e,h,i,g)},c.prototype.getCalculatedOffset=function(a,b,c,d){return"bottom"==a?{top:b.top+b.height,left:b.left+b.width/2-c/2}:"top"==a?{top:b.top-d,left:b.left+b.width/2-c/2}:"left"==a?{top:b.top+b.height/2-d/2,left:b.left-c}:{top:b.top+b.height/2-d/2,left:b.left+b.width}},c.prototype.getViewportAdjustedDelta=function(a,b,c,d){var e={top:0,left:0};if(!this.$viewport)return e;var f=this.options.viewport&&this.options.viewport.padding||0,g=this.getPosition(this.$viewport);if(/right|left/.test(a)){var h=b.top-f-g.scroll,i=b.top+f-g.scroll+d;h<g.top?e.top=g.top-h:i>g.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;j<g.left?e.left=g.left-j:k>g.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){
vendor: 5,891 bytes, lines 6-7
6;function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){;function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.7",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b<e[0])return this.activeTarget=null,this.clear();for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(void 0===e[a+1]||b<e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){ 7this.activeTarget=b,this.clear();var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")},b.prototype.clear=function(){a(this.selector).parentsUntil(this.options.target,".active").removeClass("active")};var d=a.fn.scrollspy;a.fn.scrollspy=c,a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=d,this},a(window).on("load.bs.scrollspy.data-api",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);c.call(b,b.data())})})}(jQuery),+function(a){;function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new c(this)),"string"==typeof b&&e[b]()})}var c=function(b){this.element=a(b)};c.VERSION="3.3.7",c.TRANSITION_DURATION=150,c.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a"),f=a.Event("hide.bs.tab",{relatedTarget:b[0]}),g=a.Event("show.bs.tab",{relatedTarget:e[0]});if(e.trigger(f),b.trigger(g),!g.isDefaultPrevented()&&!f.isDefaultPrevented()){var h=a(d);this.activate(b.closest("li"),c),this.activate(h,h.parent(),function(){e.trigger({type:"hidden.bs.tab",relatedTarget:b[0]}),b.trigger({type:"shown.bs.tab",relatedTarget:e[0]})})}}},c.prototype.activate=function(b,d,e){function f(){g.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){;function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;
7e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.7",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return e<c&&"top";if("bottom"==this.affixed)return null!=c?!(e+this.unpin<=f.top)&&"bottom":!(e+g<=a-d)&&"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&e<=c?"top":null!=d&&i+j>=a-d&&"bottom"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); 8 9 10 11/** 12 * @fileoverview Main function src. 13 */ 14 15// HTML5 Shiv. Must be in <head> to support older browsers. 16document.createElement('video'); 17document.createElement('audio'); 18document.createElement('track'); 19 20/** 21 * Doubles as the main function for users to create a player instance and also 22 * the main library object. 23 * 24 * **ALIASES** videojs, _V_ (deprecated) 25 * 26 * The `vjs` function can be used to initialize or retrieve a player. 27 * 28 * var myPlayer = vjs('my_video_id'); 29 * 30 * @param {String|Element} id Video element or video element ID 31 * @param {Object=} options Optional options object for config/settings 32 * @param {Function=} ready Optional ready callback 33 * @return {vjs.Player} A player instance 34 * @namespace 35 */ 36var vjs = function(id, options, ready){ 37 var tag; // Element of ID 38 39 // Allow for element or ID to be passed in 40 // String ID 41 if (typeof id === 'string') { 42 43 // Adjust for jQuery ID syntax 44 if (id.indexOf('#') === 0) { 45 id = id.slice(1); 46 } 47 48 // If a player instance has already been created for this ID return it. 49 if (vjs.players[id]) { 50 return vjs.players[id]; 51 52 // Otherwise get element for ID 53 } else { 54 tag = vjs.el(id); 55 } 56 57 // ID is a media element 58 } else { 59 tag = id; 60 } 61 62 // Check for a useable element 63 if (!tag || !tag.nodeName) { // re: nodeName, could be a box div also 64 throw new TypeError('The element or ID supplied is not valid. (videojs)'); // Returns 65 } 66 67 // Element may have a player attr referring to an already created player instance. 68 // If not, set up a new player and return the instance. 69 return tag['player'] || new vjs.Player(tag, options, ready); 70}; 71 72// Extended name, also available externally, window.videojs 73var videojs = window['videojs'] = vjs; 74 75// CDN Version. Used to target right flash swf. 76vjs.CDN_VERSION = '4.11'; 77vjs.ACCESS_PROTOCOL = ('https:' == document.location.protocol ? 'https://' : 'http://'); 78 79/** 80 * Global Player instance options, surfaced from vjs.Player.prototype.options_ 81 * vjs.options = vjs.Player.prototype.options_ 82 * All options should use string keys so they avoid 83 * renaming by closure compiler 84 * @type {Object} 85 */ 86vjs.options = { 87 // Default order of fallback technology 88 'techOrder': ['html5','flash'], 89 // techOrder: ['flash','html5'], 90 91 'html5': {}, 92 'flash': {}, 93 94 // Default of web browser is 300x150. Should rely on source width/height. 95 'width': 300, 96 'height': 150, 97 // defaultVolume: 0.85, 98 'defaultVolume': 0.00, // The freakin seaguls are driving me crazy! 99 100 // default playback rates 101 'playbackRates': [], 102 // Add playback rate selection by adding rates 103 // 'playbackRates': [0.5, 1, 1.5, 2],
104 105 // default inactivity timeout 106 'inactivityTimeout': 2000, 107 108 // Included control sets 109 'children': { 110 'mediaLoader': {}, 111 'posterImage': {}, 112 'textTrackDisplay': {}, 113 'loadingSpinner': {}, 114 'bigPlayButton': {}, 115 'controlBar': {}, 116 'errorDisplay': {} 117 }, 118 119 'language': document.getElementsByTagName('html')[0].getAttribute('lang') || navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language || 'en', 120 121 // locales and their language translations 122 'languages': {}, 123 124 // Default message to show when a video cannot be played. 125 'notSupportedMessage': 'No compatible source was found for this video.' 126}; 127 128// Set CDN Version of swf 129// The added (+) blocks the replace from changing this 4.11 string 130if (vjs.CDN_VERSION !== 'GENERATED'+'_CDN_VSN') { 131 videojs.options['flash']['swf'] = vjs.ACCESS_PROTOCOL + 'vjs.zencdn.net/'+vjs.CDN_VERSION+'/video-js.swf'; 132} 133 134/** 135 * Utility function for adding languages to the default options. Useful for 136 * amending multiple language support at runtime. 137 * 138 * Example: vjs.addLanguage('es', {'Hello':'Hola'}); 139 * 140 * @param {String} code The language code or dictionary property 141 * @param {Object} data The data values to be translated 142 * @return {Object} The resulting global languages dictionary object 143 */ 144vjs.addLanguage = function(code, data){ 145 if(vjs.options['languages'][code] !== undefined) { 146 vjs.options['languages'][code] = vjs.util.mergeOptions(vjs.options['languages'][code], data); 147 } else { 148 vjs.options['languages'][code] = data; 149 } 150 return vjs.options['languages']; 151}; 152 153/** 154 * Global player list 155 * @type {Object} 156 */ 157vjs.players = {}; 158 159/*! 160 * Custom Universal Module Definition (UMD) 161 * 162 * Video.js will never be a non-browser lib so we can simplify UMD a bunch and 163 * still support requirejs and browserify. This also needs to be closure 164 * compiler compatible, so string keys are used. 165 */ 166if (typeof define === 'function' && define['amd']) { 167 define([], function(){ return videojs; }); 168 169// checking that module is an object too because of umdjs/umd#35 170} else if (typeof exports === 'object' && typeof module === 'object') { 171 module['exports'] = videojs; 172} 173/** 174 * Core Object/Class for objects that use inheritance + constructors 175 * 176 * To create a class that can be subclassed itself, extend the CoreObject class. 177 * 178 * var Animal = CoreObject.extend(); 179 * var Horse = Animal.extend(); 180 * 181 * The constructor can be defined through the init property of an object argument. 182 * 183 * var Animal = CoreObject.extend({ 184 * init: function(name, sound){ 185 * this.name = name; 186 * } 187 * }); 188 * 189 * Other methods and properties can be added the same way, or directly to the 190 * prototype. 191 * 192 * var Animal = CoreObject.extend({ 193 * init: function(name){ 194 * this.name = name; 195 * }, 196 * getName: function(){ 197 * return this.name; 198 * }, 199 * sound: '...' 200 * }); 201 * 202 * Animal.prototype.makeSound = function(){ 203 * alert(this.sound); 204 * }; 205 * 206 * To create an instance of a class, use the create method. 207 * 208 * var fluffy = Animal.create('Fluffy'); 209 * fluffy.getName(); // -> Fluffy 210 * 211 * Methods and properties can be overridden in subclasses. 212 * 213 * var Horse = Animal.extend({ 214 * sound: 'Neighhhhh!' 215 * }); 216 * 217 * var horsey = Horse.create('Horsey'); 218 * horsey.getName(); // -> Horsey 219 * horsey.makeSound(); // -> Alert: Neighhhhh! 220 * 221 * @class 222 * @constructor 223 */ 224vjs.CoreObject = vjs['CoreObject'] = function(){}; 225// Manually exporting vjs['CoreObject'] here for Closure Compiler 226// because of the use of the extend/create class methods 227// If we didn't do this, those functions would get flattened to something like 228// `a = ...` and `this.prototype` would refer to the global object instead of 229// CoreObject 230 231/** 232 * Create a new object that inherits from this Object 233 * 234 * var Animal = CoreObject.extend(); 235 * var Horse = Animal.extend(); 236 * 237 * @param {Object} props Functions and properties to be applied to the 238 * new object's prototype 239 * @return {vjs.CoreObject}
239 An object that inherits from CoreObject 240 * @this {*} 241 */ 242vjs.CoreObject.extend = function(props){ 243 var init, subObj; 244 245 props = props || {}; 246 // Set up the constructor using the supplied init method 247 // or using the init of the parent object 248 // Make sure to check the unobfuscated version for external libs 249 init = props['init'] || props.init || this.prototype['init'] || this.prototype.init || function(){}; 250 // In Resig's simple class inheritance (previously used) the constructor 251 // is a function that calls `this.init.apply(arguments)` 252 // However that would prevent us from using `ParentObject.call(this);` 253 // in a Child constructor because the `this` in `this.init` 254 // would still refer to the Child and cause an infinite loop. 255 // We would instead have to do 256 // `ParentObject.prototype.init.apply(this, arguments);` 257 // Bleh. We're not creating a _super() function, so it's good to keep 258 // the parent constructor reference simple. 259 subObj = function(){ 260 init.apply(this, arguments); 261 }; 262 263 // Inherit from this object's prototype 264 subObj.prototype = vjs.obj.create(this.prototype); 265 // Reset the constructor property for subObj otherwise 266 // instances of subObj would have the constructor of the parent Object 267 subObj.prototype.constructor = subObj; 268 269 // Make the class extendable 270 subObj.extend = vjs.CoreObject.extend; 271 // Make a function for creating instances 272 subObj.create = vjs.CoreObject.create; 273 274 // Extend subObj's prototype with functions and other properties from props 275 for (var name in props) { 276 if (props.hasOwnProperty(name)) { 277 subObj.prototype[name] = props[name]; 278 } 279 } 280 281 return subObj; 282}; 283 284/** 285 * Create a new instance of this Object class 286 * 287 * var myAnimal = Animal.create(); 288 * 289 * @return {vjs.CoreObject} An instance of a CoreObject subclass 290 * @this {*} 291 */ 292vjs.CoreObject.create = function(){ 293 // Create a new object that inherits from this object's prototype 294 var inst = vjs.obj.create(this.prototype); 295 296 // Apply this constructor function to the new object 297 this.apply(inst, arguments); 298 299 // Return the new object 300 return inst; 301}; 302/** 303 * @fileoverview Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/) 304 * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible) 305 * This should work very similarly to jQuery's events, however it's based off the book version which isn't as 306 * robust as jquery's, so there's probably some differences. 307 */ 308 309/** 310 * Add an event listener to element 311 * It stores the handler function in a separate cache object 312 * and adds a generic handler to the element's event, 313 * along with a unique id (guid) to the element. 314 * @param {Element|Object} elem Element or object to bind listeners to 315 * @param {String|Array} type Type of event to bind to. 316 * @param {Function} fn Event listener. 317 * @private 318 */ 319vjs.on = function(elem, type, fn){ 320 if (vjs.obj.isArray(type)) { 321 return _handleMultipleEvents(vjs.on, elem, type, fn); 322 } 323 324 var data = vjs.getData(elem); 325 326 // We need a place to store all our handler data 327 if (!data.handlers) data.handlers = {}; 328 329 if (!data.handlers[type]) data.handlers[type] = []; 330 331 if (!fn.guid) fn.guid = vjs.guid++; 332 333 data.handlers[type].push(fn); 334 335 if (!data.dispatcher) { 336 data.disabled = false; 337 338 data.dispatcher = function (event){ 339 340 if (data.disabled) return; 341 event = vjs.fixEvent(event); 342 343 var handlers = data.handlers[event.type]; 344 345 if (handlers) { 346 // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off. 347 var handlersCopy = handlers.slice(0); 348 349 for (var m = 0, n = handlersCopy.length; m < n; m++) { 350 if (event.isImmediatePropagationStopped()) { 351 break; 352 } else { 353 handlersCopy[m].call(elem, event); 354 } 355 } 356 } 357 }; 358 } 359 360 if (data.handlers[type].length == 1) { 361 if (elem.addEventListener) { 362 elem.addEventListener(type, data.dispatcher, false); 363 } else if (elem.attachEvent) { 364 elem.attachEvent('on' + type, data.dispatcher); 365 } 366 } 367}; 368 369/** 370 * Removes event listeners from an element
371 * @param {Element|Object} elem Object to remove listeners from 372 * @param {String|Array=} type Type of listener to remove. Don't include to remove all events from element. 373 * @param {Function} fn Specific listener to remove. Don't include to remove listeners for an event type. 374 * @private 375 */ 376vjs.off = function(elem, type, fn) { 377 // Don't want to add a cache object through getData if not needed 378 if (!vjs.hasData(elem)) return; 379 380 var data = vjs.getData(elem); 381 382 // If no events exist, nothing to unbind 383 if (!data.handlers) { return; } 384 385 if (vjs.obj.isArray(type)) { 386 return _handleMultipleEvents(vjs.off, elem, type, fn); 387 } 388 389 // Utility function 390 var removeType = function(t){ 391 data.handlers[t] = []; 392 vjs.cleanUpEvents(elem,t); 393 }; 394 395 // Are we removing all bound events? 396 if (!type) { 397 for (var t in data.handlers) removeType(t); 398 return; 399 } 400 401 var handlers = data.handlers[type]; 402 403 // If no handlers exist, nothing to unbind 404 if (!handlers) return; 405 406 // If no listener was provided, remove all listeners for type 407 if (!fn) { 408 removeType(type); 409 return; 410 } 411 412 // We're only removing a single handler 413 if (fn.guid) { 414 for (var n = 0; n < handlers.length; n++) { 415 if (handlers[n].guid === fn.guid) { 416 handlers.splice(n--, 1); 417 } 418 } 419 } 420 421 vjs.cleanUpEvents(elem, type); 422}; 423 424/** 425 * Clean up the listener cache and dispatchers 426 * @param {Element|Object} elem Element to clean up 427 * @param {String} type Type of event to clean up 428 * @private 429 */ 430vjs.cleanUpEvents = function(elem, type) { 431 var data = vjs.getData(elem); 432 433 // Remove the events of a particular type if there are none left 434 if (data.handlers[type].length === 0) { 435 delete data.handlers[type]; 436 // data.handlers[type] = null; 437 // Setting to null was causing an error with data.handlers 438 439 // Remove the meta-handler from the element 440 if (elem.removeEventListener) { 441 elem.removeEventListener(type, data.dispatcher, false); 442 } else if (elem.detachEvent) { 443 elem.detachEvent('on' + type, data.dispatcher); 444 } 445 } 446 447 // Remove the events object if there are no types left 448 if (vjs.isEmpty(data.handlers)) { 449 delete data.handlers; 450 delete data.dispatcher; 451 delete data.disabled; 452 453 // data.handlers = null; 454 // data.dispatcher = null; 455 // data.disabled = null; 456 } 457 458 // Finally remove the expando if there is no data left 459 if (vjs.isEmpty(data)) { 460 vjs.removeData(elem); 461 } 462}; 463 464/** 465 * Fix a native event to have standard property values 466 * @param {Object} event Event object to fix 467 * @return {Object} 468 * @private 469 */ 470vjs.fixEvent = function(event) { 471 472 function returnTrue() { return true; } 473 function returnFalse() { return false; } 474 475 // Test if fixing up is needed 476 // Used to check if !event.stopPropagation instead of isPropagationStopped 477 // But native events return true for stopPropagation, but don't have 478 // other expected methods like isPropagationStopped. Seems to be a problem 479 // with the Javascript Ninja code. So we're just overriding all events now. 480 if (!event || !event.isPropagationStopped) { 481 var old = event || window.event; 482 483 event = {}; 484 // Clone the old object so that we can modify the values event = {}; 485 // IE8 Doesn't like when you mess with native event properties 486 // Firefox returns false for event.hasOwnProperty('type') and other props 487 // which makes copying more difficult. 488 // TODO: Probably best to create a whitelist of event props 489 for (var key in old) { 490 // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y 491 // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation 492 if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation') { 493 // Chrome 32+ warns if you try to copy deprecated returnValue, but 494 // we still want to if preventDefault isn't supported (IE8). 495 if (!(key == 'returnValue' && old.preventDefault)) { 496 event[key] = old[key]; 497 } 498 } 499 } 500 501 // The event occurred on this element 502 if (!event.target) { 503 event.target = event.srcElement || document; 504 } 505 506 // Handle which other element the event is related to 507 event.relatedTarget = event.fromElement === event.target ? 508 event.toElement : 509 event.fromElement; 510 511 // Stop the default browser action 512 event.preventDefault = function () { 513 if (old.preventDefault) { 514 old.preventDefault(); 515 } 516 event.returnValue = false;
517 event.isDefaultPrevented = returnTrue; 518 event.defaultPrevented = true; 519 }; 520 521 event.isDefaultPrevented = returnFalse; 522 event.defaultPrevented = false; 523 524 // Stop the event from bubbling 525 event.stopPropagation = function () { 526 if (old.stopPropagation) { 527 old.stopPropagation(); 528 } 529 event.cancelBubble = true; 530 event.isPropagationStopped = returnTrue; 531 }; 532 533 event.isPropagationStopped = returnFalse; 534 535 // Stop the event from bubbling and executing other handlers 536 event.stopImmediatePropagation = function () { 537 if (old.stopImmediatePropagation) { 538 old.stopImmediatePropagation(); 539 } 540 event.isImmediatePropagationStopped = returnTrue; 541 event.stopPropagation(); 542 }; 543 544 event.isImmediatePropagationStopped = returnFalse; 545 546 // Handle mouse position 547 if (event.clientX != null) { 548 var doc = document.documentElement, body = document.body; 549 550 event.pageX = event.clientX + 551 (doc && doc.scrollLeft || body && body.scrollLeft || 0) - 552 (doc && doc.clientLeft || body && body.clientLeft || 0); 553 event.pageY = event.clientY + 554 (doc && doc.scrollTop || body && body.scrollTop || 0) - 555 (doc && doc.clientTop || body && body.clientTop || 0); 556 } 557 558 // Handle key presses 559 event.which = event.charCode || event.keyCode; 560 561 // Fix button for mouse clicks: 562 // 0 == left; 1 == middle; 2 == right 563 if (event.button != null) { 564 event.button = (event.button & 1 ? 0 : 565 (event.button & 4 ? 1 : 566 (event.button & 2 ? 2 : 0))); 567 } 568 } 569 570 // Returns fixed-up instance 571 return event; 572}; 573 574/** 575 * Trigger an event for an element 576 * @param {Element|Object} elem Element to trigger an event on 577 * @param {Event|Object|String} event A string (the type) or an event object with a type attribute 578 * @private 579 */ 580vjs.trigger = function(elem, event) { 581 // Fetches element data and a reference to the parent (for bubbling). 582 // Don't want to add a data object to cache for every parent, 583 // so checking hasData first. 584 var elemData = (vjs.hasData(elem)) ? vjs.getData(elem) : {}; 585 var parent = elem.parentNode || elem.ownerDocument; 586 // type = event.type || event, 587 // handler; 588 589 // If an event name was passed as a string, creates an event out of it 590 if (typeof event === 'string') { 591 event = { type:event, target:elem }; 592 } 593 // Normalizes the event properties. 594 event = vjs.fixEvent(event); 595 596 // If the passed element has a dispatcher, executes the established handlers. 597 if (elemData.dispatcher) { 598 elemData.dispatcher.call(elem, event); 599 } 600 601 // Unless explicitly stopped or the event does not bubble (e.g. media events) 602 // recursively calls this function to bubble the event up the DOM. 603 if (parent && !event.isPropagationStopped() && event.bubbles !== false) { 604 vjs.trigger(parent, event); 605 606 // If at the top of the DOM, triggers the default action unless disabled. 607 } else if (!parent && !event.defaultPrevented) { 608 var targetData = vjs.getData(event.target); 609 610 // Checks if the target has a default action for this event. 611 if (event.target[event.type]) { 612 // Temporarily disables event dispatching on the target as we have already executed the handler. 613 targetData.disabled = true; 614 // Executes the default action. 615 if (typeof event.target[event.type] === 'function') { 616 event.target[event.type](); 617 } 618 // Re-enables event dispatching. 619 targetData.disabled = false; 620 } 621 } 622 623 // Inform the triggerer if the default was prevented by returning false 624 return !event.defaultPrevented; 625 /* Original version of js ninja events wasn't complete. 626 * We've since updated to the latest version, but keeping this around 627 * for now just in case. 628 */ 629 // // Added in addition to book. Book code was broke. 630 // event = typeof event === 'object' ? 631 // event[vjs.expando] ? 632 // event : 633 // new vjs.Event(type, event) : 634 // new vjs.Event(type); 635 636 // event.type = type; 637 // if (handler) { 638 // handler.call(elem, event); 639 // } 640 641 // // Clean up the event in case it is being reused 642 // event.result = undefined; 643 // event.target = elem; 644}; 645 646/** 647 * Trigger a listener only once for an event
648 * @param {Element|Object} elem Element or object to 649 * @param {String|Array} type 650 * @param {Function} fn 651 * @private 652 */ 653vjs.one = function(elem, type, fn) { 654 if (vjs.obj.isArray(type)) { 655 return _handleMultipleEvents(vjs.one, elem, type, fn); 656 } 657 var func = function(){ 658 vjs.off(elem, type, func); 659 fn.apply(this, arguments); 660 }; 661 // copy the guid to the new function so it can removed using the original function's ID 662 func.guid = fn.guid = fn.guid || vjs.guid++; 663 vjs.on(elem, type, func); 664}; 665 666/** 667 * Loops through an array of event types and calls the requested method for each type. 668 * @param {Function} fn The event method we want to use. 669 * @param {Element|Object} elem Element or object to bind listeners to 670 * @param {String} type Type of event to bind to. 671 * @param {Function} callback Event listener. 672 * @private 673 */ 674function _handleMultipleEvents(fn, elem, type, callback) { 675 vjs.arr.forEach(type, function(type) { 676 fn(elem, type, callback); //Call the event method for each one of the types 677 }); 678} 679var hasOwnProp = Object.prototype.hasOwnProperty; 680 681/** 682 * Creates an element and applies properties. 683 * @param {String=} tagName Name of tag to be created. 684 * @param {Object=} properties Element properties to be applied. 685 * @return {Element} 686 * @private 687 */ 688vjs.createEl = function(tagName, properties){ 689 var el; 690 691 tagName = tagName || 'div'; 692 properties = properties || {}; 693 694 el = document.createElement(tagName); 695 696 vjs.obj.each(properties, function(propName, val){ 697 // Not remembering why we were checking for dash 698 // but using setAttribute means you have to use getAttribute 699 700 // The check for dash checks for the aria-* attributes, like aria-label, aria-valuemin. 701 // The additional check for "role" is because the default method for adding attributes does not 702 // add the attribute "role". My guess is because it's not a valid attribute in some namespaces, although 703 // browsers handle the attribute just fine. The W3C allows for aria-* attributes to be used in pre-HTML5 docs. 704 // http://www.w3.org/TR/wai-aria-primer/#ariahtml. Using setAttribute gets around this problem. 705 if (propName.indexOf('aria-') !== -1 || propName == 'role') { 706 el.setAttribute(propName, val); 707 } else { 708 el[propName] = val; 709 } 710 }); 711 712 return el; 713}; 714 715/** 716 * Uppercase the first letter of a string 717 * @param {String} string String to be uppercased 718 * @return {String} 719 * @private 720 */ 721vjs.capitalize = function(string){ 722 return string.charAt(0).toUpperCase() + string.slice(1); 723}; 724 725/** 726 * Object functions container 727 * @type {Object} 728 * @private 729 */ 730vjs.obj = {}; 731 732/** 733 * Object.create shim for prototypal inheritance 734 * 735 * https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create 736 * 737 * @function 738 * @param {Object} obj Object to use as prototype 739 * @private 740 */ 741vjs.obj.create = Object.create || function(obj){ 742 //Create a new function called 'F' which is just an empty object. 743 function F() {} 744 745 //the prototype of the 'F' function should point to the 746 //parameter of the anonymous function. 747 F.prototype = obj; 748 749 //create a new constructor function based off of the 'F' function. 750 return new F(); 751}; 752 753/** 754 * Loop through each property in an object and call a function 755 * whose arguments are (key,value) 756 * @param {Object} obj Object of properties 757 * @param {Function} fn Function to be called on each property. 758 * @this {*} 759 * @private 760 */ 761vjs.obj.each = function(obj, fn, context){ 762 for (var key in obj) { 763 if (hasOwnProp.call(obj, key)) { 764 fn.call(context || this, key, obj[key]); 765 } 766 } 767}; 768 769/** 770 * Merge two objects together and return the original. 771 * @param {Object} obj1 772 * @param {Object} obj2 773 * @return {Object} 774 * @private 775 */ 776vjs.obj.merge = function(obj1, obj2){ 777 if (!obj2) { return obj1; } 778 for (var key in obj2){ 779 if (hasOwnProp.call(obj2, key)) { 780 obj1[key] = obj2[key]; 781 } 782 } 783 return obj1; 784}; 785 786/** 787 * Merge two objects, and merge any properties that are objects 788 * instead of just overwriting one. Uses to merge options hashes 789 * where deeper default settings are important. 790 * @param {Object} obj1 Object to override 791 * @param {Object} obj2 Overriding object 792 * @return {Object} New object. Obj1 and Obj2 will be untouched. 793 * @private 794 */ 795vjs.obj.deepMerge = function(obj1, obj2){ 796 var key, val1, val2; 797 798 // make a copy of obj1 so we're not overwriting original values. 799 // like prototype.options_ and all sub options objects 800 obj1 = vjs.obj.copy(obj1); 801 802 for (key in obj2){ 803 if (hasOwnProp.call(obj2, key)) { 804 val1 = obj1[key]; 805 val2 = obj2[key]; 806 807 // Check if both properties are pure objects and do a deep merge if so 808 if (vjs.obj.isPlain(val1) && vjs.obj.isPlain(val2)) { 809 obj1[key] = vjs.obj.deepMerge(val1, val2); 810 } else { 811 obj1[key] = obj2[key]; 812 } 813 } 814 } 815 return obj1; 816}; 817 818/** 819 * Make a copy of the supplied object 820 * @param {Object} obj Object to copy 821 * @return {Object} Copy of object 822 * @private 823 */ 824vjs.obj.copy = function(obj){ 825 return vjs.obj.merge({}, obj); 826}; 827 828/** 829 * Check if an object is plain, and not a dom node or any object sub-instance 830 * @param {Object} obj Object to check 831 * @return {Boolean} True if plain, false otherwise 832 * @private 833 */ 834vjs.obj.isPlain = function(obj){ 835 return !!obj 836 && typeof obj === 'object' 837 && obj.toString() === '[object Object]' 838 && obj.constructor === Object; 839}; 840 841/** 842 * Check if an object is Array 843* Since instanceof Array will not work on arrays created in another frame we need to use Array.isArray, but since IE8 does not support Array.isArray we need this shim 844 * @param {Object} obj Object to check 845 * @return {Boolean} True if plain, false otherwise 846 * @private 847 */ 848vjs.obj.isArray = Array.isArray || function(arr) { 849 return Object.prototype.toString.call(arr) === '[object Array]'; 850}; 851 852/** 853 * Check to see whether the input is NaN or not. 854 * NaN is the only JavaScript construct that isn't equal to itself 855 * @param {Number} num Number to check 856 * @return {Boolean} True if NaN, false otherwise 857 * @private 858 */ 859vjs.isNaN = function(num) { 860 return num !== num; 861}; 862 863/**
864 * Bind (a.k.a proxy or Context). A simple method for changing the context of a function 865 It also stores a unique id on the function so it can be easily removed from events 866 * @param {*} context The object to bind as scope 867 * @param {Function} fn The function to be bound to a scope 868 * @param {Number=} uid An optional unique ID for the function to be set 869 * @return {Function} 870 * @private 871 */ 872vjs.bind = function(context, fn, uid) { 873 // Make sure the function has a unique ID 874 if (!fn.guid) { fn.guid = vjs.guid++; } 875 876 // Create the new function that changes the context 877 var ret = function() { 878 return fn.apply(context, arguments); 879 }; 880 881 // Allow for the ability to individualize this function 882 // Needed in the case where multiple objects might share the same prototype 883 // IF both items add an event listener with the same function, then you try to remove just one 884 // it will remove both because they both have the same guid. 885 // when using this, you need to use the bind method when you remove the listener as well. 886 // currently used in text tracks 887 ret.guid = (uid) ? uid + '_' + fn.guid : fn.guid; 888 889 return ret; 890}; 891 892/** 893 * Element Data Store. Allows for binding data to an element without putting it directly on the element. 894 * Ex. Event listeners are stored here. 895 * (also from jsninja.com, slightly modified and updated for closure compiler) 896 * @type {Object} 897 * @private 898 */ 899vjs.cache = {}; 900 901/** 902 * Unique ID for an element or function 903 * @type {Number} 904 * @private 905 */ 906vjs.guid = 1; 907 908/** 909 * Unique attribute name to store an element's guid in 910 * @type {String} 911 * @constant 912 * @private 913 */ 914vjs.expando = 'vdata' + (new Date()).getTime(); 915 916/** 917 * Returns the cache object where data for an element is stored 918 * @param {Element} el Element to store data for. 919 * @return {Object} 920 * @private 921 */ 922vjs.getData = function(el){ 923 var id = el[vjs.expando]; 924 if (!id) { 925 id = el[vjs.expando] = vjs.guid++; 926 vjs.cache[id] = {}; 927 } 928 return vjs.cache[id]; 929}; 930 931/** 932 * Returns the cache object where data for an element is stored 933 * @param {Element} el Element to store data for. 934 * @return {Object} 935 * @private 936 */ 937vjs.hasData = function(el){ 938 var id = el[vjs.expando]; 939 return !(!id || vjs.isEmpty(vjs.cache[id])); 940}; 941 942/** 943 * Delete data for the element from the cache and the guid attr from getElementById 944 * @param {Element} el Remove data for an element 945 * @private 946 */ 947vjs.removeData = function(el){ 948 var id = el[vjs.expando]; 949 if (!id) { return; } 950 // Remove all stored data 951 // Changed to = null 952 // http://coding.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/ 953 // vjs.cache[id] = null; 954 delete vjs.cache[id]; 955 956 // Remove the expando property from the DOM node 957 try {
958 delete el[vjs.expando]; 959 } catch(e) { 960 if (el.removeAttribute) { 961 el.removeAttribute(vjs.expando); 962 } else { 963 // IE doesn't appear to support removeAttribute on the document element 964 el[vjs.expando] = null; 965 } 966 } 967}; 968 969/** 970 * Check if an object is empty 971 * @param {Object} obj The object to check for emptiness 972 * @return {Boolean} 973 * @private 974 */ 975vjs.isEmpty = function(obj) { 976 for (var prop in obj) { 977 // Inlude null properties as empty. 978 if (obj[prop] !== null) { 979 return false; 980 } 981 } 982 return true; 983}; 984 985/** 986 * Check if an element has a CSS class 987 * @param {Element} element Element to check 988 * @param {String} classToCheck Classname to check 989 * @private 990 */ 991vjs.hasClass = function(element, classToCheck){ 992 return ((' ' + element.className + ' ').indexOf(' ' + classToCheck + ' ') !== -1); 993}; 994 995 996/** 997 * Add a CSS class name to an element 998 * @param {Element} element Element to add class name to 999 * @param {String} classToAdd Classname to add 1000 * @private 1001 */ 1002vjs.addClass = function(element, classToAdd){ 1003 if (!vjs.hasClass(element, classToAdd)) { 1004 element.className = element.className === '' ? classToAdd : element.className + ' ' + classToAdd; 1005 } 1006}; 1007 1008/** 1009 * Remove a CSS class name from an element 1010 * @param {Element} element Element to remove from class name 1011 * @param {String} classToAdd Classname to remove 1012 * @private 1013 */ 1014vjs.removeClass = function(element, classToRemove){ 1015 var classNames, i; 1016 1017 if (!vjs.hasClass(element, classToRemove)) {return;} 1018 1019 classNames = element.className.split(' '); 1020 1021 // no arr.indexOf in ie8, and we don't want to add a big shim 1022 for (i = classNames.length - 1; i >= 0; i--) { 1023 if (classNames[i] === classToRemove) { 1024 classNames.splice(i,1); 1025 } 1026 } 1027 1028 element.className = classNames.join(' '); 1029}; 1030 1031/** 1032 * Element for testing browser HTML5 video capabilities 1033 * @type {Element} 1034 * @constant 1035 * @private 1036 */ 1037vjs.TEST_VID = vjs.createEl('video'); 1038 1039/** 1040 * Useragent for browser testing. 1041 * @type {String} 1042 * @constant 1043 * @private 1044 */ 1045vjs.USER_AGENT = navigator.userAgent; 1046 1047/** 1048 * Device is an iPhone 1049 * @type {Boolean} 1050 * @constant 1051 * @private 1052 */ 1053vjs.IS_IPHONE = (/iPhone/i).test(vjs.USER_AGENT); 1054vjs.IS_IPAD = (/iPad/i).test(vjs.USER_AGENT); 1055vjs.IS_IPOD = (/iPod/i).test(vjs.USER_AGENT); 1056vjs.IS_IOS = vjs.IS_IPHONE || vjs.IS_IPAD || vjs.IS_IPOD; 1057 1058vjs.IOS_VERSION = (function(){ 1059 var match = vjs.USER_AGENT.match(/OS (\d+)_/i); 1060 if (match && match[1]) { return match[1]; } 1061})(); 1062 1063vjs.IS_ANDROID = (/Android/i).test(vjs.USER_AGENT); 1064vjs.ANDROID_VERSION = (function() { 1065 // This matches Android Major.Minor.Patch versions 1066 // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned 1067 var match = vjs.USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i), 1068 major, 1069 minor; 1070 1071 if (!match) { 1072 return null; 1073 } 1074 1075 major = match[1] && parseFloat(match[1]); 1076 minor = match[2] && parseFloat(match[2]); 1077 1078 if (major && minor) { 1079 return parseFloat(match[1] + '.' + match[2]); 1080 } else if (major) { 1081 return major; 1082 } else { 1083 return null; 1084 } 1085})(); 1086// Old Android is defined as Version older than 2.3, and requiring a webkit version of the android browser 1087vjs.IS_OLD_ANDROID = vjs.IS_ANDROID && (/webkit/i).test(vjs.USER_AGENT) && vjs.ANDROID_VERSION < 2.3; 1088 1089vjs.IS_FIREFOX = (/Firefox/i).test(vjs.USER_AGENT); 1090vjs.IS_CHROME = (/Chrome/i).test(vjs.USER_AGENT); 1091 1092vjs.TOUCH_ENABLED = !!(('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch); 1093vjs.BACKGROUND_SIZE_SUPPORTED = 'backgroundSize' in vjs.TEST_VID.style; 1094 1095/** 1096 * Apply attributes to an HTML element. 1097 * @param {Element} el Target element. 1098 * @param {Object=} attributes Element attributes to be applied. 1099 * @private 1100 */ 1101vjs.setElementAttributes = function(el, attributes){ 1102 vjs.obj.each(attributes, function(attrName, attrValue) { 1103 if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) { 1104 el.removeAttribute(attrName); 1105 } else { 1106 el.setAttribute(attrName, (attrValue === true ? '' : attrValue)); 1107 } 1108 }); 1109}; 1110 1111/** 1112 * Get an element's attribute values, as defined on the HTML tag 1113 * Attributes are not the same as properties. They're defined on the tag 1114 * or with setAttribute (which shouldn't be used with HTML) 1115 * This will return true or false for boolean attributes. 1116 * @param {Element} tag Element from which to get tag attributes 1117 * @return {Object} 1118 * @private 1119 */ 1120vjs.getElementAttributes = function(tag){ 1121 var obj, knownBooleans, attrs, attrName, attrVal; 1122 1123 obj = {}; 1124 1125 // known boolean attributes 1126 // we can check for matching boolean properties, but older browsers 1127 // won't know about HTML5 boolean attributes that we still read from 1128 knownBooleans = ','+'autoplay,controls,loop,muted,default'+','; 1129 1130 if (tag && tag.attributes && tag.attributes.length > 0) { 1131 attrs = tag.attributes; 1132 1133 for (var i = attrs.length - 1; i >= 0; i--) { 1134 attrName = attrs[i].name; 1135 attrVal = attrs[i].value; 1136 1137 // check for known booleans 1138 // the matching element property will return a value for typeof 1139 if (typeof tag[attrName] === 'boolean' || knownBooleans.indexOf(','+attrName+',') !== -1) { 1140 // the value of an included boolean attribute is typically an empty
1141 // string ('') which would equal false if we just check for a false value. 1142 // we also don't want support bad code like autoplay='false' 1143 attrVal = (attrVal !== null) ? true : false; 1144 } 1145 1146 obj[attrName] = attrVal; 1147 } 1148 } 1149 1150 return obj; 1151}; 1152 1153/** 1154 * Get the computed style value for an element 1155 * From http://robertnyman.com/2006/04/24/get-the-rendered-style-of-an-element/ 1156 * @param {Element} el Element to get style value for 1157 * @param {String} strCssRule Style name 1158 * @return {String} Style value 1159 * @private 1160 */ 1161vjs.getComputedDimension = function(el, strCssRule){ 1162 var strValue = ''; 1163 if(document.defaultView && document.defaultView.getComputedStyle){ 1164 strValue = document.defaultView.getComputedStyle(el, '').getPropertyValue(strCssRule); 1165 1166 } else if(el.currentStyle){ 1167 // IE8 Width/Height support 1168 strValue = el['client'+strCssRule.substr(0,1).toUpperCase() + strCssRule.substr(1)] + 'px'; 1169 } 1170 return strValue; 1171}; 1172 1173/** 1174 * Insert an element as the first child node of another 1175 * @param {Element} child Element to insert 1176 * @param {[type]} parent Element to insert child into 1177 * @private 1178 */ 1179vjs.insertFirst = function(child, parent){ 1180 if (parent.firstChild) { 1181 parent.insertBefore(child, parent.firstChild); 1182 } else { 1183 parent.appendChild(child); 1184 } 1185}; 1186 1187/** 1188 * Object to hold browser support information 1189 * @type {Object} 1190 * @private 1191 */ 1192vjs.browser = {}; 1193 1194/** 1195 * Shorthand for document.getElementById() 1196 * Also allows for CSS (jQuery) ID syntax. But nothing other than IDs. 1197 * @param {String} id Element ID 1198 * @return {Element} Element with supplied ID 1199 * @private 1200 */ 1201vjs.el = function(id){ 1202 if (id.indexOf('#') === 0) { 1203 id = id.slice(1); 1204 } 1205 1206 return document.getElementById(id); 1207}; 1208 1209/** 1210 * Format seconds as a time string, H:MM:SS or M:SS 1211 * Supplying a guide (in seconds) will force a number of leading zeros 1212 * to cover the length of the guide 1213 * @param {Number} seconds Number of seconds to be turned into a string 1214 * @param {Number} guide Number (in seconds) to model the string after 1215 * @return {String} Time formatted as H:MM:SS or M:SS 1216 * @private 1217 */ 1218vjs.formatTime = function(seconds, guide) { 1219 // Default to using seconds as guide 1220 guide = guide || seconds; 1221 var s = Math.floor(seconds % 60), 1222 m = Math.floor(seconds / 60 % 60), 1223 h = Math.floor(seconds / 3600), 1224 gm = Math.floor(guide / 60 % 60), 1225 gh = Math.floor(guide / 3600); 1226 1227 // handle invalid times 1228 if (isNaN(seconds) || seconds === Infinity) { 1229 // '-' is false for all relational operators (e.g. <, >=) so this setting 1230 // will add the minimum number of fields specified by the guide 1231 h = m = s = '-'; 1232 } 1233 1234 // Check if we need to show hours 1235 h = (h > 0 || gh > 0) ? h + ':' : ''; 1236 1237 // If hours are showing, we may need to add a leading zero. 1238 // Always show at least one digit of minutes. 1239 m = (((h || gm >= 10) && m < 10) ? '0' + m : m) + ':'; 1240 1241 // Check if leading zero is need for seconds 1242 s = (s < 10) ? '0' + s : s; 1243 1244 return h + m + s; 1245}; 1246 1247// Attempt to block the ability to select text while dragging controls 1248vjs.blockTextSelection = function(){ 1249 document.body.focus(); 1250 document.onselectstart = function () { return false; }; 1251}; 1252// Turn off text selection blocking 1253vjs.unblockTextSelection = function(){ document.onselectstart = function () { return true; }; }; 1254 1255/** 1256 * Trim whitespace from the ends of a string. 1257 * @param {String} string String to trim 1258 * @return {String} Trimmed string 1259 * @private 1260 */ 1261vjs.trim = function(str){ 1262 return (str+'').replace(/^\s+|\s+$/g, ''); 1263}; 1264 1265/** 1266 * Should round off a number to a decimal place 1267 * @param {Number} num Number to round 1268 * @param {Number} dec Number of decimal places to round to 1269 * @return {Number} Rounded number 1270 * @private 1271 */ 1272vjs.round = function(num, dec) { 1273 if (!dec) { dec = 0; } 1274 return Math.round(num*Math.pow(10,dec))/Math.pow(10,dec); 1275}; 1276 1277/** 1278 * Should create a fake TimeRange object 1279 * Mimics an HTML5 time range instance, which has functions that 1280 * return the start and end times for a range 1281 * TimeRanges are returned by the buffered() method 1282 * @param {Number} start Start time in seconds 1283 * @param {Number} end End time in seconds 1284 * @return {Object} Fake TimeRange object 1285 * @private 1286 */ 1287vjs.createTimeRange = function(start, end){ 1288 return { 1289 length: 1, 1290 start: function() { return start; }, 1291 end: function() { return end; } 1292 }; 1293}; 1294 1295/** 1296 * Add to local storage (may removable) 1297 * @private 1298 */ 1299vjs.setLocalStorage = function(key, value){ 1300 try { 1301 // IE was throwing errors referencing the var anywhere without this 1302 var localStorage = window.localStorage || false; 1303 if (!localStorage) { return; } 1304 localStorage[key] = value; 1305 } catch(e) { 1306 if (e.code == 22 || e.code == 1014) { // Webkit == 22 / Firefox == 1014 1307 vjs.log('LocalStorage Full (VideoJS)', e); 1308 } else { 1309 if (e.code == 18) { 1310 vjs.log('LocalStorage not allowed (VideoJS)', e); 1311 } else { 1312 vjs.log('LocalStorage Error (VideoJS)', e); 1313 } 1314 } 1315 } 1316}; 1317 1318/** 1319 * Get absolute version of relative URL. Used to tell flash correct URL. 1320 * http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue 1321 * @param {String} url URL to make absolute 1322 * @return {String} Absolute URL 1323 * @private 1324 */ 1325vjs.getAbsoluteURL = function(url){ 1326 1327 // Check if absolute URL 1328 if (!url.match(/^https?:\/\//)) { 1329 // Convert to absolute URL. Flash hosted off-site needs an absolute URL. 1330 url = vjs.createEl('div', { 1331 innerHTML: '<a href="'+url+'">x</a>' 1332 }).firstChild.href; 1333 } 1334 1335 return url; 1336}; 1337 1338 1339/** 1340 * Resolve and parse the elements of a URL 1341 * @param {String} url The url to parse 1342 * @return {Object} An object of url details 1343 */ 1344vjs.parseUrl = function(url) { 1345 var div, a, addToBody, props, details; 1346 1347 props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host']; 1348 1349 // add the url to an anchor and let the browser parse the URL 1350 a = vjs.createEl('a', { href: url }); 1351 1352 // IE8 (and 9?) Fix
1353 // ie8 doesn't parse the URL correctly until the anchor is actually 1354 // added to the body, and an innerHTML is needed to trigger the parsing 1355 addToBody = (a.host === '' && a.protocol !== 'file:'); 1356 if (addToBody) { 1357 div = vjs.createEl('div'); 1358 div.innerHTML = '<a href="'+url+'"></a>'; 1359 a = div.firstChild; 1360 // prevent the div from affecting layout 1361 div.setAttribute('style', 'display:none; position:absolute;'); 1362 document.body.appendChild(div); 1363 } 1364 1365 // Copy the specific URL properties to a new object 1366 // This is also needed for IE8 because the anchor loses its 1367 // properties when it's removed from the dom 1368 details = {}; 1369 for (var i = 0; i < props.length; i++) { 1370 details[props[i]] = a[props[i]]; 1371 } 1372 1373 if (addToBody) { 1374 document.body.removeChild(div); 1375 } 1376 1377 return details; 1378}; 1379 1380/** 1381 * Log messages to the console and history based on the type of message 1382 * 1383 * @param {String} type The type of message, or `null` for `log` 1384 * @param {[type]} args The args to be passed to the log 1385 * @private 1386 */ 1387function _logType(type, args){ 1388 var argsArray, noop, console; 1389 1390 // convert args to an array to get array functions 1391 argsArray = Array.prototype.slice.call(args); 1392 // if there's no console then don't try to output messages 1393 // they will still be stored in vjs.log.history 1394 // Was setting these once outside of this function, but containing them 1395 // in the function makes it easier to test cases where console doesn't exist 1396 noop = function(){}; 1397 console = window['console'] || { 1398 'log': noop, 1399 'warn': noop, 1400 'error': noop 1401 }; 1402 1403 if (type) { 1404 // add the type to the front of the message 1405 argsArray.unshift(type.toUpperCase()+':'); 1406 } else { 1407 // default to log with no prefix 1408 type = 'log'; 1409 } 1410 1411 // add to history 1412 vjs.log.history.push(argsArray); 1413 1414 // add console prefix after adding to history 1415 argsArray.unshift('VIDEOJS:'); 1416 1417 // call appropriate log function 1418 if (console[type].apply) { 1419 console[type].apply(console, argsArray); 1420 } else { 1421 // ie8 doesn't allow error.apply, but it will just join() the array anyway 1422 console[type](argsArray.join(' ')); 1423 } 1424} 1425 1426/** 1427 * Log plain debug messages 1428 */ 1429vjs.log = function(){ 1430 _logType(null, arguments); 1431}; 1432 1433/** 1434 * Keep a history of log messages 1435 * @type {Array} 1436 */ 1437vjs.log.history = []; 1438 1439/** 1440 * Log error messages 1441 */ 1442vjs.log.error = function(){ 1443 _logType('error', arguments); 1444}; 1445 1446/** 1447 * Log warning messages 1448 */ 1449vjs.log.warn = function(){ 1450 _logType('warn', arguments); 1451}; 1452 1453// Offset Left 1454// getBoundingClientRect technique from John Resig http://ejohn.org/blog/getboundingclientrect-is-awesome/ 1455vjs.findPosition = function(el) { 1456 var box, docEl, body, clientLeft, scrollLeft, left, clientTop, scrollTop, top; 1457 1458 if (el.getBoundingClientRect && el.parentNode) { 1459 box = el.getBoundingClientRect(); 1460 } 1461 1462 if (!box) { 1463 return { 1464 left: 0, 1465 top: 0 1466 }; 1467 } 1468 1469 docEl = document.documentElement; 1470 body = document.body; 1471 1472 clientLeft = docEl.clientLeft || body.clientLeft || 0; 1473 scrollLeft = window.pageXOffset || body.scrollLeft; 1474 left = box.left + scrollLeft - clientLeft; 1475 1476 clientTop = docEl.clientTop || body.clientTop || 0; 1477 scrollTop = window.pageYOffset || body.scrollTop; 1478 top = box.top + scrollTop - clientTop; 1479 1480 // Android sometimes returns slightly off decimal values, so need to round 1481 return { 1482 left: vjs.round(left), 1483 top: vjs.round(top) 1484 }; 1485}; 1486 1487/** 1488 * Array functions container 1489 * @type {Object} 1490 * @private 1491 */ 1492vjs.arr = {}; 1493 1494/* 1495 * Loops through an array and runs a function for each item inside it. 1496 * @param {Array} array The array 1497 * @param {Function} callback The function to be run for each item 1498 * @param {*} thisArg The `this` binding of callback 1499 * @returns {Array} The array 1500 * @private 1501 */ 1502vjs.arr.forEach = function(array, callback, thisArg) { 1503 if (vjs.obj.isArray(array) && callback instanceof Function) { 1504 for (var i = 0, len = array.length; i < len; ++i) { 1505 callback.call(thisArg || vjs, array[i], i, array); 1506 } 1507 } 1508 1509 return array; 1510}; 1511/** 1512 * Simple http request for retrieving external files (e.g. text tracks) 1513 * 1514 * ##### Example 1515 * 1516 * // using url string 1517 * videojs.xhr('http://example.com/myfile.vtt', function(error, response, responseBody){}); 1518 * 1519 * // or options block 1520 * videojs.xhr({ 1521 * uri: 'http://example.com/myfile.vtt', 1522 * method: 'GET', 1523 * responseType: 'text' 1524 * }, function(error, response, responseBody){ 1525 * if (error) { 1526 * // log the error 1527 * } else { 1528 * // successful, do something with the response 1529 * } 1530 * }); 1531 * 1532 * 1533 * API is modeled after the Raynos/xhr, which we hope to use after 1534 * getting browserify implemented. 1535 * https://github.com/Raynos/xhr/blob/master/index.js 1536 * 1537 * @param {Object|String} options Options block or URL string 1538 * @param {Function} callback The callback function 1539 * @returns {Object} The request 1540 */ 1541vjs.xhr = function(options, callback){ 1542 var XHR, request, urlInfo, winLoc, fileUrl, crossOrigin, abortTimeout, successHandler, errorHandler; 1543 1544 // If options is a string it's the url 1545 if (typeof options === 'string') { 1546 options = { 1547 uri: options 1548 }; 1549 } 1550 1551 // Merge with default options 1552 videojs.util.mergeOptions({ 1553 method: 'GET', 1554 timeout: 45 * 1000 1555 }, options); 1556 1557 callback = callback || function(){}; 1558 1559 successHandler = function(){ 1560 window.clearTimeout(abortTimeout); 1561 callback(null, request, request.response || request.responseText); 1562 }; 1563 1564 errorHandler = function(err){ 1565 window.clearTimeout(abortTimeout); 1566 1567 if (!err || typeof err === 'string') { 1568 err = new Error(err); 1569 } 1570 1571 callback(err, request); 1572 }; 1573 1574 XHR = window.XMLHttpRequest; 1575 1576 if (typeof XHR === 'undefined') { 1577 // Shim XMLHttpRequest for older IEs 1578 XHR = function () {
1579 try { return new window.ActiveXObject('Msxml2.XMLHTTP.6.0'); } catch (e) {} 1580 try { return new window.ActiveXObject('Msxml2.XMLHTTP.3.0'); } catch (f) {} 1581 try { return new window.ActiveXObject('Msxml2.XMLHTTP'); } catch (g) {} 1582 throw new Error('This browser does not support XMLHttpRequest.'); 1583 }; 1584 } 1585 1586 request = new XHR(); 1587 // Store a reference to the url on the request instance 1588 request.uri = options.uri; 1589 1590 urlInfo = vjs.parseUrl(options.uri); 1591 winLoc = window.location; 1592 // Check if url is for another domain/origin 1593 // IE8 doesn't know location.origin, so we won't rely on it here 1594 crossOrigin = (urlInfo.protocol + urlInfo.host) !== (winLoc.protocol + winLoc.host); 1595 1596 // XDomainRequest -- Use for IE if XMLHTTPRequest2 isn't available 1597 // 'withCredentials' is only available in XMLHTTPRequest2 1598 // Also XDomainRequest has a lot of gotchas, so only use if cross domain 1599 if (crossOrigin && window.XDomainRequest && !('withCredentials' in request)) { 1600 request = new window.XDomainRequest(); 1601 request.onload = successHandler; 1602 request.onerror = errorHandler; 1603 // These blank handlers need to be set to fix ie9 1604 // http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/ 1605 request.onprogress = function(){}; 1606 request.ontimeout = function(){}; 1607 1608 // XMLHTTPRequest 1609 } else { 1610 fileUrl = (urlInfo.protocol == 'file:' || winLoc.protocol == 'file:'); 1611 1612 request.onreadystatechange = function() { 1613 if (request.readyState === 4) { 1614 if (request.timedout) { 1615 return errorHandler('timeout'); 1616 } 1617 1618 if (request.status === 200 || fileUrl && request.status === 0) { 1619 successHandler(); 1620 } else { 1621 errorHandler(); 1622 } 1623 } 1624 }; 1625 1626 if (options.timeout) { 1627 abortTimeout = window.setTimeout(function() { 1628 if (request.readyState !== 4) { 1629 request.timedout = true; 1630 request.abort(); 1631 } 1632 }, options.timeout); 1633 } 1634 } 1635 1636 // open the connection 1637 try { 1638 // Third arg is async, or ignored by XDomainRequest 1639 request.open(options.method || 'GET', options.uri, true); 1640 } catch(err) { 1641 return errorHandler(err); 1642 } 1643 1644 // withCredentials only supported by XMLHttpRequest2 1645 if(options.withCredentials) { 1646 request.withCredentials = true; 1647 } 1648 1649 if (options.responseType) { 1650 request.responseType = options.responseType; 1651 } 1652 1653 // send the request 1654 try { 1655 request.send(); 1656 } catch(err) { 1657 return errorHandler(err); 1658 } 1659 1660 return request; 1661}; 1662/** 1663 * Utility functions namespace 1664 * @namespace 1665 * @type {Object} 1666 */ 1667vjs.util = {}; 1668 1669/** 1670 * Merge two options objects, recursively merging any plain object properties as 1671 * well. Previously `deepMerge` 1672 * 1673 * @param {Object} obj1 Object to override values in 1674 * @param {Object} obj2 Overriding object 1675 * @return {Object} New object -- obj1 and obj2 will be untouched 1676 */ 1677vjs.util.mergeOptions = function(obj1, obj2){ 1678 var key, val1, val2; 1679 1680 // make a copy of obj1 so we're not overwriting original values. 1681 // like prototype.options_ and all sub options objects 1682 obj1 = vjs.obj.copy(obj1); 1683 1684 for (key in obj2){ 1685 if (obj2.hasOwnProperty(key)) { 1686 val1 = obj1[key]; 1687 val2 = obj2[key]; 1688 1689 // Check if both properties are pure objects and do a deep merge if so 1690 if (vjs.obj.isPlain(val1) && vjs.obj.isPlain(val2)) { 1691 obj1[key] = vjs.util.mergeOptions(val1, val2); 1692 } else { 1693 obj1[key] = obj2[key]; 1694 } 1695 } 1696 } 1697 return obj1; 1698};/** 1699 * @fileoverview Player Component - Base class for all UI objects 1700 * 1701 */ 1702 1703/** 1704 * Base UI Component class 1705 * 1706 * Components are embeddable UI objects that are represented by both a 1707 * javascript object and an element in the DOM. They can be children of other 1708 * components, and can have many children themselves. 1709 * 1710 * // adding a button to the player 1711 * var button = player.addChild('button'); 1712 * button.el(); // -> button element 1713 * 1714 * <div class="video-js"> 1715 * <div class="vjs-button">Button</div> 1716 * </div> 1717 * 1718 * Components are also event emitters. 1719 * 1720 * button.on('click', function(){ 1721 * console.log('Button Clicked!'); 1722 * }); 1723 * 1724 * button.trigger('customevent'); 1725 * 1726 * @param {Object} player Main Player 1727 * @param {Object=} options 1728 * @class 1729 * @constructor
1730 * @extends vjs.CoreObject 1731 */ 1732vjs.Component = vjs.CoreObject.extend({ 1733 /** 1734 * the constructor function for the class 1735 * 1736 * @constructor 1737 */ 1738 init: function(player, options, ready){ 1739 this.player_ = player; 1740 1741 // Make a copy of prototype.options_ to protect against overriding global defaults 1742 this.options_ = vjs.obj.copy(this.options_); 1743 1744 // Updated options with supplied options 1745 options = this.options(options); 1746 1747 // Get ID from options or options element if one is supplied 1748 this.id_ = options['id'] || (options['el'] && options['el']['id']); 1749 1750 // If there was no ID from the options, generate one 1751 if (!this.id_) { 1752 // Don't require the player ID function in the case of mock players 1753 this.id_ = ((player.id && player.id()) || 'no_player') + '_component_' + vjs.guid++; 1754 } 1755 1756 this.name_ = options['name'] || null; 1757 1758 // Create element if one wasn't provided in options 1759 this.el_ = options['el'] || this.createEl(); 1760 1761 this.children_ = []; 1762 this.childIndex_ = {}; 1763 this.childNameIndex_ = {}; 1764 1765 // Add any child components in options 1766 this.initChildren(); 1767 1768 this.ready(ready); 1769 // Don't want to trigger ready here or it will before init is actually 1770 // finished for all children that run this constructor 1771 1772 if (options.reportTouchActivity !== false) { 1773 this.enableTouchActivity(); 1774 } 1775 } 1776}); 1777 1778/** 1779 * Dispose of the component and all child components 1780 */ 1781vjs.Component.prototype.dispose = function(){ 1782 this.trigger({ type: 'dispose', 'bubbles': false }); 1783 1784 // Dispose all children. 1785 if (this.children_) { 1786 for (var i = this.children_.length - 1; i >= 0; i--) { 1787 if (this.children_[i].dispose) { 1788 this.children_[i].dispose(); 1789 } 1790 } 1791 } 1792 1793 // Delete child references 1794 this.children_ = null; 1795 this.childIndex_ = null; 1796 this.childNameIndex_ = null; 1797 1798 // Remove all event listeners. 1799 this.off(); 1800 1801 // Remove element from DOM 1802 if (this.el_.parentNode) { 1803 this.el_.parentNode.removeChild(this.el_); 1804 } 1805 1806 vjs.removeData(this.el_); 1807 this.el_ = null; 1808}; 1809 1810/** 1811 * Reference to main player instance 1812 * 1813 * @type {vjs.Player} 1814 * @private 1815 */ 1816vjs.Component.prototype.player_ = true; 1817 1818/** 1819 * Return the component's player 1820 * 1821 * @return {vjs.Player} 1822 */ 1823vjs.Component.prototype.player = function(){ 1824 return this.player_; 1825}; 1826 1827/** 1828 * The component's options object 1829 * 1830 * @type {Object} 1831 * @private 1832 */ 1833vjs.Component.prototype.options_; 1834 1835/** 1836 * Deep merge of options objects 1837 * 1838 * Whenever a property is an object on both options objects 1839 * the two properties will be merged using vjs.obj.deepMerge. 1840 * 1841 * This is used for merging options for child components. We 1842 * want it to be easy to override individual options on a child 1843 * component without having to rewrite all the other default options. 1844 * 1845 * Parent.prototype.options_ = { 1846 * children: { 1847 * 'childOne': { 'foo': 'bar', 'asdf': 'fdsa' }, 1848 * 'childTwo': {}, 1849 * 'childThree': {} 1850 * } 1851 * } 1852 * newOptions = { 1853 * children: { 1854 * 'childOne': { 'foo': 'baz', 'abc': '123' } 1855 * 'childTwo': null, 1856 * 'childFour': {} 1857 * } 1858 * } 1859 * 1860 * this.options(newOptions); 1861 * 1862 * RESULT 1863 * 1864 * { 1865 * children: { 1866 * 'childOne': { 'foo': 'baz', 'asdf': 'fdsa', 'abc': '123' }, 1867 * 'childTwo': null, // Disabled. Won't be initialized. 1868 * 'childThree': {}, 1869 * 'childFour': {} 1870 * } 1871 * } 1872 * 1873 * @param {Object} obj Object of new option values 1874 * @return {Object} A NEW object of this.options_ and obj merged 1875 */ 1876vjs.Component.prototype.options = function(obj){ 1877 if (obj === undefined) return this.options_; 1878 1879 return this.options_ = vjs.util.mergeOptions(this.options_, obj); 1880}; 1881 1882/** 1883 * The DOM element for the component 1884 * 1885 * @type {Element} 1886 * @private 1887 */ 1888vjs.Component.prototype.el_; 1889 1890/** 1891 * Create the component's DOM element 1892 * 1893 * @param {String=} tagName Element's node type. e.g. 'div' 1894 * @param {Object=} attributes An object of element attributes that should be set on the element 1895 * @return {Element} 1896 */ 1897vjs.Component.prototype.createEl = function(tagName, attributes){ 1898 return vjs.createEl(tagName, attributes); 1899}; 1900 1901vjs.Component.prototype.localize = function(string){ 1902 var lang = this.player_.language(), 1903 languages = this.player_.languages(); 1904 if (languages && languages[lang] && languages[lang][string]) { 1905 return languages[lang][string]; 1906 } 1907 return string; 1908}; 1909 1910/** 1911 * Get the component's DOM element 1912 * 1913 * var domEl = myComponent.el(); 1914 * 1915 * @return {Element} 1916 */ 1917vjs.Component.prototype.el = function(){ 1918 return this.el_; 1919}; 1920 1921/** 1922 * An optional element where, if defined, children will be inserted instead of 1923 * directly in `el_` 1924 * 1925 * @type {Element} 1926 * @private 1927 */ 1928vjs.Component.prototype.contentEl_; 1929 1930/** 1931 * Return the component's DOM element for embedding content. 1932 * Will either be el_ or a new element defined in createEl. 1933 * 1934 * @return {Element} 1935 */ 1936vjs.Component.prototype.contentEl = function(){ 1937 return this.contentEl_ || this.el_; 1938}; 1939 1940/** 1941 * The ID for the component 1942 * 1943 * @type {String} 1944 * @private 1945 */ 1946vjs.Component.prototype.id_; 1947 1948/** 1949 * Get the component's ID 1950 * 1951 * var id = myComponent.id(); 1952 * 1953 * @return {String} 1954 */ 1955vjs.Component.prototype.id = function(){ 1956 return this.id_; 1957}; 1958 1959/** 1960 * The name for the component. Often used to reference the component. 1961 * 1962 * @type {String} 1963 * @private 1964 */ 1965vjs.Component.prototype.name_; 1966 1967/** 1968 * Get the component's name. The name is often used to reference the component. 1969 * 1970 * var name = myComponent.name(); 1971 * 1972 * @return {String} 1973 */ 1974vjs.Component.prototype.name = function(){ 1975 return this.name_; 1976}; 1977 1978/** 1979 * Array of child components 1980 * 1981 * @type {Array} 1982 * @private 1983 */ 1984vjs.Component.prototype.children_; 1985 1986/** 1987 * Get an array of all child components 1988 * 1989 * var kids = myComponent.children(); 1990 * 1991 * @return {Array} The children 1992 */ 1993vjs.Component.prototype.children = function(){ 1994 return this.children_; 1995}; 1996 1997/** 1998 * Object of child components by ID 1999 * 2000 * @type {Object} 2001 * @private 2002 */ 2003vjs.Component.prototype.childIndex_; 2004 2005/** 2006 * Returns a child component with the provided ID 2007 * 2008 * @return {vjs.Component} 2009 */ 2010vjs.Component.prototype.getChildById = function(id){ 2011 return this.childIndex_[id]; 2012}; 2013 2014/** 2015 * Object of child components by name 2016 * 2017 * @type {Object} 2018 * @private 2019 */ 2020vjs.Component.prototype.childNameIndex_; 2021 2022/** 2023 * Returns a child component with the provided name 2024 * 2025 * @return {vjs.Component} 2026 */ 2027vjs.Component.prototype.getChild = function(name){ 2028 return this.childNameIndex_[name]; 2029}; 2030 2031/** 2032 * Adds a child component inside this component 2033 * 2034 * myComponent.el(); 2035 * // -> <div class='my-component'></div> 2036 * myComonent.children(); 2037 * // [empty array] 2038 * 2039 * var myButton = myComponent.addChild('MyButton'); 2040 * // -> <div class='my-component'><div class="my-button">myButton<div></div> 2041 * // ->
2041 myButton === myComonent.children()[0]; 2042 * 2043 * Pass in options for child constructors and options for children of the child 2044 * 2045 * var myButton = myComponent.addChild('MyButton', { 2046 * text: 'Press Me', 2047 * children: { 2048 * buttonChildExample: { 2049 * buttonChildOption: true 2050 * } 2051 * } 2052 * }); 2053 * 2054 * @param {String|vjs.Component} child The class name or instance of a child to add 2055 * @param {Object=} options Options, including options to be passed to children of the child. 2056 * @return {vjs.Component} The child component (created by this process if a string was used) 2057 * @suppress {accessControls|checkRegExp|checkTypes|checkVars|const|constantProperty|deprecated|duplicate|es5Strict|fileoverviewTags|globalThis|invalidCasts|missingProperties|nonStandardJsDocs|strictModuleDepCheck|undefinedNames|undefinedVars|unknownDefines|uselessCode|visibility} 2058 */ 2059vjs.Component.prototype.addChild = function(child, options){ 2060 var component, componentClass, componentName; 2061 2062 // If child is a string, create new component with options 2063 if (typeof child === 'string') { 2064 componentName = child; 2065 2066 // Make sure options is at least an empty object to protect against errors 2067 options = options || {}; 2068 2069 // If no componentClass in options, assume componentClass is the name lowercased 2070 // (e.g. playButton) 2071 componentClass = options['componentClass'] || vjs.capitalize(componentName); 2072 2073 // Set name through options 2074 options['name'] = componentName; 2075 2076 // Create a new object & element for this controls set 2077 // If there's no .player_, this is a player 2078 // Closure Compiler throws an 'incomplete alias' warning if we use the vjs variable directly. 2079 // Every class should be exported, so this should never be a problem here. 2080 component = new window['videojs'][componentClass](this.player_ || this, options); 2081 2082 // child is a component instance 2083 } else { 2084 component = child; 2085 } 2086 2087 this.children_.push(component); 2088 2089 if (typeof component.id === 'function') { 2090 this.childIndex_[component.id()] = component; 2091 } 2092 2093 // If a name wasn't used to create the component, check if we can use the 2094 // name function of the component 2095 componentName = componentName || (component.name && component.name()); 2096 2097 if (componentName) { 2098 this.childNameIndex_[componentName] = component; 2099 } 2100 2101 // Add the UI object's element to the container div (box) 2102 // Having an element is not required 2103 if (typeof component['el'] === 'function' && component['el']()) { 2104 this.contentEl().appendChild(component['el']()); 2105 } 2106 2107 // Return so it can stored on parent object if desired. 2108 return component; 2109}; 2110 2111/** 2112 * Remove a child component from this component's list of children, and the 2113 * child component's element from this component's element 2114 * 2115 * @param {vjs.Component} component Component to remove 2116 */ 2117vjs.Component.prototype.removeChild = function(component){ 2118 if (typeof component === 'string') { 2119 component = this.getChild(component); 2120 } 2121 2122 if (!component || !this.children_) return; 2123 2124 var childFound = false; 2125 for (var i = this.children_.length - 1; i >= 0; i--) { 2126 if (this.children_[i] === component) { 2127 childFound = true; 2128 this.children_.splice(i,1); 2129 break; 2130 } 2131 } 2132 2133 if (!childFound) return; 2134 2135 this.childIndex_[component.id] = null; 2136 this.childNameIndex_[component.name] = null; 2137 2138 var compEl = component.el(); 2139 if (compEl && compEl.parentNode === this.contentEl()) { 2140 this.contentEl().removeChild(component.el()); 2141 } 2142}; 2143 2144/** 2145 * Add and initialize default child components from options 2146 * 2147 * // when an instance of MyComponent is created, all children in options 2148 * // will be added to the instance by their name strings and options 2149 * MyComponent.prototype.options_.children = { 2150 * myChildComponent: { 2151 * myChildOption: true 2152 * } 2153 * } 2154 * 2155 * // Or when creating the component 2156 * var myComp = new MyComponent(player, { 2157 * children: { 2158 * myChildComponent: { 2159 * myChildOption: true 2160 * } 2161 * } 2162 * }); 2163 * 2164 * The children option can also be an Array of child names or 2165 * child options objects (that also include a 'name' key). 2166 * 2167 * var myComp = new MyComponent(player, { 2168 * children: [ 2169 * 'button', 2170 * { 2171 * name: 'button', 2172 * someOtherOption: true 2173 * } 2174 * ] 2175 * }); 2176 * 2177 */ 2178vjs.Component.prototype.initChildren = function(){ 2179 var parent, parentOptions, children, child, name, opts, handleAdd; 2180 2181 parent = this; 2182 parentOptions = parent.options(); 2183 children = parentOptions['children']; 2184 2185 if (children) { 2186 handleAdd = function(name, opts){ 2187 // Allow options for children to be set at the parent options 2188 // e.g. videojs(id, { controlBar: false }); 2189 // instead of videojs(id, { children: { controlBar: false }); 2190 if (parentOptions[name] !== undefined) { 2191 opts = parentOptions[name]; 2192 } 2193 2194 // Allow for disabling default components 2195 // e.g. vjs.options['children']['posterImage'] = false 2196 if (opts === false) return; 2197 2198 // Create and add the child component. 2199 // Add a direct reference to the child by name on the parent instance. 2200 // If two of the same component are used, different names should be supplied 2201 // for each 2202 parent[name] = parent.addChild(name, opts); 2203 }; 2204 2205 // Allow for an array of children details to passed in the options 2206 if (vjs.obj.isArray(children)) { 2207 for (var i = 0; i < children.length; i++) { 2208 child = children[i]; 2209 2210 if (typeof child == 'string') { 2211 // ['myComponent'] 2212 name = child; 2213 opts = {}; 2214 } else { 2215 // [{ name: 'myComponent', otherOption: true }] 2216 name = child.name; 2217 opts = child; 2218 } 2219 2220 handleAdd(name, opts); 2221 } 2222 } else { 2223 vjs.obj.each(children, handleAdd); 2224 } 2225 } 2226}; 2227 2228/** 2229 * Allows sub components to stack CSS class names 2230 * 2231 * @return {String} The constructed class name 2232 */ 2233vjs.Component.prototype.buildCSSClass = function(){ 2234 // Child classes can include a function that does: 2235 // return 'CLASS NAME' + this._super(); 2236 return ''; 2237}; 2238 2239/* Events 2240============================================================================= */ 2241
2242/** 2243 * Add an event listener to this component's element 2244 * 2245 * var myFunc = function(){ 2246 * var myComponent = this; 2247 * // Do something when the event is fired 2248 * }; 2249 * 2250 * myComponent.on('eventType', myFunc); 2251 * 2252 * The context of myFunc will be myComponent unless previously bound. 2253 * 2254 * Alternatively, you can add a listener to another element or component. 2255 * 2256 * myComponent.on(otherElement, 'eventName', myFunc); 2257 * myComponent.on(otherComponent, 'eventName', myFunc); 2258 * 2259 * The benefit of using this over `vjs.on(otherElement, 'eventName', myFunc)` 2260 * and `otherComponent.on('eventName', myFunc)` is that this way the listeners 2261 * will be automatically cleaned up when either component is disposed. 2262 * It will also bind myComponent as the context of myFunc. 2263 * 2264 * **NOTE**: When using this on elements in the page other than window 2265 * and document (both permanent), if you remove the element from the DOM 2266 * you need to call `vjs.trigger(el, 'dispose')` on it to clean up 2267 * references to it and allow the browser to garbage collect it. 2268 * 2269 * @param {String|vjs.Component} first The event type or other component 2270 * @param {Function|String} second The event handler or event type 2271 * @param {Function} third The event handler 2272 * @return {vjs.Component} self 2273 */ 2274vjs.Component.prototype.on = function(first, second, third){
2275 var target, type, fn, removeOnDispose, cleanRemover, thisComponent; 2276 2277 if (typeof first === 'string' || vjs.obj.isArray(first)) { 2278 vjs.on(this.el_, first, vjs.bind(this, second)); 2279 2280 // Targeting another component or element 2281 } else { 2282 target = first; 2283 type = second; 2284 fn = vjs.bind(this, third); 2285 thisComponent = this; 2286 2287 // When this component is disposed, remove the listener from the other component 2288 removeOnDispose = function(){ 2289 thisComponent.off(target, type, fn); 2290 }; 2291 // Use the same function ID so we can remove it later it using the ID 2292 // of the original listener 2293 removeOnDispose.guid = fn.guid; 2294 this.on('dispose', removeOnDispose); 2295 2296 // If the other component is disposed first we need to clean the reference 2297 // to the other component in this component's removeOnDispose listener 2298 // Otherwise we create a memory leak. 2299 cleanRemover = function(){ 2300 thisComponent.off('dispose', removeOnDispose); 2301 }; 2302 // Add the same function ID so we can easily remove it later 2303 cleanRemover.guid = fn.guid; 2304 2305 // Check if this is a DOM node 2306 if (first.nodeName) { 2307 // Add the listener to the other element 2308 vjs.on(target, type, fn); 2309 vjs.on(target, 'dispose', cleanRemover); 2310 2311 // Should be a component 2312 // Not using `instanceof vjs.Component` because it makes mock players difficult 2313 } else if (typeof first.on === 'function') { 2314 // Add the listener to the other component 2315 target.on(type, fn); 2316 target.on('dispose', cleanRemover); 2317 } 2318 } 2319 2320 return this; 2321}; 2322 2323/** 2324 * Remove an event listener from this component's element 2325 * 2326 * myComponent.off('eventType', myFunc); 2327 * 2328 * If myFunc is excluded, ALL listeners for the event type will be removed. 2329 * If eventType is excluded, ALL listeners will be removed from the component. 2330 * 2331 * Alternatively you can use `off` to remove listeners that were added to other 2332 * elements or components using `myComponent.on(otherComponent...`. 2333 * In this case both the event type and listener function are REQUIRED. 2334 * 2335 * myComponent.off(otherElement, 'eventType', myFunc); 2336 * myComponent.off(otherComponent, 'eventType', myFunc); 2337 * 2338 * @param {String=|vjs.Component} first The event type or other component 2339 * @param {Function=|String} second The listener function or event type 2340 * @param {Function=} third The listener for other component 2341 * @return {vjs.Component} 2342 */ 2343vjs.Component.prototype.off = function(first, second, third){ 2344 var target, otherComponent, type, fn, otherEl; 2345 2346 if (!first || typeof first === 'string' || vjs.obj.isArray(first)) { 2347 vjs.off(this.el_, first, second); 2348 } else { 2349 target = first; 2350 type = second; 2351 // Ensure there's at least a guid, even if the function hasn't been used 2352 fn = vjs.bind(this, third); 2353 2354 // Remove the dispose listener on this component, 2355 // which was given the same guid as the event listener 2356 this.off('dispose', fn); 2357 2358 if (first.nodeName) { 2359 // Remove the listener 2360 vjs.off(target, type, fn); 2361 // Remove the listener for cleaning the dispose listener 2362 vjs.off(target, 'dispose', fn); 2363 } else { 2364 target.off(type, fn); 2365 target.off('dispose', fn); 2366 } 2367 } 2368 2369 return this; 2370}; 2371 2372/** 2373 * Add an event listener to be triggered only once and then removed 2374 * 2375 * myComponent.one('eventName', myFunc); 2376 * 2377 * Alternatively you can add a listener to another element or component 2378 * that will be triggered only once. 2379 * 2380 * myComponent.one(otherElement, 'eventName', myFunc); 2381 * myComponent.one(otherComponent, 'eventName', myFunc); 2382 * 2383 * @param {String|vjs.Component} first The event type or other component 2384 * @param {Function|String} second The listener function or event type 2385 * @param {Function=} third The listener function for other component 2386 * @return {vjs.Component} 2387 */ 2388vjs.Component.prototype.one = function(first, second, third) { 2389 var target, type, fn, thisComponent, newFunc; 2390 2391 if (typeof first === 'string' || vjs.obj.isArray(first)) { 2392 vjs.one(this.el_, first, vjs.bind(this, second)); 2393 } else { 2394 target = first; 2395 type = second; 2396 fn = vjs.bind(this, third); 2397 thisComponent = this; 2398 2399 newFunc = function(){ 2400 thisComponent.off(target, type, newFunc); 2401 fn.apply(this, arguments); 2402 }; 2403 // Keep the same function ID so we can remove it later 2404 newFunc.guid = fn.guid; 2405 2406 this.on(target, type, newFunc); 2407 } 2408 2409 return this; 2410}; 2411 2412/** 2413 * Trigger an event on an element 2414 * 2415 * myComponent.trigger('eventName'); 2416 * myComponent.trigger({'type':'eventName'}); 2417 * 2418 * @param {Event|Object|String} event A string (the type) or an event object with a type attribute 2419 * @return {vjs.Component} self 2420 */ 2421vjs.Component.prototype.trigger = function(event){ 2422 vjs.trigger(this.el_, event); 2423 return this; 2424}; 2425 2426/* Ready 2427================================================================================ */ 2428/** 2429 * Is the component loaded 2430 * This can mean different things depending on the component. 2431 * 2432 * @private 2433 * @type {Boolean} 2434 */ 2435vjs.Component.prototype.isReady_; 2436 2437/** 2438 * Trigger ready as soon as initialization is finished 2439 * 2440 * Allows for delaying ready. Override on a sub class prototype.
2441 * If you set this.isReadyOnInitFinish_ it will affect all components. 2442 * Specially used when waiting for the Flash player to asynchronously load. 2443 * 2444 * @type {Boolean} 2445 * @private 2446 */ 2447vjs.Component.prototype.isReadyOnInitFinish_ = true; 2448 2449/** 2450 * List of ready listeners 2451 * 2452 * @type {Array} 2453 * @private 2454 */ 2455vjs.Component.prototype.readyQueue_; 2456 2457/** 2458 * Bind a listener to the component's ready state 2459 * 2460 * Different from event listeners in that if the ready event has already happened 2461 * it will trigger the function immediately. 2462 * 2463 * @param {Function} fn Ready listener 2464 * @return {vjs.Component} 2465 */ 2466vjs.Component.prototype.ready = function(fn){ 2467 if (fn) { 2468 if (this.isReady_) { 2469 fn.call(this); 2470 } else { 2471 if (this.readyQueue_ === undefined) { 2472 this.readyQueue_ = []; 2473 } 2474 this.readyQueue_.push(fn); 2475 } 2476 } 2477 return this; 2478}; 2479 2480/** 2481 * Trigger the ready listeners 2482 * 2483 * @return {vjs.Component} 2484 */ 2485vjs.Component.prototype.triggerReady = function(){ 2486 this.isReady_ = true; 2487 2488 var readyQueue = this.readyQueue_; 2489 2490 if (readyQueue && readyQueue.length > 0) { 2491 2492 for (var i = 0, j = readyQueue.length; i < j; i++) { 2493 readyQueue[i].call(this); 2494 } 2495 2496 // Reset Ready Queue 2497 this.readyQueue_ = []; 2498 2499 // Allow for using event listeners also, in case you want to do something everytime a source is ready. 2500 this.trigger('ready'); 2501 } 2502}; 2503 2504/* Display 2505============================================================================= */ 2506 2507/** 2508 * Check if a component's element has a CSS class name 2509 * 2510 * @param {String} classToCheck Classname to check 2511 * @return {vjs.Component} 2512 */ 2513vjs.Component.prototype.hasClass = function(classToCheck){ 2514 return vjs.hasClass(this.el_, classToCheck); 2515}; 2516 2517/** 2518 * Add a CSS class name to the component's element 2519 * 2520 * @param {String} classToAdd Classname to add 2521 * @return {vjs.Component} 2522 */ 2523vjs.Component.prototype.addClass = function(classToAdd){ 2524 vjs.addClass(this.el_, classToAdd); 2525 return this; 2526}; 2527 2528/** 2529 * Remove a CSS class name from the component's element 2530 * 2531 * @param {String} classToRemove Classname to remove 2532 * @return {vjs.Component} 2533 */ 2534vjs.Component.prototype.removeClass = function(classToRemove){ 2535 vjs.removeClass(this.el_, classToRemove); 2536 return this; 2537}; 2538 2539/** 2540 * Show the component element if hidden 2541 * 2542 * @return {vjs.Component} 2543 */ 2544vjs.Component.prototype.show = function(){ 2545 this.el_.style.display = 'block'; 2546 return this; 2547}; 2548 2549/** 2550 * Hide the component element if currently showing 2551 * 2552 * @return {vjs.Component} 2553 */ 2554vjs.Component.prototype.hide = function(){ 2555 this.el_.style.display = 'none'; 2556 return this; 2557}; 2558 2559/** 2560 * Lock an item in its visible state 2561 * To be used with fadeIn/fadeOut. 2562 * 2563 * @return {vjs.Component} 2564 * @private 2565 */ 2566vjs.Component.prototype.lockShowing = function(){ 2567 this.addClass('vjs-lock-showing'); 2568 return this; 2569}; 2570 2571/** 2572 * Unlock an item to be hidden 2573 * To be used with fadeIn/fadeOut. 2574 * 2575 * @return {vjs.Component} 2576 * @private 2577 */ 2578vjs.Component.prototype.unlockShowing = function(){ 2579 this.removeClass('vjs-lock-showing'); 2580 return this; 2581}; 2582 2583/** 2584 * Disable component by making it unshowable 2585 * 2586 * Currently private because we're moving towards more css-based states. 2587 * @private 2588 */ 2589vjs.Component.prototype.disable = function(){ 2590 this.hide(); 2591 this.show = function(){}; 2592}; 2593 2594/** 2595 * Set or get the width of the component (CSS values) 2596 *
2597 * Setting the video tag dimension values only works with values in pixels. 2598 * Percent values will not work. 2599 * Some percents can be used, but width()/height() will return the number + %, 2600 * not the actual computed width/height. 2601 * 2602 * @param {Number|String=} num Optional width number 2603 * @param {Boolean} skipListeners Skip the 'resize' event trigger 2604 * @return {vjs.Component} This component, when setting the width 2605 * @return {Number|String} The width, when getting 2606 */ 2607vjs.Component.prototype.width = function(num, skipListeners){ 2608 return this.dimension('width', num, skipListeners); 2609}; 2610 2611/** 2612 * Get or set the height of the component (CSS values) 2613 * 2614 * Setting the video tag dimension values only works with values in pixels. 2615 * Percent values will not work. 2616 * Some percents can be used, but width()/height() will return the number + %, 2617 * not the actual computed width/height. 2618 * 2619 * @param {Number|String=} num New component height 2620 * @param {Boolean=} skipListeners Skip the resize event trigger 2621 * @return {vjs.Component} This component, when setting the height 2622 * @return {Number|String} The height, when getting 2623 */ 2624vjs.Component.prototype.height = function(num, skipListeners){ 2625 return this.dimension('height', num, skipListeners); 2626}; 2627 2628/** 2629 * Set both width and height at the same time 2630 * 2631 * @param {Number|String} width 2632 * @param {Number|String} height 2633 * @return {vjs.Component} The component 2634 */ 2635vjs.Component.prototype.dimensions = function(width, height){ 2636 // Skip resize listeners on width for optimization 2637 return this.width(width, true).height(height); 2638}; 2639 2640/** 2641 * Get or set width or height 2642 * 2643 * This is the shared code for the width() and height() methods. 2644 * All for an integer, integer + 'px' or integer + '%'; 2645 * 2646 * Known issue: Hidden elements officially have a width of 0. We're defaulting 2647 * to the style.width value and falling back to computedStyle which has the 2648 * hidden element issue. Info, but probably not an efficient fix: 2649 * http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/ 2650 * 2651 * @param {String} widthOrHeight 'width' or 'height' 2652 * @param {Number|String=} num New dimension 2653 * @param {Boolean=} skipListeners Skip resize event trigger 2654 * @return {vjs.Component} The component if a dimension was set 2655 * @return {Number|String} The dimension if nothing was set 2656 * @private 2657 */ 2658vjs.Component.prototype.dimension = function(widthOrHeight, num, skipListeners){ 2659 if (num !== undefined) { 2660 if (num === null || vjs.isNaN(num)) { 2661 num = 0; 2662 } 2663 2664 // Check if using css width/height (% or px) and adjust 2665 if ((''+num).indexOf('%') !== -1 || (''+num).indexOf('px') !== -1) { 2666 this.el_.style[widthOrHeight] = num; 2667 } else if (num === 'auto') { 2668 this.el_.style[widthOrHeight] = ''; 2669 } else { 2670 this.el_.style[widthOrHeight] = num+'px'; 2671 } 2672 2673 // skipListeners allows us to avoid triggering the resize event when setting both width and height 2674 if (!skipListeners) { this.trigger('resize'); } 2675 2676 // Return component 2677 return this; 2678 } 2679 2680 // Not setting a value, so getting it 2681 // Make sure element exists 2682 if (!this.el_) return 0; 2683 2684 // Get dimension value from style 2685 var val = this.el_.style[widthOrHeight]; 2686 var pxIndex = val.indexOf('px'); 2687 if (pxIndex !== -1) { 2688 // Return the pixel value with no 'px' 2689 return parseInt(val.slice(0,pxIndex), 10); 2690 2691 // No px so using % or no style was set, so falling back to offsetWidth/height 2692 // If component has display:none, offset will return 0 2693 // TODO: handle display:none and no dimension style using px 2694 } else { 2695 2696 return parseInt(this.el_['offset'+vjs.capitalize(widthOrHeight)], 10); 2697 2698 // ComputedStyle version. 2699 // Only difference is if the element is hidden it will return 2700 // the percent value (e.g. '100%'') 2701 // instead of zero like offsetWidth returns. 2702 // var val = vjs.getComputedStyleValue(this.el_, widthOrHeight); 2703 // var pxIndex = val.indexOf('px'); 2704 2705 // if (pxIndex !== -1) { 2706 // return val.slice(0, pxIndex); 2707 // } else { 2708 // return val; 2709 // } 2710 } 2711}; 2712 2713/** 2714 * Fired when the width and/or height of the component changes 2715 * @event resize 2716 */ 2717vjs.Component.prototype.onResize; 2718 2719/** 2720 * Emit 'tap' events when touch events are supported 2721 * 2722 * This is used to support toggling the controls through a tap on the video. 2723 * 2724 * We're requiring them to be enabled because otherwise every component would 2725 * have this extra overhead unnecessarily, on mobile devices where extra 2726 * overhead is especially bad. 2727 * @private 2728 */ 2729vjs.Component.prototype.emitTapEvents = function(){ 2730 var touchStart, firstTouch, touchTime, couldBeTap, noTap, 2731 xdiff, ydiff, touchDistance, tapMovementThreshold; 2732 2733 // Track the start time so we can determine how long the touch lasted 2734 touchStart = 0; 2735 firstTouch = null; 2736 2737 // Maximum movement allowed during a touch event to still be considered a tap 2738 tapMovementThreshold = 22; 2739 2740 this.on('touchstart', function(event) { 2741 // If more than one finger, don't consider treating this as a click 2742 if (event.touches.length === 1) { 2743 firstTouch = event.touches[0]; 2744 // Record start time so we can detect a tap vs. "touch and hold" 2745 touchStart = new Date().getTime(); 2746 // Reset couldBeTap tracking 2747 couldBeTap = true; 2748 } 2749 }); 2750 2751 this.on('touchmove', function(event) { 2752 // If more than one finger, don't consider treating this as a click 2753 if (event.touches.length > 1) { 2754 couldBeTap = false;
2755 } else if (firstTouch) { 2756 // Some devices will throw touchmoves for all but the slightest of taps. 2757 // So, if we moved only a small distance, this could still be a tap 2758 xdiff = event.touches[0].pageX - firstTouch.pageX; 2759 ydiff = event.touches[0].pageY - firstTouch.pageY; 2760 touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff); 2761 if (touchDistance > tapMovementThreshold) { 2762 couldBeTap = false; 2763 } 2764 } 2765 }); 2766 2767 noTap = function(){ 2768 couldBeTap = false; 2769 }; 2770 // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s 2771 this.on('touchleave', noTap); 2772 this.on('touchcancel', noTap); 2773 2774 // When the touch ends, measure how long it took and trigger the appropriate 2775 // event 2776 this.on('touchend', function(event) { 2777 firstTouch = null; 2778 // Proceed only if the touchmove/leave/cancel event didn't happen 2779 if (couldBeTap === true) { 2780 // Measure how long the touch lasted 2781 touchTime = new Date().getTime() - touchStart; 2782 // The touch needs to be quick in order to consider it a tap 2783 if (touchTime < 250) { 2784 event.preventDefault(); // Don't let browser turn this into a click 2785 this.trigger('tap'); 2786 // It may be good to copy the touchend event object and change the 2787 // type to tap, if the other event properties aren't exact after 2788 // vjs.fixEvent runs (e.g. event.target) 2789 } 2790 } 2791 }); 2792}; 2793 2794/** 2795 * Report user touch activity when touch events occur 2796 * 2797 * User activity is used to determine when controls should show/hide. It's 2798 * relatively simple when it comes to mouse events, because any mouse event 2799 * should show the controls. So we capture mouse events that bubble up to the 2800 * player and report activity when that happens. 2801 * 2802 * With touch events it isn't as easy. We can't rely on touch events at the 2803 * player level, because a tap (touchstart + touchend) on the video itself on 2804 * mobile devices is meant to turn controls off (and on). User activity is 2805 * checked asynchronously, so what could happen is a tap event on the video 2806 * turns the controls off, then the touchend event bubbles up to the player, 2807 * which if it reported user activity, would turn the controls right back on. 2808 * (We also don't want to completely block touch events from bubbling up) 2809 * 2810 * Also a touchmove, touch+hold, and anything other than a tap is not supposed 2811 * to turn the controls back on on a mobile device. 2812 * 2813 * Here we're setting the default component behavior to report user activity 2814 * whenever touch events happen, and this can be turned off by components that 2815 * want touch events to act differently. 2816 */ 2817vjs.Component.prototype.enableTouchActivity = function() { 2818 var report, touchHolding, touchEnd; 2819 2820 // Don't continue if the root player doesn't support reporting user activity 2821 if (!this.player().reportUserActivity) { 2822 return; 2823 } 2824 2825 // listener for reporting that the user is active 2826 report = vjs.bind(this.player(), this.player().reportUserActivity); 2827 2828 this.on('touchstart', function() { 2829 report(); 2830 // For as long as the they are touching the device or have their mouse down, 2831 // we consider them active even if they're not moving their finger or mouse. 2832 // So we want to continue to update that they are active 2833 this.clearInterval(touchHolding); 2834 // report at the same interval as activityCheck 2835 touchHolding = this.setInterval(report, 250); 2836 }); 2837 2838 touchEnd = function(event) { 2839 report(); 2840 // stop the interval that maintains activity if the touch is holding 2841 this.clearInterval(touchHolding); 2842 }; 2843 2844 this.on('touchmove', report); 2845 this.on('touchend', touchEnd); 2846 this.on('touchcancel', touchEnd); 2847}; 2848 2849/** 2850 * Creates timeout and sets up disposal automatically. 2851 * @param {Function} fn The function to run after the timeout. 2852 * @param {Number} timeout Number of ms to delay before executing specified function. 2853 * @return {Number} Returns the timeout ID 2854 */ 2855vjs.Component.prototype.setTimeout = function(fn, timeout) { 2856 fn = vjs.bind(this, fn); 2857 2858 // window.setTimeout would be preferable here, but due to some bizarre issue with Sinon and/
2858or Phantomjs, we can't. 2859 var timeoutId = setTimeout(fn, timeout); 2860 2861 var disposeFn = function() { 2862 this.clearTimeout(timeoutId); 2863 }; 2864 2865 disposeFn.guid = 'vjs-timeout-'+ timeoutId; 2866 2867 this.on('dispose', disposeFn); 2868 2869 return timeoutId; 2870}; 2871 2872 2873/** 2874 * Clears a timeout and removes the associated dispose listener 2875 * @param {Number} timeoutId The id of the timeout to clear 2876 * @return {Number} Returns the timeout ID 2877 */ 2878vjs.Component.prototype.clearTimeout = function(timeoutId) { 2879 clearTimeout(timeoutId); 2880 2881 var disposeFn = function(){}; 2882 disposeFn.guid = 'vjs-timeout-'+ timeoutId; 2883 2884 this.off('dispose', disposeFn); 2885 2886 return timeoutId; 2887}; 2888 2889/** 2890 * Creates an interval and sets up disposal automatically. 2891 * @param {Function} fn The function to run every N seconds. 2892 * @param {Number} interval Number of ms to delay before executing specified function. 2893 * @return {Number} Returns the interval ID 2894 */ 2895vjs.Component.prototype.setInterval = function(fn, interval) { 2896 fn = vjs.bind(this, fn); 2897 2898 var intervalId = setInterval(fn, interval); 2899 2900 var disposeFn = function() { 2901 this.clearInterval(intervalId); 2902 }; 2903 2904 disposeFn.guid = 'vjs-interval-'+ intervalId; 2905 2906 this.on('dispose', disposeFn); 2907 2908 return intervalId; 2909}; 2910 2911/** 2912 * Clears an interval and removes the associated dispose listener 2913 * @param {Number} intervalId The id of the interval to clear 2914 * @return {Number} Returns the interval ID 2915 */ 2916vjs.Component.prototype.clearInterval = function(intervalId) { 2917 clearInterval(intervalId); 2918 2919 var disposeFn = function(){}; 2920 disposeFn.guid = 'vjs-interval-'+ intervalId; 2921 2922 this.off('dispose', disposeFn); 2923 2924 return intervalId; 2925}; 2926/* Button - Base class for all buttons 2927================================================================================ */ 2928/** 2929 * Base class for all buttons 2930 * @param {vjs.Player|Object} player 2931 * @param {Object=} options 2932 * @class 2933 * @constructor 2934 */ 2935vjs.Button = vjs.Component.extend({ 2936 /** 2937 * @constructor 2938 * @inheritDoc 2939 */ 2940 init: function(player, options){ 2941 vjs.Component.call(this, player, options); 2942 2943 this.emitTapEvents(); 2944 2945 this.on('tap', this.onClick); 2946 this.on('click', this.onClick); 2947 this.on('focus', this.onFocus); 2948 this.on('blur', this.onBlur); 2949 } 2950}); 2951 2952vjs.Button.prototype.createEl = function(type, props){ 2953 var el; 2954 2955 // Add standard Aria and Tabindex info 2956 props = vjs.obj.merge({ 2957 className: this.buildCSSClass(), 2958 'role': 'button', 2959 'aria-live': 'polite', // let the screen reader user know that the text of the button may change 2960 tabIndex: 0 2961 }, props); 2962 2963 el = vjs.Component.prototype.createEl.call(this, type, props); 2964 2965 // if innerHTML hasn't been overridden (bigPlayButton), add content elements 2966 if (!props.innerHTML) { 2967 this.contentEl_ = vjs.createEl('div', { 2968 className: 'vjs-control-content' 2969 }); 2970 2971 this.controlText_ = vjs.createEl('span', { 2972 className: 'vjs-control-text', 2973 innerHTML: this.localize(this.buttonText) || 'Need Text' 2974 }); 2975 2976 this.contentEl_.appendChild(this.controlText_); 2977 el.appendChild(this.contentEl_); 2978 } 2979 2980 return el; 2981}; 2982 2983vjs.Button.prototype.buildCSSClass = function(){ 2984 // TODO: Change vjs-control to vjs-button? 2985 return 'vjs-control ' + vjs.Component.prototype.buildCSSClass.call(this); 2986}; 2987 2988 // Click - Override with specific functionality for button 2989vjs.Button.prototype.onClick = function(){}; 2990 2991 // Focus - Add keyboard functionality to element 2992vjs.Button.prototype.onFocus = function(){ 2993 vjs.on(document, 'keydown', vjs.bind(this, this.onKeyPress)); 2994}; 2995 2996 // KeyPress (document level) - Trigger click when keys are pressed 2997vjs.Button.prototype.onKeyPress = function(event){ 2998 // Check for space bar (32) or enter (13) keys 2999 if (event.which == 32 || event.which == 13) { 3000 event.preventDefault(); 3001 this.onClick(); 3002 } 3003}; 3004 3005// Blur - Remove keyboard triggers 3006vjs.Button.prototype.onBlur = function(){ 3007 vjs.off(document, 'keydown', vjs.bind(this, this.onKeyPress)); 3008}; 3009/* Slider 3010================================================================================ */ 3011/** 3012 * The base functionality for sliders like the volume bar and seek bar 3013 * 3014 * @param {vjs.Player|Object} player 3015 * @param {Object=} options 3016 * @constructor 3017 */ 3018vjs.Slider = vjs.Component.extend({ 3019 /** @constructor */ 3020 init: function(player, options){ 3021 vjs.Component.call(this, player, options); 3022 3023 // Set property names to bar and handle to match with the child Slider class is looking for 3024 this.bar = this.getChild(this.options_['barName']); 3025 this.handle = this.getChild(this.options_['handleName']); 3026 3027 this.on('mousedown', this.onMouseDown); 3028 this.on('touchstart', this.onMouseDown); 3029 this.on('focus', this.onFocus); 3030 this.on('blur', this.onBlur); 3031 this.on('click', this.onClick); 3032 3033 this.on(player, 'controlsvisible', this.update); 3034 this.on(player, this.playerEvent, this.update); 3035 } 3036}); 3037 3038vjs.Slider.prototype.createEl = function(type, props) { 3039 props = props || {}; 3040 // Add the slider element class to all sub classes 3041 props.className = props.className + ' vjs-slider'; 3042 props = vjs.obj.merge({ 3043 'role': 'slider', 3044 'aria-valuenow': 0, 3045 'aria-valuemin': 0, 3046 'aria-valuemax': 100, 3047 tabIndex: 0 3048 }, props); 3049 3050 return vjs.Component.prototype.createEl.call(this, type, props); 3051}; 3052 3053vjs.Slider.prototype.onMouseDown = function(event){ 3054 event.preventDefault(); 3055 vjs.blockTextSelection(); 3056 this.addClass('vjs-sliding'); 3057 3058 this.on(document, 'mousemove', this.onMouseMove); 3059 this.on(document, 'mouseup', this.onMouseUp); 3060 this.on(document, 'touchmove', this.onMouseMove); 3061 this.on(document, 'touchend', this.onMouseUp); 3062 3063 this.onMouseMove(event); 3064}; 3065 3066// To be overridden by a subclass 3067vjs.Slider.prototype.onMouseMove = function(){}; 3068 3069vjs.Slider.prototype.onMouseUp = function() { 3070 vjs.unblockTextSelection(); 3071 this.removeClass('vjs-sliding'); 3072 3073 this.off(document, 'mousemove', this.onMouseMove); 3074 this.off(document, 'mouseup', this.onMouseUp); 3075 this.off(document, 'touchmove', this.onMouseMove); 3076 this.off(document, 'touchend', this.onMouseUp); 3077 3078 this.update(); 3079}; 3080 3081vjs.Slider.prototype.update = function(){ 3082 // In VolumeBar init we have a setTimeout for update that pops and update to the end of the 3083 // execution stack. The player is destroyed before then update will cause an error 3084 if (!this.el_) return; 3085 3086 // If scrubbing, we could use a cached value to make the handle keep up with the user's mouse. 3087 // On HTML5 browsers scrubbing is really smooth, but some flash players are slow, so we might want to utilize this later. 3088 // var progress = (this.player_.scrubbing) ? this.player_.getCache().currentTime / this.player_.duration() : this.player_.currentTime() / this.player_.duration(); 3089 3090 var barProgress, 3091 progress = this.getPercent(), 3092 handle = this.handle, 3093 bar = this.bar; 3094 3095 // Protect against no duration and other division issues 3096 if (isNaN(progress)) { progress = 0; } 3097 3098 barProgress = progress; 3099 3100 // If there is a handle, we need to account for the handle in our
3100calculation for progress bar 3101 // so that it doesn't fall short of or extend past the handle. 3102 if (handle) { 3103 3104 var box = this.el_, 3105 boxWidth = box.offsetWidth, 3106 3107 handleWidth = handle.el().offsetWidth, 3108 3109 // The width of the handle in percent of the containing box 3110 // In IE, widths may not be ready yet causing NaN 3111 handlePercent = (handleWidth) ? handleWidth / boxWidth : 0, 3112 3113 // Get the adjusted size of the box, considering that the handle's center never touches the left or right side. 3114 // There is a margin of half the handle's width on both sides. 3115 boxAdjustedPercent = 1 - handlePercent, 3116 3117 // Adjust the progress that we'll use to set widths to the new adjusted box width 3118 adjustedProgress = progress * boxAdjustedPercent; 3119 3120 // The bar does reach the left side, so we need to account for this in the bar's width 3121 barProgress = adjustedProgress + (handlePercent / 2); 3122 3123 // Move the handle from the left based on the adjected progress 3124 handle.el().style.left = vjs.round(adjustedProgress * 100, 2) + '%'; 3125 } 3126 3127 // Set the new bar width 3128 if (bar) { 3129 bar.el().style.width = vjs.round(barProgress * 100, 2) + '%'; 3130 } 3131}; 3132 3133vjs.Slider.prototype.calculateDistance = function(event){ 3134 var el, box, boxX, boxY, boxW, boxH, handle, pageX, pageY; 3135 3136 el = this.el_; 3137 box = vjs.findPosition(el); 3138 boxW = boxH = el.offsetWidth; 3139 handle = this.handle; 3140 3141 if (this.options()['vertical']) { 3142 boxY = box.top; 3143 3144 if (event.changedTouches) { 3145 pageY = event.changedTouches[0].pageY; 3146 } else { 3147 pageY = event.pageY; 3148 } 3149 3150 if (handle) { 3151 var handleH = handle.el().offsetHeight; 3152 // Adjusted X and Width, so handle doesn't go outside the bar 3153 boxY = boxY + (handleH / 2); 3154 boxH = boxH - handleH; 3155 } 3156 3157 // Percent that the click is through the adjusted area 3158 return Math.max(0, Math.min(1, ((boxY - pageY) + boxH) / boxH)); 3159 3160 } else { 3161 boxX = box.left; 3162 3163 if (event.changedTouches) { 3164 pageX = event.changedTouches[0].pageX; 3165 } else { 3166 pageX = event.pageX; 3167 } 3168 3169 if (handle) { 3170 var handleW = handle.el().offsetWidth; 3171 3172 // Adjusted X and Width, so handle doesn't go outside the bar 3173 boxX = boxX + (handleW / 2); 3174 boxW = boxW - handleW; 3175 } 3176 3177 // Percent that the click is through the adjusted area 3178 return Math.max(0, Math.min(1, (pageX - boxX) / boxW)); 3179 } 3180}; 3181 3182vjs.Slider.prototype.onFocus = function(){ 3183 this.on(document, 'keydown', this.onKeyPress); 3184}; 3185 3186vjs.Slider.prototype.onKeyPress = function(event){ 3187 if (event.which == 37 || event.which == 40) { // Left and Down Arrows 3188 event.preventDefault(); 3189 this.stepBack(); 3190 } else if (event.which == 38 || event.which == 39) { // Up and Right Arrows 3191 event.preventDefault(); 3192 this.stepForward(); 3193 } 3194}; 3195 3196vjs.Slider.prototype.onBlur = function(){ 3197 this.off(document, 'keydown', this.onKeyPress); 3198}; 3199 3200/** 3201 * Listener for click events on slider, used to prevent clicks 3202 * from bubbling up to parent elements like button menus. 3203 * @param {Object} event Event object 3204 */ 3205vjs.Slider.prototype.onClick = function(event){ 3206 event.stopImmediatePropagation(); 3207 event.preventDefault(); 3208}; 3209 3210/** 3211 * SeekBar Behavior includes play progress bar, and seek handle 3212 * Needed so it can determine seek position based on handle position/size 3213 * @param {vjs.Player|Object} player 3214 * @param {Object=} options 3215 * @constructor 3216 */ 3217vjs.SliderHandle = vjs.Component.extend(); 3218 3219/** 3220 * Default value of the slider 3221 * 3222 * @type {Number} 3223 * @private 3224 */ 3225vjs.SliderHandle.prototype.defaultValue = 0; 3226 3227/** @inheritDoc */ 3228vjs.SliderHandle.prototype.createEl = function(type, props) { 3229 props = props || {}; 3230 // Add the slider element class to all sub classes 3231 props.className = props.className + ' vjs-slider-handle'; 3232 props = vjs.obj.merge({ 3233 innerHTML: '<span class="vjs-control-text">'+this.defaultValue+'</span>' 3234 }, props); 3235 3236 return vjs.Component.prototype.createEl.call(this, 'div', props); 3237}; 3238/* Menu 3239================================================================================ */ 3240/** 3241 * The Menu component is used to build pop up menus, including subtitle and 3242 * captions selection menus. 3243 * 3244 * @param {vjs.Player|Object} player 3245 * @param {Object=} options 3246 * @class 3247 * @constructor 3248 */ 3249vjs.Menu = vjs.Component.extend(); 3250 3251/** 3252 * Add a menu item to the menu 3253 * @param {Object|String} component Component or component type to add 3254 */ 3255vjs.Menu.prototype.addItem = function(component){ 3256 this.addChild(component); 3257 component.on('click', vjs.bind(this, function(){ 3258 this.unlockShowing(); 3259 })); 3260}; 3261 3262/** @inheritDoc */ 3263vjs.Menu.prototype.createEl = function(){ 3264 var contentElType = this.options().contentElType || 'ul'; 3265 this.contentEl_ = vjs.createEl(contentElType, { 3266 className: 'vjs-menu-content' 3267 }); 3268 var el = vjs.Component.prototype.createEl.call(this, 'div', { 3269 append: this.contentEl_, 3270 className: 'vjs-menu' 3271 }); 3272 el.appendChild(this.contentEl_); 3273 3274 // Prevent clicks from bubbling up. Needed for Menu Buttons, 3275 // where a click on the parent is significant 3276 vjs.on(el, 'click', function(event){ 3277 event.preventDefault(); 3278 event.stopImmediatePropagation(); 3279 }); 3280 3281 return el; 3282}; 3283 3284/** 3285 * The component for a menu item. `<li>` 3286 * 3287 * @param {vjs.Player|Object} player 3288 * @param {Object=} options 3289 * @class 3290 * @constructor 3291 */ 3292vjs.MenuItem = vjs.Button.extend({ 3293 /** @constructor */ 3294 init: function(player, options){ 3295 vjs.Button.call(this, player, options); 3296 this.selected(options['selected']); 3297 } 3298}); 3299 3300/** @inheritDoc */ 3301vjs.MenuItem.prototype.createEl = function(type, props){ 3302 return vjs.Button.prototype.createEl.call(this, 'li', vjs.obj.merge({ 3303 className: 'vjs-menu-item', 3304 innerHTML: this.localize(this.options_['label']) 3305 }, props)); 3306}; 3307 3308/** 3309 * Handle a click on the menu item, and set it to selected 3310 */ 3311vjs.MenuItem.prototype.onClick = function(){ 3312 this.selected(true); 3313}; 3314 3315/** 3316 * Set this menu item as selected or not 3317 * @param {Boolean} selected 3318 */ 3319vjs.MenuItem.prototype.selected = function(selected){ 3320 if (selected) { 3321 this.addClass('vjs-selected'); 3322 this.el_.setAttribute('aria-selected',true); 3323 } else { 3324 this.removeClass('vjs-selected'); 3325 this.el_.setAttribute('aria-selected',false); 3326 } 3327}; 3328 3329 3330/** 3331 * A button class with a popup menu 3332 * @param {vjs.Player|Object} player 3333 * @param {Object=} options 3334 * @constructor 3335 */ 3336vjs.MenuButton = vjs.Button.extend({ 3337 /** @constructor */ 3338 init: function(player, options){ 3339 vjs.Button.call(this, player, options); 3340 3341 this.menu = this.createMenu(); 3342 3343 // Add list to element 3344 this.addChild(this.menu); 3345 3346 // Automatically hide empty menu buttons 3347 if (this.items && this.items.length === 0) { 3348 this.hide(); 3349 } 3350 3351 this.on('keydown', this.onKeyPress); 3352 this.el_.setAttribute('aria-haspopup', true); 3353 this.el_.setAttribute('role', 'button'); 3354 } 3355}); 3356 3357/** 3358 * Track the state of the menu button 3359 * @type {Boolean} 3360 * @private 3361 */ 3362vjs.MenuButton.prototype.buttonPressed_ = false;
3363 3364vjs.MenuButton.prototype.createMenu = function(){ 3365 var menu = new vjs.Menu(this.player_); 3366 3367 // Add a title list item to the top 3368 if (this.options().title) { 3369 menu.contentEl().appendChild(vjs.createEl('li', { 3370 className: 'vjs-menu-title', 3371 innerHTML: vjs.capitalize(this.options().title), 3372 tabindex: -1 3373 })); 3374 } 3375 3376 this.items = this['createItems'](); 3377 3378 if (this.items) { 3379 // Add menu items to the menu 3380 for (var i = 0; i < this.items.length; i++) { 3381 menu.addItem(this.items[i]); 3382 } 3383 } 3384 3385 return menu; 3386}; 3387 3388/** 3389 * Create the list of menu items. Specific to each subclass. 3390 */ 3391vjs.MenuButton.prototype.createItems = function(){}; 3392 3393/** @inheritDoc */ 3394vjs.MenuButton.prototype.buildCSSClass = function(){ 3395 return this.className + ' vjs-menu-button ' + vjs.Button.prototype.buildCSSClass.call(this); 3396}; 3397 3398// Focus - Add keyboard functionality to element 3399// This function is not needed anymore. Instead, the keyboard functionality is handled by 3400// treating the button as triggering a submenu. When the button is pressed, the submenu 3401// appears. Pressing the button again makes the submenu disappear. 3402vjs.MenuButton.prototype.onFocus = function(){}; 3403// Can't turn off list display that we turned on with focus, because list would go away. 3404vjs.MenuButton.prototype.onBlur = function(){}; 3405 3406vjs.MenuButton.prototype.onClick = function(){ 3407 // When you click the button it adds focus, which will show the menu indefinitely. 3408 // So we'll remove focus when the mouse leaves the button. 3409 // Focus is needed for tab navigation. 3410 this.one('mouseout', vjs.bind(this, function(){ 3411 this.menu.unlockShowing(); 3412 this.el_.blur(); 3413 })); 3414 if (this.buttonPressed_){ 3415 this.unpressButton(); 3416 } else { 3417 this.pressButton(); 3418 } 3419}; 3420 3421vjs.MenuButton.prototype.onKeyPress = function(event){ 3422 event.preventDefault(); 3423 3424 // Check for space bar (32) or enter (13) keys 3425 if (event.which == 32 || event.which == 13) { 3426 if (this.buttonPressed_){ 3427 this.unpressButton(); 3428 } else { 3429 this.pressButton(); 3430 } 3431 // Check for escape (27) key 3432 } else if (event.which == 27){ 3433 if (this.buttonPressed_){ 3434 this.unpressButton(); 3435 } 3436 } 3437}; 3438 3439vjs.MenuButton.prototype.pressButton = function(){ 3440 this.buttonPressed_ = true; 3441 this.menu.lockShowing(); 3442 this.el_.setAttribute('aria-pressed', true); 3443 if (this.items && this.items.length > 0) { 3444 this.items[0].el().focus(); // set the focus to the title of the submenu 3445 } 3446}; 3447 3448vjs.MenuButton.prototype.unpressButton = function(){ 3449 this.buttonPressed_ = false; 3450 this.menu.unlockShowing(); 3451 this.el_.setAttribute('aria-pressed', false); 3452}; 3453/** 3454 * Custom MediaError to mimic the HTML5 MediaError 3455 * @param {Number} code The media error code 3456 */ 3457vjs.MediaError = function(code){ 3458 if (typeof code === 'number') { 3459 this.code = code; 3460 } else if (typeof code === 'string') {
3461 // default code is zero, so this is a custom error 3462 this.message = code; 3463 } else if (typeof code === 'object') { // object 3464 vjs.obj.merge(this, code); 3465 } 3466 3467 if (!this.message) { 3468 this.message = vjs.MediaError.defaultMessages[this.code] || ''; 3469 } 3470}; 3471 3472/** 3473 * The error code that refers two one of the defined 3474 * MediaError types 3475 * @type {Number} 3476 */ 3477vjs.MediaError.prototype.code = 0; 3478 3479/** 3480 * An optional message to be shown with the error. 3481 * Message is not part of the HTML5 video spec 3482 * but allows for more informative custom errors. 3483 * @type {String} 3484 */ 3485vjs.MediaError.prototype.message = ''; 3486 3487/** 3488 * An optional status code that can be set by plugins 3489 * to allow even more detail about the error. 3490 * For example the HLS plugin might provide the specific 3491 * HTTP status code that was returned when the error 3492 * occurred, then allowing a custom error overlay 3493 * to display more information. 3494 * @type {[type]} 3495 */ 3496vjs.MediaError.prototype.status = null; 3497 3498vjs.MediaError.errorTypes = [ 3499 'MEDIA_ERR_CUSTOM', // = 0 3500 'MEDIA_ERR_ABORTED', // = 1 3501 'MEDIA_ERR_NETWORK', // = 2 3502 'MEDIA_ERR_DECODE', // = 3 3503 'MEDIA_ERR_SRC_NOT_SUPPORTED', // = 4 3504 'MEDIA_ERR_ENCRYPTED' // = 5 3505]; 3506 3507vjs.MediaError.defaultMessages = { 3508 1: 'You aborted the video playback', 3509 2: 'A network error caused the video download to fail part-way.', 3510 3: 'The video playback was aborted due to a corruption problem or because the video used features your browser did not support.', 3511 4: 'The video could not be loaded, either because the server or network failed or because the format is not supported.', 3512 5: 'The video is encrypted and we do not have the keys to decrypt it.' 3513}; 3514 3515// Add types as properties on MediaError 3516// e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4; 3517for (var errNum = 0; errNum < vjs.MediaError.errorTypes.length; errNum++) { 3518 vjs.MediaError[vjs.MediaError.errorTypes[errNum]] = errNum; 3519 // values should be accessible on both the class and instance 3520 vjs.MediaError.prototype[vjs.MediaError.errorTypes[errNum]] = errNum; 3521} 3522(function(){ 3523 var apiMap, specApi, browserApi, i; 3524 3525 /** 3526 * Store the browser-specific methods for the fullscreen API 3527 * @type {Object|undefined} 3528 * @private 3529 */ 3530 vjs.browser.fullscreenAPI; 3531 3532 // browser API methods 3533 // map approach from Screenful.js - https://github.com/sindresorhus/screenfull.js 3534 apiMap = [ 3535 // Spec: https://dvcs.w3.org/hg/fullscreen/raw-file/tip/Overview.html 3536 [ 3537 'requestFullscreen', 3538 'exitFullscreen', 3539 'fullscreenElement', 3540 'fullscreenEnabled', 3541 'fullscreenchange', 3542 'fullscreenerror' 3543 ], 3544 // WebKit 3545 [ 3546 'webkitRequestFullscreen', 3547 'webkitExitFullscreen', 3548 'webkitFullscreenElement', 3549 'webkitFullscreenEnabled', 3550 'webkitfullscreenchange', 3551 'webkitfullscreenerror' 3552 ], 3553 // Old WebKit (Safari 5.1) 3554 [ 3555 'webkitRequestFullScreen', 3556 'webkitCancelFullScreen', 3557 'webkitCurrentFullScreenElement', 3558 'webkitCancelFullScreen', 3559 'webkitfullscreenchange', 3560 'webkitfullscreenerror' 3561 ], 3562 // Mozilla 3563 [ 3564 'mozRequestFullScreen', 3565 'mozCancelFullScreen', 3566 'mozFullScreenElement', 3567 'mozFullScreenEnabled', 3568 'mozfullscreenchange', 3569 'mozfullscreenerror' 3570 ], 3571 // Microsoft 3572 [ 3573 'msRequestFullscreen', 3574 'msExitFullscreen', 3575 'msFullscreenElement', 3576 'msFullscreenEnabled', 3577 'MSFullscreenChange', 3578 'MSFullscreenError' 3579 ] 3580 ]; 3581 3582 specApi = apiMap[0]; 3583 3584 // determine the supported set of functions 3585 for (i=0; i<apiMap.length; i++) { 3586 // check for exitFullscreen function 3587 if (apiMap[i][1] in document) { 3588 browserApi = apiMap[i]; 3589 break; 3590 } 3591 } 3592 3593 // map the browser API names to the spec API names 3594 // or leave vjs.browser.fullscreenAPI undefined 3595 if (browserApi) { 3596 vjs.browser.fullscreenAPI = {}; 3597 3598 for (i=0; i<browserApi.length; i++) { 3599 vjs.browser.fullscreenAPI[specApi[i]] = browserApi[i]; 3600 } 3601 } 3602 3603})(); 3604/** 3605 * An instance of the `vjs.Player` class is created when any of the Video.js setup methods are used to initialize a video. 3606 * 3607 * ```js 3608 * var myPlayer = videojs('example_video_1'); 3609 * ``` 3610 * 3611 * In the following example, the `data-setup` attribute tells the Video.js library to create a player instance when the library is ready. 3612 * 3613 * ```html 3614 * <video id="example_video_1" data-setup='{}' controls> 3615 * <source src="my-source.mp4" type="video/mp4"> 3616 * </video> 3617 * ``` 3618 * 3619 * After an instance has been created it can be accessed globally using `Video('example_video_1')`. 3620 * 3621 * @class
3622 * @extends vjs.Component 3623 */ 3624vjs.Player = vjs.Component.extend({ 3625 3626 /** 3627 * player's constructor function 3628 * 3629 * @constructs 3630 * @method init 3631 * @param {Element} tag The original video tag used for configuring options 3632 * @param {Object=} options Player options 3633 * @param {Function=} ready Ready callback function 3634 */ 3635 init: function(tag, options, ready){ 3636 this.tag = tag; // Store the original tag used to set options 3637 3638 // Make sure tag ID exists 3639 tag.id = tag.id || 'vjs_video_' + vjs.guid++; 3640 3641 // Store the tag attributes used to restore html5 element 3642 this.tagAttributes = tag && vjs.getElementAttributes(tag); 3643 3644 // Set Options 3645 // The options argument overrides options set in the video tag 3646 // which overrides globally set options. 3647 // This latter part coincides with the load order 3648 // (tag must exist before Player) 3649 options = vjs.obj.merge(this.getTagSettings(tag), options); 3650 3651 // Update Current Language 3652 this.language_ = options['language'] || vjs.options['language']; 3653 3654 // Update Supported Languages 3655 this.languages_ = options['languages'] || vjs.options['languages']; 3656 3657 // Cache for video property values. 3658 this.cache_ = {}; 3659 3660 // Set poster 3661 this.poster_ = options['poster'] || ''; 3662 3663 // Set controls 3664 this.controls_ = !!options['controls']; 3665 // Original tag settings stored in options 3666 // now remove immediately so native controls don't flash. 3667 // May be turned back on by HTML5 tech if nativeControlsForTouch is true 3668 tag.controls = false; 3669 3670 // we don't want the player to report touch activity on itself 3671 // see enableTouchActivity in Component 3672 options.reportTouchActivity = false; 3673 3674 // Set isAudio based on whether or not an audio tag was used 3675 this.isAudio(this.tag.nodeName.toLowerCase() === 'audio'); 3676 3677 // Run base component initializing with new options. 3678 // Builds the element through createEl() 3679 // Inits and embeds any child components in opts 3680 vjs.Component.call(this, this, options, ready); 3681 3682 // Update controls className. Can't do this when the controls are initially 3683 // set because the element doesn't exist yet. 3684 if (this.controls()) { 3685 this.addClass('vjs-controls-enabled'); 3686 } else { 3687 this.addClass('vjs-controls-disabled'); 3688 } 3689 3690 if (this.isAudio()) { 3691 this.addClass('vjs-audio'); 3692 } 3693 3694 // TODO: Make this smarter. Toggle user state between touching/mousing 3695 // using events, since devices can have both touch and mouse events. 3696 // if (vjs.TOUCH_ENABLED) { 3697 // this.addClass('vjs-touch-enabled'); 3698 // } 3699 3700 // Make player easily findable by ID 3701 vjs.players[this.id_] = this; 3702 3703 if (options['plugins']) { 3704 vjs.obj.each(options['plugins'], function(key, val){ 3705 this[key](val); 3706 }, this); 3707 } 3708 3709 this.listenForUserActivity(); 3710 } 3711}); 3712 3713/** 3714 * The player's stored language code 3715 * 3716 * @type {String} 3717 * @private 3718 */ 3719vjs.Player.prototype.language_; 3720 3721/** 3722 * The player's language code 3723 * @param {String} languageCode The locale string 3724 * @return {String} The locale string when getting 3725 * @return {vjs.Player} self, when setting 3726 */ 3727vjs.Player.prototype.language = function (languageCode) { 3728 if (languageCode === undefined) { 3729 return this.language_; 3730 } 3731 3732 this.language_ = languageCode; 3733 return this; 3734}; 3735 3736/** 3737 * The player's stored language dictionary 3738 * 3739 * @type {Object} 3740 * @private 3741 */ 3742vjs.Player.prototype.languages_; 3743 3744vjs.Player.prototype.languages = function(){ 3745 return this.languages_; 3746}; 3747 3748/** 3749 * Player instance options, surfaced using vjs.options 3750 * vjs.options = vjs.Player.prototype.options_ 3751 * Make changes in vjs.options, not here. 3752 * All options should use string keys so they avoid 3753 * renaming by closure compiler 3754 * @type {Object} 3755 * @private 3756 */ 3757vjs.Player.prototype.options_ = vjs.options; 3758 3759/** 3760 * Destroys the video player and does any necessary cleanup 3761 * 3762 * myPlayer.dispose(); 3763 * 3764 * This is especially helpful if you are dynamically adding and removing videos 3765 * to/from the DOM. 3766 */ 3767vjs.Player.prototype.dispose = function(){ 3768 this.trigger('dispose'); 3769 // prevent dispose from being called twice 3770 this.off('dispose'); 3771 3772 // Kill reference to this player 3773 vjs.players[this.id_] = null; 3774 if (this.tag && this.tag['player']) { this.tag['player'] = null; } 3775 if (this.el_ && this.el_['player']) { this.el_['player'] = null; } 3776 3777 if (this.tech) { this.tech.dispose(); } 3778 3779 // Component dispose 3780 vjs.Component.prototype.dispose.call(this); 3781}; 3782 3783vjs.Player.prototype.getTagSettings = function(tag){ 3784 var tagOptions, 3785 dataSetup, 3786 options = { 3787 'sources': [], 3788 'tracks': [] 3789 }; 3790 3791 tagOptions = vjs.getElementAttributes(tag); 3792 dataSetup = tagOptions['data-setup']; 3793 3794 // Check if data-setup attr exists. 3795 if (dataSetup !== null){ 3796 // Parse options JSON 3797 // If empty string, make it a parsable json object. 3798 vjs.obj.merge(tagOptions, vjs.JSON.parse(dataSetup || '{}')); 3799 } 3800 3801 vjs.obj.merge(options, tagOptions); 3802 3803 // Get tag children settings 3804 if (tag.hasChildNodes()) { 3805 var children, child, childName, i, j; 3806 3807 children = tag.childNodes; 3808 3809 for (i=0,j=children.length; i<j; i++) { 3810 child = children[i]; 3811 // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/ 3812 childName = child.nodeName.toLowerCase(); 3813 if (childName === 'source') { 3814 options['sources'].push(vjs.getElementAttributes(child)); 3815 } else if (childName === 'track') { 3816 options['tracks'].push(vjs.getElementAttributes(child)); 3817 } 3818 } 3819 } 3820 3821 return options; 3822}; 3823 3824vjs.Player.prototype.createEl = function(){ 3825 var 3826 el = this.el_ = vjs.Component.prototype.createEl.call(this, 'div'), 3827 tag = this.tag, 3828 attrs; 3829 3830 // Remove width/height attrs from tag so CSS can make it 100% width/height 3831 tag.removeAttribute('width'); 3832 tag.removeAttribute('height'); 3833 // Empty video tag tracks so the built-in player doesn't use them also. 3834 // This may not be fast enough to stop HTML5 browsers from reading the tags 3835 // so we'll need to turn off any default tracks if we're manually doing 3836 // captions and subtitles. videoElement.textTracks 3837 if (tag.hasChildNodes()) { 3838 var nodes, nodesLength, i, node, nodeName, removeNodes; 3839 3840 nodes = tag.childNodes; 3841 nodesLength = nodes.length; 3842 removeNodes = []; 3843 3844 while (nodesLength--) { 3845 node = nodes[nodesLength]; 3846 nodeName = node.nodeName.toLowerCase(); 3847 if (nodeName === 'track') { 3848 removeNodes.push(node); 3849 } 3850 } 3851 3852 for (i=0; i<removeNodes.length; i++) { 3853 tag.removeChild(removeNodes[i]); 3854 } 3855 } 3856 3857 // Copy over all the attributes from the tag, including ID and class 3858 // ID will now reference player box, not the video tag 3859 attrs = vjs.getElementAttributes(tag); 3860 vjs.obj.each(attrs, function(attr) { 3861 // workaround so we don't totally break IE7 3862 // http://stackoverflow.com/questions/3653444/css-styles-not-applied-on-dynamic-elements-in-internet-explorer-7 3863 if (attr == 'class') { 3864 el.className = attrs[attr]; 3865 } else { 3866 el.setAttribute(attr, attrs[attr]); 3867 } 3868 }); 3869 3870 // Update tag id/class for use as HTML5 playback tech 3871 // Might think we should do this after embedding in container so .vjs-tech class 3872 // doesn't flash 100% width/height, but class only applies with .video-js parent
3873 tag.id += '_html5_api'; 3874 tag.className = 'vjs-tech'; 3875 3876 // Make player findable on elements 3877 tag['player'] = el['player'] = this; 3878 // Default state of video is paused 3879 this.addClass('vjs-paused'); 3880 3881 // Make box use width/height of tag, or rely on default implementation 3882 // Enforce with CSS since width/height attrs don't work on divs 3883 this.width(this.options_['width'], true); // (true) Skip resize listener on load 3884 this.height(this.options_['height'], true); 3885 3886 // vjs.insertFirst seems to cause the networkState to flicker from 3 to 2, so 3887 // keep track of the original for later so we can know if the source originally failed 3888 tag.initNetworkState_ = tag.networkState; 3889 3890 // Wrap video tag in div (el/box) container 3891 if (tag.parentNode) { 3892 tag.parentNode.insertBefore(el, tag); 3893 } 3894 vjs.insertFirst(tag, el); // Breaks iPhone, fixed in HTML5 setup. 3895 3896 // The event listeners need to be added before the children are added 3897 // in the component init because the tech (loaded with mediaLoader) may 3898 // fire events, like loadstart, that these events need to capture. 3899 // Long term it might be better to expose a way to do this in component.init 3900 // like component.initEventListeners() that runs between el creation and 3901 // adding children 3902 this.el_ = el; 3903 this.on('loadstart', this.onLoadStart); 3904 this.on('waiting', this.onWaiting); 3905 this.on(['canplay', 'canplaythrough', 'playing', 'ended'], this.onWaitEnd); 3906 this.on('seeking', this.onSeeking); 3907 this.on('seeked', this.onSeeked); 3908 this.on('ended', this.onEnded); 3909 this.on('play', this.onPlay); 3910 this.on('firstplay', this.onFirstPlay); 3911 this.on('pause', this.onPause); 3912 this.on('progress', this.onProgress); 3913 this.on('durationchange', this.onDurationChange); 3914 this.on('fullscreenchange', this.onFullscreenChange); 3915 3916 return el; 3917}; 3918 3919// /* Media Technology (tech) 3920// ================================================================================ */ 3921// Load/Create an instance of playback technology including element and API methods 3922// And append playback element in player div. 3923vjs.Player.prototype.loadTech = function(techName, source){ 3924 3925 // Pause and remove current playback technology 3926 if (this.tech) { 3927 this.unloadTech(); 3928 } 3929 3930 // get rid of the HTML5 video tag as soon as we are using another tech 3931 if (techName !== 'Html5' && this.tag) { 3932 vjs.Html5.disposeMediaElement(this.tag); 3933 this.tag = null; 3934 } 3935 3936 this.techName = techName; 3937 3938 // Turn off API access because we're loading a new tech that might load asynchronously 3939 this.isReady_ = false; 3940 3941 var techReady = function(){ 3942 this.player_.triggerReady(); 3943 }; 3944 3945 // Grab tech-specific options from player options and add source and parent element to use. 3946 var techOptions = vjs.obj.merge({ 'source': source, 'parentEl': this.el_ }, this.options_[techName.toLowerCase()]); 3947 3948 if (source) { 3949 this.currentType_ = source.type; 3950 if (source.src == this.cache_.src && this.cache_.currentTime > 0) { 3951 techOptions['startTime'] = this.cache_.currentTime; 3952 } 3953 3954 this.cache_.src = source.src; 3955 } 3956 3957 // Initialize tech instance 3958 this.tech = new window['videojs'][techName](this, techOptions); 3959 3960 this.tech.ready(techReady); 3961}; 3962 3963vjs.Player.prototype.unloadTech = function(){ 3964 this.isReady_ = false; 3965 3966 this.tech.dispose(); 3967 3968 this.tech = false; 3969}; 3970 3971// There's many issues around changing the size of a Flash (or other plugin) object. 3972// First is a plugin reload issue in Firefox that has been around for 11 years: https://bugzilla.mozilla.org/show_bug.cgi?id=90268 3973// Then with the new fullscreen API, Mozilla and webkit browsers will reload the flash object after going to fullscreen. 3974// To get around this, we're unloading the tech, caching source and currentTime values, and reloading the tech once the plugin is resized. 3975// reloadTech: function(betweenFn){ 3976// vjs.log('unloadingTech') 3977// this.unloadTech(); 3978// vjs.log('unloadedTech') 3979// if (betweenFn) { betweenFn.call(); } 3980// vjs.log('LoadingTech') 3981// this.loadTech(this.techName, { src: this.cache_.src }) 3982// vjs.log('loadedTech') 3983// }, 3984 3985// /* Player event handlers (how the player reacts to certain events) 3986// ================================================================================ */ 3987
3988/** 3989 * Fired when the user agent begins looking for media data 3990 * @event loadstart 3991 */ 3992vjs.Player.prototype.onLoadStart = function() { 3993 // TODO: Update to use `emptied` event instead. See #1277. 3994 3995 // reset the error state 3996 this.error(null); 3997 3998 // If it's already playing we want to trigger a firstplay event now. 3999 // The firstplay event relies on both the play and loadstart events 4000 // which can happen in any order for a new source 4001 if (!this.paused()) { 4002 this.trigger('firstplay'); 4003 } else { 4004 // reset the hasStarted state 4005 this.hasStarted(false); 4006 this.one('play', function(){ 4007 this.hasStarted(true); 4008 }); 4009 } 4010}; 4011 4012vjs.Player.prototype.hasStarted_ = false; 4013 4014vjs.Player.prototype.hasStarted = function(hasStarted){ 4015 if (hasStarted !== undefined) { 4016 // only update if this is a new value 4017 if (this.hasStarted_ !== hasStarted) { 4018 this.hasStarted_ = hasStarted; 4019 if (hasStarted) { 4020 this.addClass('vjs-has-started'); 4021 // trigger the firstplay event if this newly has played 4022 this.trigger('firstplay'); 4023 } else { 4024 this.removeClass('vjs-has-started'); 4025 } 4026 } 4027 return this; 4028 } 4029 return this.hasStarted_; 4030}; 4031 4032/** 4033 * Fired when the player has initial duration and dimension information 4034 * @event loadedmetadata 4035 */ 4036vjs.Player.prototype.onLoadedMetaData; 4037 4038/** 4039 * Fired when the player has downloaded data at the current playback position 4040 * @event loadeddata 4041 */ 4042vjs.Player.prototype.onLoadedData; 4043 4044/** 4045 * Fired when the player has finished downloading the source data 4046 * @event loadedalldata 4047 */ 4048vjs.Player.prototype.onLoadedAllData; 4049 4050/** 4051 * Fired whenever the media begins or resumes playback 4052 * @event play 4053 */ 4054vjs.Player.prototype.onPlay = function(){ 4055 this.removeClass('vjs-paused'); 4056 this.addClass('vjs-playing'); 4057}; 4058 4059/** 4060 * Fired whenever the media begins waiting 4061 * @event waiting 4062 */ 4063vjs.Player.prototype.onWaiting = function(){ 4064 this.addClass('vjs-waiting'); 4065}; 4066 4067/** 4068 * A handler for events that signal that waiting has ended 4069 * which is not consistent between browsers. See #1351 4070 * @private 4071 */ 4072vjs.Player.prototype.onWaitEnd = function(){ 4073 this.removeClass('vjs-waiting'); 4074}; 4075 4076/** 4077 * Fired whenever the player is jumping to a new time 4078 * @event seeking 4079 */ 4080vjs.Player.prototype.onSeeking = function(){ 4081 this.addClass('vjs-seeking'); 4082}; 4083 4084/** 4085 * Fired when the player has finished jumping to a new time 4086 * @event seeked 4087 */ 4088vjs.Player.prototype.onSeeked = function(){ 4089 this.removeClass('vjs-seeking'); 4090}; 4091 4092/** 4093 * Fired the first time a video is played 4094 * 4095 * Not part of the HLS spec, and we're not sure if this is the best 4096 * implementation yet, so use sparingly. If you don't have a reason to 4097 * prevent playback, use `myPlayer.one('play');` instead. 4098 * 4099 * @event firstplay 4100 */ 4101vjs.Player.prototype.onFirstPlay = function(){ 4102 //If the first starttime attribute is specified 4103 //then we will start at the given offset in seconds 4104 if(this.options_['starttime']){ 4105 this.currentTime(this.options_['starttime']); 4106 } 4107 4108 this.addClass('vjs-has-started'); 4109}; 4110 4111/** 4112 * Fired whenever the media has been paused 4113 * @event pause 4114 */ 4115vjs.Player.prototype.onPause = function(){ 4116 this.removeClass('vjs-playing'); 4117 this.addClass('vjs-paused'); 4118}; 4119 4120/** 4121 * Fired when the current playback position has changed 4122 * 4123 * During playback this is fired every 15-250 milliseconds, depending on the 4124 * playback technology in use. 4125 * @event timeupdate 4126 */ 4127vjs.Player.prototype.onTimeUpdate; 4128 4129/** 4130 * Fired while the user agent is downloading media data 4131 * @event progress 4132 */ 4133vjs.Player.prototype.onProgress = function(){ 4134 // Add custom event for when source is finished downloading. 4135 if (this.bufferedPercent() == 1) { 4136 this.trigger('loadedalldata'); 4137 } 4138}; 4139 4140/** 4141 * Fired when the end of the media resource is reached (currentTime == duration) 4142 * @event ended 4143 */ 4144vjs.Player.prototype.onEnded = function(){ 4145 if (this.options_['loop']) { 4146 this.currentTime(0); 4147 this.play(); 4148 } else if (!this.paused()) { 4149 this.pause(); 4150 } 4151}; 4152 4153/** 4154 * Fired when the duration of the media resource is first known or changed 4155 * @event durationchange 4156 */ 4157vjs.Player.prototype.onDurationChange = function(){ 4158 // Allows for caching value instead of asking player each time. 4159 // We need to get the techGet response and check for a value so we don't 4160 // accidentally cause the stack to blow up. 4161 var duration = this.techGet('duration'); 4162 if (duration) { 4163 if (duration < 0) { 4164 duration = Infinity; 4165 } 4166 this.duration(duration); 4167 // Determine if the stream is live and propagate styles down to UI. 4168 if (duration === Infinity) { 4169 this.addClass('vjs-live'); 4170 } else { 4171 this.removeClass('vjs-live'); 4172 } 4173 } 4174}; 4175 4176/** 4177 * Fired when the volume changes 4178 * @event volumechange 4179 */ 4180vjs.Player.prototype.onVolumeChange; 4181 4182/** 4183 * Fired when the player switches in or out of fullscreen mode 4184 * @event fullscreenchange 4185 */ 4186vjs.Player.prototype.onFullscreenChange = function() { 4187 if (this.isFullscreen()) { 4188 this.addClass('vjs-fullscreen'); 4189 } else { 4190 this.removeClass('vjs-fullscreen'); 4191 } 4192}; 4193 4194// /* Player API 4195// ================================================================================ */ 4196
4197/** 4198 * Object for cached values. 4199 * @private 4200 */ 4201vjs.Player.prototype.cache_; 4202 4203vjs.Player.prototype.getCache = function(){ 4204 return this.cache_; 4205}; 4206 4207// Pass values to the playback tech 4208vjs.Player.prototype.techCall = function(method, arg){ 4209 // If it's not ready yet, call method when it is 4210 if (this.tech && !this.tech.isReady_) { 4211 this.tech.ready(function(){ 4212 this[method](arg); 4213 }); 4214 4215 // Otherwise call method now 4216 } else { 4217 try { 4218 this.tech[method](arg); 4219 } catch(e) { 4220 vjs.log(e); 4221 throw e; 4222 } 4223 } 4224}; 4225 4226// Get calls can't wait for the tech, and sometimes don't need to. 4227vjs.Player.prototype.techGet = function(method){ 4228 if (this.tech && this.tech.isReady_) { 4229 4230 // Flash likes to die and reload when you hide or reposition it. 4231 // In these cases the object methods go away and we get errors. 4232 // When that happens we'll catch the errors and inform tech that it's not ready any more. 4233 try { 4234 return this.tech[method](); 4235 } catch(e) { 4236 // When building additional tech libs, an expected method may not be defined yet 4237 if (this.tech[method] === undefined) { 4238 vjs.log('Video.js: ' + method + ' method not defined for '+this.techName+' playback technology.', e); 4239 } else { 4240 // When a method isn't available on the object it throws a TypeError 4241 if (e.name == 'TypeError') { 4242 vjs.log('Video.js: ' + method + ' unavailable on '+this.techName+' playback technology element.', e); 4243 this.tech.isReady_ = false; 4244 } else { 4245 vjs.log(e); 4246 } 4247 } 4248 throw e; 4249 } 4250 } 4251 4252 return; 4253}; 4254 4255/** 4256 * start media playback 4257 * 4258 * myPlayer.play(); 4259 * 4260 * @return {vjs.Player} self 4261 */ 4262vjs.Player.prototype.play = function(){ 4263 this.techCall('play'); 4264 return this; 4265}; 4266 4267/** 4268 * Pause the video playback 4269 * 4270 * myPlayer.pause(); 4271 * 4272 * @return {vjs.Player} self 4273 */ 4274vjs.Player.prototype.pause = function(){ 4275 this.techCall('pause'); 4276 return this; 4277}; 4278 4279/** 4280 * Check if the player is paused 4281 * 4282 * var isPaused = myPlayer.paused(); 4283 * var isPlaying = !myPlayer.paused(); 4284 * 4285 * @return {Boolean} false if the media is currently playing, or true otherwise 4286 */ 4287vjs.Player.prototype.paused = function(){ 4288 // The initial state of paused should be true (in Safari it's actually false) 4289 return (this.techGet('paused') === false) ? false : true; 4290}; 4291 4292/** 4293 * Get or set the current time (in seconds) 4294 * 4295 * // get 4296 * var whereYouAt = myPlayer.currentTime(); 4297 * 4298 * // set 4299 * myPlayer.currentTime(120); // 2 minutes into the video 4300 * 4301 * @param {Number|String=} seconds The time to seek to 4302 * @return {Number} The time in seconds, when not setting 4303 * @return {vjs.Player} self, when the current time is set 4304 */ 4305vjs.Player.prototype.currentTime = function(seconds){ 4306 if (seconds !== undefined) { 4307 4308 this.techCall('setCurrentTime', seconds); 4309 4310 return this; 4311 } 4312 4313 // cache last currentTime and return. default to 0 seconds 4314 // 4315 // Caching the currentTime is meant to prevent a massive amount of reads on the tech's 4316 // currentTime when scrubbing, but may not provide much performance benefit afterall. 4317 // Should be tested. Also something has to read the actual current time or the cache will 4318 // never get updated. 4319 return this.cache_.currentTime = (this.techGet('currentTime') || 0); 4320}; 4321 4322/** 4323 * Get the length in time of the video in seconds 4324 * 4325 * var lengthOfVideo = myPlayer.duration(); 4326 * 4327 * **NOTE**: The video must have started loading before the duration can be 4328 * known, and in the case of Flash, may not be known until the video starts 4329 * playing. 4330 * 4331 * @return {Number} The duration of the video in seconds 4332 */ 4333vjs.Player.prototype.duration = function(seconds){ 4334 if (seconds !== undefined) { 4335 4336 // cache the last set value for optimized scrubbing (esp. Flash) 4337 this.cache_.duration = parseFloat(seconds); 4338 4339 return this; 4340 } 4341 4342 if (this.cache_.duration === undefined) { 4343 this.onDurationChange(); 4344 } 4345 4346 return this.cache_.duration || 0; 4347}; 4348 4349/** 4350 * Calculates how much time is left. 4351 * 4352 * var timeLeft = myPlayer.remainingTime(); 4353 *
4354 * Not a native video element function, but useful 4355 * @return {Number} The time remaining in seconds 4356 */ 4357vjs.Player.prototype.remainingTime = function(){ 4358 return this.duration() - this.currentTime(); 4359}; 4360 4361// http://dev.w3.org/html5/spec/video.html#dom-media-buffered 4362// Buffered returns a timerange object. 4363// Kind of like an array of portions of the video that have been downloaded. 4364 4365/** 4366 * Get a TimeRange object with the times of the video that have been downloaded 4367 * 4368 * If you just want the percent of the video that's been downloaded, 4369 * use bufferedPercent. 4370 * 4371 * // Number of different ranges of time have been buffered. Usually 1. 4372 * numberOfRanges = bufferedTimeRange.length, 4373 * 4374 * // Time in seconds when the first range starts. Usually 0. 4375 * firstRangeStart = bufferedTimeRange.start(0), 4376 * 4377 * // Time in seconds when the first range ends 4378 * firstRangeEnd = bufferedTimeRange.end(0), 4379 * 4380 * // Length in seconds of the first time range 4381 * firstRangeLength = firstRangeEnd - firstRangeStart; 4382 * 4383 * @return {Object} A mock TimeRange object (following HTML spec) 4384 */ 4385vjs.Player.prototype.buffered = function(){ 4386 var buffered = this.techGet('buffered'); 4387 4388 if (!buffered || !buffered.length) { 4389 buffered = vjs.createTimeRange(0,0); 4390 } 4391 4392 return buffered; 4393}; 4394 4395/** 4396 * Get the percent (as a decimal) of the video that's been downloaded 4397 * 4398 * var howMuchIsDownloaded = myPlayer.bufferedPercent(); 4399 * 4400 * 0 means none, 1 means all. 4401 * (This method isn't in the HTML5 spec, but it's very convenient) 4402 * 4403 * @return {Number} A decimal between 0 and 1 representing the percent 4404 */ 4405vjs.Player.prototype.bufferedPercent = function(){ 4406 var duration = this.duration(), 4407 buffered = this.buffered(), 4408 bufferedDuration = 0, 4409 start, end; 4410 4411 if (!duration) { 4412 return 0; 4413 } 4414 4415 for (var i=0; i<buffered.length; i++){ 4416 start = buffered.start(i); 4417 end = buffered.end(i); 4418 4419 // buffered end can be bigger than duration by a very small fraction 4420 if (end > duration) { 4421 end = duration; 4422 } 4423 4424 bufferedDuration += end - start; 4425 } 4426 4427 return bufferedDuration / duration; 4428}; 4429 4430/** 4431 * Get the ending time of the last buffered time range 4432 * 4433 * This is used in the progress bar to encapsulate all time ranges. 4434 * @return {Number} The end of the last buffered time range 4435 */ 4436vjs.Player.prototype.bufferedEnd = function(){ 4437 var buffered = this.buffered(), 4438 duration = this.duration(), 4439 end = buffered.end(buffered.length-1); 4440 4441 if (end > duration) { 4442 end = duration; 4443 } 4444 4445 return end; 4446}; 4447 4448/** 4449 * Get or set the current volume of the media 4450 * 4451 * // get 4452 * var howLoudIsIt = myPlayer.volume(); 4453 * 4454 * // set 4455 * myPlayer.volume(0.5); // Set volume to half 4456 * 4457 * 0 is off (muted), 1.0 is all the way up, 0.5 is half way. 4458 * 4459 * @param {Number} percentAsDecimal The new volume as a decimal percent 4460 * @return {Number} The current volume, when getting 4461 * @return {vjs.Player} self, when setting 4462 */ 4463vjs.Player.prototype.volume = function(percentAsDecimal){ 4464 var vol; 4465 4466 if (percentAsDecimal !== undefined) { 4467 vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal))); // Force value to between 0 and 1 4468 this.cache_.volume = vol; 4469 this.techCall('setVolume', vol); 4470 vjs.setLocalStorage('volume', vol); 4471 return this; 4472 } 4473 4474 // Default to 1 when returning current volume. 4475 vol = parseFloat(this.techGet('volume')); 4476 return (isNaN(vol)) ? 1 : vol; 4477}; 4478 4479 4480/** 4481 * Get the current muted state, or turn mute on or off 4482 * 4483 * // get 4484 * var isVolumeMuted = myPlayer.muted(); 4485 * 4486 * // set 4487 * myPlayer.muted(true); // mute the volume 4488 * 4489 * @param {Boolean=} muted True to mute, false to unmute 4490 * @return {Boolean} True if mute is on, false if not, when getting 4491 * @return {vjs.Player} self, when setting mute 4492 */ 4493vjs.Player.prototype.muted = function(muted){ 4494 if (muted !== undefined) { 4495 this.techCall('setMuted', muted); 4496 return this; 4497 } 4498 return this.techGet('muted') || false; // Default to false 4499}; 4500 4501// Check if current tech can support native fullscreen 4502// (e.g. with built in controls like iOS, so not our flash swf) 4503vjs.Player.prototype.supportsFullScreen = function(){ 4504 return this.techGet('supportsFullScreen') || false; 4505}; 4506 4507/** 4508 * is the player in fullscreen 4509 * @type {Boolean} 4510 * @private 4511 */ 4512vjs.Player.prototype.isFullscreen_ = false;
4513 4514/** 4515 * Check if the player is in fullscreen mode 4516 * 4517 * // get 4518 * var fullscreenOrNot = myPlayer.isFullscreen(); 4519 * 4520 * // set 4521 * myPlayer.isFullscreen(true); // tell the player it's in fullscreen 4522 * 4523 * NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official 4524 * property and instead document.fullscreenElement is used. But isFullscreen is 4525 * still a valuable property for internal player workings. 4526 * 4527 * @param {Boolean=} isFS Update the player's fullscreen state 4528 * @return {Boolean} true if fullscreen, false if not 4529 * @return {vjs.Player} self, when setting 4530 */ 4531vjs.Player.prototype.isFullscreen = function(isFS){ 4532 if (isFS !== undefined) { 4533 this.isFullscreen_ = !!isFS; 4534 return this; 4535 } 4536 return this.isFullscreen_; 4537}; 4538 4539/** 4540 * Old naming for isFullscreen() 4541 * @deprecated for lowercase 's' version 4542 */ 4543vjs.Player.prototype.isFullScreen = function(isFS){ 4544 vjs.log.warn('player.isFullScreen() has been deprecated, use player.isFullscreen() with a lowercase "s")'); 4545 return this.isFullscreen(isFS); 4546}; 4547 4548/** 4549 * Increase the size of the video to full screen 4550 * 4551 * myPlayer.requestFullscreen(); 4552 * 4553 * In some browsers, full screen is not supported natively, so it enters 4554 * "full window mode", where the video fills the browser window. 4555 * In browsers and devices that support native full screen, sometimes the 4556 * browser's default controls will be shown, and not the Video.js custom skin. 4557 * This includes most mobile devices (iOS, Android) and older versions of 4558 * Safari. 4559 * 4560 * @return {vjs.Player} self 4561 */ 4562vjs.Player.prototype.requestFullscreen = function(){ 4563 var fsApi = vjs.browser.fullscreenAPI; 4564 4565 this.isFullscreen(true); 4566 4567 if (fsApi) { 4568 // the browser supports going fullscreen at the element level so we can 4569 // take the controls fullscreen as well as the video 4570 4571 // Trigger fullscreenchange event after change 4572 // We have to specifically add this each time, and remove 4573 // when canceling fullscreen. Otherwise if there's multiple 4574 // players on a page, they would all be reacting to the same fullscreen 4575 // events 4576 vjs.on(document, fsApi['fullscreenchange'], vjs.bind(this, function(e){ 4577 this.isFullscreen(document[fsApi.fullscreenElement]); 4578 4579 // If cancelling fullscreen, remove event listener. 4580 if (this.isFullscreen() === false) { 4581 vjs.off(document, fsApi['fullscreenchange'], arguments.callee); 4582 } 4583 4584 this.trigger('fullscreenchange'); 4585 })); 4586 4587 this.el_[fsApi.requestFullscreen](); 4588 4589 } else if (this.tech.supportsFullScreen()) { 4590 // we can't take the video.js controls fullscreen but we can go fullscreen 4591 // with native controls 4592 this.techCall('enterFullScreen'); 4593 } else { 4594 // fullscreen isn't supported so we'll just stretch the video element to 4595 // fill the viewport 4596 this.enterFullWindow(); 4597 this.trigger('fullscreenchange'); 4598 } 4599 4600 return this; 4601}; 4602 4603/** 4604 * Old naming for requestFullscreen 4605 * @deprecated for lower case 's' version 4606 */ 4607vjs.Player.prototype.requestFullScreen = function(){ 4608 vjs.log.warn('player.requestFullScreen() has been deprecated, use player.requestFullscreen() with a lowercase "s")'); 4609 return this.requestFullscreen(); 4610}; 4611 4612 4613/** 4614 * Return the video to its normal size after having been in full screen mode 4615 * 4616 * myPlayer.exitFullscreen(); 4617 * 4618 * @return {vjs.Player} self 4619 */ 4620vjs.Player.prototype.exitFullscreen = function(){ 4621 var fsApi = vjs.browser.fullscreenAPI; 4622 this.isFullscreen(false); 4623 4624 // Check for browser element fullscreen support 4625 if (fsApi) { 4626 document[fsApi.exitFullscreen](); 4627 } else if (this.tech.supportsFullScreen()) { 4628 this.techCall('exitFullScreen'); 4629 } else { 4630 this.exitFullWindow(); 4631 this.trigger('fullscreenchange'); 4632 } 4633 4634 return this; 4635}; 4636 4637/** 4638 * Old naming for exitFullscreen 4639 * @deprecated for exitFullscreen 4640 */ 4641vjs.Player.prototype.cancelFullScreen = function(){ 4642 vjs.log.warn('player.cancelFullScreen() has been deprecated, use player.exitFullscreen()'); 4643 return this.exitFullscreen(); 4644}; 4645 4646// When fullscreen isn't supported we can stretch the video container to as wide as the browser will let us. 4647vjs.Player.prototype.enterFullWindow = function(){ 4648 this.isFullWindow = true; 4649 4650 // Storing original doc overflow value to return to when fullscreen is off 4651 this.docOrigOverflow = document.documentElement.style.overflow; 4652 4653 // Add listener for esc key to exit fullscreen 4654 vjs.on(document, 'keydown', vjs.bind(this, this.fullWindowOnEscKey)); 4655 4656 // Hide any scroll bars 4657 document.documentElement.style.overflow = 'hidden'; 4658 4659 // Apply fullscreen styles 4660 vjs.addClass(document.body, 'vjs-full-window'); 4661 4662 this.trigger('enterFullWindow'); 4663}; 4664vjs.Player.prototype.fullWindowOnEscKey = function(event){ 4665 if (event.keyCode === 27) { 4666 if (this.isFullscreen() === true) { 4667 this.exitFullscreen(); 4668 } else { 4669 this.exitFullWindow(); 4670 } 4671 } 4672}; 4673 4674vjs.Player.prototype.exitFullWindow = function(){ 4675 this.isFullWindow = false;
4676 vjs.off(document, 'keydown', this.fullWindowOnEscKey); 4677 4678 // Unhide scroll bars. 4679 document.documentElement.style.overflow = this.docOrigOverflow; 4680 4681 // Remove fullscreen styles 4682 vjs.removeClass(document.body, 'vjs-full-window'); 4683 4684 // Resize the box, controller, and poster to original sizes 4685 // this.positionAll(); 4686 this.trigger('exitFullWindow'); 4687}; 4688 4689vjs.Player.prototype.selectSource = function(sources){ 4690 // Loop through each playback technology in the options order 4691 for (var i=0,j=this.options_['techOrder'];i<j.length;i++) { 4692 var techName = vjs.capitalize(j[i]), 4693 tech = window['videojs'][techName]; 4694 4695 // Check if the current tech is defined before continuing 4696 if (!tech) { 4697 vjs.log.error('The "' + techName + '" tech is undefined. Skipped browser support check for that tech.'); 4698 continue; 4699 } 4700 4701 // Check if the browser supports this technology 4702 if (tech.isSupported()) { 4703 // Loop through each source object 4704 for (var a=0,b=sources;a<b.length;a++) { 4705 var source = b[a]; 4706 4707 // Check if source can be played with this technology 4708 if (tech['canPlaySource'](source)) { 4709 return { source: source, tech: techName }; 4710 } 4711 } 4712 } 4713 } 4714 4715 return false; 4716}; 4717 4718/** 4719 * The source function updates the video source 4720 * 4721 * There are three types of variables you can pass as the argument. 4722 * 4723 * **URL String**: A URL to the the video file. Use this method if you are sure 4724 * the current playback technology (HTML5/Flash) can support the source you 4725 * provide. Currently only MP4 files can be used in both HTML5 and Flash. 4726 * 4727 * myPlayer.src("http://www.example.com/path/to/video.mp4"); 4728 * 4729 * **Source Object (or element):** A javascript object containing information 4730 * about the source file. Use this method if you want the player to determine if 4731 * it can support the file using the type information. 4732 * 4733 * myPlayer.src({ type: "video/mp4", src: "http://www.example.com/path/to/video.mp4" }); 4734 * 4735 * **Array of Source Objects:** To provide multiple versions of the source so 4736 * that it can be played using HTML5 across browsers you can use an array of 4737 * source objects. Video.js will detect which version is supported and load that 4738 * file. 4739 * 4740 * myPlayer.src([ 4741 * { type: "video/mp4", src: "http://www.example.com/path/to/video.mp4" }, 4742 * { type: "video/webm", src: "http://www.example.com/path/to/video.webm" }, 4743 * { type: "video/ogg", src: "http://www.example.com/path/to/video.ogv" } 4744 * ]); 4745 * 4746 * @param {String|Object|Array=} source The source URL, object, or array of sources 4747 * @return {String} The current video source when getting 4748 * @return {String} The player when setting 4749 */ 4750vjs.Player.prototype.src = function(source){ 4751 if (source === undefined) { 4752 return this.techGet('src'); 4753 } 4754 4755 // case: Array of source objects to choose from and pick the best to play 4756 if (vjs.obj.isArray(source)) { 4757 this.sourceList_(source); 4758 4759 // case: URL String (http://myvideo...) 4760 } else if (typeof source === 'string') { 4761 // create a source object from the string 4762 this.src({ src: source }); 4763 4764 // case: Source object { src: '', type: '' ... } 4765 } else if (source instanceof Object) { 4766 // check if the source has a type and the loaded tech cannot play the source 4767 // if there's no type we'll just try the current tech 4768 if (source.type && !window['videojs'][this.techName]['canPlaySource'](source)) { 4769 // create a source list with the current source and send through 4770 // the tech loop to check for a compatible technology 4771 this.sourceList_([source]); 4772 } else { 4773 this.cache_.src = source.src; 4774 this.currentType_ = source.type || ''; 4775 4776 // wait until the tech is ready to set the source 4777 this.ready(function(){ 4778 4779 // The setSource tech method was added with source handlers 4780 // so older techs won't support it 4781 // We need to check the direct prototype for the case where subclasses 4782 // of the tech do not support source handlers 4783 if (window['videojs'][this.techName].prototype.hasOwnProperty('setSource')) { 4784 this.techCall('setSource', source); 4785 } else { 4786 this.techCall('src', source.src); 4787 } 4788 4789 if (this.options_['preload'] == 'auto') { 4790 this.load(); 4791 } 4792 4793 if (this.options_['autoplay']) { 4794 this.play(); 4795 } 4796 }); 4797 } 4798 } 4799 4800 return this; 4801}; 4802 4803/** 4804 * Handle an array of source objects 4805 * @param {[type]} sources Array of source objects 4806 * @private 4807 */ 4808vjs.Player.prototype.sourceList_ = function(sources){ 4809 var sourceTech = this.selectSource(sources); 4810 4811 if (sourceTech) { 4812 if (sourceTech.tech === this.techName) { 4813 // if this technology is already loaded, set the source 4814 this.src(sourceTech.source); 4815 } else { 4816 // load this technology with the chosen source 4817 this.loadTech(sourceTech.tech, sourceTech.source); 4818 } 4819 } else { 4820 // We need to wrap this in a timeout to give folks a chance to add error event handlers 4821 this.setTimeout( function() { 4822 this.error({ code: 4, message: this.localize(this.options()['notSupportedMessage']) }); 4823 }, 0); 4824 4825 // we could not find an appropriate tech, but let's still notify the delegate that this is it 4826 // this needs a better comment about why this is needed 4827 this.triggerReady(); 4828 } 4829}; 4830 4831/** 4832 * Begin loading the src data. 4833 * @return {vjs.Player} Returns the player 4834 */ 4835vjs.Player.prototype.load = function(){ 4836 this.techCall('load'); 4837 return this; 4838}; 4839 4840/** 4841 * Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4 4842 * Can be used in conjuction with `currentType` to assist in rebuilding the current source object. 4843 * @return {String} The current source 4844 */ 4845vjs.Player.prototype.currentSrc = function(){ 4846 return this.techGet('currentSrc') || this.cache_.src || ''; 4847}; 4848 4849/** 4850 * Get the current source type e.g. video/mp4 4851 * This can allow you rebuild the current source object so that you could load the same 4852 * source and tech later 4853 * @return {String} The source MIME type 4854 */ 4855vjs.Player.prototype.currentType = function(){ 4856 return this.currentType_ || ''; 4857}; 4858 4859/** 4860 * Get or set the preload attribute. 4861 * @return {String} The preload attribute value when getting 4862 * @return {vjs.Player} Returns the player when setting 4863 */ 4864vjs.Player.prototype.preload = function(value){ 4865 if (value !== undefined) { 4866 this.techCall('setPreload', value); 4867 this.options_['preload'] = value; 4868 return this; 4869 } 4870 return this.techGet('preload'); 4871}; 4872 4873/** 4874 * Get or set the autoplay attribute. 4875 * @return {String} The autoplay attribute value when getting 4876 * @return {vjs.Player} Returns the player when setting 4877 */ 4878vjs.Player.prototype.autoplay = function(value){ 4879 if (value !== undefined) { 4880 this.techCall('setAutoplay', value); 4881 this.options_['autoplay'] = value; 4882 return this; 4883 } 4884 return this.techGet('autoplay', value); 4885}; 4886 4887/** 4888 * Get or set the loop attribute on the video element. 4889 * @return {String} The loop attribute value when getting 4890 * @return {vjs.Player} Returns the player when setting 4891 */ 4892vjs.Player.prototype.loop = function(value){ 4893 if (value !== undefined) { 4894 this.techCall('setLoop', value); 4895 this.options_['loop'] = value; 4896 return this; 4897 } 4898 return this.techGet('loop'); 4899}; 4900 4901/** 4902 * the url of the poster image source 4903 * @type {String} 4904 * @private 4905 */ 4906vjs.Player.prototype.poster_; 4907 4908/** 4909 * get or set the poster image source url 4910 * 4911 * ##### EXAMPLE: 4912 * 4913 * // getting 4914 * var currentPoster = myPlayer.poster(); 4915 * 4916 * // setting 4917 * myPlayer.poster('http://example.com/myImage.jpg'); 4918 * 4919 * @param {String=} [src] Poster image source URL 4920 * @return {String} poster URL when getting 4921 * @return {vjs.Player} self when setting 4922 */ 4923vjs.Player.prototype.poster = function(src){ 4924 if (src === undefined) { 4925 return this.poster_; 4926 } 4927 4928 // The correct way to remove a poster is to set as an empty string 4929 // other falsey values will throw errors 4930 if (!src) { 4931 src = ''; 4932 } 4933 4934 // update the internal poster variable 4935 this.poster_ = src; 4936 4937 // update the tech's poster 4938 this.techCall('setPoster', src); 4939 4940 // alert components that the poster has been set 4941 this.trigger('posterchange'); 4942 4943 return this; 4944}; 4945 4946/** 4947 * Whether or not the controls are showing 4948 * @type {Boolean} 4949 * @private 4950 */ 4951vjs.Player.prototype.controls_; 4952 4953/** 4954 * Get or set whether or not the controls are showing. 4955 * @param {Boolean} controls Set controls to showing or not 4956 * @return {Boolean} Controls are showing 4957 */ 4958vjs.Player.prototype.controls = function(bool){ 4959 if (bool !== undefined) { 4960 bool = !!bool; // force boolean 4961 // Don't trigger a change event unless it actually changed 4962 if (this.controls_ !== bool) { 4963 this.controls_ = bool; 4964 if (bool) { 4965 this.removeClass('vjs-controls-disabled'); 4966 this.addClass('vjs-controls-enabled'); 4967 this.trigger('controlsenabled'); 4968 } else { 4969 this.removeClass('vjs-controls-enabled'); 4970 this.addClass('vjs-controls-disabled'); 4971 this.trigger('controlsdisabled'); 4972 } 4973 } 4974 return this; 4975 } 4976 return this.controls_; 4977}; 4978 4979vjs.Player.prototype.usingNativeControls_; 4980 4981/** 4982 * Toggle native controls on/off. Native controls are the controls built into 4983 * devices (e.g. default iPhone controls), Flash, or other techs 4984 * (e.g. Vimeo Controls) 4985 * 4986 * **This should only be set by the current tech, because only the tech knows 4987 * if it can support native controls** 4988 * 4989 * @param {Boolean} bool True signals that native controls are on 4990 * @return {vjs.Player} Returns the player 4991 * @private 4992 */ 4993vjs.Player.prototype.usingNativeControls = function(bool){ 4994 if (bool !== undefined) { 4995 bool = !!bool; // force boolean 4996 // Don't trigger a change event unless it actually changed 4997 if (this.usingNativeControls_ !== bool) { 4998 this.usingNativeControls_ = bool; 4999 if (bool) {
5000 this.addClass('vjs-using-native-controls'); 5001 5002 /** 5003 * player is using the native device controls 5004 * 5005 * @event usingnativecontrols 5006 * @memberof vjs.Player 5007 * @instance 5008 * @private 5009 */ 5010 this.trigger('usingnativecontrols'); 5011 } else { 5012 this.removeClass('vjs-using-native-controls'); 5013 5014 /** 5015 * player is using the custom HTML controls 5016 * 5017 * @event usingcustomcontrols 5018 * @memberof vjs.Player 5019 * @instance 5020 * @private 5021 */ 5022 this.trigger('usingcustomcontrols'); 5023 } 5024 } 5025 return this; 5026 } 5027 return this.usingNativeControls_; 5028}; 5029 5030/** 5031 * Store the current media error 5032 * @type {Object} 5033 * @private 5034 */ 5035vjs.Player.prototype.error_ = null; 5036 5037/** 5038 * Set or get the current MediaError 5039 * @param {*} err A MediaError or a String/Number to be turned into a MediaError 5040 * @return {vjs.MediaError|null} when getting 5041 * @return {vjs.Player} when setting 5042 */ 5043vjs.Player.prototype.error = function(err){ 5044 if (err === undefined) { 5045 return this.error_; 5046 } 5047 5048 // restoring to default 5049 if (err === null) { 5050 this.error_ = err; 5051 this.removeClass('vjs-error'); 5052 return this; 5053 } 5054 5055 // error instance 5056 if (err instanceof vjs.MediaError) { 5057 this.error_ = err; 5058 } else { 5059 this.error_ = new vjs.MediaError(err); 5060 } 5061 5062 // fire an error event on the player 5063 this.trigger('error'); 5064 5065 // add the vjs-error classname to the player 5066 this.addClass('vjs-error'); 5067 5068 // log the name of the error type and any message 5069 // ie8 just logs "[object object]" if you just log the error object 5070 vjs.log.error('(CODE:'+this.error_.code+' '+vjs.MediaError.errorTypes[this.error_.code]+')', this.error_.message, this.error_); 5071 5072 return this; 5073}; 5074 5075/** 5076 * Returns whether or not the player is in the "ended" state. 5077 * @return {Boolean} True if the player is in the ended state, false if not. 5078 */ 5079vjs.Player.prototype.ended = function(){ return this.techGet('ended'); }; 5080 5081/** 5082 * Returns whether or not the player is in the "seeking" state. 5083 * @return {Boolean} True if the player is in the seeking state, false if not. 5084 */ 5085vjs.Player.prototype.seeking = function(){ return this.techGet('seeking'); }; 5086 5087// When the player is first initialized, trigger activity so components 5088// like the control bar show themselves if needed 5089vjs.Player.prototype.userActivity_ = true; 5090vjs.Player.prototype.reportUserActivity = function(event){ 5091 this.userActivity_ = true; 5092}; 5093 5094vjs.Player.prototype.userActive_ = true; 5095vjs.Player.prototype.userActive = function(bool){ 5096 if (bool !== undefined) { 5097 bool = !!bool; 5098 if (bool !== this.userActive_) { 5099 this.userActive_ = bool; 5100 if (bool) { 5101 // If the user was inactive and is now active we want to reset the 5102 // inactivity timer 5103 this.userActivity_ = true; 5104 this.removeClass('vjs-user-inactive'); 5105 this.addClass('vjs-user-active'); 5106 this.trigger('useractive'); 5107 } else { 5108 // We're switching the state to inactive manually, so erase any other 5109 // activity 5110 this.userActivity_ = false;
5111 5112 // Chrome/Safari/IE have bugs where when you change the cursor it can 5113 // trigger a mousemove event. This causes an issue when you're hiding 5114 // the cursor when the user is inactive, and a mousemove signals user 5115 // activity. Making it impossible to go into inactive mode. Specifically 5116 // this happens in fullscreen when we really need to hide the cursor. 5117 // 5118 // When this gets resolved in ALL browsers it can be removed 5119 // https://code.google.com/p/chromium/issues/detail?id=103041 5120 if(this.tech) { 5121 this.tech.one('mousemove', function(e){ 5122 e.stopPropagation(); 5123 e.preventDefault(); 5124 }); 5125 } 5126 5127 this.removeClass('vjs-user-active'); 5128 this.addClass('vjs-user-inactive'); 5129 this.trigger('userinactive'); 5130 } 5131 } 5132 return this; 5133 } 5134 return this.userActive_; 5135}; 5136 5137vjs.Player.prototype.listenForUserActivity = function(){ 5138 var onActivity, onMouseMove, onMouseDown, mouseInProgress, onMouseUp, 5139 activityCheck, inactivityTimeout, lastMoveX, lastMoveY; 5140 5141 onActivity = vjs.bind(this, this.reportUserActivity); 5142 5143 onMouseMove = function(e) { 5144 // #1068 - Prevent mousemove spamming 5145 // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970 5146 if(e.screenX != lastMoveX || e.screenY != lastMoveY) { 5147 lastMoveX = e.screenX; 5148 lastMoveY = e.screenY; 5149 onActivity(); 5150 } 5151 }; 5152 5153 onMouseDown = function() { 5154 onActivity(); 5155 // For as long as the they are touching the device or have their mouse down, 5156 // we consider them active even if they're not moving their finger or mouse. 5157 // So we want to continue to update that they are active 5158 this.clearInterval(mouseInProgress); 5159 // Setting userActivity=true now and setting the interval to the same time 5160 // as the activityCheck interval (250) should ensure we never miss the 5161 // next activityCheck 5162 mouseInProgress = this.setInterval(onActivity, 250); 5163 }; 5164 5165 onMouseUp = function(event) { 5166 onActivity(); 5167 // Stop the interval that maintains activity if the mouse/touch is down 5168 this.clearInterval(mouseInProgress); 5169 }; 5170 5171 // Any mouse movement will be considered user activity 5172 this.on('mousedown', onMouseDown); 5173 this.on('mousemove', onMouseMove); 5174 this.on('mouseup', onMouseUp); 5175 5176 // Listen for keyboard navigation 5177 // Shouldn't need to use inProgress interval because of key repeat 5178 this.on('keydown', onActivity); 5179 this.on('keyup', onActivity); 5180 5181 // Run an interval every 250 milliseconds instead of stuffing everything into 5182 // the mousemove/touchmove function itself, to prevent performance degradation. 5183 // `this.reportUserActivity` simply sets this.userActivity_ to true, which 5184 // then gets picked up by this loop 5185 // http://ejohn.org/blog/learning-from-twitter/ 5186 activityCheck = this.setInterval(function() { 5187 // Check to see if mouse/touch activity has happened 5188 if (this.userActivity_) { 5189 // Reset the activity tracker 5190 this.userActivity_ = false;
5191 5192 // If the user state was inactive, set the state to active 5193 this.userActive(true); 5194 5195 // Clear any existing inactivity timeout to start the timer over 5196 this.clearTimeout(inactivityTimeout); 5197 5198 var timeout = this.options()['inactivityTimeout']; 5199 if (timeout > 0) { 5200 // In <timeout> milliseconds, if no more activity has occurred the 5201 // user will be considered inactive 5202 inactivityTimeout = this.setTimeout(function () { 5203 // Protect against the case where the inactivityTimeout can trigger just 5204 // before the next user activity is picked up by the activityCheck loop 5205 // causing a flicker 5206 if (!this.userActivity_) { 5207 this.userActive(false); 5208 } 5209 }, timeout); 5210 } 5211 } 5212 }, 250); 5213}; 5214 5215/** 5216 * Gets or sets the current playback rate. 5217 * @param {Boolean} rate New playback rate to set. 5218 * @return {Number} Returns the new playback rate when setting 5219 * @return {Number} Returns the current playback rate when getting 5220 */ 5221vjs.Player.prototype.playbackRate = function(rate) { 5222 if (rate !== undefined) { 5223 this.techCall('setPlaybackRate', rate); 5224 return this; 5225 } 5226 5227 if (this.tech && this.tech['featuresPlaybackRate']) { 5228 return this.techGet('playbackRate'); 5229 } else { 5230 return 1.0; 5231 } 5232 5233}; 5234 5235/** 5236 * Store the current audio state 5237 * @type {Boolean} 5238 * @private 5239 */ 5240vjs.Player.prototype.isAudio_ = false;
5241 5242/** 5243 * Gets or sets the audio flag 5244 * 5245 * @param {Boolean} bool True signals that this is an audio player. 5246 * @return {Boolean} Returns true if player is audio, false if not when getting 5247 * @return {vjs.Player} Returns the player if setting 5248 * @private 5249 */ 5250vjs.Player.prototype.isAudio = function(bool) { 5251 if (bool !== undefined) { 5252 this.isAudio_ = !!bool; 5253 return this; 5254 } 5255 5256 return this.isAudio_; 5257}; 5258 5259// Methods to add support for 5260// networkState: function(){ return this.techCall('networkState'); }, 5261// readyState: function(){ return this.techCall('readyState'); }, 5262// initialTime: function(){ return this.techCall('initialTime'); }, 5263// startOffsetTime: function(){ return this.techCall('startOffsetTime'); }, 5264// played: function(){ return this.techCall('played'); }, 5265// seekable: function(){ return this.techCall('seekable'); }, 5266// videoTracks: function(){ return this.techCall('videoTracks'); }, 5267// audioTracks: function(){ return this.techCall('audioTracks'); }, 5268// videoWidth: function(){ return this.techCall('videoWidth'); }, 5269// videoHeight: function(){ return this.techCall('videoHeight'); }, 5270// defaultPlaybackRate: function(){ return this.techCall('defaultPlaybackRate'); }, 5271// mediaGroup: function(){ return this.techCall('mediaGroup'); }, 5272// controller: function(){ return this.techCall('controller'); }, 5273// defaultMuted: function(){ return this.techCall('defaultMuted'); } 5274 5275// TODO 5276// currentSrcList: the array of sources including other formats and bitrates 5277// playList: array of source lists in order of playback 5278/** 5279 * Container of main controls 5280 * @param {vjs.Player|Object} player 5281 * @param {Object=} options 5282 * @class 5283 * @constructor 5284 * @extends vjs.Component 5285 */ 5286vjs.ControlBar = vjs.Component.extend(); 5287 5288vjs.ControlBar.prototype.options_ = { 5289 loadEvent: 'play', 5290 children: { 5291 'playToggle': {}, 5292 'currentTimeDisplay': {}, 5293 'timeDivider': {}, 5294 'durationDisplay': {}, 5295 'remainingTimeDisplay': {}, 5296 'liveDisplay': {}, 5297 'progressControl': {}, 5298 'fullscreenToggle': {}, 5299 'volumeControl': {}, 5300 'muteToggle': {}, 5301 // 'volumeMenuButton': {}, 5302 'playbackRateMenuButton': {} 5303 } 5304}; 5305 5306vjs.ControlBar.prototype.createEl = function(){ 5307 return vjs.createEl('div', { 5308 className: 'vjs-control-bar' 5309 }); 5310}; 5311/** 5312 * Displays the live indicator 5313 * TODO - Future make it click to snap to live 5314 * @param {vjs.Player|Object} player 5315 * @param {Object=} options 5316 * @constructor 5317 */ 5318vjs.LiveDisplay = vjs.Component.extend({ 5319 init: function(player, options){ 5320 vjs.Component.call(this, player, options); 5321 } 5322}); 5323 5324vjs.LiveDisplay.prototype.createEl = function(){ 5325 var el = vjs.Component.prototype.createEl.call(this, 'div', { 5326 className: 'vjs-live-controls vjs-control' 5327 }); 5328 5329 this.contentEl_ = vjs.createEl('div', { 5330 className: 'vjs-live-display', 5331 innerHTML: '<span class="vjs-control-text">' + this.localize('Stream Type') + '</span>' + this.localize('LIVE'), 5332 'aria-live': 'off' 5333 }); 5334 5335 el.appendChild(this.contentEl_); 5336 5337 return el; 5338}; 5339/** 5340 * Button to toggle between play and pause 5341 * @param {vjs.Player|Object} player 5342 * @param {Object=} options 5343 * @class 5344 * @constructor 5345 */ 5346vjs.PlayToggle = vjs.Button.extend({ 5347 /** @constructor */ 5348 init: function(player, options){ 5349 vjs.Button.call(this, player, options); 5350 5351 this.on(player, 'play', this.onPlay); 5352 this.on(player, 'pause', this.onPause); 5353 } 5354}); 5355 5356vjs.PlayToggle.prototype.buttonText = 'Play'; 5357 5358vjs.PlayToggle.prototype.buildCSSClass = function(){ 5359 return 'vjs-play-control ' + vjs.Button.prototype.buildCSSClass.call(this); 5360}; 5361 5362// OnClick - Toggle between play and pause 5363vjs.PlayToggle.prototype.onClick = function(){ 5364 if (this.player_.paused()) { 5365 this.player_.play(); 5366 } else { 5367 this.player_.pause(); 5368 } 5369}; 5370 5371 // OnPlay - Add the vjs-playing class to the element so it can change appearance 5372vjs.PlayToggle.prototype.onPlay = function(){ 5373 this.removeClass('vjs-paused'); 5374 this.addClass('vjs-playing'); 5375 this.el_.children[0].children[0].innerHTML = this.localize('Pause'); // change the button text to "Pause" 5376}; 5377 5378 // OnPause - Add the vjs-paused class to the element so it can change appearance 5379vjs.PlayToggle.prototype.onPause = function(){ 5380 this.removeClass('vjs-playing'); 5381 this.addClass('vjs-paused'); 5382 this.el_.children[0].children[0].innerHTML = this.localize('Play'); // change the button text to "Play" 5383}; 5384/** 5385 * Displays the current time 5386 * @param {vjs.Player|Object} player 5387 * @param {Object=} options 5388 * @constructor 5389 */ 5390vjs.CurrentTimeDisplay = vjs.Component.extend({ 5391 /** @constructor */ 5392 init: function(player, options){ 5393 vjs.Component.call(this, player, options); 5394 5395 this.on(player, 'timeupdate', this.updateContent); 5396 } 5397}); 5398 5399vjs.CurrentTimeDisplay.prototype.createEl = function(){ 5400 var el = vjs.Component.prototype.createEl.call(this, 'div', {
5401 className: 'vjs-current-time vjs-time-controls vjs-control' 5402 }); 5403 5404 this.contentEl_ = vjs.createEl('div', { 5405 className: 'vjs-current-time-display', 5406 innerHTML: '<span class="vjs-control-text">Current Time </span>' + '0:00', // label the current time for screen reader users 5407 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes 5408 }); 5409 5410 el.appendChild(this.contentEl_); 5411 return el; 5412}; 5413 5414vjs.CurrentTimeDisplay.prototype.updateContent = function(){ 5415 // Allows for smooth scrubbing, when player can't keep up. 5416 var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); 5417 this.contentEl_.innerHTML = '<span class="vjs-control-text">' + this.localize('Current Time') + '</span> ' + vjs.formatTime(time, this.player_.duration()); 5418}; 5419 5420/** 5421 * Displays the duration 5422 * @param {vjs.Player|Object} player 5423 * @param {Object=} options 5424 * @constructor 5425 */ 5426vjs.DurationDisplay = vjs.Component.extend({ 5427 /** @constructor */ 5428 init: function(player, options){ 5429 vjs.Component.call(this, player, options); 5430 5431 // this might need to be changed to 'durationchange' instead of 'timeupdate' eventually, 5432 // however the durationchange event fires before this.player_.duration() is set, 5433 // so the value cannot be written out using this method. 5434 // Once the order of durationchange and this.player_.duration() being set is figured out, 5435 // this can be updated. 5436 this.on(player, 'timeupdate', this.updateContent); 5437 } 5438}); 5439 5440vjs.DurationDisplay.prototype.createEl = function(){ 5441 var el = vjs.Component.prototype.createEl.call(this, 'div', { 5442 className: 'vjs-duration vjs-time-controls vjs-control' 5443 }); 5444 5445 this.contentEl_ = vjs.createEl('div', { 5446 className: 'vjs-duration-display', 5447 innerHTML: '<span class="vjs-control-text">' + this.localize('Duration Time') + '</span> ' + '0:00', // label the duration time for screen reader users 5448 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes 5449 }); 5450 5451 el.appendChild(this.contentEl_); 5452 return el; 5453}; 5454 5455vjs.DurationDisplay.prototype.updateContent = function(){ 5456 var duration = this.player_.duration(); 5457 if (duration) { 5458 this.contentEl_.innerHTML = '<span class="vjs-control-text">' + this.localize('Duration Time') + '</span> ' + vjs.formatTime(duration); // label the duration time for screen reader users 5459 } 5460}; 5461 5462/** 5463 * The separator between the current time and duration 5464 * 5465 * Can be hidden if it's not needed in the design. 5466 * 5467 * @param {vjs.Player|Object} player 5468 * @param {Object=} options 5469 * @constructor 5470 */ 5471vjs.TimeDivider = vjs.Component.extend({ 5472 /** @constructor */ 5473 init: function(player, options){ 5474 vjs.Component.call(this, player, options); 5475 } 5476}); 5477 5478vjs.TimeDivider.prototype.createEl = function(){ 5479 return vjs.Component.prototype.createEl.call(this, 'div', { 5480 className: 'vjs-time-divider', 5481 innerHTML: '<div><span>/</span></div>' 5482 }); 5483}; 5484 5485/** 5486 * Displays the time left in the video 5487 * @param {vjs.Player|Object} player 5488 * @param {Object=} options 5489 * @constructor 5490 */ 5491vjs.RemainingTimeDisplay = vjs.Component.extend({ 5492 /** @constructor */ 5493 init: function(player, options){ 5494 vjs.Component.call(this, player, options); 5495 5496 this.on(player, 'timeupdate', this.updateContent); 5497 } 5498}); 5499 5500vjs.RemainingTimeDisplay.prototype.createEl = function(){ 5501 var el = vjs.Component.prototype.createEl.call(this, 'div', { 5502 className: 'vjs-remaining-time vjs-time-controls vjs-control' 5503 }); 5504 5505 this.contentEl_ = vjs.createEl('div', { 5506 className: 'vjs-remaining-time-display', 5507 innerHTML: '<span class="vjs-control-text">' + this.localize('Remaining Time') + '</span> ' + '-0:00', // label the remaining time for screen reader users 5508 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes 5509 }); 5510 5511 el.appendChild(this.contentEl_); 5512 return el; 5513}; 5514 5515vjs.RemainingTimeDisplay.prototype.updateContent = function(){ 5516 if (this.player_.duration()) { 5517 this.contentEl_.innerHTML = '<span class="vjs-control-text">' + this.localize('Remaining Time') + '</span> ' + '-'+ vjs.formatTime(this.player_.remainingTime()); 5518 } 5519 5520 // Allows for smooth scrubbing, when player can't keep up. 5521 // var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); 5522 // this.contentEl_.innerHTML = vjs.formatTime(time, this.player_.duration()); 5523}; 5524/** 5525 * Toggle fullscreen video 5526 * @param {vjs.Player|Object} player 5527 * @param {Object=} options 5528 * @class 5529 * @extends vjs.Button 5530 */ 5531vjs.FullscreenToggle = vjs.Button.extend({ 5532 /** 5533 * @constructor 5534 * @memberof vjs.FullscreenToggle 5535 * @instance 5536 */ 5537 init: function(player, options){ 5538 vjs.Button.call(this, player, options); 5539 } 5540}); 5541 5542vjs.FullscreenToggle.prototype.buttonText = 'Fullscreen'; 5543
5544vjs.FullscreenToggle.prototype.buildCSSClass = function(){ 5545 return 'vjs-fullscreen-control ' + vjs.Button.prototype.buildCSSClass.call(this); 5546}; 5547 5548vjs.FullscreenToggle.prototype.onClick = function(){ 5549 if (!this.player_.isFullscreen()) { 5550 this.player_.requestFullscreen(); 5551 this.controlText_.innerHTML = this.localize('Non-Fullscreen'); 5552 } else { 5553 this.player_.exitFullscreen(); 5554 this.controlText_.innerHTML = this.localize('Fullscreen'); 5555 } 5556}; 5557/** 5558 * The Progress Control component contains the seek bar, load progress, 5559 * and play progress 5560 * 5561 * @param {vjs.Player|Object} player 5562 * @param {Object=} options 5563 * @constructor 5564 */ 5565vjs.ProgressControl = vjs.Component.extend({ 5566 /** @constructor */ 5567 init: function(player, options){ 5568 vjs.Component.call(this, player, options); 5569 } 5570}); 5571 5572vjs.ProgressControl.prototype.options_ = { 5573 children: { 5574 'seekBar': {} 5575 } 5576}; 5577 5578vjs.ProgressControl.prototype.createEl = function(){ 5579 return vjs.Component.prototype.createEl.call(this, 'div', { 5580 className: 'vjs-progress-control vjs-control' 5581 }); 5582}; 5583 5584/** 5585 * Seek Bar and holder for the progress bars 5586 * 5587 * @param {vjs.Player|Object} player 5588 * @param {Object=} options 5589 * @constructor 5590 */ 5591vjs.SeekBar = vjs.Slider.extend({ 5592 /** @constructor */ 5593 init: function(player, options){ 5594 vjs.Slider.call(this, player, options); 5595 this.on(player, 'timeupdate', this.updateARIAAttributes); 5596 player.ready(vjs.bind(this, this.updateARIAAttributes)); 5597 } 5598}); 5599 5600vjs.SeekBar.prototype.options_ = { 5601 children: { 5602 'loadProgressBar': {}, 5603 'playProgressBar': {}, 5604 'seekHandle': {} 5605 }, 5606 'barName': 'playProgressBar', 5607 'handleName': 'seekHandle' 5608}; 5609 5610vjs.SeekBar.prototype.playerEvent = 'timeupdate'; 5611 5612vjs.SeekBar.prototype.createEl = function(){ 5613 return vjs.Slider.prototype.createEl.call(this, 'div', { 5614 className: 'vjs-progress-holder', 5615 'aria-label': 'video progress bar' 5616 }); 5617}; 5618 5619vjs.SeekBar.prototype.updateARIAAttributes = function(){ 5620 // Allows for smooth scrubbing, when player can't keep up. 5621 var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); 5622 this.el_.setAttribute('aria-valuenow',vjs.round(this.getPercent()*100, 2)); // machine readable value of progress bar (percentage complete) 5623 this.el_.setAttribute('aria-valuetext',vjs.formatTime(time, this.player_.duration())); // human readable value of progress bar (time complete) 5624}; 5625 5626vjs.SeekBar.prototype.getPercent = function(){ 5627 return this.player_.currentTime() / this.player_.duration(); 5628}; 5629 5630vjs.SeekBar.prototype.onMouseDown = function(event){ 5631 vjs.Slider.prototype.onMouseDown.call(this, event); 5632 5633 this.player_.scrubbing = true; 5634 5635 this.videoWasPlaying = !this.player_.paused(); 5636 this.player_.pause(); 5637}; 5638 5639vjs.SeekBar.prototype.onMouseMove = function(event){ 5640 var newTime = this.calculateDistance(event) * this.player_.duration(); 5641 5642 // Don't let video end while scrubbing. 5643 if (newTime == this.player_.duration()) { newTime = newTime - 0.1; } 5644 5645 // Set new time (tell player to seek to new time) 5646 this.player_.currentTime(newTime); 5647}; 5648 5649vjs.SeekBar.prototype.onMouseUp = function(event){ 5650 vjs.Slider.prototype.onMouseUp.call(this, event); 5651 5652 this.player_.scrubbing = false; 5653 if (this.videoWasPlaying) { 5654 this.player_.play(); 5655 } 5656}; 5657 5658vjs.SeekBar.prototype.stepForward = function(){ 5659 this.player_.currentTime(this.player_.currentTime() + 5); // more quickly fast forward for keyboard-only users 5660}; 5661 5662vjs.SeekBar.prototype.stepBack = function(){ 5663 this.player_.currentTime(this.player_.currentTime() - 5); // more quickly rewind for keyboard-only users 5664}; 5665 5666/** 5667 * Shows load progress 5668 * 5669 * @param {vjs.Player|Object} player 5670 * @param {Object=} options 5671 * @constructor 5672 */ 5673vjs.LoadProgressBar = vjs.Component.extend({ 5674 /** @constructor */ 5675 init: function(player, options){ 5676 vjs.Component.call(this, player, options); 5677 this.on(player, 'progress', this.update); 5678 } 5679}); 5680 5681vjs.LoadProgressBar.prototype.createEl = function(){ 5682 return vjs.Component.prototype.createEl.call(this, 'div', { 5683 className: 'vjs-load-progress', 5684 innerHTML: '<span class="vjs-control-text"><span>' + this.localize('Loaded') + '</span>: 0%</span>' 5685 }); 5686}; 5687 5688vjs.LoadProgressBar.prototype.update = function(){ 5689 var i, start, end, part, 5690 buffered = this.player_.buffered(), 5691 duration = this.player_.duration(), 5692 bufferedEnd = this.player_.bufferedEnd(), 5693 children = this.el_.children, 5694 // get the percent width of a time compared to the total end 5695 percentify = function (time, end){ 5696 var percent = (time / end) || 0; // no NaN 5697 return (percent * 100) + '%'; 5698 }; 5699 5700 // update the width of the progress bar 5701 this.el_.style.width = percentify(bufferedEnd, duration); 5702 5703 // add child elements to represent the individual buffered time ranges 5704 for (i = 0; i < buffered.length; i++) { 5705 start = buffered.start(i), 5706 end = buffered.end(i), 5707 part = children[i]; 5708 5709 if (!part) { 5710 part = this.el_.appendChild(vjs.createEl()); 5711 } 5712 5713 // set the percent based on the width of the progress bar (bufferedEnd) 5714 part.style.left = percentify(start, bufferedEnd); 5715 part.style.width = percentify(end - start, bufferedEnd); 5716 } 5717 5718 // remove unused buffered range elements 5719 for (i = children.length; i > buffered.length; i--) { 5720 this.el_.removeChild(children[i-1]); 5721 } 5722}; 5723 5724/** 5725 * Shows play progress 5726 * 5727 * @param {vjs.Player|Object} player 5728 * @param {Object=} options 5729 * @constructor 5730 */ 5731vjs.PlayProgressBar = vjs.Component.extend({ 5732 /** @constructor */ 5733 init: function(player, options){ 5734 vjs.Component.call(this, player, options); 5735 } 5736}); 5737 5738vjs.PlayProgressBar.prototype.createEl = function(){ 5739 return vjs.Component.prototype.createEl.call(this, 'div', { 5740 className: 'vjs-play-progress', 5741 innerHTML: '<span class="vjs-control-text"><span>' + this.localize('Progress') + '</span>: 0%</span>' 5742 }); 5743}; 5744 5745/** 5746 * The Seek Handle shows the current position of the playhead during playback, 5747 * and can be dragged to adjust the playhead. 5748 * 5749 * @param {vjs.Player|Object} player 5750 * @param {Object=} options 5751 * @constructor 5752 */ 5753vjs.SeekHandle = vjs.SliderHandle.extend({ 5754 init: function(player, options) { 5755 vjs.SliderHandle.call(this, player, options); 5756 this.on(player, 'timeupdate', this.updateContent); 5757 } 5758}); 5759 5760/** 5761 * The default value for the handle content, which may be read by screen readers 5762 * 5763 * @type {String} 5764 * @private 5765 */ 5766vjs.SeekHandle.prototype.defaultValue = '00:00'; 5767 5768/** @inheritDoc */ 5769vjs.SeekHandle.prototype.createEl = function() { 5770 return vjs.SliderHandle.prototype.createEl.call(this, 'div', { 5771 className: 'vjs-seek-handle', 5772 'aria-live': 'off' 5773 }); 5774}; 5775 5776vjs.SeekHandle.prototype.updateContent = function() { 5777 var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); 5778 this.el_.innerHTML = '<span class="vjs-control-text">' + vjs.formatTime(time, this.player_.duration()) + '</span>'; 5779}; 5780/** 5781 * The component for controlling the volume level 5782 * 5783 * @param {vjs.Player|Object} player 5784 * @param {Object=} options 5785 * @constructor 5786 */ 5787vjs.VolumeControl = vjs.Component.extend({ 5788 /** @constructor */ 5789 init: function(player, options){ 5790 vjs.Component.call(this, player, options); 5791 5792 // hide volume controls when they're not supported by the current tech 5793 if (player.tech && player.tech['featuresVolumeControl'] === false) { 5794 this.addClass('vjs-hidden'); 5795 } 5796 this.on(player, 'loadstart', function(){ 5797 if (player.tech['featuresVolumeControl'] === false) { 5798 this.addClass('vjs-hidden'); 5799 } else { 5800 this.removeClass('vjs-hidden'); 5801 } 5802 }); 5803 } 5804}); 5805 5806vjs.VolumeControl.prototype.options_ = { 5807 children: { 5808 'volumeBar': {} 5809 } 5810}; 5811 5812vjs.VolumeControl.prototype.createEl = function(){ 5813 return vjs.Component.prototype.createEl.call(this, 'div', { 5814 className: 'vjs-volume-control vjs-control' 5815 }); 5816}; 5817 5818/** 5819 * The bar that contains the volume level and can be clicked on to adjust the level 5820 * 5821 * @param {vjs.Player|Object} player 5822 * @param {Object=} options 5823 * @constructor 5824 */ 5825vjs.VolumeBar = vjs.Slider.extend({ 5826 /** @constructor */ 5827 init: function(player, options){ 5828 vjs.Slider.call(this, player, options); 5829 this.on(player, 'volumechange', this.updateARIAAttributes); 5830 player.ready(vjs.bind(this, this.updateARIAAttributes)); 5831 } 5832}); 5833 5834vjs.VolumeBar.prototype.updateARIAAttributes = function(){ 5835 // Current value of volume bar as a percentage 5836 this.el_.setAttribute('aria-valuenow',vjs.round(this.player_.volume()*100, 2)); 5837 this.el_.setAttribute('aria-valuetext',vjs.round(this.player_.volume()*100, 2)+'%'); 5838}; 5839 5840vjs.VolumeBar.prototype.options_ = { 5841 children: { 5842 'volumeLevel': {}, 5843 'volumeHandle': {} 5844 }, 5845 'barName': 'volumeLevel', 5846 'handleName': 'volumeHandle' 5847}; 5848 5849vjs.VolumeBar.prototype.playerEvent = 'volumechange'; 5850 5851vjs.VolumeBar.prototype.createEl = function(){ 5852 return vjs.Slider.prototype.createEl.call(this, 'div', { 5853 className: 'vjs-volume-bar', 5854 'aria-label': 'volume level' 5855 }); 5856}; 5857 5858vjs.VolumeBar.prototype.onMouseMove = function(event) { 5859 if (this.player_.muted()) { 5860 this.player_.muted(false); 5861 } 5862 5863 this.player_.volume(this.calculateDistance(event)); 5864}; 5865 5866vjs.VolumeBar.prototype.getPercent = function(){ 5867 if (this.player_.muted()) { 5868 return 0; 5869 } else { 5870 return this.player_.volume(); 5871 } 5872}; 5873 5874vjs.VolumeBar.prototype.stepForward = function(){ 5875 this.player_.volume(this.player_.volume() + 0.1); 5876}; 5877 5878vjs.VolumeBar.prototype.stepBack = function(){ 5879 this.player_.volume(this.player_.volume() - 0.1); 5880}; 5881 5882/** 5883 * Shows volume level 5884 * 5885 * @param {vjs.Player|Object} player 5886 * @param {Object=} options 5887 * @constructor 5888 */ 5889vjs.VolumeLevel = vjs.Component.extend({ 5890 /** @constructor */ 5891 init: function(player, options){ 5892 vjs.Component.call(this, player, options); 5893 } 5894}); 5895 5896vjs.VolumeLevel.prototype.createEl = function(){ 5897 return vjs.Component.prototype.createEl.call(this, 'div', { 5898 className: 'vjs-volume-level', 5899 innerHTML: '<span class="vjs-control-text"></span>' 5900 }); 5901}; 5902 5903/** 5904 * The volume handle can be dragged to adjust the volume level 5905 * 5906 * @param {vjs.Player|Object} player 5907 * @param {Object=} options 5908 * @constructor 5909 */ 5910 vjs.VolumeHandle = vjs.SliderHandle.extend(); 5911 5912 vjs.VolumeHandle.prototype.defaultValue = '00:00'; 5913 5914 /** @inheritDoc */ 5915 vjs.VolumeHandle.prototype.createEl = function(){ 5916 return vjs.SliderHandle.prototype.createEl.call(this, 'div', { 5917 className: 'vjs-volume-handle' 5918 }); 5919 }; 5920/** 5921 * A button component for muting the audio 5922 * 5923 * @param {vjs.Player|Object} player 5924 * @param {Object=} options 5925 * @constructor 5926 */ 5927vjs.MuteToggle = vjs.Button.extend({ 5928 /** @constructor */ 5929 init: function(player, options){ 5930 vjs.Button.call(this, player, options); 5931 5932 this.on(player, 'volumechange', this.update); 5933 5934 // hide mute toggle if the current tech doesn't support volume control 5935 if (player.tech && player.tech['featuresVolumeControl'] === false) { 5936 this.addClass('vjs-hidden'); 5937 } 5938 5939 this.on(player, 'loadstart', function(){ 5940 if (player.tech['featuresVolumeControl'] === false) { 5941 this.addClass('vjs-hidden'); 5942 } else { 5943 this.removeClass('vjs-hidden'); 5944 } 5945 }); 5946 } 5947}); 5948 5949vjs.MuteToggle.prototype.createEl = function(){ 5950 return vjs.Button.prototype.createEl.call(this, 'div', { 5951 className: 'vjs-mute-control vjs-control', 5952 innerHTML: '<div><span class="vjs-control-text">' + this.localize('Mute') + '</span></div>' 5953 }); 5954}; 5955 5956vjs.MuteToggle.prototype.onClick = function(){ 5957 this.player_.muted( this.player_.muted() ? false : true ); 5958}; 5959 5960vjs.MuteToggle.prototype.update = function(){ 5961 var vol = this.player_.volume(), 5962 level = 3; 5963 5964 if (vol === 0 || this.player_.muted()) { 5965 level = 0; 5966 } else if (vol < 0.33) { 5967 level = 1; 5968 } else if (vol < 0.67) { 5969 level = 2; 5970 } 5971 5972 // Don't rewrite the button text if the actual text doesn't change. 5973 // This causes unnecessary and confusing information for screen reader users. 5974 // This check is needed because this function gets called every time the volume level is changed. 5975 if(this.player_.muted()){ 5976 if(this.el_.children[0].children[0].innerHTML!=this.localize('Unmute')){ 5977 this.el_.children[0].children[0].innerHTML = this.localize('Unmute'); // change the button text to "Unmute" 5978 } 5979 } else { 5980 if(this.el_.children[0].children[0].innerHTML!=this.localize('Mute')){ 5981 this.el_.children[0].children[0].innerHTML = this.localize('Mute'); // change the button text to "Mute" 5982 } 5983 } 5984 5985 /* TODO improve muted icon classes */ 5986 for (var i = 0; i < 4; i++) { 5987 vjs.removeClass(this.el_, 'vjs-vol-'+i); 5988 } 5989 vjs.addClass(this.el_, 'vjs-vol-'+level); 5990}; 5991/** 5992 * Menu button with a popup for showing the volume slider. 5993 * @constructor 5994 */ 5995vjs.VolumeMenuButton = vjs.MenuButton.extend({ 5996 /** @constructor */ 5997 init: function(player, options){ 5998 vjs.MenuButton.call(this, player, options); 5999 6000 // Same listeners as MuteToggle 6001 this.on(player, 'volumechange', this.update); 6002 6003 // hide mute toggle if the current tech doesn't support volume control 6004 if (player.tech && player.tech['featuresVolumeControl'] === false) { 6005 this.addClass('vjs-hidden'); 6006 } 6007 this.on(player, 'loadstart', function(){ 6008 if (player.tech['featuresVolumeControl'] === false) { 6009 this.addClass('vjs-hidden'); 6010 } else { 6011 this.removeClass('vjs-hidden'); 6012 } 6013 }); 6014 this.addClass('vjs-menu-button'); 6015 } 6016}); 6017 6018vjs.VolumeMenuButton.prototype.createMenu = function(){ 6019 var menu = new vjs.Menu(this.player_, { 6020 contentElType: 'div' 6021 }); 6022 var vc = new vjs.VolumeBar(this.player_, this.options_['volumeBar']); 6023 vc.on('focus', function() { 6024 menu.lockShowing(); 6025 }); 6026 vc.on('blur', function() { 6027 menu.unlockShowing(); 6028 }); 6029 menu.addChild(vc); 6030 return menu; 6031}; 6032 6033vjs.VolumeMenuButton.prototype.onClick = function(){ 6034 vjs.MuteToggle.prototype.onClick.call(this); 6035 vjs.MenuButton.prototype.onClick.call(this); 6036}; 6037 6038vjs.VolumeMenuButton.prototype.createEl = function(){ 6039 return vjs.Button.prototype.createEl.call(this, 'div', { 6040 className: 'vjs-volume-menu-button vjs-menu-button vjs-control', 6041 innerHTML: '<div><span class="vjs-control-text">' + this.localize('Mute') + '</span></div>' 6042 }); 6043}; 6044vjs.VolumeMenuButton.prototype.update = vjs.MuteToggle.prototype.update; 6045/** 6046 * The component for controlling the playback rate 6047 * 6048 * @param {vjs.Player|Object} player 6049 * @param {Object=} options 6050 * @constructor 6051 */ 6052vjs.PlaybackRateMenuButton = vjs.MenuButton.extend({ 6053 /** @constructor */ 6054 init: function(player, options){ 6055 vjs.MenuButton.call(this, player, options); 6056 6057 this.updateVisibility(); 6058 this.updateLabel(); 6059 6060 this.on(player, 'loadstart', this.updateVisibility); 6061 this.on(player, 'ratechange', this.updateLabel); 6062 } 6063}); 6064 6065vjs.PlaybackRateMenuButton.prototype.buttonText = 'Playback Rate'; 6066vjs.PlaybackRateMenuButton.prototype.className = 'vjs-playback-rate'; 6067 6068vjs.PlaybackRateMenuButton.prototype.createEl = function(){ 6069 var el = vjs.MenuButton.prototype.createEl.call(this); 6070 6071 this.labelEl_ = vjs.createEl('div', { 6072 className: 'vjs-playback-rate-value', 6073 innerHTML: 1.0 6074 }); 6075 6076 el.appendChild(this.labelEl_); 6077 6078 return el; 6079}; 6080 6081// Menu creation 6082vjs.PlaybackRateMenuButton.prototype.createMenu = function(){ 6083 var menu = new vjs.Menu(this.player()); 6084 var rates = this.player().options()['playbackRates']; 6085 6086 if (rates) { 6087 for (var i = rates.length - 1; i >= 0; i--) { 6088 menu.addChild( 6089 new vjs.PlaybackRateMenuItem(this.player(), { 'rate': rates[i] + 'x'}) 6090 ); 6091 } 6092 } 6093 6094 return menu; 6095}; 6096 6097vjs.PlaybackRateMenuButton.prototype.updateARIAAttributes = function(){ 6098 // Current playback rate 6099 this.el().setAttribute('aria-valuenow', this.player().playbackRate()); 6100}; 6101 6102vjs.PlaybackRateMenuButton.prototype.onClick = function(){ 6103 // select next rate option 6104 var currentRate = this.player().playbackRate(); 6105 var rates = this.player().options()['playbackRates']; 6106 // this will select first one if the last one currently selected 6107 var newRate = rates[0]; 6108 for (var i = 0; i <rates.length ; i++) { 6109 if (rates[i] > currentRate) { 6110 newRate = rates[i]; 6111 break; 6112 } 6113 } 6114 this.player().playbackRate(newRate); 6115}; 6116 6117vjs.PlaybackRateMenuButton.prototype.playbackRateSupported = function(){ 6118 return this.player().tech
6118 6119 && this.player().tech['featuresPlaybackRate'] 6120 && this.player().options()['playbackRates'] 6121 && this.player().options()['playbackRates'].length > 0 6122 ; 6123}; 6124 6125/** 6126 * Hide playback rate controls when they're no playback rate options to select 6127 */ 6128vjs.PlaybackRateMenuButton.prototype.updateVisibility = function(){ 6129 if (this.playbackRateSupported()) { 6130 this.removeClass('vjs-hidden'); 6131 } else { 6132 this.addClass('vjs-hidden'); 6133 } 6134}; 6135 6136/** 6137 * Update button label when rate changed 6138 */ 6139vjs.PlaybackRateMenuButton.prototype.updateLabel = function(){ 6140 if (this.playbackRateSupported()) { 6141 this.labelEl_.innerHTML = this.player().playbackRate() + 'x'; 6142 } 6143}; 6144 6145/** 6146 * The specific menu item type for selecting a playback rate 6147 * 6148 * @constructor 6149 */ 6150vjs.PlaybackRateMenuItem = vjs.MenuItem.extend({ 6151 contentElType: 'button', 6152 /** @constructor */ 6153 init: function(player, options){ 6154 var label = this.label = options['rate']; 6155 var rate = this.rate = parseFloat(label, 10); 6156 6157 // Modify options for parent MenuItem class's init. 6158 options['label'] = label; 6159 options['selected'] = rate === 1; 6160 vjs.MenuItem.call(this, player, options); 6161 6162 this.on(player, 'ratechange', this.update); 6163 } 6164}); 6165 6166vjs.PlaybackRateMenuItem.prototype.onClick = function(){ 6167 vjs.MenuItem.prototype.onClick.call(this); 6168 this.player().playbackRate(this.rate); 6169}; 6170 6171vjs.PlaybackRateMenuItem.prototype.update = function(){ 6172 this.selected(this.player().playbackRate() == this.rate); 6173}; 6174/* Poster Image 6175================================================================================ */ 6176/** 6177 * The component that handles showing the poster image. 6178 * 6179 * @param {vjs.Player|Object} player 6180 * @param {Object=} options 6181 * @constructor 6182 */ 6183vjs.PosterImage = vjs.Button.extend({ 6184 /** @constructor */ 6185 init: function(player, options){ 6186 vjs.Button.call(this, player, options); 6187 6188 this.update(); 6189 player.on('posterchange', vjs.bind(this, this.update)); 6190 } 6191}); 6192 6193/** 6194 * Clean up the poster image 6195 */ 6196vjs.PosterImage.prototype.dispose = function(){ 6197 this.player().off('posterchange', this.update); 6198 vjs.Button.prototype.dispose.call(this); 6199}; 6200 6201/** 6202 * Create the poster image element 6203 * @return {Element} 6204 */ 6205vjs.PosterImage.prototype.createEl = function(){ 6206 var el = vjs.createEl('div', { 6207 className: 'vjs-poster', 6208 6209 // Don't want poster to be tabbable. 6210 tabIndex: -1 6211 }); 6212 6213 // To ensure the poster image resizes while maintaining its original aspect 6214 // ratio, use a div with `background-size` when available. For browsers that 6215 // do not support `background-size` (e.g. IE8), fall back on using a regular 6216 // img element. 6217 if (!vjs.BACKGROUND_SIZE_SUPPORTED) { 6218 this.fallbackImg_ = vjs.createEl('img'); 6219 el.appendChild(this.fallbackImg_); 6220 } 6221 6222 return el; 6223}; 6224 6225/** 6226 * Event handler for updates to the player's poster source 6227 */ 6228vjs.PosterImage.prototype.update = function(){ 6229 var url = this.player().poster(); 6230 6231 this.setSrc(url); 6232 6233 // If there's no poster source we should display:none on this component 6234 // so it's not still clickable or right-clickable 6235 if (url) { 6236 // Remove the display style property that hide() adds 6237 // as opposed to show() which sets display to block 6238 // In the future it might be worth creating an `unhide` component method 6239 this.el_.style.display = ''; 6240 } else { 6241 this.hide(); 6242 } 6243}; 6244 6245/** 6246 * Set the poster source depending on the display method 6247 */ 6248vjs.PosterImage.prototype.setSrc = function(url){ 6249 var backgroundImage; 6250 6251 if (this.fallbackImg_) { 6252 this.fallbackImg_.src = url; 6253 } else { 6254 backgroundImage = ''; 6255 // Any falsey values should stay as an empty string, otherwise 6256 // this will throw an extra error 6257 if (url) { 6258 backgroundImage = 'url("' + url + '")'; 6259 } 6260 6261 this.el_.style.backgroundImage = backgroundImage; 6262 } 6263}; 6264 6265/** 6266 * Event handler for clicks on the poster image 6267 */ 6268vjs.PosterImage.prototype.onClick = function(){ 6269 // We don't want a click to trigger playback when controls are disabled 6270 // but CSS should be hiding the poster to prevent that from happening 6271 this.player_.play(); 6272}; 6273/* Loading Spinner 6274================================================================================ */ 6275/** 6276 * Loading spinner for waiting events 6277 * @param {vjs.Player|Object} player 6278 * @param {Object=} options 6279 * @class 6280 * @constructor 6281 */ 6282vjs.LoadingSpinner = vjs.Component.extend({ 6283 /** @constructor */ 6284 init: function(player, options){ 6285 vjs.Component.call(this, player, options); 6286 6287 // MOVING DISPLAY HANDLING TO CSS 6288 6289 // player.on('canplay', vjs.bind(this, this.hide)); 6290 // player.on('canplaythrough', vjs.bind(this, this.hide)); 6291 // player.on('playing', vjs.bind(this, this.hide)); 6292 // player.on('seeking', vjs.bind(this, this.show)); 6293 6294 // in some browsers seeking does not trigger the 'playing' event, 6295 // so we also need to trap 'seeked' if we are going to set a 6296 // 'seeking' event 6297 // player.on('seeked', vjs.bind(this, this.hide)); 6298 6299 // player.on('ended', vjs.bind(this, this.hide)); 6300 6301 // Not showing spinner on stalled any more. Browsers may stall and then not trigger any events that would remove the spinner. 6302 // Checked in Chrome 16 and Safari 5.1.2. http://help.videojs.com/discussions/problems/883-why-is-the-download-progress-s
6302howing 6303 // player.on('stalled', vjs.bind(this, this.show)); 6304 6305 // player.on('waiting', vjs.bind(this, this.show)); 6306 } 6307}); 6308 6309vjs.LoadingSpinner.prototype.createEl = function(){ 6310 return vjs.Component.prototype.createEl.call(this, 'div', { 6311 className: 'vjs-loading-spinner' 6312 }); 6313}; 6314/* Big Play Button 6315================================================================================ */ 6316/** 6317 * Initial play button. Shows before the video has played. The hiding of the 6318 * big play button is done via CSS and player states. 6319 * @param {vjs.Player|Object} player 6320 * @param {Object=} options 6321 * @class 6322 * @constructor 6323 */ 6324vjs.BigPlayButton = vjs.Button.extend(); 6325 6326vjs.BigPlayButton.prototype.createEl = function(){ 6327 return vjs.Button.prototype.createEl.call(this, 'div', { 6328 className: 'vjs-big-play-button', 6329 innerHTML: '<span aria-hidden="true"></span>', 6330 'aria-label': 'play video' 6331 }); 6332}; 6333 6334vjs.BigPlayButton.prototype.onClick = function(){ 6335 this.player_.play(); 6336}; 6337/** 6338 * Display that an error has occurred making the video unplayable 6339 * @param {vjs.Player|Object} player 6340 * @param {Object=} options 6341 * @constructor 6342 */ 6343vjs.ErrorDisplay = vjs.Component.extend({ 6344 init: function(player, options){ 6345 vjs.Component.call(this, player, options); 6346 6347 this.update(); 6348 this.on(player, 'error', this.update); 6349 } 6350}); 6351 6352vjs.ErrorDisplay.prototype.createEl = function(){ 6353 var el = vjs.Component.prototype.createEl.call(this, 'div', { 6354 className: 'vjs-error-display' 6355 }); 6356 6357 this.contentEl_ = vjs.createEl('div'); 6358 el.appendChild(this.contentEl_); 6359 6360 return el; 6361}; 6362 6363vjs.ErrorDisplay.prototype.update = function(){ 6364 if (this.player().error()) { 6365 this.contentEl_.innerHTML = this.localize(this.player().error().message); 6366 } 6367}; 6368/** 6369 * @fileoverview Media Technology Controller - Base class for media playback 6370 * technology controllers like Flash and HTML5 6371 */ 6372 6373/** 6374 * Base class for media (HTML5 Video, Flash) controllers 6375 * @param {vjs.Player|Object} player Central player instance 6376 * @param {Object=} options Options object 6377 * @constructor 6378 */ 6379vjs.MediaTechController = vjs.Component.extend({ 6380 /** @constructor */ 6381 init: function(player, options, ready){ 6382 options = options || {}; 6383 // we don't want the tech to report user activity automatically. 6384 // This is done manually in addControlsListeners 6385 options.reportTouchActivity = false; 6386 vjs.Component.call(this, player, options, ready); 6387 6388 // Manually track progress in cases where the browser/flash player doesn't report it. 6389 if (!this['featuresProgressEvents']) { 6390 this.manualProgressOn(); 6391 } 6392 6393 // Manually track timeupdates in cases where the browser/flash player doesn't report it. 6394 if (!this['featuresTimeupdateEvents']) { 6395 this.manualTimeUpdatesOn(); 6396 } 6397 6398 this.initControlsListeners(); 6399 } 6400}); 6401 6402/** 6403 * Set up click and touch listeners for the playback element 6404 * On desktops, a click on the video itself will toggle playback, 6405 * on a mobile device a click on the video toggles controls. 6406 * (toggling controls is done by toggling the user state between active and 6407 * inactive) 6408 * 6409 * A tap can signal that a user has become active, or has become inactive 6410 * e.g. a quick tap on an iPhone movie should reveal the controls. Another 6411 * quick tap should hide them again (signaling the user is in an inactive 6412 * viewing state) 6413 * 6414 * In addition to this, we still want the user to be considered inactive after 6415 * a few seconds of inactivity. 6416 * 6417 * Note: the only part of iOS interaction we can't mimic with this setup 6418 * is a touch and hold on the video element counting as activity in order to 6419 * keep the controls showing, but that shouldn't be an issue. A touch and hold on 6420 * any controls will still keep the user active 6421 */ 6422vjs.MediaTechController.prototype.initControlsListeners = function(){ 6423 var player, activateControls; 6424 6425 player = this.player(); 6426 6427 activateControls = function(){ 6428 if (player.controls() && !player.usingNativeControls()) { 6429 this.addControlsListeners(); 6430 } 6431 }; 6432 6433 // Set up event listeners once the tech is ready and has an element to apply 6434 // listeners to 6435 this.ready(activateControls); 6436 this.on(player, 'controlsenabled', activateControls); 6437 this.on(player, 'controlsdisabled', this.removeControlsListeners); 6438 6439 // if we're loading the playback object after it has started loading or playing the 6440 // video (often with autoplay on) then the loadstart event has already fired and we 6441 // need to fire it manually because many things rely on it. 6442 // Long term we might consider how we would do this for other events like 'canplay'
6443 // that may also have fired. 6444 this.ready(function(){ 6445 if (this.networkState && this.networkState() > 0) { 6446 this.player().trigger('loadstart'); 6447 } 6448 }); 6449}; 6450 6451vjs.MediaTechController.prototype.addControlsListeners = function(){ 6452 var userWasActive; 6453 6454 // Some browsers (Chrome & IE) don't trigger a click on a flash swf, but do 6455 // trigger mousedown/up. 6456 // http://stackoverflow.com/questions/1444562/javascript-onclick-event-over-flash-object 6457 // Any touch events are set to block the mousedown event from happening 6458 this.on('mousedown', this.onClick); 6459 6460 // If the controls were hidden we don't want that to change without a tap event 6461 // so we'll check if the controls were already showing before reporting user 6462 // activity 6463 this.on('touchstart', function(event) { 6464 userWasActive = this.player_.userActive(); 6465 }); 6466 6467 this.on('touchmove', function(event) { 6468 if (userWasActive){ 6469 this.player().reportUserActivity(); 6470 } 6471 }); 6472 6473 this.on('touchend', function(event) { 6474 // Stop the mouse events from also happening 6475 event.preventDefault(); 6476 }); 6477 6478 // Turn on component tap events 6479 this.emitTapEvents(); 6480 6481 // The tap listener needs to come after the touchend listener because the tap 6482 // listener cancels out any reportedUserActivity when setting userActive(false) 6483 this.on('tap', this.onTap); 6484}; 6485 6486/** 6487 * Remove the listeners used for click and tap controls. This is needed for 6488 * toggling to controls disabled, where a tap/touch should do nothing. 6489 */ 6490vjs.MediaTechController.prototype.removeControlsListeners = function(){ 6491 // We don't want to just use `this.off()` because there might be other needed 6492 // listeners added by techs that extend this. 6493 this.off('tap'); 6494 this.off('touchstart'); 6495 this.off('touchmove'); 6496 this.off('touchleave'); 6497 this.off('touchcancel'); 6498 this.off('touchend'); 6499 this.off('click'); 6500 this.off('mousedown'); 6501}; 6502 6503/** 6504 * Handle a click on the media element. By default will play/pause the media. 6505 */ 6506vjs.MediaTechController.prototype.onClick = function(event){ 6507 // We're using mousedown to detect clicks thanks to Flash, but mousedown 6508 // will also be triggered with right-clicks, so we need to prevent that 6509 if (event.button !== 0) return; 6510 6511 // When controls are disabled a click should not toggle playback because 6512 // the click is considered a control 6513 if (this.player().controls()) { 6514 if (this.player().paused()) { 6515 this.player().play(); 6516 } else { 6517 this.player().pause(); 6518 } 6519 } 6520}; 6521 6522/** 6523 * Handle a tap on the media element. By default it will toggle the user 6524 * activity state, which hides and shows the controls. 6525 */ 6526vjs.MediaTechController.prototype.onTap = function(){ 6527 this.player().userActive(!this.player().userActive()); 6528}; 6529 6530/* Fallbacks for unsupported event types 6531================================================================================ */ 6532// Manually trigger progress events based on changes to the buffered amount 6533// Many flash players and older HTML5 browsers don't send progress or progress-like events 6534vjs.MediaTechController.prototype.manualProgressOn = function(){ 6535 this.manualProgress = true; 6536 6537 // Trigger progress watching when a source begins loading 6538 this.trackProgress(); 6539}; 6540 6541vjs.MediaTechController.prototype.manualProgressOff = function(){ 6542 this.manualProgress = false; 6543 this.stopTrackingProgress(); 6544}; 6545 6546vjs.MediaTechController.prototype.trackProgress = function(){ 6547 this.progressInterval = this.setInterval(function(){ 6548 // Don't trigger unless buffered amount is greater than last time 6549 6550 var bufferedPercent = this.player().bufferedPercent(); 6551 6552 if (this.bufferedPercent_ != bufferedPercent) { 6553 this.player().trigger('progress'); 6554 } 6555 6556 this.bufferedPercent_ = bufferedPercent; 6557 6558 if (bufferedPercent === 1) { 6559 this.stopTrackingProgress(); 6560 } 6561 }, 500); 6562}; 6563vjs.MediaTechController.prototype.stopTrackingProgress = function(){ this.clearInterval(this.progressInterval); }; 6564 6565/*! Time Tracking -------------------------------------------------------------- */ 6566vjs.MediaTechController.prototype.manualTimeUpdatesOn = function(){ 6567 var player = this.player_; 6568 6569 this.manualTimeUpdates = true; 6570 6571 this.on(player, 'play', this.trackCurrentTime); 6572 this.on(player, 'pause', this.stopTrackingCurrentTime); 6573 // timeupdate is also called by .currentTime whenever current time is set 6574 6575 // Watch for native timeupdate event 6576 this.one('timeupdate', function(){
6577 // Update known progress support for this playback technology 6578 this['featuresTimeupdateEvents'] = true; 6579 // Turn off manual progress tracking 6580 this.manualTimeUpdatesOff(); 6581 }); 6582}; 6583 6584vjs.MediaTechController.prototype.manualTimeUpdatesOff = function(){ 6585 this.manualTimeUpdates = false; 6586 this.stopTrackingCurrentTime(); 6587 this.off('play', this.trackCurrentTime); 6588 this.off('pause', this.stopTrackingCurrentTime); 6589}; 6590 6591vjs.MediaTechController.prototype.trackCurrentTime = function(){ 6592 if (this.currentTimeInterval) { this.stopTrackingCurrentTime(); } 6593 this.currentTimeInterval = this.setInterval(function(){ 6594 this.player().trigger('timeupdate'); 6595 }, 250); // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 6596}; 6597 6598// Turn off play progress tracking (when paused or dragging) 6599vjs.MediaTechController.prototype.stopTrackingCurrentTime = function(){ 6600 this.clearInterval(this.currentTimeInterval); 6601 6602 // #1002 - if the video ends right before the next timeupdate would happen, 6603 // the progress bar won't make it all the way to the end 6604 this.player().trigger('timeupdate'); 6605}; 6606 6607vjs.MediaTechController.prototype.dispose = function() { 6608 // Turn off any manual progress or timeupdate tracking 6609 if (this.manualProgress) { this.manualProgressOff(); } 6610 6611 if (this.manualTimeUpdates) { this.manualTimeUpdatesOff(); } 6612 6613 vjs.Component.prototype.dispose.call(this); 6614}; 6615 6616vjs.MediaTechController.prototype.setCurrentTime = function() { 6617 // improve the accuracy of manual timeupdates 6618 if (this.manualTimeUpdates) { this.player().trigger('timeupdate'); } 6619}; 6620 6621/** 6622 * Provide a default setPoster method for techs 6623 * 6624 * Poster support for techs should be optional, so we don't want techs to 6625 * break if they don't have a way to set a poster. 6626 */ 6627vjs.MediaTechController.prototype.setPoster = function(){}; 6628 6629vjs.MediaTechController.prototype['featuresVolumeControl'] = true; 6630 6631// Resizing plugins using request fullscreen reloads the plugin 6632vjs.MediaTechController.prototype['featuresFullscreenResize'] = false; 6633vjs.MediaTechController.prototype['featuresPlaybackRate'] = false; 6634 6635// Optional events that we can manually mimic with timers 6636// currently not triggered by video-js-swf 6637vjs.MediaTechController.prototype['featuresProgressEvents'] = false; 6638vjs.MediaTechController.prototype['featuresTimeupdateEvents'] = false; 6639 6640/** 6641 * A functional mixin for techs that want to use the Source Handler pattern. 6642 * 6643 * ##### EXAMPLE: 6644 * 6645 * videojs.MediaTechController.withSourceHandlers.call(MyTech); 6646 * 6647 */ 6648vjs.MediaTechController.withSourceHandlers = function(Tech){ 6649 /** 6650 * Register a source handler 6651 * Source handlers are scripts for handling specific formats. 6652 * The source handler pattern is used for adaptive formats (HLS, DASH) that 6653 * manually load video data and feed it into a Source Buffer (Media Source Extensions) 6654 * @param {Function} handler The source handler 6655 * @param {Boolean} first Register it before any existing handlers 6656 */ 6657 Tech.registerSourceHandler = function(handler, index){ 6658 var handlers = Tech.sourceHandlers; 6659 6660 if (!handlers) { 6661 handlers = Tech.sourceHandlers = []; 6662 } 6663 6664 if (index === undefined) { 6665 // add to the end of the list 6666 index = handlers.length; 6667 } 6668 6669 handlers.splice(index, 0, handler); 6670 }; 6671 6672 /** 6673 * Return the first source handler that supports the source 6674 * TODO: Answer question: should 'probably' be prioritized over 'maybe' 6675 * @param {Object} source The source object 6676 * @returns {Object} The first source handler that supports the source 6677 * @returns {null} Null if no source handler is found 6678 */ 6679 Tech.selectSourceHandler = function(source){ 6680 var handlers = Tech.sourceHandlers || [], 6681 can; 6682 6683 for (var i = 0; i < handlers.length; i++) { 6684 can = handlers[i].canHandleSource(source); 6685 6686 if (can) { 6687 return handlers[i]; 6688 } 6689 } 6690 6691 return null; 6692 }; 6693 6694 /** 6695 * Check if the tech can support the given source 6696 * @param {Object} srcObj The source object 6697 * @return {String} 'probably', 'maybe', or '' (empty string) 6698 */ 6699 Tech.canPlaySource = function(srcObj){ 6700 var sh = Tech.selectSourceHandler(srcObj); 6701 6702 if (sh) { 6703 return sh.canHandleSource(srcObj); 6704 } 6705 6706 return ''; 6707 }; 6708 6709 /** 6710 * Create a function for setting the source using a source object 6711 * and source handlers. 6712 * Should never be called unless a source handler was found. 6713 * @param {Object} source A source object with src and type keys 6714 * @return {vjs.MediaTechController} self 6715 */ 6716 Tech.prototype.setSource = function(source){ 6717 var sh = Tech.selectSourceHandler(source); 6718 6719 // Dispose any existing source handler 6720 this.disposeSourceHandler(); 6721 this.off('dispose', this.disposeSourceHandler); 6722 6723 this.currentSource_ = source; 6724 this.sourceHandler_ = sh.handleSource(source, this); 6725 this.on('dispose', this.disposeSourceHandler); 6726 6727 return this; 6728 }; 6729 6730 /** 6731 * Clean up any existing source handler 6732 */ 6733 Tech.prototype.disposeSourceHandler = function(){ 6734 if (this.sourceHandler_ && this.sourceHandler_.dispose) { 6735 this.sourceHandler_.dispose(); 6736 } 6737 }; 6738 6739}; 6740/** 6741 * @fileoverview HTML5 Media Controller - Wrapper for HTML5 Media API 6742 */ 6743 6744/** 6745 * HTML5 Media Controller - Wrapper for HTML5 Media API 6746 * @param {vjs.Player|Object} player 6747 * @param {Object=} options 6748 * @param {Function=} ready 6749 * @constructor 6750 */ 6751vjs.Html5 = vjs.MediaTechController.extend({ 6752 /** @constructor */ 6753 init: function(player, options, ready){ 6754 vjs.MediaTechController.call(this, player, options, ready); 6755 6756 this.setupTriggers(); 6757 6758 var source = options['source']; 6759 6760 // Set the source if one is provided 6761 // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted) 6762 // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source 6763 // anyway so the error gets fired. 6764 if (source && (this.el_.currentSrc !== source.src || (player.tag && player.tag.initNetworkState_ === 3))) { 6765 this.setSource(source); 6766 } 6767 6768 // Determine if native controls should be used 6769 // Our goal should be to get the custom controls on mobile solid everywhere 6770 // so we can remove this all together. Right now this will block custom 6771 // controls on touch enabled laptops like the Chrome Pixel 6772 if (vjs.TOUCH_ENABLED && player.options()['nativeControlsForTouch'] === true) { 6773 this.useNativeControls(); 6774 } 6775 6776 // Chrome and Safari both have issues with autoplay. 6777 // In Safari (5.1.1), when we move the video element into the container div, autoplay doesn't work. 6778 // In Chrome (15), if you have autoplay + a poster + no controls, the video gets hidden (but audio plays) 6779 // This fixes both issues. Need to wait for API, so it updates displays correctly 6780 player.ready(function(){ 6781 if (this.tag && this.options_['autoplay'] && this.paused()) { 6782 delete this.tag['poster']; // Chrome Fix. Fixed in Chrome v16. 6783 this.play(); 6784 } 6785 }); 6786 6787 this.triggerReady(); 6788 } 6789}); 6790 6791vjs.Html5.prototype.dispose = function(){ 6792 vjs.Html5.disposeMediaElement(this.el_); 6793 vjs.MediaTechController.prototype.dispose.call(this); 6794}; 6795 6796vjs.Html5.prototype.createEl = function(){ 6797 var player = this.player_, 6798 // If possible, reuse original tag for HTML5 playback technology element 6799 el = player.tag, 6800 newEl, 6801 clone; 6802 6803 // Check if this browser supports moving the element into the box. 6804 // On the iPhone video will break if you move the element, 6805 // So we have to create a brand new element. 6806 if (!el || this['movingMediaElementInDOM'] === false) { 6807 6808 // If the original tag is still there, clone and remove it. 6809 if (el) { 6810 clone = el.cloneNode(false); 6811 vjs.Html5.disposeMediaElement(el); 6812 el = clone; 6813 player.tag = null; 6814 } else { 6815 el = vjs.createEl('video'); 6816 vjs.setElementAttributes(el, 6817 vjs.obj.merge(player.tagAttributes || {}, { 6818 id:player.id() + '_html5_api', 6819 'class':'vjs-tech' 6820 }) 6821 ); 6822 } 6823 // associate the player with the new tag 6824 el['player'] = player; 6825
6826 vjs.insertFirst(el, player.el()); 6827 } 6828 6829 // Update specific tag settings, in case they were overridden 6830 var settingsAttrs = ['autoplay','preload','loop','muted']; 6831 for (var i = settingsAttrs.length - 1; i >= 0; i--) { 6832 var attr = settingsAttrs[i]; 6833 var overwriteAttrs = {}; 6834 if (typeof player.options_[attr] !== 'undefined') { 6835 overwriteAttrs[attr] = player.options_[attr]; 6836 } 6837 vjs.setElementAttributes(el, overwriteAttrs); 6838 } 6839 6840 return el; 6841 // jenniisawesome = true; 6842}; 6843 6844// Make video events trigger player events 6845// May seem verbose here, but makes other APIs possible. 6846// Triggers removed using this.off when disposed 6847vjs.Html5.prototype.setupTriggers = function(){ 6848 for (var i = vjs.Html5.Events.length - 1; i >= 0; i--) { 6849 this.on(vjs.Html5.Events[i], this.eventHandler); 6850 } 6851}; 6852 6853vjs.Html5.prototype.eventHandler = function(evt){ 6854 // In the case of an error on the video element, set the error prop 6855 // on the player and let the player handle triggering the event. On 6856 // some platforms, error events fire that do not cause the error 6857 // property on the video element to be set. See #1465 for an example. 6858 if (evt.type == 'error' && this.error()) { 6859 this.player().error(this.error().code); 6860 6861 // in some cases we pass the event directly to the player 6862 } else { 6863 // No need for media events to bubble up. 6864 evt.bubbles = false; 6865 6866 this.player().trigger(evt); 6867 } 6868}; 6869 6870vjs.Html5.prototype.useNativeControls = function(){ 6871 var tech, player, controlsOn, controlsOff, cleanUp; 6872 6873 tech = this; 6874 player = this.player(); 6875 6876 // If the player controls are enabled turn on the native controls 6877 tech.setControls(player.controls()); 6878 6879 // Update the native controls when player controls state is updated 6880 controlsOn = function(){ 6881 tech.setControls(true); 6882 }; 6883 controlsOff = function(){ 6884 tech.setControls(false); 6885 }; 6886 player.on('controlsenabled', controlsOn); 6887 player.on('controlsdisabled', controlsOff); 6888 6889 // Clean up when not using native controls anymore 6890 cleanUp = function(){ 6891 player.off('controlsenabled', controlsOn); 6892 player.off('controlsdisabled', controlsOff); 6893 }; 6894 tech.on('dispose', cleanUp); 6895 player.on('usingcustomcontrols', cleanUp); 6896 6897 // Update the state of the player to using native controls 6898 player.usingNativeControls(true); 6899}; 6900 6901 6902vjs.Html5.prototype.play = function(){ this.el_.play(); }; 6903vjs.Html5.prototype.pause = function(){ this.el_.pause(); }; 6904vjs.Html5.prototype.paused = function(){ return this.el_.paused; }; 6905 6906vjs.Html5.prototype.currentTime = function(){ return this.el_.currentTime; }; 6907vjs.Html5.prototype.setCurrentTime = function(seconds){ 6908 try { 6909 this.el_.currentTime = seconds; 6910 } catch(e) { 6911 vjs.log(e, 'Video is not ready. (Video.js)'); 6912 // this.warning(VideoJS.warnings.videoNotReady); 6913 } 6914}; 6915 6916vjs.Html5.prototype.duration = function(){ return this.el_.duration || 0; }; 6917vjs.Html5.prototype.buffered = function(){ return this.el_.buffered; }; 6918 6919vjs.Html5.prototype.volume = function(){ return this.el_.volume; }; 6920vjs.Html5.prototype.setVolume = function(percentAsDecimal){ this.el_.volume = percentAsDecimal; }; 6921vjs.Html5.prototype.muted = function(){ return this.el_.muted; }; 6922vjs.Html5.prototype.setMuted = function(muted){ this.el_.muted = muted; }; 6923 6924vjs.Html5.prototype.width = function(){ return this.el_.offsetWidth; }; 6925vjs.Html5.prototype.height = function(){ return this.el_.offsetHeight; }; 6926 6927vjs.Html5.prototype.supportsFullScreen = function(){ 6928 if (typeof this.el_.webkitEnterFullScreen == 'function') { 6929 6930 // Seems to be broken in Chromium/Chrome && Safari in Leopard 6931 if (/Android/.test(vjs.USER_AGENT) || !/Chrome|Mac OS X 10.5/.test(vjs.USER_AGENT)) { 6932 return true; 6933 } 6934 } 6935 return false; 6936}; 6937 6938vjs.Html5.prototype.enterFullScreen = function(){ 6939 var video = this.el_; 6940 6941 if ('webkitDisplayingFullscreen' in video) { 6942 this.one('webkitbeginfullscreen', function() { 6943 this.player_.isFullscreen(true); 6944 6945 this.one('webkitendfullscreen', function() { 6946 this.player_.isFullscreen(false); 6947 this.player_.trigger('fullscreenchange'); 6948 }); 6949 6950 this.player_.trigger('fullscreenchange'); 6951 }); 6952 } 6953 6954 if (video.paused && video.networkState <= video.HAVE_METADATA) { 6955 // attempt to prime the video element for programmatic access 6956 // this isn't necessary on the desktop but shouldn't hurt 6957 this.el_.play(); 6958 6959 // playing and pausing synchronously during the transition to fullscreen 6960 // can get iOS ~6.1 devices into a play/pause loop 6961 this.setTimeout(function(){ 6962 video.pause(); 6963 video.webkitEnterFullScreen(); 6964 }, 0); 6965 } else { 6966 video.webkitEnterFullScreen(); 6967 } 6968}; 6969 6970vjs.Html5.prototype.exitFullScreen = function(){ 6971 this.el_.webkitExitFullScreen(); 6972}; 6973 6974 6975vjs.Html5.prototype.src = function(src) { 6976 if (src === undefined) { 6977 return this.el_.src; 6978 } else { 6979 // Setting src through `src` instead of `setSrc` will be deprecated 6980 this.setSrc(src); 6981 } 6982}; 6983 6984vjs.Html5.prototype.setSrc = function(src) { 6985 this.el_.src = src; 6986}; 6987 6988vjs.Html5.prototype.load = function(){ this.el_.load(); }; 6989vjs.Html5.prototype.currentSrc = function(){ return this.el_.currentSrc; }; 6990 6991vjs.Html5.prototype.poster = function(){ return this.el_.poster; }; 6992vjs.Html5.prototype.setPoster = function(val){ this.el_.poster = val; }; 6993 6994vjs.Html5.prototype.preload = function(){ return this.el_.preload; }; 6995vjs.Html5.prototype.setPreload = function(val){ this.el_.preload = val; }; 6996 6997vjs.Html5.prototype.autoplay = function(){ return this.el_.autoplay; }; 6998vjs.Html5.prototype.setAutoplay = function(val){ this.el_.autoplay = val; }; 6999 7000vjs.Html5.prototype.controls = function(){ return this.el_.controls; }; 7001vjs.Html5.prototype.setControls = function(val){ this.el_.controls = !!val; }; 7002 7003vjs.Html5.prototype.loop = function(){ return this.el_.loop; }; 7004vjs.Html5.prototype.setLoop = function(val){ this.el_.loop = val; }; 7005 7006vjs.Html5.prototype.error = function(){ return this.el_.error; }; 7007vjs.Html5.prototype.seeking = function(){ return this.el_.seeking; }; 7008vjs.Html5.prototype.ended = function(){ return this.el_.ended; }; 7009vjs.Html5.prototype.defaultMuted = function(){ return this.el_.defaultMuted; }; 7010 7011vjs.Html5.prototype.playbackRate = function(){ return this.el_.playbackRate; }; 7012vjs.Html5.prototype.setPlaybackRate = function(val){ this.el_.playbackRate = val; }; 7013 7014vjs.Html5.prototype.networkState = function(){ return this.el_.networkState; }; 7015 7016 7017/** 7018 * Check if HTML5 video is supported by this browser/device 7019 * @return {Boolean} 7020 */ 7021vjs.Html5.isSupported = function(){ 7022 // IE9 with no Media Player is a LIAR! (#984) 7023 try { 7024 vjs.TEST_VID['volume'] = 0.5; 7025 } catch (e) { 7026 return false;
7027 } 7028 7029 return !!vjs.TEST_VID.canPlayType; 7030}; 7031 7032// Add Source Handler pattern functions to this tech 7033vjs.MediaTechController.withSourceHandlers(vjs.Html5); 7034 7035/** 7036 * The default native source handler. 7037 * This simply passes the source to the video element. Nothing fancy. 7038 * @param {Object} source The source object 7039 * @param {vjs.Html5} tech The instance of the HTML5 tech 7040 */ 7041vjs.Html5.nativeSourceHandler = {}; 7042 7043/** 7044 * Check if the video element can handle the source natively 7045 * @param {Object} source The source object 7046 * @return {String} 'probably', 'maybe', or '' (empty string) 7047 */ 7048vjs.Html5.nativeSourceHandler.canHandleSource = function(source){ 7049 var ext; 7050 7051 function canPlayType(type){ 7052 // IE9 on Windows 7 without MediaPlayer throws an error here 7053 // https://github.com/videojs/video.js/issues/519 7054 try { 7055 return !!vjs.TEST_VID.canPlayType(type); 7056 } catch(e) { 7057 return ''; 7058 } 7059 } 7060 7061 // If a type was provided we should rely on that 7062 if (source.type) { 7063 return canPlayType(source.type); 7064 } else { 7065 // If no type, fall back to checking 'video/[EXTENSION]' 7066 ext = source.src.match(/\.([^\/\?]+)(\?[^\/]+)?$/i)[1]; 7067 return canPlayType('video/'+ext); 7068 } 7069}; 7070 7071/** 7072 * Pass the source to the video element 7073 * Adaptive source handlers will have more complicated workflows before passing 7074 * video data to the video element 7075 * @param {Object} source The source object 7076 * @param {vjs.Html5} tech The instance of the Html5 tech 7077 */ 7078vjs.Html5.nativeSourceHandler.handleSource = function(source, tech){ 7079 tech.setSrc(source.src); 7080}; 7081 7082/** 7083 * Clean up the source handler when disposing the player or switching sources.. 7084 * (no cleanup is needed when supporting the format natively) 7085 */ 7086vjs.Html5.nativeSourceHandler.dispose = function(){}; 7087 7088// Register the native source handler 7089vjs.Html5.registerSourceHandler(vjs.Html5.nativeSourceHandler); 7090 7091/** 7092 * Check if the volume can be changed in this browser/device. 7093 * Volume cannot be changed in a lot of mobile devices. 7094 * Specifically, it can't be changed from 1 on iOS. 7095 * @return {Boolean} 7096 */ 7097vjs.Html5.canControlVolume = function(){ 7098 var volume = vjs.TEST_VID.volume; 7099 vjs.TEST_VID.volume = (volume / 2) + 0.1; 7100 return volume !== vjs.TEST_VID.volume; 7101}; 7102 7103/** 7104 * Check if playbackRate is supported in this browser/device. 7105 * @return {[type]} [description] 7106 */ 7107vjs.Html5.canControlPlaybackRate = function(){ 7108 var playbackRate = vjs.TEST_VID.playbackRate; 7109 vjs.TEST_VID.playbackRate = (playbackRate / 2) + 0.1; 7110 return playbackRate !== vjs.TEST_VID.playbackRate; 7111}; 7112 7113/** 7114 * Set the tech's volume control support status 7115 * @type {Boolean} 7116 */ 7117vjs.Html5.prototype['featuresVolumeControl'] = vjs.Html5.canControlVolume(); 7118 7119/** 7120 * Set the tech's playbackRate support status 7121 * @type {Boolean} 7122 */ 7123vjs.Html5.prototype['featuresPlaybackRate'] = vjs.Html5.canControlPlaybackRate(); 7124 7125/** 7126 * Set the tech's status on moving the video element. 7127 * In iOS, if you move a video element in the DOM, it breaks video playback. 7128 * @type {Boolean} 7129 */ 7130vjs.Html5.prototype['movingMediaElementInDOM'] = !vjs.IS_IOS; 7131 7132/** 7133 * Set the the tech's fullscreen resize support status. 7134 * HTML video is able to automatically resize when going to fullscreen. 7135 * (No longer appears to be used. Can probably be removed.) 7136 */ 7137vjs.Html5.prototype['featuresFullscreenResize'] = true; 7138 7139/** 7140 * Set the tech's progress event support status 7141 * (this disables the manual progress events of the MediaTechController) 7142 */ 7143vjs.Html5.prototype['featuresProgressEvents'] = true; 7144 7145// HTML5 Feature detection and Device Fixes --------------------------------- // 7146(function() { 7147 var canPlayType, 7148 mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i, 7149 mp4RE = /^video\/mp4/i; 7150 7151 vjs.Html5.patchCanPlayType = function() { 7152 // Android 4.0 and above can play HLS to some extent but it reports being unable to do so 7153 if (vjs.ANDROID_VERSION >= 4.0) { 7154 if (!canPlayType) { 7155 canPlayType = vjs.TEST_VID.constructor.prototype.canPlayType; 7156 } 7157 7158 vjs.TEST_VID.constructor.prototype.canPlayType = function(type) { 7159 if (type && mpegurlRE.test(type)) { 7160 return 'maybe'; 7161 } 7162 return canPlayType.call(this, type); 7163 }; 7164 } 7165 7166 // Override Android 2.2 and less canPlayType method which is broken 7167 if (vjs.IS_OLD_ANDROID) { 7168 if (!canPlayType) { 7169 canPlayType = vjs.TEST_VID.constructor.prototype.canPlayType; 7170 } 7171 7172 vjs.TEST_VID.constructor.prototype.canPlayType = function(type){ 7173 if (type && mp4RE.test(type)) { 7174 return 'maybe'; 7175 } 7176 return canPlayType.call(this, type); 7177 }; 7178 } 7179 }; 7180 7181 vjs.Html5.unpatchCanPlayType = function() { 7182 var r = vjs.TEST_VID.constructor.prototype.canPlayType; 7183 vjs.TEST_VID.constructor.prototype.canPlayType = canPlayType; 7184 canPlayType = null; 7185 return r; 7186 }; 7187 7188 // by default, patch the video element 7189 vjs.Html5.patchCanPlayType(); 7190})(); 7191 7192// List of all HTML5 events (various uses). 7193vjs.Html5.Events = 'loadstart,suspend,abort,error,emptied,stalled,loadedmetadata,loadeddata,canplay,canplaythrough,playing,waiting,seeking,seeked,ended,durationchange,timeupdate,progress,play,pause,ratechange,volumechange'.split(','); 7194 7195vjs.Html5.disposeMediaElement = function(el){ 7196 if (!el) { return; } 7197 7198 el['player'] = null; 7199 7200 if (el.parentNode) { 7201 el.parentNode.removeChild(el); 7202 } 7203 7204 // remove any child track or source nodes to prevent their loading 7205 while(el.hasChildNodes()) { 7206 el.removeChild(el.firstChild); 7207 } 7208 7209 // remove any src reference. not setting `src=''` because that causes a warning 7210 // in firefox 7211 el.removeAttribute('src'); 7212 7213 // force the media element to update its loading state by calling load() 7214 // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793) 7215 if (typeof el.load === 'function') {
7216 // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) 7217 (function() { 7218 try { 7219 el.load(); 7220 } catch (e) { 7221 // not supported 7222 } 7223 })(); 7224 } 7225}; 7226/** 7227 * @fileoverview VideoJS-SWF - Custom Flash Player with HTML5-ish API 7228 * https://github.com/zencoder/video-js-swf 7229 * Not using setupTriggers. Using global onEvent func to distribute events 7230 */ 7231 7232/** 7233 * Flash Media Controller - Wrapper for fallback SWF API 7234 * 7235 * @param {vjs.Player} player 7236 * @param {Object=} options 7237 * @param {Function=} ready 7238 * @constructor 7239 */ 7240vjs.Flash = vjs.MediaTechController.extend({ 7241 /** @constructor */ 7242 init: function(player, options, ready){ 7243 vjs.MediaTechController.call(this, player, options, ready); 7244 7245 var source = options['source'], 7246 7247 // Which element to embed in 7248 parentEl = options['parentEl'], 7249 7250 // Create a temporary element to be replaced by swf object 7251 placeHolder = this.el_ = vjs.createEl('div', { id: player.id() + '_temp_flash' }), 7252 7253 // Generate ID for swf object 7254 objId = player.id()+'_flash_api', 7255 7256 // Store player options in local var for optimization 7257 // TODO: switch to using player methods instead of options 7258 // e.g. player.autoplay(); 7259 playerOptions = player.options_, 7260 7261 // Merge default flashvars with ones passed in to init 7262 flashVars = vjs.obj.merge({ 7263 7264 // SWF Callback Functions 7265 'readyFunction': 'videojs.Flash.onReady', 7266 'eventProxyFunction': 'videojs.Flash.onEvent', 7267 'errorEventProxyFunction': 'videojs.Flash.onError', 7268 7269 // Player Settings 7270 'autoplay': playerOptions.autoplay, 7271 'preload': playerOptions.preload, 7272 'loop': playerOptions.loop, 7273 'muted': playerOptions.muted 7274 7275 }, options['flashVars']), 7276 7277 // Merge default parames with ones passed in 7278 params = vjs.obj.merge({ 7279 'wmode': 'opaque', // Opaque is needed to overlay controls, but can affect playback performance 7280 'bgcolor': '#000000' // Using bgcolor prevents a white flash when the object is loading 7281 }, options['params']), 7282 7283 // Merge default attributes with ones passed in 7284 attributes = vjs.obj.merge({ 7285 'id': objId, 7286 'name': objId, // Both ID and Name needed or swf to identify itself 7287 'class': 'vjs-tech' 7288 }, options['attributes']) 7289 ; 7290 7291 // If source was supplied pass as a flash var. 7292 if (source) { 7293 this.ready(function(){ 7294 this.setSource(source); 7295 }); 7296 } 7297 7298 // Add placeholder to player div 7299 vjs.insertFirst(placeHolder, parentEl); 7300 7301 // Having issues with Flash reloading on certain page actions (hide/resize/fullscreen) in certain browsers 7302 // This allows resetting the playhead when we catch the reload 7303 if (options['startTime']) { 7304 this.ready(function(){ 7305 this.load(); 7306 this.play(); 7307 this['currentTime'](options['startTime']); 7308 }); 7309 } 7310 7311 // firefox doesn't bubble mousemove events to parent. videojs/video-js-swf#37 7312 // bugzilla bug: https://bugzilla.mozilla.org/show_bug.cgi?id=836786 7313 if (vjs.IS_FIREFOX) { 7314 this.ready(function(){ 7315 this.on('mousemove', function(){ 7316 // since it's a custom event, don't bubble higher than the player 7317 this.player().trigger({ 'type':'mousemove', 'bubbles': false }); 7318 }); 7319 }); 7320 } 7321 7322 // native click events on the SWF aren't triggered on IE11, Win8.1RT 7323 // use stageclick events triggered from inside the SWF instead 7324 player.on('stageclick', player.reportUserActivity); 7325 7326 this.el_ = vjs.Flash.embed(options['swf'], placeHolder, flashVars, params, attributes); 7327 } 7328}); 7329 7330vjs.Flash.prototype.dispose = function(){ 7331 vjs.MediaTechController.prototype.dispose.call(this); 7332}; 7333 7334vjs.Flash.prototype.play = function(){ 7335 this.el_.vjs_play(); 7336}; 7337 7338vjs.Flash.prototype.pause = function(){ 7339 this.el_.vjs_pause(); 7340}; 7341 7342vjs.Flash.prototype.src = function(src){ 7343 if (src === undefined) { 7344 return this['currentSrc'](); 7345 } 7346 7347 // Setting src through `src` not `setSrc` will be deprecated 7348 return this.setSrc(src); 7349}; 7350
7351vjs.Flash.prototype.setSrc = function(src){ 7352 // Make sure source URL is absolute. 7353 src = vjs.getAbsoluteURL(src); 7354 this.el_.vjs_src(src); 7355 7356 // Currently the SWF doesn't autoplay if you load a source later. 7357 // e.g. Load player w/ no source, wait 2s, set src. 7358 if (this.player_.autoplay()) { 7359 var tech = this; 7360 this.setTimeout(function(){ tech.play(); }, 0); 7361 } 7362}; 7363 7364vjs.Flash.prototype['setCurrentTime'] = function(time){ 7365 this.lastSeekTarget_ = time; 7366 this.el_.vjs_setProperty('currentTime', time); 7367 vjs.MediaTechController.prototype.setCurrentTime.call(this); 7368}; 7369 7370vjs.Flash.prototype['currentTime'] = function(time){ 7371 // when seeking make the reported time keep up with the requested time 7372 // by reading the time we're seeking to 7373 if (this.seeking()) { 7374 return this.lastSeekTarget_ || 0; 7375 } 7376 return this.el_.vjs_getProperty('currentTime'); 7377}; 7378 7379vjs.Flash.prototype['currentSrc'] = function(){ 7380 if (this.currentSource_) { 7381 return this.currentSource_.src; 7382 } else { 7383 return this.el_.vjs_getProperty('currentSrc'); 7384 } 7385}; 7386 7387vjs.Flash.prototype.load = function(){ 7388 this.el_.vjs_load(); 7389}; 7390 7391vjs.Flash.prototype.poster = function(){ 7392 this.el_.vjs_getProperty('poster'); 7393}; 7394vjs.Flash.prototype['setPoster'] = function(){ 7395 // poster images are not handled by the Flash tech so make this a no-op 7396}; 7397 7398vjs.Flash.prototype.buffered = function(){ 7399 return vjs.createTimeRange(0, this.el_.vjs_getProperty('buffered')); 7400}; 7401 7402vjs.Flash.prototype.supportsFullScreen = function(){ 7403 return false; // Flash does not allow fullscreen through javascript 7404}; 7405 7406vjs.Flash.prototype.enterFullScreen = function(){ 7407 return false; 7408}; 7409 7410(function(){ 7411 // Create setters and getters for attributes 7412 var api = vjs.Flash.prototype, 7413 readWrite = 'rtmpConnection,rtmpStream,preload,defaultPlaybackRate,playbackRate,autoplay,loop,mediaGroup,controller,controls,volume,muted,defaultMuted'.split(','), 7414 readOnly = 'error,networkState,readyState,seeking,initialTime,duration,startOffsetTime,paused,played,seekable,ended,videoTracks,audioTracks,videoWidth,videoHeight,textTracks'.split(','), 7415 // Overridden: buffered, currentTime, currentSrc 7416 i; 7417 7418 function createSetter(attr){ 7419 var attrUpper = attr.charAt(0).toUpperCase() + attr.slice(1); 7420 api['set'+attrUpper] = function(val){ return this.el_.vjs_setProperty(attr, val); }; 7421 } 7422 function createGetter(attr) { 7423 api[attr] = function(){ return this.el_.vjs_getProperty(attr); }; 7424 } 7425 7426 // Create getter and setters for all read/write attributes 7427 for (i = 0; i < readWrite.length; i++) { 7428 createGetter(readWrite[i]); 7429 createSetter(readWrite[i]); 7430 } 7431 7432 // Create getters for read-only attributes 7433 for (i = 0; i < readOnly.length; i++) { 7434 createGetter(readOnly[i]); 7435 } 7436})(); 7437 7438/* Flash Support Testing -------------------------------------------------------- */ 7439 7440vjs.Flash.isSupported = function(){ 7441 return vjs.Flash.version()[0] >= 10; 7442 // return swfobject.hasFlashPlayerVersion('10'); 7443}; 7444 7445// Add Source Handler pattern functions to this tech 7446vjs.MediaTechController.withSourceHandlers(vjs.Flash); 7447 7448/** 7449 * The default native source handler. 7450 * This simply passes the source to the video element. Nothing fancy. 7451 * @param {Object} source The source object 7452 * @param {vjs.Flash} tech The instance of the Flash tech 7453 */ 7454vjs.Flash.nativeSourceHandler = {}; 7455 7456/** 7457 * Check Flash can handle the source natively 7458 * @param {Object} source The source object 7459 * @return {String} 'probably', 'maybe', or '' (empty string) 7460 */ 7461vjs.Flash.nativeSourceHandler.canHandleSource = function(source){ 7462 var type; 7463 7464 if (!source.type) { 7465 return ''; 7466 } 7467 7468 // Strip code information from the type because we don't get that specific 7469 type = source.type.replace(/;.*/,'').toLowerCase(); 7470 7471 if (type in vjs.Flash.formats) { 7472 return 'maybe'; 7473 } 7474 7475 return ''; 7476}; 7477 7478/** 7479 * Pass the source to the flash object 7480 * Adaptive source handlers will have more complicated workflows before passing 7481 * video data to the video element 7482 * @param {Object} source The source object 7483 * @param {vjs.Flash} tech The instance of the Flash tech 7484 */
7485vjs.Flash.nativeSourceHandler.handleSource = function(source, tech){ 7486 tech.setSrc(source.src); 7487}; 7488 7489/** 7490 * Clean up the source handler when disposing the player or switching sources.. 7491 * (no cleanup is needed when supporting the format natively) 7492 */ 7493vjs.Flash.nativeSourceHandler.dispose = function(){}; 7494 7495// Register the native source handler 7496vjs.Flash.registerSourceHandler(vjs.Flash.nativeSourceHandler); 7497 7498vjs.Flash.formats = { 7499 'video/flv': 'FLV', 7500 'video/x-flv': 'FLV', 7501 'video/mp4': 'MP4', 7502 'video/m4v': 'MP4' 7503}; 7504 7505vjs.Flash['onReady'] = function(currSwf){ 7506 var el, player; 7507 7508 el = vjs.el(currSwf); 7509 7510 // get player from the player div property 7511 player = el && el.parentNode && el.parentNode['player']; 7512 7513 // if there is no el or player then the tech has been disposed 7514 // and the tech element was removed from the player div 7515 if (player) { 7516 // reference player on tech element 7517 el['player'] = player; 7518 // check that the flash object is really ready 7519 vjs.Flash['checkReady'](player.tech); 7520 } 7521}; 7522 7523// The SWF isn't always ready when it says it is. Sometimes the API functions still need to be added to the object. 7524// If it's not ready, we set a timeout to check again shortly. 7525vjs.Flash['checkReady'] = function(tech){ 7526 // stop worrying if the tech has been disposed 7527 if (!tech.el()) { 7528 return; 7529 } 7530 7531 // check if API property exists 7532 if (tech.el().vjs_getProperty) { 7533 // tell tech it's ready 7534 tech.triggerReady(); 7535 } else { 7536 // wait longer 7537 this.setTimeout(function(){ 7538 vjs.Flash['checkReady'](tech); 7539 }, 50); 7540 } 7541}; 7542 7543// Trigger events from the swf on the player
7544vjs.Flash['onEvent'] = function(swfID, eventName){ 7545 var player = vjs.el(swfID)['player']; 7546 player.trigger(eventName); 7547}; 7548 7549// Log errors from the swf 7550vjs.Flash['onError'] = function(swfID, err){ 7551 var player = vjs.el(swfID)['player']; 7552 var msg = 'FLASH: '+err; 7553 7554 if (err == 'srcnotfound') { 7555 player.error({ code: 4, message: msg }); 7556 7557 // errors we haven't categorized into the media errors 7558 } else { 7559 player.error(msg); 7560 } 7561}; 7562 7563// Flash Version Check 7564vjs.Flash.version = function(){ 7565 var version = '0,0,0'; 7566 7567 // IE 7568 try { 7569 version = new window.ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version').replace(/\D+/g, ',').match(/^,?(.+),?$/)[1]; 7570 7571 // other browsers 7572 } catch(e) { 7573 try { 7574 if (navigator.mimeTypes['application/x-shockwave-flash'].enabledPlugin){ 7575 version = (navigator.plugins['Shockwave Flash 2.0'] || navigator.plugins['Shockwave Flash']).description.replace(/\D+/g, ',').match(/^,?(.+),?$/)[1]; 7576 } 7577 } catch(err) {} 7578 } 7579 return version.split(','); 7580}; 7581 7582// Flash embedding method. Only used in non-iframe mode 7583vjs.Flash.embed = function(swf, placeHolder, flashVars, params, attributes){ 7584 var code = vjs.Flash.getEmbedCode(swf, flashVars, params, attributes), 7585 7586 // Get element by embedding code and retrieving created element 7587 obj = vjs.createEl('div', { innerHTML: code }).childNodes[0], 7588 7589 par = placeHolder.parentNode 7590 ; 7591 7592 placeHolder.parentNode.replaceChild(obj, placeHolder); 7593 7594 // IE6 seems to have an issue where it won't initialize the swf object after injecting it. 7595 // This is a dumb fix 7596 var newObj = par.childNodes[0]; 7597 setTimeout(function(){ 7598 newObj.style.display = 'block'; 7599 }, 1000); 7600 7601 return obj; 7602 7603}; 7604 7605vjs.Flash.getEmbedCode = function(swf, flashVars, params, attributes){ 7606 7607 var objTag = '<object type="application/x-shockwave-flash" ', 7608 flashVarsString = '', 7609 paramsString = '', 7610 attrsString = ''; 7611 7612 // Convert flash vars to string 7613 if (flashVars) { 7614 vjs.obj.each(flashVars, function(key, val){ 7615 flashVarsString += (key + '=' + val + '&'); 7616 }); 7617 } 7618 7619 // Add swf, flashVars, and other default params 7620 params = vjs.obj.merge({ 7621 'movie': swf, 7622 'flashvars': flashVarsString, 7623 'allowScriptAccess': 'always', // Required to talk to swf 7624 'allowNetworking': 'all' // All should be default, but having security issues. 7625 }, params); 7626 7627 // Create param tags string 7628 vjs.obj.each(params, function(key, val){ 7629 paramsString += '<param name="'+key+'" value="'+val+'" />'; 7630 }); 7631 7632 attributes = vjs.obj.merge({ 7633 // Add swf to attributes (need both for IE and Others to work) 7634 'data': swf, 7635 7636 // Default to 100% width/height 7637 'width': '100%', 7638 'height': '100%' 7639 7640 }, attributes); 7641 7642 // Create Attributes string 7643 vjs.obj.each(attributes, function(key, val){ 7644 attrsString += (key + '="' + val + '" '); 7645 }); 7646 7647 return objTag + attrsString + '>' + paramsString + '</object>'; 7648}; 7649vjs.Flash.streamingFormats = { 7650 'rtmp/mp4': 'MP4', 7651 'rtmp/flv': 'FLV' 7652}; 7653 7654vjs.Flash.streamFromParts = function(connection, stream) { 7655 return connection + '&' + stream; 7656}; 7657 7658vjs.Flash.streamToParts = function(src) { 7659 var parts = { 7660 connection: '', 7661 stream: '' 7662 }; 7663 7664 if (! src) { 7665 return parts; 7666 } 7667 7668 // Look for the normal URL separator we expect, '&'. 7669 // If found, we split the URL into two pieces around the 7670 // first '&'. 7671 var connEnd = src.indexOf('&'); 7672 var streamBegin; 7673 if (connEnd !== -1) { 7674 streamBegin = connEnd + 1; 7675 } 7676 else { 7677 // If there's not a '&', we use the last '/' as the delimiter. 7678 connEnd = streamBegin = src.lastIndexOf('/') + 1; 7679 if (connEnd === 0) { 7680 // really, there's not a '/'? 7681 connEnd = streamBegin = src.length; 7682 } 7683 } 7684 parts.connection = src.substring(0, connEnd); 7685 parts.stream = src.substring(streamBegin, src.length); 7686 7687 return parts; 7688}; 7689 7690vjs.Flash.isStreamingType = function(srcType) { 7691 return srcType in vjs.Flash.streamingFormats; 7692}; 7693 7694// RTMP has four variations, any string starting 7695// with one of these protocols should be valid 7696vjs.Flash.RTMP_RE = /^rtmp[set]?:\/\//i; 7697
7698vjs.Flash.isStreamingSrc = function(src) { 7699 return vjs.Flash.RTMP_RE.test(src); 7700}; 7701 7702/** 7703 * A source handler for RTMP urls 7704 * @type {Object} 7705 */ 7706vjs.Flash.rtmpSourceHandler = {}; 7707 7708/** 7709 * Check Flash can handle the source natively 7710 * @param {Object} source The source object 7711 * @return {String} 'probably', 'maybe', or '' (empty string) 7712 */ 7713vjs.Flash.rtmpSourceHandler.canHandleSource = function(source){ 7714 if (vjs.Flash.isStreamingType(source.type) || vjs.Flash.isStreamingSrc(source.src)) { 7715 return 'maybe'; 7716 } 7717 7718 return ''; 7719}; 7720 7721/** 7722 * Pass the source to the flash object 7723 * Adaptive source handlers will have more complicated workflows before passing 7724 * video data to the video element 7725 * @param {Object} source The source object 7726 * @param {vjs.Flash} tech The instance of the Flash tech 7727 */ 7728vjs.Flash.rtmpSourceHandler.handleSource = function(source, tech){ 7729 var srcParts = vjs.Flash.streamToParts(source.src); 7730 7731 tech.setRtmpConnection(srcParts.connection); 7732 tech.setRtmpStream(srcParts.stream); 7733}; 7734 7735// Register the native source handler 7736vjs.Flash.registerSourceHandler(vjs.Flash.rtmpSourceHandler); 7737/** 7738 * The Media Loader is the component that decides which playback technology to load 7739 * when the player is initialized. 7740 * 7741 * @constructor 7742 */ 7743vjs.MediaLoader = vjs.Component.extend({ 7744 /** @constructor */ 7745 init: function(player, options, ready){ 7746 vjs.Component.call(this, player, options, ready); 7747 7748 // If there are no sources when the player is initialized, 7749 // load the first supported playback technology. 7750 if (!player.options_['sources'] || player.options_['sources'].length === 0) { 7751 for (var i=0,j=player.options_['techOrder']; i<j.length; i++) { 7752 var techName = vjs.capitalize(j[i]), 7753 tech = window['videojs'][techName]; 7754 7755 // Check if the browser supports this technology 7756 if (tech && tech.isSupported()) { 7757 player.loadTech(techName); 7758 break; 7759 } 7760 } 7761 } else { 7762 // // Loop through playback technologies (HTML5, Flash) and check for support. 7763 // // Then load the best source. 7764 // // A few assumptions here: 7765 // // All playback technologies respect preload false. 7766 player.src(player.options_['sources']); 7767 } 7768 } 7769}); 7770/** 7771 * @fileoverview Text Tracks 7772 * Text tracks are tracks of timed text events. 7773 * Captions - text displayed over the video for the hearing impaired 7774 * Subtitles - text displayed over the video for those who don't understand language in the video 7775 * Chapters - text displayed in a menu allowing the user to jump to particular points (chapters) in the video 7776 * Descriptions (not supported yet) - audio descriptions that are read back to the user by a screen reading device 7777 */ 7778 7779// Player Additions - Functions add to the player object for easier access to tracks 7780 7781/** 7782 * List of associated text tracks 7783 * @type {Array} 7784 * @private 7785 */ 7786vjs.Player.prototype.textTracks_; 7787 7788/** 7789 * Get an array of associated text tracks. captions, subtitles, chapters, descriptions 7790 * http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks 7791 * @return {Array} Array of track objects 7792 * @private 7793 */ 7794vjs.Player.prototype.textTracks = function(){ 7795 this.textTracks_ = this.textTracks_ || []; 7796 return this.textTracks_; 7797}; 7798 7799/** 7800 * Add a text track 7801 * In addition to the W3C settings we allow adding additional info through options. 7802 * http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack 7803 * @param {String} kind Captions, subtitles, chapters, descriptions, or metadata 7804 * @param {String=} label Optional label 7805 * @param {String=} language Optional language 7806 * @param {Object=} options Additional track options, like src 7807 * @private 7808 */ 7809vjs.Player.prototype.addTextTrack = function(kind, label, language, options){ 7810 var tracks = this.textTracks_ = this.textTracks_ || []; 7811 options = options || {}; 7812 7813 options['kind'] = kind; 7814 options['label'] = label; 7815 options['language'] = language; 7816 7817 // HTML5 Spec says default to subtitles.
7818 // Uppercase first letter to match class names 7819 var Kind = vjs.capitalize(kind || 'subtitles'); 7820 7821 // Create correct texttrack class. CaptionsTrack, etc. 7822 var track = new window['videojs'][Kind + 'Track'](this, options); 7823 7824 tracks.push(track); 7825 7826 // If track.dflt() is set, start showing immediately 7827 // TODO: Add a process to determine the best track to show for the specific kind 7828 // In case there are multiple defaulted tracks of the same kind 7829 // Or the user has a set preference of a specific language that should override the default 7830 // Note: The setTimeout is a workaround because with the html5 tech, the player is 'ready' 7831 // before it's child components (including the textTrackDisplay) have finished loading. 7832 if (track.dflt()) { 7833 this.ready(function(){ 7834 this.setTimeout(function(){ 7835 track.player().showTextTrack(track.id()); 7836 }, 0); 7837 }); 7838 } 7839 7840 return track; 7841}; 7842 7843/** 7844 * Add an array of text tracks. captions, subtitles, chapters, descriptions 7845 * Track objects will be stored in the player.textTracks() array 7846 * @param {Array} trackList Array of track elements or objects (fake track elements) 7847 * @private 7848 */ 7849vjs.Player.prototype.addTextTracks = function(trackList){ 7850 var trackObj; 7851 7852 for (var i = 0; i < trackList.length; i++) { 7853 trackObj = trackList[i]; 7854 this.addTextTrack(trackObj['kind'], trackObj['label'], trackObj['language'], trackObj); 7855 } 7856 7857 return this; 7858}; 7859 7860// Show a text track 7861// disableSameKind: disable all other tracks of the same kind. Value should be a track kind (captions, etc.) 7862vjs.Player.prototype.showTextTrack = function(id, disableSameKind){ 7863 var tracks = this.textTracks_, 7864 i = 0, 7865 j = tracks.length, 7866 track, showTrack, kind; 7867 7868 // Find Track with same ID 7869 for (;i<j;i++) { 7870 track = tracks[i]; 7871 if (track.id() === id) { 7872 track.show(); 7873 showTrack = track; 7874 7875 // Disable tracks of the same kind 7876 } else if (disableSameKind && track.kind() == disableSameKind && track.mode() > 0) { 7877 track.disable(); 7878 } 7879 } 7880 7881 // Get track kind from shown track or disableSameKind 7882 kind = (showTrack) ? showTrack.kind() : ((disableSameKind) ? disableSameKind : false); 7883 7884 // Trigger trackchange event, captionstrackchange, subtitlestrackchange, etc. 7885 if (kind) { 7886 this.trigger(kind+'trackchange'); 7887 } 7888 7889 return this; 7890}; 7891 7892/** 7893 * The base class for all text tracks 7894 * 7895 * Handles the parsing, hiding, and showing of text track cues 7896 * 7897 * @param {vjs.Player|Object} player 7898 * @param {Object=} options 7899 * @constructor 7900 */ 7901vjs.TextTrack = vjs.Component.extend({ 7902 /** @constructor */ 7903 init: function(player, options){ 7904 vjs.Component.call(this, player, options); 7905 7906 // Apply track info to track object 7907 // Options will often be a track element 7908 7909 // Build ID if one doesn't exist 7910 this.id_ = options['id'] || ('vjs_' + options['kind'] + '_' + options['language'] + '_' + vjs.guid++); 7911 this.src_ = options['src']; 7912 // 'default' is a reserved keyword in js so we use an abbreviated version 7913 this.dflt_ = options['default'] || options['dflt']; 7914 this.title_ = options['title']; 7915 this.language_ = options['srclang']; 7916 this.label_ = options['label']; 7917 this.cues_ = []; 7918 this.activeCues_ = []; 7919 this.readyState_ = 0; 7920 this.mode_ = 0; 7921 7922 player.on('dispose', vjs.bind(this, this.deactivate, this.id_)); 7923 } 7924}); 7925 7926/** 7927 * Track kind value. Captions, subtitles, etc. 7928 * @private 7929 */ 7930vjs.TextTrack.prototype.kind_; 7931 7932/** 7933 * Get the track kind value 7934 * @return {String} 7935 */ 7936vjs.TextTrack.prototype.kind = function(){ 7937 return this.kind_; 7938}; 7939 7940/** 7941 * Track src value 7942 * @private 7943 */ 7944vjs.TextTrack.prototype.src_; 7945 7946/** 7947 * Get the track src value 7948 * @return {String} 7949 */ 7950vjs.TextTrack.prototype.src = function(){ 7951 return this.src_; 7952}; 7953 7954/** 7955 * Track default value 7956 * If default is used, subtitles/captions to start showing 7957 * @private 7958 */ 7959vjs.TextTrack.prototype.dflt_; 7960 7961/** 7962 * Get the track default value. ('default' is a reserved keyword) 7963 * @return {Boolean} 7964 */ 7965vjs.TextTrack.prototype.dflt = function(){ 7966 return this.dflt_; 7967}; 7968 7969/** 7970 * Track title value 7971 * @private 7972 */ 7973vjs.TextTrack.prototype.title_; 7974 7975/** 7976 * Get the track title value 7977 * @return {String} 7978 */ 7979vjs.TextTrack.prototype.title = function(){ 7980 return this.title_; 7981}; 7982 7983/** 7984 * Language - two letter string to represent track language, e.g. 'en' for English 7985 * Spec def: readonly attribute DOMString language; 7986 * @private 7987 */ 7988vjs.TextTrack.prototype.language_; 7989 7990/** 7991 * Get the track language value 7992 * @return {String} 7993 */ 7994vjs.TextTrack.prototype.language = function(){ 7995 return this.language_; 7996}; 7997 7998/** 7999 * Track label e.g. 'English' 8000 * Spec def: readonly attribute DOMString label; 8001 * @private 8002 */ 8003vjs.TextTrack.prototype.label_; 8004 8005/** 8006 * Get the track label value 8007 * @return {String} 8008 */ 8009vjs.TextTrack.prototype.label = function(){ 8010 return this.label_; 8011}; 8012 8013/** 8014 * All cues of the track. Cues have a startTime, endTime, text, and other properties. 8015 * Spec def: readonly attribute TextTrackCueList cues; 8016 * @private 8017 */ 8018vjs.TextTrack.prototype.cues_; 8019 8020/** 8021 * Get the track cues 8022 * @return {Array} 8023 */ 8024vjs.TextTrack.prototype.cues = function(){ 8025 return this.cues_; 8026}; 8027 8028/** 8029 * ActiveCues is all cues that are currently showing 8030 * Spec def: readonly attribute TextTrackCueList activeCues; 8031 * @private 8032 */ 8033vjs.TextTrack.prototype.activeCues_; 8034 8035/** 8036 * Get the track active cues 8037 * @return {Array} 8038 */ 8039vjs.TextTrack.prototype.activeCues = function(){ 8040 return this.activeCues_; 8041}; 8042 8043/** 8044 * ReadyState describes if the text file has been loaded 8045 * const unsigned short NONE = 0; 8046 * const unsigned short LOADING = 1; 8047 * const unsigned short LOADED = 2; 8048 * const unsigned short ERROR = 3; 8049 * readonly attribute unsigned short readyState; 8050 * @private 8051 */ 8052vjs.TextTrack.prototype.readyState_; 8053 8054/** 8055 * Get the track readyState 8056 * @return {Number} 8057 */ 8058vjs.TextTrack.prototype.readyState = function(){ 8059 return this.readyState_; 8060}; 8061 8062/** 8063 * Mode describes if the track is showing, hidden, or disabled 8064 * const unsigned short OFF = 0; 8065 * const unsigned short HIDDEN = 1; (still triggering cuechange events, but not visible) 8066 * const unsigned short SHOWING = 2; 8067 * attribute unsigned short mode; 8068 * @private 8069 */ 8070vjs.TextTrack.prototype.mode_; 8071 8072/** 8073 * Get the track mode 8074 * @return {Number} 8075 */ 8076vjs.TextTrack.prototype.mode = function(){ 8077 return this.mode_; 8078}; 8079 8080/** 8081 * Create basic div to hold cue text 8082 * @return {Element} 8083 */ 8084vjs.TextTrack.prototype.createEl = function(){ 8085 return vjs.Component.prototype.createEl.call(this, 'div', { 8086 className: 'vjs-' + this.kind_ + ' vjs-text-track' 8087 }); 8088}; 8089 8090/** 8091 * Show: Mode Showing (2) 8092 * Indicates that the text track is active. If no attempt has yet been made to obtain the track's cues, the user agent will perform such an attempt momentarily. 8093 * The user agent is maintaining a list of which cues are active, and events are being fired accordingly. 8094 * In addition, for text tracks whose kind is subtitles or captions, the cues are being displayed over the video as appropriate; 8095 * for text tracks whose kind is descriptions, the user agent is making the cues available to the user in a
8095non-visual fashion; 8096 * and for text tracks whose kind is chapters, the user agent is making available to the user a mechanism by which the user can navigate to any point in the media resource by selecting a cue. 8097 * The showing by default state is used in conjunction with the default attribute on track elements to indicate that the text track was enabled due to that attribute. 8098 * This allows the user agent to override the state if a later track is discovered that is more appropriate per the user's preferences. 8099 */ 8100vjs.TextTrack.prototype.show = function(){ 8101 this.activate(); 8102 8103 this.mode_ = 2; 8104 8105 // Show element. 8106 vjs.Component.prototype.show.call(this); 8107}; 8108 8109/** 8110 * Hide: Mode Hidden (1) 8111 * Indicates that the text track is active, but that the user agent is not actively displaying the cues. 8112 * If no attempt has yet been made to obtain the track's cues, the user agent will perform such an attempt momentarily. 8113 * The user agent is maintaining a list of which cues are active, and events are being fired accordingly. 8114 */ 8115vjs.TextTrack.prototype.hide = function(){ 8116 // When hidden, cues are still triggered. Disable to stop triggering. 8117 this.activate(); 8118 8119 this.mode_ = 1; 8120 8121 // Hide element. 8122 vjs.Component.prototype.hide.call(this); 8123}; 8124 8125/** 8126 * Disable: Mode Off/Disable (0) 8127 * Indicates that the text track is not active. Other than for the purposes of exposing the track in the DOM, the user agent is ignoring the text track. 8128 * No cues are active, no events are fired, and the user agent will not attempt to obtain the track's cues. 8129 */ 8130vjs.TextTrack.prototype.disable = function(){ 8131 // If showing, hide. 8132 if (this.mode_ == 2) { this.hide(); } 8133 8134 // Stop triggering cues 8135 this.deactivate(); 8136 8137 // Switch Mode to Off 8138 this.mode_ = 0; 8139}; 8140 8141/** 8142 * Turn on cue tracking. Tracks that are showing OR hidden are active. 8143 */ 8144vjs.TextTrack.prototype.activate = function(){ 8145 // Load text file if it hasn't been yet. 8146 if (this.readyState_ === 0) { this.load(); } 8147 8148 // Only activate if not already active. 8149 if (this.mode_ === 0) { 8150 // Update current cue on timeupdate 8151 // Using unique ID for bind function so other tracks don't remove listener 8152 this.player_.on('timeupdate', vjs.bind(this, this.update, this.id_)); 8153 8154 // Reset cue time on media end 8155 this.player_.on('ended', vjs.bind(this, this.reset, this.id_)); 8156 8157 // Add to display 8158 if (this.kind_ === 'captions' || this.kind_ === 'subtitles') { 8159 this.player_.getChild('textTrackDisplay').addChild(this); 8160 } 8161 } 8162}; 8163 8164/** 8165 * Turn off cue tracking. 8166 */ 8167vjs.TextTrack.prototype.deactivate = function(){ 8168 // Using unique ID for bind function so other tracks don't remove listener 8169 this.player_.off('timeupdate', vjs.bind(this, this.update, this.id_)); 8170 this.player_.off('ended', vjs.bind(this, this.reset, this.id_)); 8171 this.reset(); // Reset 8172 8173 // Remove from display 8174 this.player_.getChild('textTrackDisplay').removeChild(this); 8175}; 8176 8177// A readiness state 8178// One of the following: 8179// 8180// Not loaded 8181// Indicates that the text track is known to exist (e.g. it has been declared with a track element), but its cues have not been obtained. 8182// 8183// Loading 8184// Indicates that the text track is loading and there have been no fatal errors encountered so far. Further cues might still be added to the track. 8185// 8186// Loaded 8187// Indicates that the text track has been loaded with no fatal errors. No new cues will be added to the track except if the text track corresponds to a MutableTextTrack object. 8188// 8189// Failed to load 8190// Indicates that the text track was enabled, but when the user agent attempted to obtain it, this failed in some way (e.g. URL could not be resolved, network error, unknown text track format). Some or all of the cues are likely missing and will not be obtained. 8191vjs.TextTrack.prototype.load = function(){ 8192 8193 // Only load if not loaded yet. 8194 if (this.readyState_ === 0) { 8195 this.readyState_ = 1; 8196 vjs.xhr(this.src_, vjs.bind(this, function(err, response, responseBody){ 8197 if (err) { 8198 return this.onError(err); 8199 } 8200 8201 this.parseCues(responseBody); 8202 })); 8203 } 8204 8205}; 8206 8207vjs.TextTrack.prototype.onError = function(err){ 8208 this.error = err; 8209 this.readyState_ = 3; 8210 this.trigger('error'); 8211}; 8212 8213// Parse the WebVTT text format for cue times. 8214// TODO: Separate parser into own class so alternative timed text formats can be used. (TTML, DFXP) 8215vjs.TextTrack.prototype.parseCues = function(srcContent) { 8216 var cue, time, text, 8217 lines = srcContent.split('\n'), 8218 line = '', id; 8219 8220 for (var i=1, j=lines.length; i<j; i++) { 8221 // Line 0 should be 'WEBVTT', so skipping i=0 8222 8223 line = vjs.trim(lines[i]);
8223 // Trim whitespace and linebreaks 8224 8225 if (line) { // Loop until a line with content 8226 8227 // First line could be an optional cue ID 8228 // Check if line has the time separator 8229 if (line.indexOf('-->') == -1) { 8230 id = line; 8231 // Advance to next line for timing. 8232 line = vjs.trim(lines[++i]); 8233 } else { 8234 id = this.cues_.length; 8235 } 8236 8237 // First line - Number 8238 cue = { 8239 id: id, // Cue Number 8240 index: this.cues_.length // Position in Array 8241 }; 8242 8243 // Timing line 8244 time = line.split(/[\t ]+/); 8245 cue.startTime = this.parseCueTime(time[0]); 8246 cue.endTime = this.parseCueTime(time[2]); 8247 8248 // Additional lines - Cue Text 8249 text = []; 8250 8251 // Loop until a blank line or end of lines 8252 // Assuming trim('') returns false for blank lines 8253 while (lines[++i] && (line = vjs.trim(lines[i]))) { 8254 text.push(line); 8255 } 8256 8257 cue.text = text.join('<br/>'); 8258 8259 // Add this cue 8260 this.cues_.push(cue); 8261 } 8262 } 8263 8264 this.readyState_ = 2; 8265 this.trigger('loaded'); 8266}; 8267 8268 8269vjs.TextTrack.prototype.parseCueTime = function(timeText) { 8270 var parts = timeText.split(':'), 8271 time = 0, 8272 hours, minutes, other, seconds, ms; 8273 8274 // Check if optional hours place is included 8275 // 00:00:00.000 vs. 00:00.000 8276 if (parts.length == 3) { 8277 hours = parts[0]; 8278 minutes = parts[1]; 8279 other = parts[2]; 8280 } else { 8281 hours = 0; 8282 minutes = parts[0]; 8283 other = parts[1]; 8284 } 8285 8286 // Break other (seconds, milliseconds, and flags) by spaces 8287 // TODO: Make additional cue layout settings work with flags 8288 other = other.split(/\s+/); 8289 // Remove seconds. Seconds is the first part before any spaces. 8290 seconds = other.splice(0,1)[0]; 8291 // Could use either . or , for decimal 8292 seconds = seconds.split(/\.|,/); 8293 // Get milliseconds 8294 ms = parseFloat(seconds[1]); 8295 seconds = seconds[0]; 8296 8297 // hours => seconds 8298 time += parseFloat(hours) * 3600; 8299 // minutes => seconds 8300 time += parseFloat(minutes) * 60; 8301 // Add seconds 8302 time += parseFloat(seconds); 8303 // Add milliseconds 8304 if (ms) { time += ms/1000; } 8305 8306 return time; 8307}; 8308 8309// Update active cues whenever timeupdate events are triggered on the player. 8310vjs.TextTrack.prototype.update = function(){ 8311 if (this.cues_.length > 0) { 8312 8313 // Get current player time, adjust for track offset 8314 var offset = this.player_.options()['trackTimeOffset'] || 0; 8315 var time = this.player_.currentTime() + offset; 8316 8317 // Check if the new time is outside the time box created by the the last update. 8318 if (this.prevChange === undefined || time < this.prevChange || this.nextChange <= time) { 8319 var cues = this.cues_, 8320 8321 // Create a new time box for this state. 8322 newNextChange = this.player_.duration(), // Start at beginning of the timeline 8323 newPrevChange = 0, // Start at end 8324 8325 reverse = false, // Set the direction of the loop through the cues. Optimized the cue check. 8326 newCues = [], // Store new active cues. 8327 8328 // Store where in the loop the current active cues are, to provide a smart starting point for the next loop. 8329 firstActiveIndex, lastActiveIndex, 8330 cue, i; // Loop vars 8331 8332 // Check if time is going forwards or backwards (scrubbing/rewinding) 8333 // If we know the direction we can optimize the starting position and direction of the loop through the cues array. 8334 if (time >= this.nextChange || this.nextChange === undefined) { // NextChange should happen 8335 // Forwards, so start at the index of the first active cue and loop forward 8336 i = (this.firstActiveIndex !== undefined) ? this.firstActiveIndex : 0; 8337 } else { 8338 // Backwards, so start at the index of the last active cue and loop backward 8339 reverse = true; 8340 i = (this.lastActiveIndex !== undefined) ? this.lastActiveIndex : cues.length - 1; 8341 } 8342 8343 while (true) { // Loop until broken 8344 cue = cues[i]; 8345 8346 // Cue ended at this point 8347 if (cue.endTime <= time) { 8348 newPrevChange = Math.max(newPrevChange, cue.endTime); 8349 8350 if (cue.active) { 8351 cue.active = false;
8352 } 8353 8354 // No earlier cues should have an active start time. 8355 // Nevermind. Assume first cue could have a duration the same as the video. 8356 // In that case we need to loop all the way back to the beginning. 8357 // if (reverse && cue.startTime) { break; } 8358 8359 // Cue hasn't started 8360 } else if (time < cue.startTime) { 8361 newNextChange = Math.min(newNextChange, cue.startTime); 8362 8363 if (cue.active) { 8364 cue.active = false; 8365 } 8366 8367 // No later cues should have an active start time. 8368 if (!reverse) { break; } 8369 8370 // Cue is current 8371 } else { 8372 8373 if (reverse) { 8374 // Add cue to front of array to keep in time order 8375 newCues.splice(0,0,cue); 8376 8377 // If in reverse, the first current cue is our lastActiveCue 8378 if (lastActiveIndex === undefined) { lastActiveIndex = i; } 8379 firstActiveIndex = i; 8380 } else { 8381 // Add cue to end of array 8382 newCues.push(cue); 8383 8384 // If forward, the first current cue is our firstActiveIndex 8385 if (firstActiveIndex === undefined) { firstActiveIndex = i; } 8386 lastActiveIndex = i; 8387 } 8388 8389 newNextChange = Math.min(newNextChange, cue.endTime); 8390 newPrevChange = Math.max(newPrevChange, cue.startTime); 8391 8392 cue.active = true; 8393 } 8394 8395 if (reverse) { 8396 // Reverse down the array of cues, break if at first 8397 if (i === 0) { break; } else { i--; } 8398 } else { 8399 // Walk up the array fo cues, break if at last 8400 if (i === cues.length - 1) { break; } else { i++; } 8401 } 8402 8403 } 8404 8405 this.activeCues_ = newCues; 8406 this.nextChange = newNextChange; 8407 this.prevChange = newPrevChange; 8408 this.firstActiveIndex = firstActiveIndex; 8409 this.lastActiveIndex = lastActiveIndex; 8410 8411 this.updateDisplay(); 8412 8413 this.trigger('cuechange'); 8414 } 8415 } 8416}; 8417 8418// Add cue HTML to display 8419vjs.TextTrack.prototype.updateDisplay = function(){ 8420 var cues = this.activeCues_, 8421 html = '', 8422 i=0,j=cues.length; 8423 8424 for (;i<j;i++) { 8425 html += '<span class="vjs-tt-cue">'+cues[i].text+'</span>'; 8426 } 8427 8428 this.el_.innerHTML = html; 8429}; 8430 8431// Set all loop helper values back 8432vjs.TextTrack.prototype.reset = function(){ 8433 this.nextChange = 0; 8434 this.prevChange = this.player_.duration(); 8435 this.firstActiveIndex = 0; 8436 this.lastActiveIndex = 0; 8437}; 8438 8439// Create specific track types 8440/** 8441 * The track component for managing the hiding and showing of captions 8442 * 8443 * @constructor 8444 */ 8445vjs.CaptionsTrack = vjs.TextTrack.extend(); 8446vjs.CaptionsTrack.prototype.kind_ = 'captions'; 8447// Exporting here because Track creation requires the track kind 8448// to be available on global object. e.g. new window['videojs'][Kind + 'Track'] 8449 8450/** 8451 * The track component for managing the hiding and showing of subtitles 8452 * 8453 * @constructor 8454 */ 8455vjs.SubtitlesTrack = vjs.TextTrack.extend(); 8456vjs.SubtitlesTrack.prototype.kind_ = 'subtitles'; 8457 8458/** 8459 * The track component for managing the hiding and showing of chapters 8460 * 8461 * @constructor 8462 */ 8463vjs.ChaptersTrack = vjs.TextTrack.extend(); 8464vjs.ChaptersTrack.prototype.kind_ = 'chapters'; 8465 8466 8467/* Text Track Display 8468============================================================================= */ 8469// Global container for both subtitle and captions text. Simple div container. 8470 8471/** 8472 * The component for displaying text track cues 8473 * 8474 * @constructor 8475 */ 8476vjs.TextTrackDisplay = vjs.Component.extend({ 8477 /** @constructor */ 8478 init: function(player, options, ready){ 8479 vjs.Component.call(this, player, options, ready); 8480 8481 // This used to be called during player init, but was causing an error 8482 // if a track should show by default and the display hadn't loaded yet. 8483 // Should probably be moved to an external track loader when we support 8484 // tracks that don't need a display. 8485 if (player.options_['tracks'] && player.options_['tracks'].length > 0) { 8486 this.player_.addTextTracks(player.options_['tracks']); 8487 } 8488 } 8489}); 8490 8491vjs.TextTrackDisplay.prototype.createEl = function(){ 8492 return vjs.Component.prototype.createEl.call(this, 'div', { 8493 className: 'vjs-text-track-display' 8494 }); 8495}; 8496 8497 8498/** 8499 * The specific menu item type for selecting a language within a text track kind 8500 * 8501 * @constructor 8502 */ 8503vjs.TextTrackMenuItem = vjs.MenuItem.extend({ 8504 /** @constructor */ 8505 init: function(player, options){ 8506 var track = this.track = options['track']; 8507 8508 // Modify options for parent MenuItem class's init. 8509 options['label'] = track.label(); 8510 options['selected'] = track.dflt(); 8511 vjs.MenuItem.call(this, player, options); 8512 8513 this.on(player, track.kind() + 'trackchange', this.update); 8514 } 8515}); 8516 8517vjs.TextTrackMenuItem.prototype.onClick = function(){ 8518 vjs.MenuItem.prototype.onClick.call(this); 8519 this.player_.showTextTrack(this.track.id_, this.track.kind()); 8520}; 8521 8522vjs.TextTrackMenuItem.prototype.update = function(){ 8523 this.selected(this.track.mode() == 2); 8524}; 8525 8526/** 8527 * A special menu item for turning of a specific type of text track 8528 * 8529 * @constructor 8530 */ 8531vjs.OffTextTrackMenuItem = vjs.TextTrackMenuItem.extend({ 8532 /** @constructor */ 8533 init: function(player, options){ 8534 // Create pseudo track info 8535 // Requires options['kind'] 8536 options['track'] = { 8537 kind: function() { return options['kind']; }, 8538 player: player, 8539 label: function(){ return options['kind'] + ' off'; }, 8540 dflt: function(){ return false; }, 8541 mode: function(){ return false; } 8542 }; 8543 vjs.TextTrackMenuItem.call(this, player, options); 8544 this.selected(true); 8545 } 8546}); 8547 8548vjs.OffTextTrackMenuItem.prototype.onClick = function(){ 8549 vjs.TextTrackMenuItem.prototype.onClick.call(this); 8550 this.player_.showTextTrack(this.track.id_, this.track.kind()); 8551}; 8552 8553vjs.OffTextTrackMenuItem.prototype.update = function(){ 8554 var tracks = this.player_.textTracks(), 8555 i=0, j=tracks.length, track, 8556 off = true; 8557 8558 for (;i<j;i++) { 8559 track = tracks[i]; 8560 if (track.kind() == this.track.kind() && track.mode() == 2) { 8561 off = false;
8562 } 8563 } 8564 8565 this.selected(off); 8566}; 8567 8568/** 8569 * The base class for buttons that toggle specific text track types (e.g. subtitles) 8570 * 8571 * @constructor 8572 */ 8573vjs.TextTrackButton = vjs.MenuButton.extend({ 8574 /** @constructor */ 8575 init: function(player, options){ 8576 vjs.MenuButton.call(this, player, options); 8577 8578 if (this.items.length <= 1) { 8579 this.hide(); 8580 } 8581 } 8582}); 8583 8584// vjs.TextTrackButton.prototype.buttonPressed = false; 8585 8586// vjs.TextTrackButton.prototype.createMenu = function(){ 8587// var menu = new vjs.Menu(this.player_); 8588 8589// // Add a title list item to the top 8590// // menu.el().appendChild(vjs.createEl('li', { 8591// // className: 'vjs-menu-title', 8592// // innerHTML: vjs.capitalize(this.kind_), 8593// // tabindex: -1 8594// // })); 8595 8596// this.items = this.createItems(); 8597 8598// // Add menu items to the menu 8599// for (var i = 0; i < this.items.length; i++) { 8600// menu.addItem(this.items[i]); 8601// } 8602 8603// // Add list to element 8604// this.addChild(menu); 8605 8606// return menu; 8607// }; 8608 8609// Create a menu item for each text track 8610vjs.TextTrackButton.prototype.createItems = function(){ 8611 var items = [], track; 8612 8613 // Add an OFF menu item to turn all tracks off 8614 items.push(new vjs.OffTextTrackMenuItem(this.player_, { 'kind': this.kind_ })); 8615 8616 for (var i = 0; i < this.player_.textTracks().length; i++) { 8617 track = this.player_.textTracks()[i]; 8618 if (track.kind() === this.kind_) { 8619 items.push(new vjs.TextTrackMenuItem(this.player_, { 8620 'track': track 8621 })); 8622 } 8623 } 8624 8625 return items; 8626}; 8627 8628/** 8629 * The button component for toggling and selecting captions 8630 * 8631 * @constructor 8632 */ 8633vjs.CaptionsButton = vjs.TextTrackButton.extend({ 8634 /** @constructor */ 8635 init: function(player, options, ready){ 8636 vjs.TextTrackButton.call(this, player, options, ready); 8637 this.el_.setAttribute('aria-label','Captions Menu'); 8638 } 8639}); 8640vjs.CaptionsButton.prototype.kind_ = 'captions'; 8641vjs.CaptionsButton.prototype.buttonText = 'Captions'; 8642vjs.CaptionsButton.prototype.className = 'vjs-captions-button'; 8643 8644/** 8645 * The button component for toggling and selecting subtitles 8646 * 8647 * @constructor 8648 */ 8649vjs.SubtitlesButton = vjs.TextTrackButton.extend({ 8650 /** @constructor */ 8651 init: function(player, options, ready){ 8652 vjs.TextTrackButton.call(this, player, options, ready); 8653 this.el_.setAttribute('aria-label','Subtitles Menu'); 8654 } 8655}); 8656vjs.SubtitlesButton.prototype.kind_ = 'subtitles'; 8657vjs.SubtitlesButton.prototype.buttonText = 'Subtitles'; 8658vjs.SubtitlesButton.prototype.className = 'vjs-subtitles-button'; 8659 8660// Chapters act much differently than other text tracks 8661// Cues are navigation vs. other tracks of alternative languages 8662/** 8663 * The button component for toggling and selecting chapters 8664 * 8665 * @constructor 8666 */ 8667vjs.ChaptersButton = vjs.TextTrackButton.extend({ 8668 /** @constructor */ 8669 init: function(player, options, ready){ 8670 vjs.TextTrackButton.call(this, player, options, ready); 8671 this.el_.setAttribute('aria-label','Chapters Menu'); 8672 } 8673}); 8674vjs.ChaptersButton.prototype.kind_ = 'chapters'; 8675vjs.ChaptersButton.prototype.buttonText = 'Chapters'; 8676vjs.ChaptersButton.prototype.className = 'vjs-chapters-button'; 8677 8678// Create a menu item for each text track 8679vjs.ChaptersButton.prototype.createItems = function(){ 8680 var items = [], track; 8681 8682 for (var i = 0; i < this.player_.textTracks().length; i++) { 8683 track = this.player_.textTracks()[i]; 8684 if (track.kind() === this.kind_) { 8685 items.push(new vjs.TextTrackMenuItem(this.player_, { 8686 'track': track 8687 })); 8688 } 8689 } 8690 8691 return items; 8692}; 8693 8694vjs.ChaptersButton.prototype.createMenu = function(){ 8695 var tracks = this.player_.textTracks(), 8696 i = 0, 8697 j = tracks.length, 8698 track, chaptersTrack, 8699 items = this.items = []; 8700 8701 for (;i<j;i++) { 8702 track = tracks[i]; 8703 if (track.kind() == this.kind_) { 8704 if (track.readyState() === 0) { 8705 track.load(); 8706 track.on('loaded', vjs.bind(this, this.createMenu)); 8707 } else { 8708 chaptersTrack = track; 8709 break; 8710 } 8711 } 8712 } 8713 8714 var menu = this.menu; 8715 if (menu === undefined) { 8716 menu = new vjs.Menu(this.player_); 8717 menu.contentEl().appendChild(vjs.createEl('li', { 8718 className: 'vjs-menu-title', 8719 innerHTML: vjs.capitalize(this.kind_), 8720 tabindex: -1 8721 })); 8722 } 8723
8724 if (chaptersTrack) { 8725 var cues = chaptersTrack.cues_, cue, mi; 8726 i = 0; 8727 j = cues.length; 8728 8729 for (;i<j;i++) { 8730 cue = cues[i]; 8731 8732 mi = new vjs.ChaptersTrackMenuItem(this.player_, { 8733 'track': chaptersTrack, 8734 'cue': cue 8735 }); 8736 8737 items.push(mi); 8738 8739 menu.addChild(mi); 8740 } 8741 this.addChild(menu); 8742 } 8743 8744 if (this.items.length > 0) { 8745 this.show(); 8746 } 8747 8748 return menu; 8749}; 8750 8751 8752/** 8753 * @constructor 8754 */ 8755vjs.ChaptersTrackMenuItem = vjs.MenuItem.extend({ 8756 /** @constructor */ 8757 init: function(player, options){ 8758 var track = this.track = options['track'], 8759 cue = this.cue = options['cue'], 8760 currentTime = player.currentTime(); 8761 8762 // Modify options for parent MenuItem class's init. 8763 options['label'] = cue.text; 8764 options['selected'] = (cue.startTime <= currentTime && currentTime < cue.endTime); 8765 vjs.MenuItem.call(this, player, options); 8766 8767 track.on('cuechange', vjs.bind(this, this.update)); 8768 } 8769}); 8770 8771vjs.ChaptersTrackMenuItem.prototype.onClick = function(){ 8772 vjs.MenuItem.prototype.onClick.call(this); 8773 this.player_.currentTime(this.cue.startTime); 8774 this.update(this.cue.startTime); 8775}; 8776 8777vjs.ChaptersTrackMenuItem.prototype.update = function(){ 8778 var cue = this.cue, 8779 currentTime = this.player_.currentTime(); 8780 8781 // vjs.log(currentTime, cue.startTime); 8782 this.selected(cue.startTime <= currentTime && currentTime < cue.endTime); 8783}; 8784 8785// Add Buttons to controlBar 8786vjs.obj.merge(vjs.ControlBar.prototype.options_['children'], { 8787 'subtitlesButton': {}, 8788 'captionsButton': {}, 8789 'chaptersButton': {} 8790}); 8791 8792// vjs.Cue = vjs.Component.extend({ 8793// /** @constructor */ 8794// init: function(player, options){ 8795// vjs.Component.call(this, player, options); 8796// } 8797// }); 8798/** 8799 * @fileoverview Add JSON support 8800 * @suppress {undefinedVars} 8801 * (Compiler doesn't like JSON not being declared) 8802 */ 8803 8804/** 8805 * Javascript JSON implementation 8806 * (Parse Method Only) 8807 * https://github.com/douglascrockford/JSON-js/blob/master/json2.js 8808 * Only using for parse method when parsing data-setup attribute JSON. 8809 * @suppress {undefinedVars} 8810 * @namespace 8811 * @private 8812 */ 8813vjs.JSON; 8814 8815if (typeof window.JSON !== 'undefined' && typeof window.JSON.parse === 'function') { 8816 vjs.JSON = window.JSON; 8817 8818} else { 8819 vjs.JSON = {}; 8820 8821 var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; 8822 8823 /** 8824 * parse the json 8825 * 8826 * @memberof vjs.JSON 8827 * @param {String} text The JSON string to parse 8828 * @param {Function=} [reviver] Optional function that can transform the results 8829 * @return {Object|Array} The parsed JSON 8830 */ 8831 vjs.JSON.parse = function (text, reviver) { 8832 var j; 8833 8834 function walk(holder, key) { 8835 var k, v, value = holder[key]; 8836 if (value && typeof value === 'object') { 8837 for (k in value) { 8838 if (Object.prototype.hasOwnProperty.call(value, k)) { 8839 v = walk(value, k); 8840 if (v !== undefined) { 8841 value[k] = v; 8842 } else { 8843 delete value[k]; 8844 } 8845 } 8846 } 8847 } 8848 return reviver.call(holder, key, value); 8849 } 8850 text = String(text); 8851 cx.lastIndex = 0; 8852 if (cx.test(text)) { 8853 text = text.replace(cx, function (a) { 8854 return '\\u' + 8855 ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 8856 }); 8857 } 8858 8859 if (/^[\],:{}\s]*$/ 8860 .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') 8861 .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') 8862 .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 8863 8864 j = eval('(' + text + ')'); 8865 8866 return typeof reviver === 'function' ? 8867 walk({'': j}, '') : j; 8868 } 8869 8870 throw new SyntaxError('JSON.parse(): invalid or malformed JSON data'); 8871 }; 8872} 8873/** 8874 * @fileoverview Functions for automatically setting up a player 8875 * based on the data-setup attribute of the video tag 8876 */ 8877 8878// Automatically set up any tags that have a data-setup attribute 8879vjs.autoSetup = function(){ 8880 var options, mediaEl, player, i, e; 8881 8882 // One day, when we stop supporting IE8, go back to this, but in the meantime...*hack hack hack* 8883 // var vids = Array.prototype.slice.call(document.getElementsByTagName('video')); 8884 // var audios = Array.prototype.slice.call(document.getElementsByTagName('audio')); 8885 // var mediaEls = vids.concat(audios); 8886 8887 // Because IE8 doesn't support calling slice on a node list, we need to loop through each list of elements 8888 // to build up a new, combined list of elements. 8889 var vids = document.getElementsByTagName('video'); 8890 var audios = document.getElementsByTagName('audio'); 8891 var mediaEls = []; 8892 if (vids && vids.length > 0) { 8893 for(i=0, e=vids.length; i<e; i++) { 8894 mediaEls.push(vids[i]); 8895 } 8896 } 8897 if (audios && audios.length > 0) { 8898 for(i=0, e=audios.length; i<e; i++) { 8899 mediaEls.push(audios[i]); 8900 } 8901 } 8902 8903 // Check if any media elements exist 8904 if (mediaEls && mediaEls.length > 0) { 8905 8906 for (i=0,e=mediaEls.length; i<e; i++) { 8907 mediaEl = mediaEls[i]; 8908 8909 // Check if element exists, has getAttribute func. 8910 // IE seems to consider typeof el.getAttribute == 'object' instead of 'function' like expected, at least when loading the player immediately. 8911 if (mediaEl && mediaEl.getAttribute) { 8912 8913 // Make sure this player hasn't already been set up. 8914 if (mediaEl['player'] === undefined) { 8915 options = mediaEl.getAttribute('data-setup'); 8916 8917 // Check if data-setup attr exists. 8918 // We only auto-setup if they've added the data-setup attr. 8919 if (options !== null) { 8920 // Create new video.js instance. 8921 player = videojs(mediaEl); 8922 } 8923 } 8924 8925 // If getAttribute isn't defined, we need to wait for the DOM. 8926 } else { 8927 vjs.autoSetupTimeout(1); 8928 break; 8929 } 8930 } 8931 8932 // No videos were found, so keep looping unless page is finished loading. 8933 } else if (!vjs.windowLoaded) { 8934 vjs.autoSetupTimeout(1); 8935 } 8936}; 8937 8938// Pause to let the DOM keep processing 8939vjs.autoSetupTimeout = function(wait){ 8940 setTimeout(vjs.autoSetup, wait); 8941}; 8942 8943if (document.readyState === 'complete') { 8944 vjs.windowLoaded = true; 8945} else { 8946 vjs.one(window, 'load', function(){ 8947 vjs.windowLoaded = true; 8948 }); 8949} 8950 8951// Run Auto-load players 8952// You have to wait at least once in case this script is loaded after your video in the DOM (weird behavior only with minified version) 8953vjs.autoSetupTimeout(1); 8954/** 8955 * the method for registering a video.js plugin 8956 * 8957 * @param {String} name The name of the plugin 8958 * @param {Function} init The function that is run when the player inits 8959 */ 8960vjs.plugin = function(name, init){ 8961 vjs.Player.prototype[name] = init; 8962}; 8963 8964 8965 8966 8967/** 8968 * jQuery Unveil 8969 * A very lightweight jQuery plugin to lazy load images 8970 * http://luis-almeida.github.com/unveil 8971 * 8972 * Licensed under the MIT license. 8973 * Copyright 2013 LuÃÂs Almeida 8974 * https://github.com/luis-almeida 8975 */ 8976 8977;(function($) { 8978 8979 $.fn.unveil = function(threshold, callback) { 8980 8981 var $w = $(window), 8982 th = threshold || 0, 8983 retina = window.devicePixelRatio > 1, 8984 attrib = retina? "data-src-retina" : "data-src", 8985 images = this, 8986 loaded; 8987 8988 this.one("unveil", function() { 8989 var source = this.getAttribute(attrib); 8990 source = source || this.getAttribute("data-src"); 8991 if (source) { 8992 this.setAttribute("src", source); 8993 if (typeof callback === "function") callback.call(this); 8994 } 8995 }); 8996 8997 function unveil() { 8998 var inview = images.filter(function() { 8999 var $e = $(this); 9000 if ($e.is(":hidden")) return; 9001 9002 var wt = $w.scrollTop(), 9003 wb = wt + $w.height(), 9004 et = $e.offset().top, 9005 eb = et + $e.height(); 9006 9007 return eb >= wt - th && et <= wb + th; 9008 }); 9009 9010 loaded = inview.trigger("unveil"); 9011 images = images.not(loaded); 9012 } 9013 9014 $w.on("scroll.unveil resize.unveil lookup.unveil", unveil); 9015 9016 unveil(); 9017 9018 return this; 9019 9020 }; 9021 9022})(window.jQuery || window.Zepto); 9023 9024 9025 9026 9027/** 9028 * Bootstrap Carousel Swipe v1.1 9029 * 9030 * jQuery plugin to enable swipe gestures on Bootstrap 3 carousels. 9031 * Examples and documentation: https://github.com/maaaaark/bcSwipe 9032 * 9033 * Licensed under the MIT license. 9034 */ 9035(function($) { 9036 $.fn.bcSwipe = function(settings) { 9037 var config = { threshold: 50 }; 9038 if (settings) { 9039 $.extend(config, settings); 9040 } 9041 9042 this.each(function() { 9043 var stillMoving = false;
9044 var start; 9045 9046 if ('ontouchstart' in document.documentElement) { 9047 this.addEventListener('touchstart', onTouchStart, false); 9048 } 9049 9050 function onTouchStart(e) { 9051 if (e.touches.length == 1) { 9052 start = e.touches[0].pageX; 9053 stillMoving = true; 9054 this.addEventListener('touchmove', onTouchMove, false); 9055 } 9056 } 9057 9058 function onTouchMove(e) { 9059 if (stillMoving) { 9060 var x = e.touches[0].pageX; 9061 var difference = start - x; 9062 if (Math.abs(difference) >= config.threshold) { 9063 cancelTouch(); 9064 if (difference > 0) { 9065 $(this).carousel('next'); 9066 } 9067 else { 9068 $(this).carousel('prev'); 9069 } 9070 } 9071 } 9072 } 9073 9074 function cancelTouch() { 9075 this.removeEventListener('touchmove', onTouchMove); 9076 start = null; 9077 stillMoving = false; 9078 } 9079 }); 9080 9081 return this; 9082 }; 9083})(jQuery); 9084 9085 9086 9087 9088; 9089 9090var folder = document.currentScript.getAttribute('data-folder'); 9091var category = document.currentScript.getAttribute('data-category'); 9092var tag = document.currentScript.getAttribute('data-tag'); 9093 9094 9095function stringToBoolean(string) { 9096 if (string === null) { 9097 return null; 9098 } 9099 if (string.hasOwnProperty('toString')) { 9100 string = string.toString(); 9101 } 9102 if (string.hasOwnProperty('toLowerCase')) { 9103 string = string.toLowerCase(); 9104 } 9105 var response = null; 9106 var truth = ['true', 'yes', '1']; 9107 var falsehood = ['false', 'no', '0']; 9108 if (truth.indexOf(string) > -1) { 9109 response = true; 9110 } 9111 else if (falsehood.indexOf(string) > -1) { 9112 response = false; 9113 } 9114 return response; 9115} 9116 9117function setCookie(name, value, days) { 9118 var expires = ""; 9119 if (days) { 9120 var date = new Date(); 9121 date.setTime(date.getTime() + (days*24*60*60*1000)); 9122 expires = "; expires=" + date.toUTCString(); 9123 } 9124 document.cookie = name + "=" + value + expires + "; path=/"; 9125} 9126 9127function getCookie(name) { 9128 var nameEQ = name + "="; 9129 var ca = document.cookie.split(';'); 9130 for(var i=0;i < ca.length;i++) { 9131 var c = ca[i]; 9132 while (c.charAt(0)==' ') c = c.substring(1,c.length); 9133 if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); 9134 } 9135 return null; 9136} 9137 9138function deleteCookie(name) { 9139 setCookie(name,"",-1); 9140} 9141 9142var request = {}; 9143var user = {}; 9144async function get_request() { 9145 9146 let article_opinions = {}; 9147 let floorplan_opinions = {}; 9148 let company_opinions = {}; 9149 let listing_opinions = {}; 9150 let notification_ids = []; 9151 9152 request = await $.ajax({ 9153 url: "/get-request/", 9154 dataType: 'json', 9155 method: 'get', 9156 }); 9157 9158 user = request['user']; 9159 9160 if (user['is_authenticated'] && user['is_staff']) { 9161 $("#admin-bar").show(); 9162 } 9163 else { 9164 $("#admin-bar").hide(); 9165 } 9166 9167 if (user['is_authenticated']) { 9168 $("#mobile-signin-container > a").attr({"href": "/accounts/logout/"}); 9169 $("#mobile-signin-container > a").text("Sign-Out"); 9170 } 9171 else { 9172 $("#mobile-signin-container > a").attr({"href": "/accounts/"}); 9173 $("#mobile-signin-container > a").text("Sign-In"); 9174 } 9175 9176 // messages 9177 if (request['messages'].length > 0) { 9178 $(".messages").show(); 9179 $(".messages").removeAttr("hidden"); 9180 $(".messages").css({"margin": "16px 0px;"}); 9181 } 9182 else { 9183 $(".messages").hide(); 9184 $(".messages").attr({"hidden": true}); 9185 } 9186 for (let message of request['messages']) { 9187 let li = $(document.createElement("li")); 9188 li.addClass(message['tags']); 9189 li.html(message['message']); 9190 $("ul.messages").append(li); 9191 li.find(".alert").each(function () { 9192 alert($(this).text()); 9193 $(this).remove(); 9194 }); 9195 } 9196 9197 // csrf_tokens 9198 $("form[method='post' i][action^='/']:not(:has(input[name='csrfmiddlewaretoken']))").each(function () { 9199 let form = $(this); 9200 let input = $(document.createElement('input')); 9201 input.attr({ 9202 "hidden": "", 9203 "name": "csrfmiddlewaretoken", 9204 "value": request['csrf_token'], 9205 }); 9206 form.prepend(input); 9207 }); 9208 9209 if (user['is_authenticated']) { 9210 $(".is-authenticated").show(); 9211 $(".not-authenticated").hide(); 9212 } 9213 else { 9214 $(".is-authenticated").hide(); 9215 $(".not-authenticated").show(); 9216 } 9217 9218 // welcome back 9219 if (user['is_authenticated']) {
9220 $("#welcome-back-username").text(user['username']); 9221 } 9222 // end welcome back 9223 9224 // index the opinions 9225 if (user['account'].hasOwnProperty('article_opinions')) { 9226 for (let opinion of user['account']['article_opinions']) { 9227 article_opinions[opinion['article']['id']] = opinion['article']['rating']; 9228 } 9229 } 9230 if (user['account'].hasOwnProperty('floorplan_opinions')) { 9231 for (let opinion of user['account']['floorplan_opinions']) { 9232 floorplan_opinions[opinion['floorplan']['id']] = opinion['floorplan']['rating']; 9233 } 9234 } 9235 if (user['account'].hasOwnProperty('company_opinions')) { 9236 for (let opinion of user['account']['company_opinions']) { 9237 company_opinions[opinion['company']['id']] = opinion['company']['rating']; 9238 } 9239 } 9240 if (user['account'].hasOwnProperty('listing_opinions')) { 9241 for (let opinion of user['account']['listing_opinions']) { 9242 listing_opinions[opinion['listing']['id']] = opinion['listing']['rating']; 9243 } 9244 } 9245 if (user['account'].hasOwnProperty('notifications')) { 9246 for (let notification of user['account']['notifications']) { 9247 notification_ids.push(parseInt(notification['id'])); 9248 } 9249 } 9250 // end index the opinions 9251 9252 // favorite buttons 9253 $("a.favorite").each(function () { 9254 let elmt = $(this); 9255 let id = parseInt(elmt.data('instance')); 9256 let model = elmt.data('model'); 9257 let favorite = null; 9258 9259 if (model == 'article') { 9260 favorite = article_opinions.hasOwnProperty(id); 9261 } 9262 else if (model == 'floorplan') { 9263 favorite = floorplan_opinions.hasOwnProperty(id); 9264 } 9265 else if (model == 'company') { 9266 favorite = company_opinions.hasOwnProperty(id); 9267 } 9268 else if (model == 'listing') { 9269 favorite = listing_opinions.hasOwnProperty(id); 9270 } 9271 9272 if (user['is_authenticated'] && favorite) { 9273 elmt.attr({ 9274 "href": "/accounts/favorite/?model="+model+"&instance="+id+"&favorite=false", 9275 "title": "Remove this "+model+" from your favorites.", 9276 "aria-label": "Remove this "+model+" from your favorites.", 9277 "aria-description": "This "+model+" is currently in your favorites. Click here to remove it from your favorites.", 9278 }); 9279 elmt.addClass("is-favorite"); 9280 elmt.data({"favorite": "false"}); 9281 elmt.attr({"data-favorite": "false"}); 9282 elmt.find(".favorite-button-text").text("In Favorites"); 9283 } 9284 else if (user['is_authenticated'] && !favorite) { 9285 elmt.attr({ 9286 "href": "/accounts/favorite/?model="+model+"&instance="+id+"&favorite=true", 9287 "title": "Add this "+model+" to your favorites.", 9288 "aria-label": "Add this "+model+" to your favorites.", 9289 "aria-description": "This "+model+" is not currently in your favorites. Click here to add it to your favorites.", 9290 }); 9291 elmt.removeClass("is-favorite"); 9292 elmt.data({"favorite": "true"}); 9293 elmt.attr({"data-favorite": "true"}); 9294 elmt.find(".favorite-button-text").text("Add to Favorites"); 9295 } 9296 else if (!user['is_authenticated']) { 9297 elmt.attr({ 9298 "href": "/accounts/", 9299 "title": "Register with us to save your favorite "+model+"s for future reference.", 9300 "aria-label": "Register with us to save your favorite "+model+"s for future reference.", 9301 "aria-description": "Register with us to save your favorite "+model+"s for future reference. Click here to go to the account registration page.", 9302 }); 9303 elmt.find(".favorite-button-text").text("Add to Favorites"); 9304 elmt.find("span:first-child").css({"color": "white"}); 9305 } 9306 9307 elmt.css({"display": "inline-block"}); 9308 9309 }); 9310 9311 // save floorplan popup 9312 let cookie_name = "hide_save_floorplan_popup"; 9313 let hide_save_floorplan_popup = getCookie(cookie_name); 9314 if (!hide_save_floorplan_popup && $("#save-floorplan-popup a.favorite.is-favorite").length == 0) { 9315 $("#save-floorplan-popup").show(); 9316 $("a.favorite").on("click", function(e) { 9317 $("#save-floorplan-popup").hide(); 9318 }); 9319 $("#close-save-floorplan-popup").on("click", function(e) { 9320 $("#save-floorplan-popup").hide(); 9321 setCookie(cookie_name, 1); 9322 }); 9323 } 9324 // end save floorplan popup 9325 9326 // end favorite buttons 9327 9328 // category notifications 9329 $("#notification-checkbox").each(function () { 9330 let elmt = $(this); 9331 let id = elmt.data("id"); 9332 if (notification_ids.indexOf(id) != -1) { 9333 elmt.prop({"checked": true}); 9334 } 9335 else { 9336 elmt.prop({"checked": false}); 9337 } 9338 }); 9339 // end category notifications 9340 9341 // collage 9342 if (window.location.pathname == "/") { 9343 // bedrooms 9344 if (user['account']['min_bedrooms'] <= 1 && user['account']['max_bedrooms'] >= 1) { 9345 $("input#1bd").prop("checked", true); 9346 } 9347 else if (user['account']['min_bedrooms'] <= 2 && user['account']['max_bedrooms'] >= 2) { 9348 $("input#2bd").prop("checked", true); 9349 } 9350 else if (user['account']['min_bedrooms'] <= 3 && user['account']['max_bedrooms'] >= 3) { 9351 $("input#3bd").prop("checked", true); 9352 } 9353 else if (user['account']['max_bedrooms'] >= 4) { 9354 $("input#4bd").prop("checked", true); 9355 } 9356 // end bedrooms 9357 9358 // bathrooms 9359 if (user['account']['min_bathrooms'] <= 1 && user['account']['max_bathrooms'] >= 1) { 9360 $("input#1ba").prop("checked", true); 9361 } 9362 else if (user['account']['min_bathrooms'] <= 2 && user['account']['max_bathrooms'] >= 2) { 9363 $("input#2ba").prop("checked", true); 9364 } 9365 else if (user['account']['min_bathrooms'] <= 3 && user['account']['max_bathrooms'] >= 3) { 9366 $("input#3ba").prop("checked", true); 9367 } 9368 else if (user['account']['max_bathrooms'] >= 4) { 9369 $("input#4ba").prop("checked", true); 9370 } 9371 // end bathrooms 9372 9373 // area 9374 if (user['account']['min_area'] >= 0 && user['account']['max_area'] <= 1000) { 9375 $("select[name='area'] > option[value='0']").attr({"selected": "selected"}); 9376 } 9377 else if (user['account']['min_area'] >= 1000 && user['account']['max_area'] <= 2000) { 9378 $("select[name='area'] > option[value='1000']").attr({"selected": "selected"}); 9379 } 9380 else if (user['account']['min_area'] >= 2000 && user['account']['max_area'] <= 3000) { 9381 $("select[name='area'] > option[value='2000']").attr({"selected": "selected"}); 9382 } 9383 else if (user['account']['min_area'] >= 3000 && user['account']['max_area'] <= 4000) { 9384 $("select[name='area'] > option[value='3000']").attr({"selected": "selected"}); 9385 } 9386 else if (user['account']['min_area'] >= 4000) { 9387 $("select[name='area'] > option[value='4000']").attr({"selected": "selected"}); 9388 } 9389 // end area 9390 9391 // floors 9392 if (user['account']['min_floors'] <= 1 && user['account']['max_floors'] >= 1) { 9393 $("input#1f").prop("checked", true); 9394 } 9395 else if (user['account']['min_floors'] <= 2 && user['account']['max_floors'] >= 2) { 9396 $("input#2f").prop("checked", true); 9397 } 9398 else if (user['account']['min_floors'] <= 3 && user['account']['max_floors'] >= 3) { 9399 $("input#3f").prop("checked", true); 9400 } 9401 else if (user['account']['max_floors'] >= 4) { 9402 $("input#4f").prop("checked", true); 9403 } 9404 // end floors 9405 } 9406 // end collage 9407 9408 if (user['is_authenticated'] && user['is_staff']) { 9409 await $.getScript("/assets/js/percent-memory-usage.js"); 9410 } 9411 9412 if (user['is_authenticated'] && user['is_staff']) { 9413 $("a[href='/admin/']").show(); 9414 } 9415 else { 9416 $("a[href='/admin/']").remove(); 9417 } 9418 9419 // user account 9420 if (window.location.pathname == "/accounts/") { 9421 if (user['is_authenticated']) { 9422 $("#authenticated").show(); 9423 $("#not-authenticated").hide(); 9424 } 9425 else { 9426 $("#authenticated").hide(); 9427 $("#not-authenticated").show(); 9428 } 9429 9430 if (user.hasOwnProperty('has_realestate_listings') && user['has_realestate_listings']) { 9431 $(".has-realestate-listings").show(); 9432 $(".no-realestate-listings").hide(); 9433 } 9434 else { 9435 $(".has-realestate-listings").hide(); 9436 $(".no-realestate-listings").show(); 9437 } 9438 } 9439 // end user account 9440 9441 // company account 9442 if (window.location.pathname == "/companies/accounts/") { 9443 if (user['is_authenticated']) { 9444 $("#logged-in").show(); 9445 $("#logged-out").hide(); 9446 } 9447 else { 9448 $("#logged-in").hide(); 9449 $("#logged-out").show(); 9450 } 9451 } 9452 // end company account 9453 9454 // real-estate 9455 if (window.location.pathname.startsWith("/real-estate/")) { 9456 } 9457 // end real-estate 9458 9459 // readers choice 9460 if (request.hasOwnProperty('readers_choice_votes') && window.location.pathname.startsWith("/readers-choice/")) { 9461 for (let [home_id, vote_id] of Object.entries(request['readers_choice_votes'])) { 9462 let button = $("button.vote[data-home='"+home_id+"'][data-opinion]"); 9463 button.html("Voted For!"); 9464 button.css({ 9465 "background-color": "#005003", 9466 "color": "white", 9467 }); 9468 button.data({"opinion": 0}); 9469 button.attr({"data-opinion": 0}); 9470 } 9471 } 9472 // end readers choice 9473 9474 // events 9475 if (window.location.pathname.startsWith("/events/")) { 9476 user['can_create_events'] = (user['is_authenticated'] && (user['is_staff'] || user['has_companies'])); 9477 9478 if (user['can_create_events']) { 9479 $("#create-event-link").show(); 9480 } 9481 else { 9482 $("#create-event-link").hide(); 9483 } 9484 9485 if (user['can_create_events'] && user['events']) { 9486 for (event of user['events']) { 9487 let li = document.createElement("li"); 9488 let a = document.createElement("a"); 9489 a.href = event.url; 9490 a.innerText = event['name']; 9491 li.append(a); 9492 $("#my-events-tabpanel ul").append(li); 9493 } 9494 $("button[data-id='my-events-tabpanel']").show(); 9495 } 9496 else { 9497 $("button[data-id='my-events-tabpanel']").hide(); 9498 } 9499 9500 } 9501 // end events 9502 9503 // gtm 9504 window.dataLayer = window.dataLayer || []; 9505 window.dataLayer.push({ 9506 'event': 'analytics_init',
9507 'authenticated': stringToBoolean(user['is_authenticated']), 9508 'category': category, 9509 'tag': tag, 9510 }); 9511 // end gtm 9512 9513} 9514 9515class HotSpot { 9516 9517 constructor(elmt) { 9518 9519 let self = this; 9520 this.elmt = elmt; 9521 let popover = elmt.next(".hotspot-popover"); 9522 this.popover = popover; 9523 this.down = false; 9524 9525 elmt.on("mousedown", function(e) { 9526 self.mousedown(e); 9527 }); 9528 9529 $("body").on("mouseup", function(e) { 9530 self.mouseup(e); 9531 }); 9532 9533 $("body").on("mousemove", function(e) { 9534 self.mousemove(e); 9535 }); 9536 9537 elmt.on("mouseout", function(e) { 9538 self.mouseout(e); 9539 }); 9540 9541 popover.on("mouseout", function(e) { 9542 self.mouseout(e); 9543 }); 9544 9545 elmt.on("mouseover", function(e) { 9546 self.mouseover(e); 9547 }); 9548 9549 popover.find("a[href]").attr({"target": "_blank"}); 9550 9551 let preloaded_img = new Image(); 9552 preloaded_img.src = self.popover.find("img").first().attr('src'); 9553 9554 } 9555 9556 async diagram_dimensions() { 9557 let self = this; 9558 return new Promise((resolve, reject) => { 9559 let img = self.elmt.parent().children("img").first(); 9560 if (img.prop('complete')) { 9561 let dimensions = self.elmt.parent()[0].getBoundingClientRect(); 9562 resolve(dimensions); 9563 } else { 9564 img.on("load", function () { 9565 let dimensions = self.elmt.parent()[0].getBoundingClientRect(); 9566 resolve(dimensions); 9567 }); 9568 } 9569 }); 9570 } 9571 9572 async popover_dimensions() { 9573 let self = this; 9574 return new Promise((resolve, reject) => { 9575 let img = self.popover.find("img").first(); 9576 if (img.prop('complete')) { 9577 let dimensions = self.popover[0].getBoundingClientRect(); 9578 resolve(dimensions); 9579 } else { 9580 img.on("load", function () { 9581 let dimensions = self.popover[0].getBoundingClientRect(); 9582 resolve(dimensions); 9583 }); 9584 } 9585 }); 9586 } 9587 9588 async determine_popover_orientation(callback) { 9589 let elmt = this.elmt; 9590 let popover = this.popover; 9591 let top = parseFloat(elmt[0].style.top.replace("%", "")); 9592 let left = parseFloat(elmt[0].style.left.replace("%", "")); 9593 let diagram_dimensions = await this.diagram_dimensions(); 9594 let popover_dimensions = await this.popover_dimensions(); 9595 // Quadrant 1 9596 if (top <= 50 && left <= 50) { 9597 popover.css({ 9598 "margin": "20px 0px 0px 20px", 9599 "top": top + "%", 9600 "left": left + "%", 9601 }); 9602 } 9603 // Quadrant 2 9604 else if (top <= 50 && left >= 50) { 9605 left -= popover_dimensions.width/diagram_dimensions.width*100; 9606 popover.css({ 9607 "margin": "20px 0px 0px 0px", 9608 "top": top + "%", 9609 "left": left + "%", 9610 }); 9611 } 9612 // Quadrant 3 9613 else if (top >= 50 && left >= 50) { 9614 left -= (popover_dimensions.width)/diagram_dimensions.width*100; 9615 top -= (popover_dimensions.height)/diagram_dimensions.height*100; 9616 popover.css({ 9617 "margin": "0px", 9618 "top": top + "%", 9619 "left": left + "%", 9620 }); 9621 } 9622 // Quadrant 4 9623 else if (top >= 50 && left <= 50) { 9624 top -= (popover_dimensions.height+20)/diagram_dimensions.height*100; 9625 popover.css({ 9626 "margin": "0px 0px 0px 20px", 9627 "top": top + "%", 9628 "left": left + "%", 9629 }); 9630 } 9631 callback(); 9632 } 9633 9634 mousedown(event) { 9635 if (this.down) { 9636 return; 9637 } 9638 event.preventDefault(); 9639 this.down = true; 9640 this.offset = this.elmt.offset(); 9641 this.x0 = event.pageX; 9642 this.y0 = event.pageY; 9643 this.elmt.css({"transition": "none"}); 9644 this.popover.css({"display": "block", "visibility": "visible", "opacity": 1}); 9645 } 9646 9647 mousemove(event) { 9648 if (!this.down) { 9649 return; 9650 } 9651 9652 let cursor_x = event.pageX - window.scrollX; 9653 let cursor_y = event.pageY - window.scrollY; 9654 let parent_dimensions = this.elmt.parent()[0].getBoundingClientRect(); 9655 let parent_x = parent_dimensions.x; 9656 let parent_y = parent_dimensions.y; 9657 let left = cursor_x - parent_x - this.elmt.width()/2; 9658 let top = cursor_y - parent_y - this.elmt.height()/2; 9659 this.elmt.css({top: top + "px", left: left + "px"}); 9660 this.popover.css({"display": "none"}); 9661 9662 } 9663 9664 mouseup(event) { 9665 if (!this.down) { 9666 return; 9667 } 9668 event.preventDefault(); 9669 this.down = false; 9670 this.elmt.css({"transition": ".5s all"}); 9671 let x1 = event.pageX; 9672 let y1 = event.pageY; 9673 let dx = x1 - this.x0; 9674 let dy = y1 - this.y0; 9675 this.request(dx, dy); 9676 } 9677 9678 mouseover(event) { 9679 event.preventDefault(); 9680 let self = this; 9681 this.determine_popover_orientation(function() { 9682 self.popover.css({"visibility": "visible", "opacity": 1}); 9683 }); 9684 } 9685 9686 mouseout(event) { 9687 event.preventDefault(); 9688 let self = this; 9689 setTimeout(function () { 9690 if (!self.elmt[0].matches(":hover") && !self.popover[0].matches(":hover")) { 9691 self.popover.css({"visibility": "hidden", "opacity": 0}); 9692 } 9693 }, 1000); 9694 } 9695 9696 request(dx, dy) { 9697 let self = this; 9698 9699 var request = $.ajax({ 9700 url: "/floorplans/move_hotspot/", 9701 method: "POST", 9702 data: { 9703 "id": self.elmt.data("id"), 9704 "dx": dx, 9705 "dy": dy 9706 }, 9707 dataType: "json" 9708 }); 9709 9710 request.done(function(data) { 9711 self.elmt.css({top: data.y+"%", left: data.x+"%"}); 9712 self.popover.css({"display": "block"}); 9713 let admin_row = $("tr.dynamic-hotspots > td.original > input[value='"+self.elmt.data("id")+"']").closest("tr"); 9714 admin_row.find("td.field-x > p").text(data.x); 9715 admin_row.find("td.field-y > p").text(data.y); 9716 }); 9717 9718 request.fail(function(jqXHR, textStatus) { 9719 console.log("Request failed: " + textStatus); 9720 }); 9721 } 9722} 9723 9724class Editor { 9725 9726 static save(elmt, event) { 9727 var app = elmt.attr('data-app'); 9728 var model = elmt.attr('data-model'); 9729 var instance = elmt.attr('data-instance'); 9730 var field = elmt.attr('data-field'); 9731 $('#editor').remove(); 9732 var revision = elmt.html(); 9733 var answer = confirm("To save this change, press Okay. Otherwise, press Cancel."); 9734 if (answer == true) { 9735 this.request(app, model, instance, field, revision) 9736 } 9737 Editor.cancel(elmt, event); 9738 } 9739 9740 static edit(elmt, event) { 9741 elmt.attr('contenteditable', 'true'); 9742 $('#editor > button#editor-edit').text('Cancel'); 9743 $("#editor > button#editor-edit").unbind('click'); 9744 $("#editor > button#editor-edit").click(function(event) { 9745 Editor.cancel(elmt, event); 9746 }); 9747 } 9748 9749 static cancel(elmt, event) { 9750 elmt.attr('contenteditable', 'false'); 9751 $('#editor > button#editor-edit').text('Edit'); 9752 $("#editor > button#editor-edit").unbind('click'); 9753 $("#editor > button#editor-edit").click(function(event) { 9754 Editor.edit(elmt, event); 9755 }); 9756 } 9757 9758 static hoverin(elmt, event) { 9759 if (!$('#editor').length) { 9760 elmt.append("\ 9761 <div id='editor' contenteditable='false' style='position:absolute;top:0px;left:0px;z-index:10;'>\ 9762 <button id='editor-edit'>Edit</button>\ 9763 <button id='editor-save'>Save</button>\ 9764 </div>\ 9765 "); 9766 $("#editor > button#editor-edit").click(function(event) { 9767 Editor.edit(elmt, event); 9768 }); 9769 9770 $("#editor > button#editor-save").click(function(event) { 9771 Editor.save(elmt, event); 9772 }); 9773 } 9774 } 9775 9776 static hoverout(elmt, event) { 9777 if ($('#editor').length) { 9778 $('#editor').remove(); 9779 } 9780 } 9781 9782 static request(app, model, instance, field, revision) { 9783 var request = $.ajax({ 9784 url: "/edit/", 9785 method: "POST", 9786 data: { 9787 "app": app, 9788 "model": model, 9789 "instance": instance, 9790 "field": field, 9791 "revision": revision 9792 }, 9793 dataType: "json" 9794 }); 9795 9796 request.done(function(data) { 9797 console.log(data); 9798 }); 9799 9800 request.fail(function(jqXHR, textStatus) { 9801 console.log("Request failed: " + textStatus); 9802 }); 9803 } 9804 9805} 9806 9807class WindowShade { 9808 9809 constructor (button) { 9810 9811 button.on("click", function () { 9812 let windowshade = button.parent().children(".windowshade"); 9813 if (windowshade.css("display") == 'none') { 9814 windowshade.show(1000); 9815 button.html("Hide advanced search"); 9816 } 9817 else { 9818 windowshade.hide(1000); 9819 button.html("Click here for advanced search"); 9820 } 9821 }); 9822 9823 } 9824 9825} 9826 9827// GalleryForm class. Used by the companies/accounts/accounts page to allow companies to edit their galleries. Also used by the /admin/media/gallery/ pages for the same purpose. 9828class GalleryForm { 9829 9830 static addForm(elmt, event) { 9831 var index = parseInt(document.getElementById('id_gallery-TOTAL_FORMS').value); 9832 var form = "\ 9833 <div>\ 9834 <table>\ 9835 <tr>\ 9836 <th>\ 9837 <label for='id_gallery-"+index+"-file'>Upload:</label>\ 9838 </th>\ 9839 <td>\ 9840 <input id='id_gallery-"+index+"-file' name='gallery-"+index+"-file' type='file'>\ 9841 </td>\ 9842 </tr>\ 9843 <tr>\ 9844 <th>\ 9845 <label for='id_gallery-"+index+"-url'>Url:</label>\ 9846 </th>\ 9847 <td>\ 9848 <input id='id_gallery-"+index+"-url' name='gallery-"+index+"-url' type='url'>\ 9849 </td>\ 9850 </tr>\ 9851 <tr>\ 9852 <th>\ 9853 <label for='id_gallery-"+index+"-alt'>Alt:</label>\ 9854 </th>\ 9855 <td>\ 9856 <input id='id_gallery-"+index+"-alt' name='gallery-"+index+"-alt' type='text'>\ 9857 </td>\ 9858 </tr>\ 9859 <tr>\ 9860 <th>\ 9861 <label for='id_gallery-"+index+"-order'>Order:</label>\ 9862 </th>\ 9863 <td>\ 9864 <input id='id_gallery-"+index+"-order' name='gallery-"+index+"-order' type='number'>\ 9865 </td>\ 9866 </tr>\ 9867 <tr>\ 9868 <td colspan='2'>\ 9869 <button class='gallery-form-remove' onclick='GalleryForm.removeForm($(this), event);'>-</button>\ 9870 </td>\ 9871 </tr>\ 9872 </table>\ 9873 </div>\ 9874 "; 9875 event.preventDefault(); 9876 elmt.before(form); 9877 document.getElementById('id_gallery-TOTAL_FORMS').value = index + 1; 9878 } 9879 9880 static removeForm(elmt, event) { 9881 var index = parseInt(document.getElementById('id_gallery-TOTAL_FORMS').value); 9882 event.preventDefault(); 9883 var parent = elmt.parent().parent().parent().parent().parent(); 9884 parent.find('input').val(""); 9885 parent.css({"display": "none"}); 9886 } 9887 9888} 9889 9890// MediaForm class. Used by the /admin/media/media/ page. 9891class MediaForm { 9892 9893 static addForm(elmt, event) { 9894 var index = parseInt(document.getElementById('id_media-TOTAL_FORMS').value); 9895 var form = "\ 9896 <div>\ 9897 <table>\ 9898 <tr>\ 9899 <th>\ 9900 <label for='id_media-"+index+"-file'>Upload:</label>\ 9901 </th>\ 9902 <td>\ 9903 <input id='id_media-"+index+"-file' name='media-"+index+"-file' type='file'>\ 9904 </td>\ 9905 </tr>\ 9906 <tr>\ 9907 <th>\ 9908 <label for='id_media-"+index+"-url'>Url:</label>\ 9909 </th>\ 9910 <td>\ 9911 <input id='id_media-"+index+"-url' name='media-"+index+"-url' type='url'>\ 9912 </td>\ 9913 </tr>\ 9914 <tr>\ 9915 <th>\ 9916 <label for='id_media-"+index+"-alt'>Alt:</label>\ 9917 </th>\ 9918 <td>\ 9919 <input id='id_media-"+index+"-alt' name='media-"+index+"-alt' type='text'>\ 9920 </td>\ 9921 </tr>\ 9922 <tr>\ 9923 <th>\ 9924 <label for='id_media-"+index+"-order'>Order:</label>\ 9925 </th>\ 9926 <td>\ 9927 <input id='id_media-"+index+"-order' name='media-"+index+"-order' type='number'>\ 9928 </td>\ 9929 </tr>\ 9930 <tr>\ 9931 <td colspan='2'>\ 9932 <button class='media-form-remove' onclick='MediaForm.removeForm($(this), event);'>-</button>\ 9933 </td>\ 9934 </tr>\ 9935 </table>\ 9936 </div>\ 9937 "; 9938 event.preventDefault(); 9939 elmt.before(form); 9940 document.getElementById('id_media-TOTAL_FORMS').value = index + 1; 9941 } 9942 9943 static removeForm(elmt, event) { 9944 var index = parseInt(document.getElementById('id_media-TOTAL_FORMS').value); 9945 event.preventDefault(); 9946 var parent = elmt.parent().parent().parent().parent().parent(); 9947 parent.find('input').val(""); 9948 parent.css({"display": "none"}); 9949 } 9950 9951} 9952 9953function favorite(elmt, event) { 9954 event.preventDefault(); 9955 9956 let model = elmt.attr('data-model'); 9957 let instance = elmt.attr('data-instance'); 9958 let favorite = elmt.attr('data-favorite'); 9959 9960 let r = $.ajax({ 9961 url: "/accounts/favorite/", 9962 method: "POST", 9963 data: { 9964 "model": model, 9965 "instance": instance, 9966 "favorite": favorite, 9967 }, 9968 dataType: "json" 9969 }); 9970 9971 r.done(function(data) { 9972 let small = elmt.hasClass('small-favorite'); 9973 if (data.hasOwnProperty('redirect')) { 9974 window.location.href = data['redirect']; 9975 } 9976 if (data.hasOwnProperty('favorite') && data['favorite']) { 9977 $(".favorite[data-model='"+model+"'][data-instance='"+instance+"']").each(function () { 9978 let elmt = $(this); 9979 elmt.addClass('is-favorite'); 9980 elmt.data({"favorite": "false"}); 9981 elmt.attr('data-favorite', 'false'); 9982 elmt.attr('title', 'Remove this article from your favorites.'); 9983 if (small) { 9984 elmt.html("<span>★</span>"); 9985 } 9986 else { 9987 elmt.html("\ 9988 <span>★</span> In Favorites\ 9989 "); 9990 } 9991 }); 9992 } 9993 else { 9994 $(".favorite[data-model='"+model+"'][data-instance='"+instance+"']").each(function () { 9995 let elmt = $(this); 9996 elmt.removeClass('is-favorite'); 9997 elmt.data({"favorite": "true"}); 9998 elmt.attr('data-favorite', 'true'); 9999 elmt.attr('title', 'Add this article to your favorites.'); 10000 if (small) { 10001 elmt.html("<span>★</span>"); 10002 } 10003 else { 10004 elmt.html("\ 10005 <span>★</span> Add to Favorites\ 10006 "); 10007 } 10008 }); 10009 } 10010 }); 10011 10012 r.fail(function(jqXHR, textStatus) { 10013 console.log("Request failed: " + textStatus); 10014 }); 10015} 10016 10017// Subscribe to notification 10018function toggleNotificationSubscription(elmt, event) { 10019 var app = elmt.attr('data-app'); 10020 var model = elmt.attr('data-model'); 10021 var instance = elmt.attr('data-instance'); 10022 var request = $.ajax({ 10023 url: "/accounts/notifications/toggle/", 10024 method: "POST", 10025 data: { 10026 "app": app, 10027 "model": model, 10028 "instance": instance 10029 }, 10030 dataType: "json" 10031 }); 10032 10033 request.done(function(data) { 10034 console.log(data); 10035 if (data.hasOwnProperty("redirect")) { 10036 window.location.href = data["redirect"]; 10037 } 10038 elmt.attr("checked", data["checked"]); 10039 }); 10040 10041 request.fail(function(jqXHR, textStatus) { 10042 console.log(jqXHR + textStatus); 10043 }); 10044} 10045 10046// Send notifications about new floorplan. 10047function sendNewFloorPlanNotifications(elmt, event) { 10048 event.preventDefault(); 10049 elmt.prop("disabled", true); 10050 var floorplan = elmt.attr('data-floorplan'); 10051 10052 var request = $.ajax({ 10053 url: "/accounts/notifications/new-floorplan/", 10054 method: "POST", 10055 data: { 10056 "floorplan": floorplan 10057 }, 10058 dataType: "json" 10059 }); 10060 10061 request.done(function(data) { 10062 console.log(data); 10063 elmt.prop("disabled", false); 10064 $('#floorplan_form').submit(); 10065 }); 10066 10067 request.fail(function(jqXHR, textStatus) { 10068 console.log(jqXHR + textStatus); 10069 elmt.prop("disabled", false); 10070 $('#floorplan_form').submit(); 10071 }); 10072} 10073 10074// Send notifications about new article. 10075function sendNewArticleNotifications(elmt, event) { 10076 event.preventDefault(); 10077 elmt.prop("disabled", true); 10078 var article = elmt.attr('data-article'); 10079 10080 var request = $.ajax({ 10081 url: "/accounts/notifications/new-article/", 10082 method: "POST", 10083 data: { 10084 "article": article 10085 }, 10086 dataType: "json" 10087 }); 10088 10089 request.done(function(data) { 10090 console.log(data); 10091 elmt.prop("disabled", false); 10092 $('#article_form').submit(); 10093 }); 10094 10095 request.fail(function(jqXHR, textStatus) { 10096 console.log(jqXHR + textStatus); 10097 elmt.prop("disabled", false); 10098 $('#article_form').submit(); 10099 }); 10100} 10101 10102function initialize_featured_companies_slideshow() { 10103 var description = $('.active ~ .featured-companies-description').html(); 10104 $('#featured-companies-description').html(description); 10105 $('#homepage-featured-companies-slideshow').on('slid.bs.carousel', function() { 10106 var description = $('.active ~ .featured-companies-description').html(); 10107 $('#featured-companies-description').html(description); 10108 }); 10109} 10110 10111function initialize_home_tours_slideshow() { 10112 var description = $('.active ~ .home-tours-description').html(); 10113 $('#home-tours-description').html(description); 10114 $('#homepage-home-tours-slideshow').on('slid.bs.carousel', function() { 10115 var description = $('.active ~ .home-tours-description').html(); 10116 $('#home-tours-description').html(description); 10117 }); 10118} 10119 10120function external_link_click(link) { 10121 // Send the click to Google Analytics. 10122 var category = null; 10123 if (link.startsWith('mailto')) { 10124 category = "outbound email"; 10125 } 10126 else if (link.startsWith('tel')) { 10127 category = "outbound phone"; 10128 } 10129 else if (link.startsWith('http')) { 10130 category = "outbound website"; 10131 } 10132 else { 10133 return; 10134 } 10135 ga('site_tracker.send', 'event', category, 'click', link, { 10136 'transport': 'beacon' 10137 }); 10138 10139 // Record the click in our own database too. 10140 var request = $.ajax({ 10141 url: "/companies/click/", 10142 method: "POST", 10143 data: { 10144 "link": link, 10145 }, 10146 dataType: "json" 10147 }); 10148 10149 request.done(function(data) { 10150 window.location = link; 10151 }); 10152 10153 request.fail(function(jqXHR, textStatus) { 10154 window.location = link; 10155 }); 10156 10157} 10158 10159function toggle(elmt) { 10160 if (elmt.css("display") == 'none') { 10161 elmt.show(); 10162 } 10163 else { 10164 elmt.hide(); 10165 } 10166} 10167 10168function hideMobileNav(event) { 10169 if(!$(event.target).closest('#mobile-nav-container').length) { 10170 if($('#mobile-nav
10170').css("display") != 'none') { 10171 $('#mobile-nav').hide(); 10172 } 10173 } 10174} 10175 10176function hideMobileSearch(event) { 10177 if(!$(event.target).closest('#mobile-search-container').length) { 10178 if($('#mobile-search').css("display") != 'none') { 10179 $('#mobile-search').hide(); 10180 } 10181 } 10182} 10183 10184function toggleRegistrationForm(elmt, event) { 10185 event.preventDefault(); 10186 var registration_form = $('#registration-form-box'); 10187 if (registration_form.css("display") == 'none') { 10188 elmt.html('Already registered?'); 10189 registration_form.show(); 10190 } 10191 else { 10192 elmt.html('Not yet registered?'); 10193 registration_form.hide(); 10194 } 10195} 10196 10197function pinterestify() { 10198 var containerId = 'content-container'; 10199 var minHeight = 100; 10200 var minWidth = 100; 10201 var imgSrc = '/assets/img/global/pinterest-save.avif'; 10202 10203 function createIcons() { 10204 var images = $('#'+containerId+' img').not('[data-pin-nopin][data-pin-no-hover]').filter(function(){ 10205 return ($(this).width() >= minWidth) && ($(this).height() >= minHeight) 10206 }) 10207 var i = 0; 10208 images.each(function() { 10209 var offset = $(this).offset(); 10210 var top = offset.top + 8; 10211 var left = offset.left + 10; 10212 var link = $("\ 10213 <a class='pinterest-icon' data-pin-do='buttonPin' data-pin-custom='true' style='display:none;position:absolute;left:"+left+"px;top:"+top+"px;width:52px;height:28px;z-index:1000;'>\ 10214 <img src='"+imgSrc+"' />\ 10215 </a>\ 10216 "); 10217 $(document.body).append(link); 10218 $(this).on('mouseenter', function() { 10219 link.show(); 10220 }); 10221 $(this).on('mouseleave', function(e) { 10222 if ((e.pageX < left || e.pageX > left + link.width()) || (e.pageY < top || e.pageY > top + link.height())) { 10223 link.hide(); 10224 } 10225 }); 10226 link.on('click', function(e) { 10227 e.preventDefault(); 10228 PinUtils.pinOne({ 10229 'url': window.location.href, 10230 'media': $(this).attr('src'), 10231 'description': $(this).attr('alt') 10232 }); 10233 }); 10234 i += 1; 10235 }); 10236 } 10237 setTimeout(function(){ 10238 createIcons(); 10239 }, 1000); 10240 10241 $(window).resize(function() { 10242 $('.pinterest-icon').remove(); 10243 createIcons(); 10244 }); 10245 10246} 10247 10248function init() { 10249 $("li:contains('SUBSCRIBE')").css("background-color", "yellow"); 10250 10251 if ($.prototype.unveil) { 10252 $("img[data-src]").unveil(); 10253 } 10254 10255 $(".hotspot").each(function() { 10256 let hotspot = new HotSpot($(this)); 10257 }); 10258 10259 $(".windowshade-button").each(function () { 10260 new WindowShade($(this)); 10261 }); 10262 10263 $(".gallery-form-add").click(function(event) { 10264 GalleryForm.addForm($(this), event); 10265 }); 10266 10267 $(".gallery-form-remove").click(function(event) { 10268 GalleryForm.removeForm($(this), event); 10269 }); 10270 10271 $(".media-form-add").click(function(event) { 10272 MediaForm.addForm($(this), event); 10273 }); 10274 10275 $(".media-form-remove").click(function(event) { 10276 MediaForm.removeForm($(this), event); 10277 }); 10278 10279 $(".favorite").click(function(event) { 10280 favorite($(this), event); 10281 }); 10282 10283 $(".editable").hover( 10284 function(event) { 10285 Editor.hoverin($(this), event); 10286 }, 10287 function(event) { 10288 Editor.hoverout($(this), event); 10289 } 10290 ); 10291 10292 $("#send-new-floorplan-notifications-button").click(function(event) { 10293 sendNewFloorPlanNotifications($(this), event); 10294 }); 10295 10296 $("#send-new-article-notifications-button").click(function(event) { 10297 sendNewArticleNotifications($(this), event); 10298 }); 10299 10300 $('#notification-checkbox').click(function(event) { 10301 toggleNotificationSubscription($(this), event); 10302 }); 10303 10304 $('*[data-toggle="popover"][data-trigger="hover"]').on("mouseover", function(event) { 10305 $(this).popover(); 10306 }); 10307 10308 $( "a.external-link" ).click(function(event) { 10309 event.preventDefault(); 10310 external_link_click(this.href); 10311 }); 10312 10313 $(document).click(function(event) { 10314 hideMobileNav(event); 10315 hideMobileSearch(event); 10316 }) 10317 10318 // Provide character counts in admin 10319 if (window.location.href.indexOf('/admin/') !== -1) { 10320 $("textarea, input[type='text']").after(function() { 10321 return "<div style='color:gray;font-size:10px;float:right;margin: 10px;'>Character Count: <span class='character-count'>"+$(this).val().length+"</span></div>"; 10322 }); 10323 $("textarea, input[type='text']").on('change keyup paste', function(event) { 10324 $(this).next().children(".character-count").first().html($(this).val().length); 10325 }); 10326 } 10327 10328 //Close popups 10329 $('.close-popup').on("click", function() { 10330 $('.popup').hide(); 10331 }); 10332 10333 $("#close-sticky-footer").on("click", function () { 10334 $("#sticky-footer").remove();
10335 setCookie("hide_sticky_footer", 1, 1); 10336 }); 10337 10338 // Stop any scripts that have not loaded after 10 seconds after the page has loaded. 10339 window.setTimeout(function() { 10340 window.stop(); 10341 }, 10000); 10342 10343 // Bind carousel control pageviews 10344 $('#featured-floorplans-slideshow a.carousel-control').on('click', function() { 10345 ga('site_tracker.send', 'pageview'); 10346 ga('site_tracker.send', 'event', 'floorplan slideshow', 'slide advance'); 10347 }); 10348 10349 $('#featured-companies-slideshow a.carousel-control').on('click', function() { 10350 ga('site_tracker.send', 'pageview'); 10351 ga('site_tracker.send', 'event', 'company slideshow', 'slide advance'); 10352 }); 10353 10354 $('#homepage-featured-companies-slideshow a.carousel-control').on('click', function() { 10355 ga('site_tracker.send', 'pageview'); 10356 ga('site_tracker.send', 'event', 'homepage company slideshow', 'slide advance'); 10357 }); 10358 10359 $('#carousel a.carousel-control').on('click', function() { 10360 ga('site_tracker.send', 'pageview'); 10361 ga('site_tracker.send', 'event', 'embedded carousel', 'slide advance'); 10362 }); 10363 10364 $(".carousel-thumbnails [data-target][data-slide-to]").on("keyup", function (e) { 10365 let elmt = $(this); 10366 let carousel = $(elmt.data('target')); 10367 if (e.keyCode == 13 && typeof carousel.carousel === 'function') { 10368 carousel.carousel(elmt.data('slide-to')); 10369 } 10370 }); 10371 10372 if (typeof($('.carousel').bcSwipe) === 'function') { 10373 $('.carousel').bcSwipe({ threshold: 50 }); 10374 } 10375 10376 $("img[src]").on("error", function () { 10377 if (!folder) { 10378 return; 10379 } 10380 let img = $(this); 10381 let src = '/assets/img/'+folder+'/placeholder.avif'; 10382 if (img.attr("src") == src) { 10383 return; 10384 } 10385 img.attr('src', src); 10386 img.addClass("missing"); 10387 }); 10388 10389 initialize_featured_companies_slideshow(); 10390 initialize_home_tours_slideshow(); 10391 // pinterestify(); 10392 get_request(); 10393 10394} 10395 10396$(document).ready(function() { 10397 init(); 10398}); 10399 10400 10401 10402 10403 10404 10405 10406!function(){;function t(t,e){var i;for(i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function e(t){if(!this||this.find!==e.prototype.find)return new e(t);if(this.length=0,t)if("string"==typeof t&&(t=this.find(t)),t.nodeType||t===t.window)this.length=1,this[0]=t;else{var i=t.length;for(this.length=i;i;)i-=1,this[i]=t[i]}}e.extend=t,e.contains=function(t,e){do if(e=e.parentNode,e===t)return!0;while(e);return!1},e.parseJSON=function(t){return window.JSON&&JSON.parse(t)},t(e.prototype,{find:function(t){var i=this[0]||document;return"string"==typeof t&&(t=i.querySelectorAll?i.querySelectorAll(t):"#"===t.charAt(0)?i.getElementById(t.slice(1)):i.getElementsByTagName(t)),new e(t)},hasClass:function(t){return!!this[0]&&new RegExp("(^|\\s+)"+t+"(\\s+|$)").test(this[0].className)},addClass:function(t){for(var e,i=this.length;i;){if(i-=1,e=this[i],!e.className)return e.className=t,this;if(this.hasClass(t))return this;e.className+=" "+t}return this},removeClass:function(t){for(var e,i=new RegExp("(^|\\s+)"+t+"(\\s+|$)"),s=this.length;s;)s-=1,e=this[s],e.className=e.className.replace(i," ");return this},on:function(t,e){for(var i,s,n=t.split(/\s+/);n.length;)for(t=n.shift(),i=this.length;i;)i-=1,s=this[i],s.addEventListener?s.addEventListener(t,e,!1):s.attachEvent&&s.attachEvent("on"+t,e);return this},off:function(t,e){for(var i,s,n=t.split(/\s+/);n.length;)for(t=n.shift(),i=this.length;i;)i-=1,s=this[i],s.removeEventListener?s.removeEventListener(t,e,!1):s.detachEvent&&s.detachEvent("on"+t,e);return this},empty:function(){for(var t,e=this.length;e;)for(e-=1,t=this[e];t.hasChildNodes();)t.removeChild(t.lastChild);return this},first:function(){return new e(this[0])}}),"function"==typeof define&&define.amd?define(function(){return e}):(window.blueimp=window.blueimp||{},window.blueimp.helper=e)}(),function(t){;"function"==typeof define&&define.amd?define(["./blueimp-helper"],t):(window.blueimp=window.blueimp||{},window.blueimp.Gallery=t(window.blueimp.helper||window.jQuery))}(function(t){;function e(t,i){return void 0===document.body.style.maxHeight?null:this&&this.options===e.prototype.options?t&&t.length?(this.list=t,this.num=t.length,this.initOptions(i),void this.initialize()):void this.console.log("blueimp Gallery: No or empty list provided as first argument.",t):new e(t,i)}return t.extend(e.prototype,{options:{container:"#blueimp-gallery",slidesContainer:"div",titleElement:"h3",displayClass:"blueimp-gallery-display",controlsClass:"blueimp-gallery-controls",singleClass:"blueimp-gallery-single",leftEdgeClass:"blueimp-gallery-left",rightEdgeClass:"blueimp-gallery-right",playingClass:"blueimp-gallery-playing",slideClass:"slide",slideLoadingClass:"slide-loading",slideErrorClass:"slide-error",slideContentClass:"slide-content",toggleClass:"toggle",prevClass:"prev",nextClass:"next",closeClass:"close",playPauseClass:"play-pause",typeProperty:"type",titleProperty:"title",urlProperty:"href",srcsetProperty:"urlset",displayTransition:!0,clearSlides:!0,stretchImages:!1,toggleControlsOnReturn:!0,toggleControlsOnSlideClick:!0,toggleSlideshowOnSpace:!0,enableKeyboardNavigation:!0,closeOnEscape:!0,closeOnSlideClick:!0,closeOnSwipeUpOrDown:!0,emulateTouchEvents:!0,stopTouchEventsPropagation:!1,hidePageScrollbars:!0,disableScroll:!0,
10406carousel:!1,continuous:!0,unloadElements:!0,startSlideshow:!1,slideshowInterval:5e3,index:0,preloadRange:2,transitionSpeed:400,slideshowTransitionSpeed:void 0,event:void 0,onopen:void 0,onopened:void 0,onslide:void 0,onslideend:void 0,onslidecomplete:void 0,onclose:void 0,onclosed:void 0},carouselOptions:{hidePageScrollbars:!1,toggleControlsOnReturn:!1,toggleSlideshowOnSpace:!1,enableKeyboardNavigation:!1,closeOnEscape:!1,closeOnSlideClick:!1,closeOnSwipeUpOrDown:!1,disableScroll:!1,startSlideshow:!0},console:window.console&&"function"==typeof window.console.log?window.console:{log:function(){}},support:function(e){function i(){var t,i,s=n.transition;document.body.appendChild(e),s&&(t=s.name.slice(0,-9)+"ransform",void 0!==e.style[t]&&(e.style[t]="translateZ(0)",i=window.getComputedStyle(e).getPropertyValue(s.prefix+"transform"),n.transform={prefix:s.prefix,name:t,translate:!0,translateZ:!!i&&"none"!==i})),void 0!==e.style.backgroundSize&&(n.backgroundSize={},e.style.backgroundSize="contain",n.backgroundSize.contain="contain"===window.getComputedStyle(e).getPropertyValue("background-size"),e.style.backgroundSize="cover",n.backgroundSize.cover="cover"===window.getComputedStyle(e).getPropertyValue("background-size")),document.body.removeChild(e)}var s,n={touch:void 0!==window.ontouchstart||window.DocumentTouch&&document instanceof DocumentTouch},o={webkitTransition:{end:"webkitTransitionEnd",prefix:"-webkit-"},MozTransition:{end:"transitionend",prefix:"-moz-"},OTransition:{end:"otransitionend",prefix:"-o-"},transition:{end:"transitionend",prefix:""}};for(s in o)if(o.hasOwnProperty(s)&&void 0!==e.style[s]){n.transition=o[s],n.transition.name=s;break}return document.body?i():t(document).on("DOMContentLoaded",i),n}(document.createElement("div")),requestAnimationFrame:window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame,initialize:function(){return this.initStartIndex(),this.initWidget()!==!1&&(this.initEventListeners(),this.onslide(this.index),this.ontransitionend(),void(this.options.startSlideshow&&this.play()))},slide:function(t,e){window.clearTimeout(this.timeout);var i,s,n,o=this.index;if(o!==t&&1!==this.num){if(e||(e=this.options.transitionSpeed),this.support.transform){for(this.options.continuous||(t=this.circle(t)),i=Math.abs(o-t)/(o-t),this.options.continuous&&(s=i,i=-this.positions[this.circle(t)]/this.slideWidth,i!==s&&(t=-i*this.num+t)),n=Math.abs(o-t)-1;n;)n-=1,this.move(this.circle((t>o?t:o)-n-1),this.slideWidth*i,0);t=this.circle(t),this.move(o,this.slideWidth*i,e),this.move(t,0,e),this.options.continuous&&this.move(this.circle(t-i),-(this.slideWidth*i),0)}else t=this.circle(t),this.animate(o*-this.slideWidth,t*-this.slideWidth,e);this.onslide(t)}},getIndex:function(){return this.index},getNumber:function(){return this.num},prev:function(){(this.options.continuous||this.index)&&this.slide(this.index-1)},next:function(){(this.options.continuous||this.index<this.num-1)&&this.slide(this.index+1)},play:function(t){var e=this;window.clearTimeout(this.timeout),this.interval=t||this.options.slideshowInterval,this.elements[this.index]>1&&(this.timeout=this.setTimeout(!this.requestAnimationFrame&&this.slide||function(t,i){e.animationFrameId=e.requestAnimationFrame.call(window,function(){e.slide(t,i)})},[this.index+1,this.options.slideshowTransitionSpeed],this.interval)),this.container.addClass(this.options.playingClass)},pause:function(){window.clearTimeout(this.timeout),this.interval=null,this.container.removeClass(this.options.playingClass)},add:function(t){var e;for(t.concat||(t=Array.prototype.slice.call(t)),this.list.concat||(this.list=Array.prototype.slice.call(this.list)),this.list=this.list.concat(t),this.num=this.list.length,this.num>2&&null===this.options.continuous&&(this.options.continuous=!0,this.container.removeClass(this.options.leftEdgeClass)),this.container.removeClass(this.options.rightEdgeClass).removeClass(this.options.singleClass),e=this.num-t.length;e<this.num;e+=1)this.addSlide(e),this.positionSlide(e);this.positions.length=this.num,this.initSlides(!0)},resetSlides:function(){this.slidesContainer.empty(),this.unloadAllSlides(),this.slides=[]},handleClose:function(){var t=this.options;this.destroyEventListeners(),this.pause(),this.container[0].style.display="none",this.container.removeClass(t.displayClass).removeClass(t.singleClass).removeClass(t.leftEdgeClass).removeClass(t.rightEdgeClass),t.hidePageScrollbars&&(document.body.style.overflow=this.bodyOverflowStyle),this.options.clearSlides&&this.resetSlides(),this.options.onclosed&&this.options.onclosed.call(this)},close:function(){function t(i){i.target===e.container[0]&&(e.container.off(e.support.transition.end,t),e.handleClose())}var e=this;this.options.onclose&&this.options.onclose.call(this),this.support.transition&&this.options.displayTransition?(this.container.on(this.support.transition.end,t),this.container.removeClass(this.options.displayClass)):this.handleClose()},circle:function(t){return(this.num+t%this.num)%this.num}
10406,move:function(t,e,i){this.translateX(t,e,i),this.positions[t]=e},translate:function(t,e,i,s){var n=this.slides[t].style,o=this.support.transition,r=this.support.transform;n[o.name+"Duration"]=s+"ms",n[r.name]="translate("+e+"px, "+i+"px)"+(r.translateZ?" translateZ(0)":"")},translateX:function(t,e,i){this.translate(t,e,0,i)},translateY:function(t,e,i){this.translate(t,0,e,i)},animate:function(t,e,i){if(!i)return void(this.slidesContainer[0].style.left=e+"px");var s=this,n=(new Date).getTime(),o=window.setInterval(function(){var r=(new Date).getTime()-n;return r>i?(s.slidesContainer[0].style.left=e+"px",s.ontransitionend(),void window.clearInterval(o)):void(s.slidesContainer[0].style.left=(e-t)*(Math.floor(r/i*100)/100)+t+"px")},4)},preventDefault:function(t){t.preventDefault?t.preventDefault():t.returnValue=!1},stopPropagation:function(t){t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},onresize:function(){this.initSlides(!0)},onmousedown:function(t){t.which&&1===t.which&&"VIDEO"!==t.target.nodeName&&(t.preventDefault(),(t.originalEvent||t).touches=[{pageX:t.pageX,pageY:t.pageY}],this.ontouchstart(t))},onmousemove:function(t){this.touchStart&&((t.originalEvent||t).touches=[{pageX:t.pageX,pageY:t.pageY}],this.ontouchmove(t))},onmouseup:function(t){this.touchStart&&(this.ontouchend(t),delete this.touchStart)},onmouseout:function(e){if(this.touchStart){var i=e.target,s=e.relatedTarget;s&&(s===i||t.contains(i,s))||this.onmouseup(e)}},ontouchstart:function(t){this.options.stopTouchEventsPropagation&&this.stopPropagation(t);var e=(t.originalEvent||t).touches[0];this.touchStart={x:e.pageX,y:e.pageY,time:Date.now()},this.isScrolling=void 0,this.touchDelta={}},ontouchmove:function(t){this.options.stopTouchEventsPropagation&&this.stopPropagation(t);var e,i,s=(t.originalEvent||t).touches[0],n=(t.originalEvent||t).scale,o=this.index;if(!(s.length>1||n&&1!==n))if(this.options.disableScroll&&t.preventDefault(),this.touchDelta={x:s.pageX-this.touchStart.x,y:s.pageY-this.touchStart.y},e=this.touchDelta.x,void 0===this.isScrolling&&(this.isScrolling=this.isScrolling||Math.abs(e)<Math.abs(this.touchDelta.y)),this.isScrolling)this.options.closeOnSwipeUpOrDown&&this.translateY(o,this.touchDelta.y+this.positions[o],0);else for(t.preventDefault(),window.clearTimeout(this.timeout),this.options.continuous?i=[this.circle(o+1),o,this.circle(o-1)]:(this.touchDelta.x=e/=!o&&e>0||o===this.num-1&&e<0?Math.abs(e)/this.slideWidth+1:1,i=[o],o&&i.push(o-1),o<this.num-1&&i.unshift(o+1));i.length;)o=i.pop(),this.translateX(o,e+this.positions[o],0)},ontouchend:function(t){this.options.stopTouchEventsPropagation&&this.stopPropagation(t);var e,i,s,n,o,r=this.index,l=this.options.transitionSpeed,a=this.slideWidth,h=Number(Date.now()-this.touchStart.time)<250,d=h&&Math.abs(this.touchDelta.x)>20||Math.abs(this.touchDelta.x)>a/2,c=!r&&this.touchDelta.x>0||r===this.num-1&&this.touchDelta.x<0,u=!d&&this.options.closeOnSwipeUpOrDown&&(h&&Math.abs(this.touchDelta.y)>20||Math.abs(this.touchDelta.y)>this.slideHeight/2);this.options.continuous&&(c=!1),e=this.touchDelta.x<0?-1:1,this.isScrolling?u?this.close():this.translateY(r,0,l):d&&!c?(i=r+e,s=r-e,n=a*e,o=-a*e,this.options.continuous?(this.move(this.circle(i),n,0),this.move(this.circle(r-2*e),o,0)):i>=0&&i<this.num&&this.move(i,n,0),this.move(r,this.positions[r]+n,l),this.move(this.circle(s),this.positions[this.circle(s)]+n,l),r=this.circle(s),this.onslide(r)):this.options.continuous?(this.move(this.circle(r-1),-a,l),this.move(r,0,l),this.move(this.circle(r+1),a,l)):(r&&this.move(r-1,-a,l),this.move(r,0,l),r<this.num-1&&this.move(r+1,a,l))},ontouchcancel:function(t){this.touchStart&&(this.ontouchend(t),delete this.touchStart)},ontransitionend:function(t){var e=this.slides[this.index];t&&e!==t.target||(this.interval&&this.play(),this.setTimeout(this.options.onslideend,[this.index,e]))},oncomplete:function(e){var i,s=e.target||e.srcElement,n=s&&s.parentNode;s&&n&&(i=this.getNodeIndex(n),t(n).removeClass(this.options.slideLoadingClass),"error"===e.type?(t(n).addClass(this.options.slideErrorClass),this.elements[i]=3):this.elements[i]=2,s.clientHeight>this.container[0].clientHeight&&(s.style.maxHeight=this.container[0].clientHeight),this.interval&&this.slides[this.index]===n&&this.play(),this.setTimeout(this.options.onslidecomplete,[i,n]))},onload:function(t){this.oncomplete(t)},onerror:function(t){this.oncomplete(t)},onkeydown:function(t){switch(t.which||t.keyCode){case 13:this.options.toggleControlsOnReturn&&(this.preventDefault(t),this.toggleControls());break;case 27:this.options.closeOnEscape&&(this.close(),t.stopImmediatePropagation());break;case 32:this.options.toggleSlideshowOnSpace&&(this.preventDefault(t),this.toggleSlideshow());break;case 37:this.options.enableKeyboardNavigation&&(this.preventDefault(t),this.prev());break;case 39:this.options.enableKeyboardNavigation&&(this.preventDefault(t),this.next())}},handleClick:function(e){function i(e){return t(n).hasClass(e)||t(o).hasClass(e)}var s=this.options,n=e.target||e.srcElement,o=n.parentNode;i(s.toggleClass)?(this.preventDefault(e),this.toggleControls()):i(s.prevClass)?(this.preventDefault(e),this.prev()):i(s.nextClass)?(this.preventDefault(e),this.next()):i(s.closeClass)?(this.preventDefault(e),this.close()):i(s.playPauseClass)?(this.preventDefault(e),this.toggleSlideshow()):o===this.slidesContainer[0]?s.closeOnSlideClick?(this.preventDefault(e),this.close()):s.toggleControlsOnSlideClick&&(this.preventDefault(e),this.toggleControls()):o.parentNode&&o.parentNode===this.slidesContainer[0]&&s.toggleControlsOnSlideClick&&(this.preventDefault(e),this.toggleControls())},onclick:function(t){return this.options.emulateTouchEvents&&this.touchDelta&&(Math.abs(this.touchDelta.x)>20||Math.abs(this.touchDelta.y)>20)?void delete this.touchDelta:this.handleClick(t)},updateEdgeClasses:function(t){t?this.container.removeClass(this.options.leftEdgeClass):this.container.addClass(this.options.leftEdgeClass),t===this.num-1?this.container.addClass(this.options.rightEdgeClass):this.container.removeClass(this.options.rightEdgeClass)},handleSlide:function(t){this.options.continuous||this.updateEdgeClasses(t),this.loadElements(t),this.options.unloadElements&&this.unloadElements(t),this.setTitle(t)}
10406,onslide:function(t){this.index=t,this.handleSlide(t),this.setTimeout(this.options.onslide,[t,this.slides[t]])},setTitle:function(t){var e=this.slides[t].firstChild.title,i=this.titleElement;i.length&&(this.titleElement.empty(),e&&i[0].appendChild(document.createTextNode(e)))},setTimeout:function(t,e,i){var s=this;return t&&window.setTimeout(function(){t.apply(s,e||[])},i||0)},imageFactory:function(e,i){function s(e){if(!n){if(e={type:e.type,target:o},!o.parentNode)return l.setTimeout(s,[e]);n=!0,t(a).off("load error",s),d&&"load"===e.type&&(o.style.background='url("'+h+'") center no-repeat',o.style.backgroundSize=d),i(e)}}var n,o,r,l=this,a=this.imagePrototype.cloneNode(!1),h=e,d=this.options.stretchImages;return"string"!=typeof h&&(h=this.getItemProperty(e,this.options.urlProperty),r=this.getItemProperty(e,this.options.titleProperty)),d===!0&&(d="contain"),d=this.support.backgroundSize&&this.support.backgroundSize[d]&&d,d?o=this.elementPrototype.cloneNode(!1):(o=a,a.draggable=!1),r&&(o.title=r),t(a).on("load error",s),a.src=h,o},createElement:function(e,i){var s=e&&this.getItemProperty(e,this.options.typeProperty),n=s&&this[s.split("/")[0]+"Factory"]||this.imageFactory,o=e&&n.call(this,e,i),r=this.getItemProperty(e,this.options.srcsetProperty);return o||(o=this.elementPrototype.cloneNode(!1),this.setTimeout(i,[{type:"error",target:o}])),r&&o.setAttribute("srcset",r),t(o).addClass(this.options.slideContentClass),o},loadElement:function(e){this.elements[e]||(this.slides[e].firstChild?this.elements[e]=t(this.slides[e]).hasClass(this.options.slideErrorClass)?3:2:(this.elements[e]=1,t(this.slides[e]).addClass(this.options.slideLoadingClass),this.slides[e].appendChild(this.createElement(this.list[e],this.proxyListener))))},loadElements:function(t){var e,i=Math.min(this.num,2*this.options.preloadRange+1),s=t;for(e=0;e<i;e+=1)s+=e*(e%2===0?-1:1),s=this.circle(s),this.loadElement(s)},unloadElements:function(t){var e,i;for(e in this.elements)this.elements.hasOwnProperty(e)&&(i=Math.abs(t-e),i>this.options.preloadRange&&i+this.options.preloadRange<this.num&&(this.unloadSlide(e),delete this.elements[e]))},addSlide:function(t){var e=this.slidePrototype.cloneNode(!1);e.setAttribute("data-index",t),this.slidesContainer[0].appendChild(e),this.slides.push(e)},positionSlide:function(t){var e=this.slides[t];e.style.width=this.slideWidth+"px",this.support.transform&&(e.style.left=t*-this.slideWidth+"px",this.move(t,this.index>t?-this.slideWidth:this.index<t?this.slideWidth:0,0))},initSlides:function(e){var i,s;for(e||(this.positions=[],this.positions.length=this.num,this.elements={},this.imagePrototype=document.createElement("img"),this.elementPrototype=document.createElement("div"),this.slidePrototype=document.createElement("div"),t(this.slidePrototype).addClass(this.options.slideClass),this.slides=this.slidesContainer[0].children,i=this.options.clearSlides||this.slides.length!==this.num),this.slideWidth=this.container[0].offsetWidth,this.slideHeight=this.container[0].offsetHeight,this.slidesContainer[0].style.width=this.num*this.slideWidth+"px",i&&this.resetSlides(),s=0;s<this.num;s+=1)i&&this.addSlide(s),this.positionSlide(s);this.options.continuous&&this.support.transform&&(this.move(this.circle(this.index-1),-this.slideWidth,0),this.move(this.circle(this.index+1),this.slideWidth,0)),this.support.transform||(this.slidesContainer[0].style.left=this.index*-this.slideWidth+"px")},unloadSlide:function(t){var e,i;e=this.slides[t],i=e.firstChild,null!==i&&e.removeChild(i)},unloadAllSlides:function(){var t,e;for(t=0,e=this.slides.length;t<e;t++)this.unloadSlide(t)},toggleControls:function(){var t=this.options.controlsClass;this.container.hasClass(t)?this.container.removeClass(t):this.container.addClass(t)},toggleSlideshow:function(){this.interval?this.pause():this.play()},getNodeIndex:function(t){return parseInt(t.getAttribute("data-index"),10)},getNestedProperty:function(t,e){return e.replace(/\[(?:'([^']+)'|"([^"]+)"|(\d+))\]|(?:(?:^|\.)([^\.\[]+))/g,function(e,i,s,n,o){var r=o||i||s||n&&parseInt(n,10);e&&t&&(t=t[r])}),t},getDataProperty:function(e,i){if(e.getAttribute){var s=e.getAttribute("data-"+i.replace(/([A-Z])/g,"-$1").toLowerCase());if("string"==typeof s){if(/^(true|false|null|-?\d+(\.\d+)?|\{[\s\S]*\}|\[[\s\S]*\])$/.test(s))try{return t.parseJSON(s)}catch(t){}return s}}},getItemProperty:function(t,e){var i=t[e];return void 0===i&&(i=this.getDataProperty(t,e),void 0===i&&(i=this.getNestedProperty(t,e))),i},initStartIndex:function(){var t,e=this.options.index,i=this.options.urlProperty;if(e&&"number"!=typeof e)for(t=0;t<this.num;t+=1)if(this.list[t]===e||this.getItemProperty(this.list[t],i)===this.getItemProperty(e,i)){e=t;break}this.index=this.circle(parseInt(e,10)||0)},initEventListeners:function(){function e(t){var e=i.support.transition&&i.support.transition.end===t.type?"transitionend":t.type;i["on"+e](t)}var i=this,s=this.slidesContainer;t(window).on("resize",e),t(document.body).on("keydown",e),this.container.on("click",e),this.support.touch?s.on("touchstart touchmove touchend touchcancel",e):this.options.emulateTouchEvents&&this.support.transition&&s.on("mousedown mousemove mouseup mouseout",e),this.support.transition&&s.on(this.support.transition.end,e),this.proxyListener=e},destroyEventListeners:function(){var e=this.slidesContainer,i=this.proxyListener;t(window).off("resize",i),t(document.body).off("keydown",i),this.container.off("click",i),this.support.touch?e.off("touchstart touchmove touchend touchcancel",i):this.options.emulateTouchEvents&&this.support.transition&&e.off("mousedown mousemove mouseup mouseout",i),this.support.transition&&e.off(this.support.transition.end,i)},handleOpen:function(){this.options.onopened&&this.options.onopened.call(this)},initWidget:function(){function e(t){t.target===i.container[0]&&(i.container.off(i.support.transition.end,e),i.handleOpen())}var i=this;return this.container=t(this.options.container),this.container.length?(this.slidesContainer=this.container.find(this.options.slidesContainer).first(),this.slidesContainer.length?(this.titleElement=this.container.find(this.options.titleElement).first(),1===this.num&&this.container.addClass(this.options.singleClass),this.options.onopen&&this.options.onopen.call(this),this.support.transition&&this.options.displayTransition?this.container.on(this.support.transition.end,e):this.handleOpen(),this.options.hidePageScrollbars&&(this.bodyOverflowStyle=document.body.style.overflow,document.body.style.overflow="hidden"),this.container[0].style.display="block",this.initSlides(),void this.container.addClass(this.options.displayClass)):(this.console.log("blueimp Gallery: Slides container not found.",this.options.slidesContainer),!1)):(this.console.log("blueimp Gallery: Widget container not found.",this.options.container),!1)},initOptions:function(e){this.options=t.extend({},this.options),(e&&e.carousel||this.options.carousel&&(!e||e.carousel!==!1))&&t.extend(this.options,this.carouselOptions),t.extend(this.options,e),this.num<3&&(this.options.continuous=!!this.options.continuous&&null),this.support.transition||(this.options.emulateTouchEvents=!1),this.options.event&&this.preventDefault(this.options.event)}}),e}),function(t){;"function"==typeof define&&define.amd?define(["./blueimp-helper","./blueimp-gallery"],t):t(window.blueimp.helper||window.jQuery,window.blueimp.Gallery)}(function(t,e){;t.extend(e.prototype.options,{fullScreen:!1});var i=e.prototype.initialize,s=e.prototype.close;return t.extend(e.prototype,{getFullScreenElement:function(){return document.fullscreenElement||document.webkitFullscreenElement||document.mozFullScreenElement||document.msFullscreenElement},requestFullScreen:function(t){t.requestFullscreen?t.requestFullscreen():t.webkitRequestFullscreen?t.webkitRequestFullscreen():t.mozRequestFullScreen?t.mozRequestFullScreen():t.msRequestFullscreen&&t.msRequestFullscreen()},exitFullScreen:function(){document.exitFullscreen?document.exitFullscreen():document.webkitCancelFullScreen?document.webkitCancelFullScreen():document.mozCancelFullScreen?document.mozCancelFullScreen():document.msExitFullscreen&&document.msExitFullscreen()},initialize:function(){i.call(this),this.options.fullScreen&&!this.getFullScreenElement()&&this.requestFullScreen(this.container[0])},close:function(){this.getFullScreenElement()===this.container[0]&&this.exitFullScreen(),s.call(this)}}),e}),function(t){;"function"==typeof define&&define.amd?define(["./blueimp-helper","./blueimp-gallery"],t):t(window.blueimp.helper||window.jQuery,window.blueimp.Gallery)}(function(t,e){;t.extend(e.prototype.options,{indicatorContainer:"ol",activeIndicatorClass:"active",thumbnailProperty:"thumbnail",thumbnailIndicators:!0});var i=e.prototype.initSlides,s=e.prototype.addSlide,n=e.prototype.resetSlides,o=e.prototype.handleClick,r=e.prototype.handleSlide,l=e.prototype.handleClose;return t.extend(e.prototype,{createIndicator:function(e){var i,s,n=this.indicatorPrototype.cloneNode(!1),o=this.getItemProperty(e,this.options.titleProperty),r=this.options.thumbnailProperty;return this.options.thumbnailIndicators&&(r&&(i=this.getItemProperty(e,r)),void 0===i&&(s=e.getElementsByTagName&&t(e).find("img")[0],s&&(i=s.src
10406)),i&&(n.style.backgroundImage='url("'+i+'")')),o&&(n.title=o),n},addIndicator:function(t){if(this.indicatorContainer.length){var e=this.createIndicator(this.list[t]);e.setAttribute("data-index",t),this.indicatorContainer[0].appendChild(e),this.indicators.push(e)}},setActiveIndicator:function(e){this.indicators&&(this.activeIndicator&&this.activeIndicator.removeClass(this.options.activeIndicatorClass),this.activeIndicator=t(this.indicators[e]),this.activeIndicator.addClass(this.options.activeIndicatorClass))},initSlides:function(t){t||(this.indicatorContainer=this.container.find(this.options.indicatorContainer),this.indicatorContainer.length&&(this.indicatorPrototype=document.createElement("li"),this.indicators=this.indicatorContainer[0].children)),i.call(this,t)},addSlide:function(t){s.call(this,t),this.addIndicator(t)},resetSlides:function(){n.call(this),this.indicatorContainer.empty(),this.indicators=[]},handleClick:function(t){var e=t.target||t.srcElement,i=e.parentNode;if(i===this.indicatorContainer[0])this.preventDefault(t),this.slide(this.getNodeIndex(e));else{if(i.parentNode!==this.indicatorContainer[0])return o.call(this,t);this.preventDefault(t),this.slide(this.getNodeIndex(i))}},handleSlide:function(t){r.call(this,t),this.setActiveIndicator(t)},handleClose:function(){this.activeIndicator&&this.activeIndicator.removeClass(this.options.activeIndicatorClass),l.call(this)}}),e}),function(t){;"function"==typeof define&&define.amd?define(["./blueimp-helper","./blueimp-gallery"],t):t(window.blueimp.helper||window.jQuery,window.blueimp.Gallery)}(function(t,e){;t.extend(e.prototype.options,{videoContentClass:"video-content",videoLoadingClass:"video-loading",videoPlayingClass:"video-playing",videoPosterProperty:"poster",videoSourcesProperty:"sources"});var i=e.prototype.handleSlide;return t.extend(e.prototype,{handleSlide:function(t){i.call(this,t),this.playingVideo&&this.playingVideo.pause()},videoFactory:function(e,i,s){var n,o,r,l,a,h=this,d=this.options,c=this.elementPrototype.cloneNode(!1),u=t(c),p=[{type:"error",target:c}],m=s||document.createElement("video"),y=this.getItemProperty(e,d.urlProperty),f=this.getItemProperty(e,d.typeProperty),g=this.getItemProperty(e,d.titleProperty),v=this.getItemProperty(e,d.videoPosterProperty),C=this.getItemProperty(e,d.videoSourcesProperty);if(u.addClass(d.videoContentClass),g&&(c.title=g),m.canPlayType)if(y&&f&&m.canPlayType(f))m.src=y;else if(C)for(;C.length;)if(o=C.shift(),y=this.getItemProperty(o,d.urlProperty),f=this.getItemProperty(o,d.typeProperty),y&&f&&m.canPlayType(f)){m.src=y;break}return v&&(m.poster=v,n=this.imagePrototype.cloneNode(!1),t(n).addClass(d.toggleClass),n.src=v,n.draggable=!1,c.appendChild(n)),r=document.createElement("a"),r.setAttribute("target","_blank"),s||r.setAttribute("download",g),r.href=y,m.src&&(m.controls=!0,(s||t(m)).on("error",function(){h.setTimeout(i,p)}).on("pause",function(){m.seeking||(l=!1,u.removeClass(h.options.videoLoadingClass).removeClass(h.options.videoPlayingClass),a&&h.container.addClass(h.options.controlsClass),delete h.playingVideo,h.interval&&h.play())}).on("playing",function(){l=!1,u.removeClass(h.options.videoLoadingClass).addClass(h.options.videoPlayingClass),h.container.hasClass(h.options.controlsClass)?(a=!0,h.container.removeClass(h.options.controlsClass)):a=!1}).on("play",function(){window.clearTimeout(h.timeout),l=!0,u.addClass(h.options.videoLoadingClass),h.playingVideo=m}),t(r).on("click",function(t){h.preventDefault(t),l?m.pause():m.play()}),c.appendChild(s&&s.element||m)),c.appendChild(r),this.setTimeout(i,[{type:"load",target:c}]),c}}),e}),function(t){;"function"==typeof define&&define.amd?define(["./blueimp-helper","./blueimp-gallery-video"],t):t(window.blueimp.helper||window.jQuery,window.blueimp.Gallery)}(function(t,e){;if(!window.postMessage)return e;t.extend(e.prototype.options,{vimeoVideoIdProperty:"vimeo",vimeoPlayerUrl:"//player.vimeo.com/video/VIDEO_ID?api=1&player_id=PLAYER_ID",vimeoPlayerIdPrefix:"vimeo-player-",vimeoClickToPlay:!0});var i=e.prototype.textFactory||e.prototype.imageFactory,s=function(t,e,
10406i,s){this.url=t,this.videoId=e,this.playerId=i,this.clickToPlay=s,this.element=document.createElement("div"),this.listeners={}},n=0;return t.extend(s.prototype,{canPlayType:function(){return!0},on:function(t,e){return this.listeners[t]=e,this},loadAPI:function(){function e(){!s&&n.playOnReady&&n.play(),s=!0}for(var i,s,n=this,o="//f.vimeocdn.com/js/froogaloop2.min.js",r=document.getElementsByTagName("script"),l=r.length;l;)if(l-=1,r[l].src===o){i=r[l];break}i||(i=document.createElement("script"),i.src=o),t(i).on("load",e),r[0].parentNode.insertBefore(i,r[0]),/loaded|complete/.test(i.readyState)&&e()},onReady:function(){var t=this;this.ready=!0,this.player.addEvent("play",function(){t.hasPlayed=!0,t.onPlaying()}),this.player.addEvent("pause",function(){t.onPause()}),this.player.addEvent("finish",function(){t.onPause()}),this.playOnReady&&this.play()},onPlaying:function(){this.playStatus<2&&(this.listeners.playing(),this.playStatus=2)},onPause:function(){this.listeners.pause(),delete this.playStatus},insertIframe:function(){var t=document.createElement("iframe");t.src=this.url.replace("VIDEO_ID",this.videoId).replace("PLAYER_ID",this.playerId),t.id=this.playerId,this.element.parentNode.replaceChild(t,this.element),this.element=t},play:function(){var t=this;this.playStatus||(this.listeners.play(),this.playStatus=1),this.ready?!this.hasPlayed&&(this.clickToPlay||window.navigator&&/iP(hone|od|ad)/.test(window.navigator.platform))?this.onPlaying():this.player.api("play"):(this.playOnReady=!0,window.$f?this.player||(this.insertIframe(),this.player=$f(this.element),this.player.addEvent("ready",function(){t.onReady()})):this.loadAPI())},pause:function(){this.ready?this.player.api("pause"):this.playStatus&&(delete this.playOnReady,this.listeners.pause(),delete this.playStatus)}}),t.extend(e.prototype,{VimeoPlayer:s,textFactory:function(t,e){var o=this.options,r=this.getItemProperty(t,o.vimeoVideoIdProperty);return r?(void 0===this.getItemProperty(t,o.urlProperty)&&(t[o.urlProperty]="//vimeo.com/"+r),n+=1,this.videoFactory(t,e,new s(o.vimeoPlayerUrl,r,o.vimeoPlayerIdPrefix+n,o.vimeoClickToPlay))):i.call(this,t,e)}}),e}),function(t){;"function"==typeof define&&define.amd?define(["./blueimp-helper","./blueimp-gallery-video"],t):t(window.blueimp.helper||window.jQuery,window.blueimp.Gallery)}(function(t,e){;if(!window.postMessage)return e;t.extend(e.prototype.options,{youTubeVideoIdProperty:"youtube",youTubePlayerVars:{wmode:"transparent"},youTubeClickToPlay:!0});var i=e.prototype.textFactory||e.prototype.imageFactory,s=function(t,e,i){this.videoId=t,this.playerVars=e,this.clickToPlay=i,this.element=document.createElement("div"),this.listeners={}};return t.extend(s.prototype,{canPlayType:function(){return!0},on:function(t,e){return this.listeners[t]=e,this},loadAPI:function(){var t,e=this,i=window.onYouTubeIframeAPIReady,s="//www.youtube.com/iframe_api",n=document.getElementsByTagName("script"),o=n.length;for(window.onYouTubeIframeAPIReady=function(){i&&i.apply(this),e.playOnReady&&e.play()};o;)if(o-=1,n[o].src===s)return;t=document.createElement("script"),t.src=s,n[0].parentNode.insertBefore(t,n[0])},onReady:function(){this.ready=!0,this.playOnReady&&this.play()},onPlaying:function(){this.playStatus<2&&(this.listeners.playing(),this.playStatus=2)},onPause:function(){e.prototype.setTimeout.call(this,this.checkSeek,null,2e3)},checkSeek:function(){this.stateChange!==YT.PlayerState.PAUSED&&this.stateChange!==YT.PlayerState.ENDED||(this.listeners.pause(),delete this.playStatus)},onStateChange:function(t){switch(t.data){case YT.PlayerState.PLAYING:this.hasPlayed=!0,this.onPlaying();break;case YT.PlayerState.PAUSED:case YT.PlayerState.ENDED:this.onPause()}this.stateChange=t.data},onError:function(t){this.listeners.error(t)},play:function(){var t=this;this.playStatus||(this.listeners.play(),this.playStatus=1),this.ready?!this.hasPlayed&&(this.clickToPlay||window.navigator&&/iP(hone|od|ad)/.test(window.navigator.platform))?this.onPlaying():this.player.playVideo():(this.playOnReady=!0, 10407window.YT&&YT.Player?this.player||(this.player=new YT.Player(this.element,{videoId:this.videoId,playerVars:this.playerVars,events:{onReady:function(){t.onReady()},onStateChange:function(e){t.onStateChange(e)},onError:function(e){t.onError(e)}}})):this.loadAPI())},pause:function(){this.ready?this.player.pauseVideo():this.playStatus&&(delete this.playOnReady,this.listeners.pause(),delete this.playStatus)}}),t.extend(e.prototype,{YouTubePlayer:s,textFactory:function(t,e){var n=this.options,o=this.getItemProperty(t,n.youTubeVideoIdProperty);return o?(void 0===this.getItemProperty(t,n.urlProperty)&&(t[n.urlProperty]="//www.youtube.com/watch?v="+o),void 0===this.getItemProperty(t,n.videoPosterProperty)&&(t[n.videoPosterProperty]="//img.youtube.com/vi/"+o+"/maxresdefault.jpg"),this.videoFactory(t,e,new s(o,n.youTubePlayerVars,n.youTubeClickToPlay))):i.call(this,t,e)}}),e}); 10408//# sourceMappingURL=blueimp-gallery.min.js.map 10409 10410 10411 10412/* PLEASE DO NOT COPY AND PASTE THIS CODE. */(function(){var w=window,C='___grecaptcha_cfg',cfg=w[C]=w[C]||{},N='grecaptcha';var gr=w[N]=w[N]||{};gr.ready=gr.ready||function(f){(cfg['fns']=cfg['fns']||[]).push(f);};w['__recaptcha_api']='https://www.google.com/recaptcha/api2/';(cfg['render']=cfg['render']||[]).push('onload');(cfg['anchor-ms']=cfg['anchor-ms']||[]).push(20000);(cfg['execute-ms']=cfg['execute-ms']||[]).push(30000);w['__google_recaptcha_client']=true;var d=document,po=d.createElement('script');po.type='text/javascript';po.async=true; po.charset='utf-8';
vendor: 5,379 bytes, lines 10412-10545
10412po.src='https://www.gstatic.com/recaptcha/releases/kemdRjWFxNjgsGdhRslyEPwU/recaptcha__en.js';po.crossOrigin='anonymous';po.integrity='sha384-toUWaWp6L6nokvZl8QQUGm7dsWYyqVz4D3KmfSkWqx5lUaKsDkc76RU6HbKjCLLn';var e=d.querySelector('script[nonce]'),n=e&&(e['nonce']||e.getAttribute('nonce'));if(n){po.setAttribute('nonce',n);}var s=d.getElementsByTagName('script')[0];s.parentNode.insertBefore(po, s);})(); 10413 10414 10415 10416/*! 10417 * Lightbox v2.10.0 10418 * by Lokesh Dhakar 10419 * 10420 * More info: 10421 * http://lokeshdhakar.com/projects/lightbox2/ 10422 * 10423 * Copyright 2007, 2018 Lokesh Dhakar 10424 * Released under the MIT license 10425 * https://github.com/lokesh/lightbox2/blob/master/LICENSE 10426 * 10427 * @preserve 10428 */ 10429 10430// Uses Node, AMD or browser globals to create a module. 10431(function (root, factory) { 10432 if (typeof define === 'function' && define.amd) { 10433 // AMD. Register as an anonymous module. 10434 define(['jquery'], factory); 10435 } else if (typeof exports === 'object') { 10436 // Node. Does not work with strict CommonJS, but 10437 // only CommonJS-like environments that support module.exports, 10438 // like Node. 10439 module.exports = factory(require('jquery')); 10440 } else { 10441 // Browser globals (root is window) 10442 root.lightbox = factory(root.jQuery); 10443 } 10444}(this, function ($) { 10445 10446 function Lightbox(options) { 10447 this.album = []; 10448 this.currentImageIndex = void 0; 10449 this.init(); 10450 10451 // options 10452 this.options = $.extend({}, this.constructor.defaults); 10453 this.option(options); 10454 } 10455 10456 // Descriptions of all options available on the demo site: 10457 // http://lokeshdhakar.com/projects/lightbox2/index.html#options 10458 Lightbox.defaults = { 10459 albumLabel: 'Image %1 of %2', 10460 alwaysShowNavOnTouchDevices: false, 10461 fadeDuration: 600, 10462 fitImagesInViewport: true, 10463 imageFadeDuration: 600, 10464 // maxWidth: 800, 10465 // maxHeight: 600, 10466 positionFromTop: 50, 10467 resizeDuration: 700, 10468 showImageNumberLabel: true, 10469 wrapAround: false, 10470 disableScrolling: false, 10471 /* 10472 Sanitize Title 10473 If the caption data is trusted, for example you are hardcoding it in, then leave this to false. 10474 This will free you to add html tags, such as links, in the caption. 10475 10476 If the caption data is user submitted or from some other untrusted source, then set this to true 10477 to prevent xss and other injection attacks. 10478 */ 10479 sanitizeTitle: false 10480 }; 10481 10482 Lightbox.prototype.option = function(options) { 10483 $.extend(this.options, options); 10484 }; 10485 10486 Lightbox.prototype.imageCountLabel = function(currentImageNum, totalImages) { 10487 return this.options.albumLabel.replace(/%1/g, currentImageNum).replace(/%2/g, totalImages); 10488 }; 10489 10490 Lightbox.prototype.init = function() { 10491 var self = this; 10492 // Both enable and build methods require the body tag to be in the DOM. 10493 $(document).ready(function() { 10494 self.enable(); 10495 self.build(); 10496 }); 10497 }; 10498 10499 // Loop through anchors and areamaps looking for either data-lightbox attributes or rel attributes 10500 // that contain 'lightbox'. When these are clicked, start lightbox. 10501 Lightbox.prototype.enable = function() { 10502 var self = this; 10503 $('body').on('click', 'a[rel^=lightbox], area[rel^=lightbox], a[data-lightbox], area[data-lightbox]', function(event) { 10504 self.start($(event.currentTarget)); 10505 return false; 10506 }); 10507 }; 10508 10509 // Build html for the lightbox and the overlay. 10510 // Attach event handlers to the new DOM elements. click click click 10511 Lightbox.prototype.build = function() { 10512 if ($('#lightbox').length > 0) { 10513 return; 10514 } 10515 10516 var self = this; 10517 $('<div id="lightboxOverlay" class="lightboxOverlay"></div><div id="lightbox" class="lightbox"><div class="lb-outerContainer"><div class="lb-container"><img class="lb-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /><div class="lb-nav"><a class="lb-prev" href="" ></a><a class="lb-next" href="" ></a></div><div class="lb-loader"><a class="lb-cancel"></a></div></div></div><div class="lb-dataContainer"><div class="lb-data"><div class="lb-details"><span class="lb-caption"></span><span class="lb-number"></span></div><div class="lb-closeContainer"><a class="lb-close"></a></div></div></div></div>').appendTo($('body')); 10518 10519 // Cache jQuery objects 10520 this.$lightbox = $('#lightbox'); 10521 this.$overlay = $('#lightboxOverlay'); 10522 this.$outerContainer = this.$lightbox.find('.lb-outerContainer'); 10523 this.$container = this.$lightbox.find('.lb-container'); 10524 this.$image = this.$lightbox.find('.lb-image'); 10525 this.$nav = this.$lightbox.find('.lb-nav'); 10526 10527 // Store css values for future lookup 10528 this.containerPadding = { 10529 top: parseInt(this.$container.css('padding-top'), 10), 10530 right: parseInt(this.$container.css('padding-right'), 10), 10531 bottom: parseInt(this.$container.css('padding-bottom'), 10), 10532 left: parseInt(this.$container.css('padding-left'), 10) 10533 }; 10534 10535 this.imageBorderWidth = { 10536 top: parseInt(this.$image.css('border-top-width'), 10), 10537 right: parseInt(this.$image.css('border-right-width'), 10), 10538 bottom: parseInt(this.$image.css('border-bottom-width'), 10), 10539 left: parseInt(this.$image.css('border-left-width'), 10) 10540 }; 10541 10542 // Attach event handlers to the newly minted DOM elements 10543 this.$overlay.hide().on('click', function() { 10544 self.end(); 10545 return false;
10546 }); 10547 10548 this.$lightbox.hide().on('click', function(event) { 10549 if ($(event.target).attr('id') === 'lightbox') { 10550 self.end(); 10551 } 10552 return false; 10553 }); 10554 10555 this.$outerContainer.on('click', function(event) { 10556 if ($(event.target).attr('id') === 'lightbox') { 10557 self.end(); 10558 } 10559 return false; 10560 }); 10561 10562 this.$lightbox.find('.lb-prev').on('click', function() { 10563 if (self.currentImageIndex === 0) { 10564 self.changeImage(self.album.length - 1); 10565 } else { 10566 self.changeImage(self.currentImageIndex - 1); 10567 } 10568 return false; 10569 }); 10570 10571 this.$lightbox.find('.lb-next').on('click', function() { 10572 if (self.currentImageIndex === self.album.length - 1) { 10573 self.changeImage(0); 10574 } else { 10575 self.changeImage(self.currentImageIndex + 1); 10576 } 10577 return false; 10578 }); 10579 10580 /* 10581 Show context menu for image on right-click 10582 10583 There is a div containing the navigation that spans the entire image and lives above of it. If 10584 you right-click, you are right clicking this div and not the image. This prevents users from 10585 saving the image or using other context menu actions with the image. 10586 10587 To fix this, when we detect the right mouse button is pressed down, but not yet clicked, we 10588 set pointer-events to none on the nav div. This is so that the upcoming right-click event on 10589 the next mouseup will bubble down to the image. Once the right-click/contextmenu event occurs 10590 we set the pointer events back to auto for the nav div so it can capture hover and left-click 10591 events as usual. 10592 */ 10593 this.$nav.on('mousedown', function(event) { 10594 if (event.which === 3) { 10595 self.$nav.css('pointer-events', 'none'); 10596 10597 self.$lightbox.one('contextmenu', function() { 10598 setTimeout(function() { 10599 this.$nav.css('pointer-events', 'auto'); 10600 }.bind(self), 0); 10601 }); 10602 } 10603 }); 10604 10605 10606 this.$lightbox.find('.lb-loader, .lb-close').on('click', function() { 10607 self.end(); 10608 return false; 10609 }); 10610 }; 10611 10612 // Show overlay and lightbox. If the image is part of a set, add siblings to album array. 10613 Lightbox.prototype.start = function($link) { 10614 var self = this; 10615 var $window = $(window); 10616 10617 $window.on('resize', $.proxy(this.sizeOverlay, this)); 10618 10619 $('select, object, embed').css({ 10620 visibility: 'hidden' 10621 }); 10622 10623 this.sizeOverlay(); 10624 10625 this.album = []; 10626 var imageNumber = 0; 10627 10628 function addToAlbum($link) { 10629 self.album.push({ 10630 alt: $link.attr('data-alt'), 10631 link: $link.attr('href'), 10632 title: $link.attr('data-title') || $link.attr('title') 10633 }); 10634 } 10635 10636 // Support both data-lightbox attribute and rel attribute implementations 10637 var dataLightboxValue = $link.attr('data-lightbox'); 10638 var $links; 10639 10640 if (dataLightboxValue) { 10641 $links = $($link.prop('tagName') + '[data-lightbox="' + dataLightboxValue + '"]'); 10642 for (var i = 0; i < $links.length; i = ++i) { 10643 addToAlbum($($links[i])); 10644 if ($links[i] === $link[0]) { 10645 imageNumber = i; 10646 } 10647 } 10648 } else { 10649 if ($link.attr('rel') === 'lightbox') { 10650 // If image is not part of a set 10651 addToAlbum($link); 10652 } else { 10653 // If image is part of a set 10654 $links = $($link.prop('tagName') + '[rel="' + $link.attr('rel') + '"]'); 10655 for (var j = 0; j < $links.length; j = ++j) { 10656 addToAlbum($($links[j])); 10657 if ($links[j] === $link[0]) { 10658 imageNumber = j; 10659 } 10660 } 10661 } 10662 } 10663 10664 // Position Lightbox 10665 var top = $window.scrollTop() + this.options.positionFromTop; 10666 var left = $window.scrollLeft(); 10667 this.$lightbox.css({ 10668 top: top + 'px', 10669 left: left + 'px' 10670 }).fadeIn(this.options.fadeDuration); 10671 10672 // Disable scrolling of the page while open 10673 if (this.options.disableScrolling) { 10674 $('html').addClass('lb-disable-scrolling'); 10675 } 10676 10677 this.changeImage(imageNumber); 10678 }; 10679 10680 // Hide most UI elements in preparation for the animated resizing of the lightbox. 10681 Lightbox.prototype.changeImage = function(imageNumber) { 10682 var self = this; 10683 10684 this.disableKeyboardNav(); 10685 var $image = this.$lightbox.find('.lb-image'); 10686 10687 this.$overlay.fadeIn(this.options.fadeDuration); 10688 10689 $('.lb-loader').fadeIn('slow'); 10690 this.$lightbox.find('.lb-image, .lb-nav, .lb-prev, .lb-next, .lb-dataContainer, .lb-numbers, .lb-caption').hide(); 10691 10692 this.$outerContainer.addClass('animating'); 10693 10694 // When image to show is preloaded, we send the width and height to sizeContainer() 10695 var preloader = new Image(); 10696 preloader.onload = function() { 10697 var $preloader; 10698 var imageHeight; 10699 var imageWidth; 10700 var maxImageHeight; 10701 var maxImageWidth; 10702 var windowHeight; 10703 var windowWidth; 10704 10705 $image.attr({ 10706 'alt': self.album[imageNumber].alt, 10707 'src': self.album[imageNumber].link 10708 }); 10709 10710 $preloader = $(preloader); 10711 10712 $image.width(preloader.width); 10713 $image.height(preloader.height); 10714 10715 if (self.options.fitImagesInViewport) { 10716 // Fit image inside the viewport. 10717 // Take into account the border around the image and an additional 10px gutter on each side. 10718 10719 windowWidth = $(window).width(); 10720 windowHeight = $(window).height(); 10721 maxImageWidth = windowWidth - self.containerPadding.left - self.containerPadding.right - self.imageBorderWidth.left - self.imageBorderWidth.right - 20; 10722 maxImageHeight = windowHeight - self.containerPadding.top - self.containerPadding.bottom - self.imageBorderWidth.top - self.imageBorderWidth.bottom - 120; 10723 10724 // Check if image size is larger then maxWidth|maxHeight in settings 10725 if (self.options.maxWidth && self.options.maxWidth < maxImageWidth) { 10726 maxImageWidth = self.options.maxWidth; 10727 } 10728 if (self.options.maxHeight && self.options.maxHeight < maxImageWidth) { 10729 maxImageHeight = self.options.maxHeight; 10730 } 10731 10732 // Is the current image's width or height is greater than the maxImageWidth or maxImageHeight 10733 // option than we need to size down while maintaining the aspect ratio. 10734 if ((preloader.width > maxImageWidth) || (preloader.height > maxImageHeight)) { 10735 if ((preloader.width / maxImageWidth) > (preloader.height / maxImageHeight)) { 10736 imageWidth = maxImageWidth; 10737 imageHeight = parseInt(preloader.height / (preloader.width / imageWidth), 10); 10738 $image.width(imageWidth); 10739 $image.height(imageHeight); 10740 } else { 10741 imageHeight = maxImageHeight; 10742 imageWidth = parseInt(preloader.width / (preloader.height / imageHeight), 10); 10743 $image.width(imageWidth); 10744 $image.height(imageHeight); 10745 } 10746 } 10747 } 10748 self.sizeContainer($image.width(), $image.height()); 10749 }; 10750 10751 preloader.src = this.album[imageNumber].link; 10752 this.currentImageIndex = imageNumber; 10753 }; 10754 10755 // Stretch overlay to fit the viewport 10756 Lightbox.prototype.sizeOverlay = function() { 10757 this.$overlay 10758 .width($(document).width()) 10759 .height($(document).height()); 10760 }; 10761 10762 // Animate the size of the lightbox to fit the image we are showing 10763 Lightbox.prototype.sizeContainer = function(imageWidth, imageHeight) { 10764 var self = this; 10765 10766 var oldWidth = this.$outerContainer.outerWidth(); 10767 var oldHeight = this.$outerContainer.outerHeight(); 10768 var newWidth = imageWidth + this.containerPadding.left + this.containerPadding.right + this.imageBorderWidth.left + this.imageBorderWidth.right; 10769 var newHeight = imageHeight + this.containerPadding.top + this.containerPadding.bottom + this.imageBorderWidth.top + this.imageBorderWidth.bottom; 10770 10771 function postResize() { 10772 self.$lightbox.find('.lb-dataContainer').width(newWidth); 10773 self.$lightbox.find('.lb-prevLink').height(newHeight); 10774 self.$lightbox.find('.lb-nextLink').height(newHeight); 10775 self.showImage(); 10776 } 10777 10778 if (oldWidth !== newWidth || oldHeight !== newHeight) { 10779 this.$outerContainer.animate({ 10780 width: newWidth, 10781 height: newHeight 10782 }, this.options.resizeDuration, 'swing', function() { 10783 postResize(); 10784 }); 10785 } else { 10786 postResize(); 10787 } 10788 }; 10789 10790 // Display the image and its details and begin preload neighboring images. 10791 Lightbox.prototype.showImage = function() { 10792 this.$lightbox.find('.lb-loader').stop(true).hide(); 10793 this.$lightbox.find('.lb-image').fadeIn(this.options.imageFadeDuration); 10794 10795 this.updateNav(); 10796 this.updateDetails(); 10797 this.preloadNeighboringImages(); 10798 this.enableKeyboardNav(); 10799 }; 10800 10801 // Display previous and next navigation if appropriate. 10802 Lightbox.prototype.updateNav = function() { 10803 // Check to see if the browser supports touch events. If so, we take the conservative approach 10804 // and assume that mouse hover events are not supported and always show prev/next navigation 10805 // arrows in image sets. 10806 var alwaysShowNav = false;
10807 try { 10808 document.createEvent('TouchEvent'); 10809 alwaysShowNav = (this.options.alwaysShowNavOnTouchDevices) ? true : false; 10810 } catch (e) {} 10811 10812 this.$lightbox.find('.lb-nav').show(); 10813 10814 if (this.album.length > 1) { 10815 if (this.options.wrapAround) { 10816 if (alwaysShowNav) { 10817 this.$lightbox.find('.lb-prev, .lb-next').css('opacity', '1'); 10818 } 10819 this.$lightbox.find('.lb-prev, .lb-next').show(); 10820 } else { 10821 if (this.currentImageIndex > 0) { 10822 this.$lightbox.find('.lb-prev').show(); 10823 if (alwaysShowNav) { 10824 this.$lightbox.find('.lb-prev').css('opacity', '1'); 10825 } 10826 } 10827 if (this.currentImageIndex < this.album.length - 1) { 10828 this.$lightbox.find('.lb-next').show(); 10829 if (alwaysShowNav) { 10830 this.$lightbox.find('.lb-next').css('opacity', '1'); 10831 } 10832 } 10833 } 10834 } 10835 }; 10836 10837 // Display caption, image number, and closing button. 10838 Lightbox.prototype.updateDetails = function() { 10839 var self = this; 10840 10841 // Enable anchor clicks in the injected caption html. 10842 // Thanks Nate Wright for the fix. @https://github.com/NateWr 10843 if (typeof this.album[this.currentImageIndex].title !== 'undefined' && 10844 this.album[this.currentImageIndex].title !== '') { 10845 var $caption = this.$lightbox.find('.lb-caption'); 10846 if (this.options.sanitizeTitle) { 10847 $caption.text(this.album[this.currentImageIndex].title); 10848 } else { 10849 $caption.html(this.album[this.currentImageIndex].title); 10850 } 10851 $caption.fadeIn('fast') 10852 .find('a').on('click', function(event) { 10853 if ($(this).attr('target') !== undefined) { 10854 window.open($(this).attr('href'), $(this).attr('target')); 10855 } else { 10856 location.href = $(this).attr('href'); 10857 } 10858 }); 10859 } 10860 10861 if (this.album.length > 1 && this.options.showImageNumberLabel) { 10862 var labelText = this.imageCountLabel(this.currentImageIndex + 1, this.album.length); 10863 this.$lightbox.find('.lb-number').text(labelText).fadeIn('fast'); 10864 } else { 10865 this.$lightbox.find('.lb-number').hide(); 10866 } 10867 10868 this.$outerContainer.removeClass('animating'); 10869 10870 this.$lightbox.find('.lb-dataContainer').fadeIn(this.options.resizeDuration, function() { 10871 return self.sizeOverlay(); 10872 }); 10873 }; 10874 10875 // Preload previous and next images in set. 10876 Lightbox.prototype.preloadNeighboringImages = function() { 10877 if (this.album.length > this.currentImageIndex + 1) { 10878 var preloadNext = new Image(); 10879 preloadNext.src = this.album[this.currentImageIndex + 1].link; 10880 } 10881 if (this.currentImageIndex > 0) { 10882 var preloadPrev = new Image(); 10883 preloadPrev.src = this.album[this.currentImageIndex - 1].link; 10884 } 10885 }; 10886 10887 Lightbox.prototype.enableKeyboardNav = function() { 10888 $(document).on('keyup.keyboard', $.proxy(this.keyboardAction, this)); 10889 }; 10890 10891 Lightbox.prototype.disableKeyboardNav = function() { 10892 $(document).off('.keyboard'); 10893 }; 10894 10895 Lightbox.prototype.keyboardAction = function(event) { 10896 var KEYCODE_ESC = 27; 10897 var KEYCODE_LEFTARROW = 37; 10898 var KEYCODE_RIGHTARROW = 39; 10899 10900 var keycode = event.keyCode; 10901 var key = String.fromCharCode(keycode).toLowerCase(); 10902 if (keycode === KEYCODE_ESC || key.match(/x|o|c/)) { 10903 this.end(); 10904 } else if (key === 'p' || keycode === KEYCODE_LEFTARROW) { 10905 if (this.currentImageIndex !== 0) { 10906 this.changeImage(this.currentImageIndex - 1); 10907 } else if (this.options.wrapAround && this.album.length > 1) { 10908 this.changeImage(this.album.length - 1); 10909 } 10910 } else if (key === 'n' || keycode === KEYCODE_RIGHTARROW) { 10911 if (this.currentImageIndex !== this.album.length - 1) { 10912 this.changeImage(this.currentImageIndex + 1); 10913 } else if (this.options.wrapAround && this.album.length > 1) { 10914 this.changeImage(0); 10915 } 10916 } 10917 }; 10918 10919 // Closing time. :-( 10920 Lightbox.prototype.end = function() { 10921 this.disableKeyboardNav(); 10922 $(window).off('resize', this.sizeOverlay); 10923 this.$lightbox.fadeOut(this.options.fadeDuration); 10924 this.$overlay.fadeOut(this.options.fadeDuration); 10925 $('select, object, embed').css({ 10926 visibility: 'visible' 10927 }); 10928 if (this.options.disableScrolling) { 10929 $('html').removeClass('lb-disable-scrolling'); 10930 } 10931 }; 10932 10933 return new Lightbox(); 10934})); 10935 10936 10937 10938 10939$(document).ready(function() { 10940 10941 $('.hide-dropdown-header').on('click', function(event) { 10942 event.preventDefault(); 10943 $('#dropdown-header-collapsing-part').slideUp(); 10944 setCookie('dropdown_header', false, 3650); 10945 $('#hrtohide').fadeIn() 10946 }); 10947 10948 $('.show-dropdown-header').on('click', function(event) { 10949 if ($(window).width() <= 928) { 10950 window.location.href = $(event.target).attr("href"); 10951 return; 10952 } 10953 event.preventDefault(); 10954 $('#dropdown-header-collapsing-part').slideDown(); 10955 $(event.target).css("background-color", "{{ request.site.data.color }}"); 10956 $(event.target).siblings().css("background-color", "{{ request.site.data.color }}"); 10957 10958 $('#dropdown-header-collapsing-part').children().not(document.getElementsByClassName('hide-dropdown-header')).hide(); 10959 $('#dropdown-header-collapsing-part').children("#"+$(event.target).data('id')).show(); 10960 $('#hrtohide').fadeOut() 10961 }); 10962 10963}); 10964 10965 10966
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.