.\n\n.list-group {\n // No need to set list-style: none; since .list-group-item is block level\n margin-bottom: 20px;\n padding-left: 0; // reset padding because ul and ol\n}\n\n\n// Individual list items\n//\n// Use on `li`s or `div`s within the `.list-group` parent.\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n // Place the border on the list items and negative margin up for better styling\n margin-bottom: -1px;\n background-color: @list-group-bg;\n border: 1px solid @list-group-border;\n\n // Round the first and last items\n &:first-child {\n .border-top-radius(@list-group-border-radius);\n }\n &:last-child {\n margin-bottom: 0;\n .border-bottom-radius(@list-group-border-radius);\n }\n}\n\n\n// Linked list items\n//\n// Use anchor elements instead of `li`s or `div`s to create linked list items.\n// Includes an extra `.active` modifier class for showing selected items.\n\na.list-group-item {\n color: @list-group-link-color;\n\n .list-group-item-heading {\n color: @list-group-link-heading-color;\n }\n\n // Hover state\n &:hover,\n &:focus {\n text-decoration: none;\n color: @list-group-link-hover-color;\n background-color: @list-group-hover-bg;\n }\n}\n\n.list-group-item {\n // Disabled state\n &.disabled,\n &.disabled:hover,\n &.disabled:focus {\n background-color: @list-group-disabled-bg;\n color: @list-group-disabled-color;\n cursor: @cursor-disabled;\n\n // Force color to inherit for custom content\n .list-group-item-heading {\n color: inherit;\n }\n .list-group-item-text {\n color: @list-group-disabled-text-color;\n }\n }\n\n // Active class on item itself, not parent\n &.active,\n &.active:hover,\n &.active:focus {\n z-index: 2; // Place active items above their siblings for proper border styling\n color: @list-group-active-color;\n background-color: @list-group-active-bg;\n border-color: @list-group-active-border;\n\n // Force color to inherit for custom content\n .list-group-item-heading,\n .list-group-item-heading > small,\n .list-group-item-heading > .small {\n color: inherit;\n }\n .list-group-item-text {\n color: @list-group-active-text-color;\n }\n }\n}\n\n\n// Contextual variants\n//\n// Add modifier classes to change text and background color on individual items.\n// Organizationally, this must come after the `:hover` states.\n\n.list-group-item-variant(success; @state-success-bg; @state-success-text);\n.list-group-item-variant(info; @state-info-bg; @state-info-text);\n.list-group-item-variant(warning; @state-warning-bg; @state-warning-text);\n.list-group-item-variant(danger; @state-danger-bg; @state-danger-text);\n\n\n// Custom content options\n//\n// Extra classes for creating well-formatted content within `.list-group-item`s.\n\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n","// List Groups\n\n.list-group-item-variant(@state; @background; @color) {\n .list-group-item-@{state} {\n color: @color;\n background-color: @background;\n\n a& {\n color: @color;\n\n .list-group-item-heading {\n color: inherit;\n }\n\n &:hover,\n &:focus {\n color: @color;\n background-color: darken(@background, 5%);\n }\n &.active,\n &.active:hover,\n &.active:focus {\n color: #fff;\n background-color: @color;\n border-color: @color;\n }\n }\n }\n}\n","//\n// Panels\n// --------------------------------------------------\n\n\n// Base class\n.panel {\n margin-bottom: @line-height-computed;\n background-color: @panel-bg;\n border: 1px solid transparent;\n border-radius: @panel-border-radius;\n .box-shadow(0 1px 1px rgba(0,0,0,.05));\n}\n\n// Panel contents\n.panel-body {\n padding: @panel-body-padding;\n &:extend(.clearfix all);\n}\n\n// Optional heading\n.panel-heading {\n padding: @panel-heading-padding;\n border-bottom: 1px solid transparent;\n .border-top-radius((@panel-border-radius - 1));\n\n > .dropdown .dropdown-toggle {\n color: inherit;\n }\n}\n\n// Within heading, strip any `h*` tag of its default margins for spacing.\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: ceil((@font-size-base * 1.125));\n color: inherit;\n\n > a {\n color: inherit;\n }\n}\n\n// Optional footer (stays gray in every modifier class)\n.panel-footer {\n padding: @panel-footer-padding;\n background-color: @panel-footer-bg;\n border-top: 1px solid @panel-inner-border;\n .border-bottom-radius((@panel-border-radius - 1));\n}\n\n\n// List groups in panels\n//\n// By default, space out list group content from panel headings to account for\n// any kind of custom content between the two.\n\n.panel {\n > .list-group,\n > .panel-collapse > .list-group {\n margin-bottom: 0;\n\n .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n }\n\n // Add border top radius for first one\n &:first-child {\n .list-group-item:first-child {\n border-top: 0;\n .border-top-radius((@panel-border-radius - 1));\n }\n }\n // Add border bottom radius for last one\n &:last-child {\n .list-group-item:last-child {\n border-bottom: 0;\n .border-bottom-radius((@panel-border-radius - 1));\n }\n }\n }\n}\n// Collapse space between when there's no additional content.\n.panel-heading + .list-group {\n .list-group-item:first-child {\n border-top-width: 0;\n }\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n\n// Tables in panels\n//\n// Place a non-bordered `.table` within a panel (not within a `.panel-body`) and\n// watch it go full width.\n\n.panel {\n > .table,\n > .table-responsive > .table,\n > .panel-collapse > .table {\n margin-bottom: 0;\n\n caption {\n padding-left: @panel-body-padding;\n padding-right: @panel-body-padding;\n }\n }\n // Add border top radius for first one\n > .table:first-child,\n > .table-responsive:first-child > .table:first-child {\n .border-top-radius((@panel-border-radius - 1));\n\n > thead:first-child,\n > tbody:first-child {\n > tr:first-child {\n border-top-left-radius: (@panel-border-radius - 1);\n border-top-right-radius: (@panel-border-radius - 1);\n\n td:first-child,\n th:first-child {\n border-top-left-radius: (@panel-border-radius - 1);\n }\n td:last-child,\n th:last-child {\n border-top-right-radius: (@panel-border-radius - 1);\n }\n }\n }\n }\n // Add border bottom radius for last one\n > .table:last-child,\n > .table-responsive:last-child > .table:last-child {\n .border-bottom-radius((@panel-border-radius - 1));\n\n > tbody:last-child,\n > tfoot:last-child {\n > tr:last-child {\n border-bottom-left-radius: (@panel-border-radius - 1);\n border-bottom-right-radius: (@panel-border-radius - 1);\n\n td:first-child,\n th:first-child {\n border-bottom-left-radius: (@panel-border-radius - 1);\n }\n td:last-child,\n th:last-child {\n border-bottom-right-radius: (@panel-border-radius - 1);\n }\n }\n }\n }\n > .panel-body + .table,\n > .panel-body + .table-responsive,\n > .table + .panel-body,\n > .table-responsive + .panel-body {\n border-top: 1px solid @table-border-color;\n }\n > .table > tbody:first-child > tr:first-child th,\n > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n }\n > .table-bordered,\n > .table-responsive > .table-bordered {\n border: 0;\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th:first-child,\n > td:first-child {\n border-left: 0;\n }\n > th:last-child,\n > td:last-child {\n border-right: 0;\n }\n }\n }\n > thead,\n > tbody {\n > tr:first-child {\n > td,\n > th {\n border-bottom: 0;\n }\n }\n }\n > tbody,\n > tfoot {\n > tr:last-child {\n > td,\n > th {\n border-bottom: 0;\n }\n }\n }\n }\n > .table-responsive {\n border: 0;\n margin-bottom: 0;\n }\n}\n\n\n// Collapsable panels (aka, accordion)\n//\n// Wrap a series of panels in `.panel-group` to turn them into an accordion with\n// the help of our collapse JavaScript plugin.\n\n.panel-group {\n margin-bottom: @line-height-computed;\n\n // Tighten up margin so it's only between panels\n .panel {\n margin-bottom: 0;\n border-radius: @panel-border-radius;\n\n + .panel {\n margin-top: 5px;\n }\n }\n\n .panel-heading {\n border-bottom: 0;\n\n + .panel-collapse > .panel-body,\n + .panel-collapse > .list-group {\n border-top: 1px solid @panel-inner-border;\n }\n }\n\n .panel-footer {\n border-top: 0;\n + .panel-collapse .panel-body {\n border-bottom: 1px solid @panel-inner-border;\n }\n }\n}\n\n\n// Contextual variations\n.panel-default {\n .panel-variant(@panel-default-border; @panel-default-text; @panel-default-heading-bg; @panel-default-border);\n}\n.panel-primary {\n .panel-variant(@panel-primary-border; @panel-primary-text; @panel-primary-heading-bg; @panel-primary-border);\n}\n.panel-success {\n .panel-variant(@panel-success-border; @panel-success-text; @panel-success-heading-bg; @panel-success-border);\n}\n.panel-info {\n .panel-variant(@panel-info-border; @panel-info-text; @panel-info-heading-bg; @panel-info-border);\n}\n.panel-warning {\n .panel-variant(@panel-warning-border; @panel-warning-text; @panel-warning-heading-bg; @panel-warning-border);\n}\n.panel-danger {\n .panel-variant(@panel-danger-border; @panel-danger-text; @panel-danger-heading-bg; @panel-danger-border);\n}\n","// Panels\n\n.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) {\n border-color: @border;\n\n & > .panel-heading {\n color: @heading-text-color;\n background-color: @heading-bg-color;\n border-color: @heading-border;\n\n + .panel-collapse > .panel-body {\n border-top-color: @border;\n }\n .badge {\n color: @heading-bg-color;\n background-color: @heading-text-color;\n }\n }\n & > .panel-footer {\n + .panel-collapse > .panel-body {\n border-bottom-color: @border;\n }\n }\n}\n","// Embeds responsive\n//\n// Credit: Nicolas Gallagher and SUIT CSS.\n\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n\n .embed-responsive-item,\n iframe,\n embed,\n object,\n video {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n height: 100%;\n width: 100%;\n border: 0;\n }\n\n // Modifier class for 16:9 aspect ratio\n &.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n }\n\n // Modifier class for 4:3 aspect ratio\n &.embed-responsive-4by3 {\n padding-bottom: 75%;\n }\n}\n","//\n// Wells\n// --------------------------------------------------\n\n\n// Base class\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: @well-bg;\n border: 1px solid @well-border;\n border-radius: @border-radius-base;\n .box-shadow(inset 0 1px 1px rgba(0,0,0,.05));\n blockquote {\n border-color: #ddd;\n border-color: rgba(0,0,0,.15);\n }\n}\n\n// Sizes\n.well-lg {\n padding: 24px;\n border-radius: @border-radius-large;\n}\n.well-sm {\n padding: 9px;\n border-radius: @border-radius-small;\n}\n","//\n// Close icons\n// --------------------------------------------------\n\n\n.close {\n float: right;\n font-size: (@font-size-base * 1.5);\n font-weight: @close-font-weight;\n line-height: 1;\n color: @close-color;\n text-shadow: @close-text-shadow;\n .opacity(.2);\n\n &:hover,\n &:focus {\n color: @close-color;\n text-decoration: none;\n cursor: pointer;\n .opacity(.5);\n }\n\n // Additional properties for button version\n // iOS requires the button element instead of an anchor tag.\n // If you want the anchor version, it requires `href=\"#\"`.\n button& {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n }\n}\n","//\n// Modals\n// --------------------------------------------------\n\n// .modal-open - body class for killing the scroll\n// .modal - container to scroll within\n// .modal-dialog - positioning shell for the actual modal\n// .modal-content - actual modal w/ bg and corners and shit\n\n// Kill the scroll on the body\n.modal-open {\n overflow: hidden;\n}\n\n// Container that the modal scrolls within\n.modal {\n display: none;\n overflow: hidden;\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: @zindex-modal;\n -webkit-overflow-scrolling: touch;\n\n // Prevent Chrome on Windows from adding a focus outline. For details, see\n // https://github.com/twbs/bootstrap/pull/10951.\n outline: 0;\n\n // When fading in the modal, animate it to slide down\n &.fade .modal-dialog {\n .translate(0, -25%);\n .transition-transform(~\"0.3s ease-out\");\n }\n &.in .modal-dialog { .translate(0, 0) }\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n// Shell div to position the modal with bottom padding\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n\n// Actual modal\n.modal-content {\n position: relative;\n background-color: @modal-content-bg;\n border: 1px solid @modal-content-fallback-border-color; //old browsers fallback (ie8 etc)\n border: 1px solid @modal-content-border-color;\n border-radius: @border-radius-large;\n .box-shadow(0 3px 9px rgba(0,0,0,.5));\n background-clip: padding-box;\n // Remove focus outline from opened modal\n outline: 0;\n}\n\n// Modal background\n.modal-backdrop {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n background-color: @modal-backdrop-bg;\n // Fade for backdrop\n &.fade { .opacity(0); }\n &.in { .opacity(@modal-backdrop-opacity); }\n}\n\n// Modal header\n// Top section of the modal w/ title and dismiss\n.modal-header {\n padding: @modal-title-padding;\n border-bottom: 1px solid @modal-header-border-color;\n min-height: (@modal-title-padding + @modal-title-line-height);\n}\n// Close icon\n.modal-header .close {\n margin-top: -2px;\n}\n\n// Title text within header\n.modal-title {\n margin: 0;\n line-height: @modal-title-line-height;\n}\n\n// Modal body\n// Where all modal content resides (sibling of .modal-header and .modal-footer)\n.modal-body {\n position: relative;\n padding: @modal-inner-padding;\n}\n\n// Footer (for actions)\n.modal-footer {\n padding: @modal-inner-padding;\n text-align: right; // right align buttons\n border-top: 1px solid @modal-footer-border-color;\n &:extend(.clearfix all); // clear it in case folks use .pull-* classes on buttons\n\n // Properly space out buttons\n .btn + .btn {\n margin-left: 5px;\n margin-bottom: 0; // account for input[type=\"submit\"] which gets the bottom margin like all other inputs\n }\n // but override that for button groups\n .btn-group .btn + .btn {\n margin-left: -1px;\n }\n // and override it for block buttons as well\n .btn-block + .btn-block {\n margin-left: 0;\n }\n}\n\n// Measure scrollbar width for padding body during modal show/hide\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n\n// Scale up the modal\n@media (min-width: @screen-sm-min) {\n // Automatically set modal's width for larger viewports\n .modal-dialog {\n width: @modal-md;\n margin: 30px auto;\n }\n .modal-content {\n .box-shadow(0 5px 15px rgba(0,0,0,.5));\n }\n\n // Modal sizes\n .modal-sm { width: @modal-sm; }\n}\n\n@media (min-width: @screen-md-min) {\n .modal-lg { width: @modal-lg; }\n}\n","//\n// Tooltips\n// --------------------------------------------------\n\n\n// Base class\n.tooltip {\n position: absolute;\n z-index: @zindex-tooltip;\n display: block;\n visibility: visible;\n // Reset font and text propertes given new insertion method\n font-family: @font-family-base;\n font-size: @font-size-small;\n font-weight: normal;\n line-height: 1.4;\n .opacity(0);\n\n &.in { .opacity(@tooltip-opacity); }\n &.top { margin-top: -3px; padding: @tooltip-arrow-width 0; }\n &.right { margin-left: 3px; padding: 0 @tooltip-arrow-width; }\n &.bottom { margin-top: 3px; padding: @tooltip-arrow-width 0; }\n &.left { margin-left: -3px; padding: 0 @tooltip-arrow-width; }\n}\n\n// Wrapper for the tooltip content\n.tooltip-inner {\n max-width: @tooltip-max-width;\n padding: 3px 8px;\n color: @tooltip-color;\n text-align: center;\n text-decoration: none;\n background-color: @tooltip-bg;\n border-radius: @border-radius-base;\n}\n\n// Arrows\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n// Note: Deprecated .top-left, .top-right, .bottom-left, and .bottom-right as of v3.3.1\n.tooltip {\n &.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -@tooltip-arrow-width;\n border-width: @tooltip-arrow-width @tooltip-arrow-width 0;\n border-top-color: @tooltip-arrow-color;\n }\n &.top-left .tooltip-arrow {\n bottom: 0;\n right: @tooltip-arrow-width;\n margin-bottom: -@tooltip-arrow-width;\n border-width: @tooltip-arrow-width @tooltip-arrow-width 0;\n border-top-color: @tooltip-arrow-color;\n }\n &.top-right .tooltip-arrow {\n bottom: 0;\n left: @tooltip-arrow-width;\n margin-bottom: -@tooltip-arrow-width;\n border-width: @tooltip-arrow-width @tooltip-arrow-width 0;\n border-top-color: @tooltip-arrow-color;\n }\n &.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -@tooltip-arrow-width;\n border-width: @tooltip-arrow-width @tooltip-arrow-width @tooltip-arrow-width 0;\n border-right-color: @tooltip-arrow-color;\n }\n &.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -@tooltip-arrow-width;\n border-width: @tooltip-arrow-width 0 @tooltip-arrow-width @tooltip-arrow-width;\n border-left-color: @tooltip-arrow-color;\n }\n &.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -@tooltip-arrow-width;\n border-width: 0 @tooltip-arrow-width @tooltip-arrow-width;\n border-bottom-color: @tooltip-arrow-color;\n }\n &.bottom-left .tooltip-arrow {\n top: 0;\n right: @tooltip-arrow-width;\n margin-top: -@tooltip-arrow-width;\n border-width: 0 @tooltip-arrow-width @tooltip-arrow-width;\n border-bottom-color: @tooltip-arrow-color;\n }\n &.bottom-right .tooltip-arrow {\n top: 0;\n left: @tooltip-arrow-width;\n margin-top: -@tooltip-arrow-width;\n border-width: 0 @tooltip-arrow-width @tooltip-arrow-width;\n border-bottom-color: @tooltip-arrow-color;\n }\n}\n","//\n// Popovers\n// --------------------------------------------------\n\n\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: @zindex-popover;\n display: none;\n max-width: @popover-max-width;\n padding: 1px;\n // Reset font and text propertes given new insertion method\n font-family: @font-family-base;\n font-size: @font-size-base;\n font-weight: normal;\n line-height: @line-height-base;\n text-align: left;\n background-color: @popover-bg;\n background-clip: padding-box;\n border: 1px solid @popover-fallback-border-color;\n border: 1px solid @popover-border-color;\n border-radius: @border-radius-large;\n .box-shadow(0 5px 10px rgba(0,0,0,.2));\n\n // Overrides for proper insertion\n white-space: normal;\n\n // Offset the popover to account for the popover arrow\n &.top { margin-top: -@popover-arrow-width; }\n &.right { margin-left: @popover-arrow-width; }\n &.bottom { margin-top: @popover-arrow-width; }\n &.left { margin-left: -@popover-arrow-width; }\n}\n\n.popover-title {\n margin: 0; // reset heading margin\n padding: 8px 14px;\n font-size: @font-size-base;\n background-color: @popover-title-bg;\n border-bottom: 1px solid darken(@popover-title-bg, 5%);\n border-radius: (@border-radius-large - 1) (@border-radius-large - 1) 0 0;\n}\n\n.popover-content {\n padding: 9px 14px;\n}\n\n// Arrows\n//\n// .arrow is outer, .arrow:after is inner\n\n.popover > .arrow {\n &,\n &:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n }\n}\n.popover > .arrow {\n border-width: @popover-arrow-outer-width;\n}\n.popover > .arrow:after {\n border-width: @popover-arrow-width;\n content: \"\";\n}\n\n.popover {\n &.top > .arrow {\n left: 50%;\n margin-left: -@popover-arrow-outer-width;\n border-bottom-width: 0;\n border-top-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n border-top-color: @popover-arrow-outer-color;\n bottom: -@popover-arrow-outer-width;\n &:after {\n content: \" \";\n bottom: 1px;\n margin-left: -@popover-arrow-width;\n border-bottom-width: 0;\n border-top-color: @popover-arrow-color;\n }\n }\n &.right > .arrow {\n top: 50%;\n left: -@popover-arrow-outer-width;\n margin-top: -@popover-arrow-outer-width;\n border-left-width: 0;\n border-right-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n border-right-color: @popover-arrow-outer-color;\n &:after {\n content: \" \";\n left: 1px;\n bottom: -@popover-arrow-width;\n border-left-width: 0;\n border-right-color: @popover-arrow-color;\n }\n }\n &.bottom > .arrow {\n left: 50%;\n margin-left: -@popover-arrow-outer-width;\n border-top-width: 0;\n border-bottom-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n border-bottom-color: @popover-arrow-outer-color;\n top: -@popover-arrow-outer-width;\n &:after {\n content: \" \";\n top: 1px;\n margin-left: -@popover-arrow-width;\n border-top-width: 0;\n border-bottom-color: @popover-arrow-color;\n }\n }\n\n &.left > .arrow {\n top: 50%;\n right: -@popover-arrow-outer-width;\n margin-top: -@popover-arrow-outer-width;\n border-right-width: 0;\n border-left-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n border-left-color: @popover-arrow-outer-color;\n &:after {\n content: \" \";\n right: 1px;\n border-right-width: 0;\n border-left-color: @popover-arrow-color;\n bottom: -@popover-arrow-width;\n }\n }\n}\n","//\n// Carousel\n// --------------------------------------------------\n\n\n// Wrapper for the slide container and indicators\n.carousel {\n position: relative;\n}\n\n.carousel-inner {\n position: relative;\n overflow: hidden;\n width: 100%;\n\n > .item {\n display: none;\n position: relative;\n .transition(.6s ease-in-out left);\n\n // Account for jankitude on images\n > img,\n > a > img {\n &:extend(.img-responsive);\n line-height: 1;\n }\n\n // WebKit CSS3 transforms for supported devices\n @media all and (transform-3d), (-webkit-transform-3d) {\n transition: transform .6s ease-in-out;\n backface-visibility: hidden;\n perspective: 1000;\n\n &.next,\n &.active.right {\n transform: translate3d(100%, 0, 0);\n left: 0;\n }\n &.prev,\n &.active.left {\n transform: translate3d(-100%, 0, 0);\n left: 0;\n }\n &.next.left,\n &.prev.right,\n &.active {\n transform: translate3d(0, 0, 0);\n left: 0;\n }\n }\n }\n\n > .active,\n > .next,\n > .prev {\n display: block;\n }\n\n > .active {\n left: 0;\n }\n\n > .next,\n > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n }\n\n > .next {\n left: 100%;\n }\n > .prev {\n left: -100%;\n }\n > .next.left,\n > .prev.right {\n left: 0;\n }\n\n > .active.left {\n left: -100%;\n }\n > .active.right {\n left: 100%;\n }\n\n}\n\n// Left/right controls for nav\n// ---------------------------\n\n.carousel-control {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n width: @carousel-control-width;\n .opacity(@carousel-control-opacity);\n font-size: @carousel-control-font-size;\n color: @carousel-control-color;\n text-align: center;\n text-shadow: @carousel-text-shadow;\n // We can't have this transition here because WebKit cancels the carousel\n // animation if you trip this while in the middle of another animation.\n\n // Set gradients for backgrounds\n &.left {\n #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001));\n }\n &.right {\n left: auto;\n right: 0;\n #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5));\n }\n\n // Hover/focus state\n &:hover,\n &:focus {\n outline: 0;\n color: @carousel-control-color;\n text-decoration: none;\n .opacity(.9);\n }\n\n // Toggles\n .icon-prev,\n .icon-next,\n .glyphicon-chevron-left,\n .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n z-index: 5;\n display: inline-block;\n }\n .icon-prev,\n .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n }\n .icon-next,\n .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n }\n .icon-prev,\n .icon-next {\n width: 20px;\n height: 20px;\n margin-top: -10px;\n font-family: serif;\n }\n\n\n .icon-prev {\n &:before {\n content: '\\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039)\n }\n }\n .icon-next {\n &:before {\n content: '\\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A)\n }\n }\n}\n\n// Optional indicator pips\n//\n// Add an unordered list with the following class and add a list item for each\n// slide your carousel holds.\n\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n margin-left: -30%;\n padding-left: 0;\n list-style: none;\n text-align: center;\n\n li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n border: 1px solid @carousel-indicator-border-color;\n border-radius: 10px;\n cursor: pointer;\n\n // IE8-9 hack for event handling\n //\n // Internet Explorer 8-9 does not support clicks on elements without a set\n // `background-color`. We cannot use `filter` since that's not viewed as a\n // background color by the browser. Thus, a hack is needed.\n //\n // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we\n // set alpha transparency for the best results possible.\n background-color: #000 \\9; // IE8\n background-color: rgba(0,0,0,0); // IE9\n }\n .active {\n margin: 0;\n width: 12px;\n height: 12px;\n background-color: @carousel-indicator-active-bg;\n }\n}\n\n// Optional captions\n// -----------------------------\n// Hidden by default for smaller viewports\n.carousel-caption {\n position: absolute;\n left: 15%;\n right: 15%;\n bottom: 20px;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: @carousel-caption-color;\n text-align: center;\n text-shadow: @carousel-text-shadow;\n & .btn {\n text-shadow: none; // No shadow for button elements in carousel-caption\n }\n}\n\n\n// Scale up controls for tablets and up\n@media screen and (min-width: @screen-sm-min) {\n\n // Scale up the controls a smidge\n .carousel-control {\n .glyphicon-chevron-left,\n .glyphicon-chevron-right,\n .icon-prev,\n .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -15px;\n font-size: 30px;\n }\n .glyphicon-chevron-left,\n .icon-prev {\n margin-left: -15px;\n }\n .glyphicon-chevron-right,\n .icon-next {\n margin-right: -15px;\n }\n }\n\n // Show and left align the captions\n .carousel-caption {\n left: 20%;\n right: 20%;\n padding-bottom: 30px;\n }\n\n // Move up the indicators\n .carousel-indicators {\n bottom: 20px;\n }\n}\n","// Clearfix\n//\n// For modern browsers\n// 1. The space content is one way to avoid an Opera bug when the\n// contenteditable attribute is included anywhere else in the document.\n// Otherwise it causes space to appear at the top and bottom of elements\n// that are clearfixed.\n// 2. The use of `table` rather than `block` is only necessary if using\n// `:before` to contain the top-margins of child elements.\n//\n// Source: http://nicolasgallagher.com/micro-clearfix-hack/\n\n.clearfix() {\n &:before,\n &:after {\n content: \" \"; // 1\n display: table; // 2\n }\n &:after {\n clear: both;\n }\n}\n","// Center-align a block level element\n\n.center-block() {\n display: block;\n margin-left: auto;\n margin-right: auto;\n}\n","// CSS image replacement\n//\n// Heads up! v3 launched with with only `.hide-text()`, but per our pattern for\n// mixins being reused as classes with the same name, this doesn't hold up. As\n// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`.\n//\n// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757\n\n// Deprecated as of v3.0.1 (will be removed in v4)\n.hide-text() {\n font: ~\"0/0\" a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n\n// New mixin to use as of v3.0.1\n.text-hide() {\n .hide-text();\n}\n","//\n// Responsive: Utility classes\n// --------------------------------------------------\n\n\n// IE10 in Windows (Phone) 8\n//\n// Support for responsive views via media queries is kind of borked in IE10, for\n// Surface/desktop in split view and for Windows Phone 8. This particular fix\n// must be accompanied by a snippet of JavaScript to sniff the user agent and\n// apply some conditional CSS to *only* the Surface/desktop Windows 8. Look at\n// our Getting Started page for more information on this bug.\n//\n// For more information, see the following:\n//\n// Issue: https://github.com/twbs/bootstrap/issues/10497\n// Docs: http://getbootstrap.com/getting-started/#support-ie10-width\n// Source: http://timkadlec.com/2013/01/windows-phone-8-and-device-width/\n// Source: http://timkadlec.com/2012/10/ie10-snap-mode-and-responsive-design/\n\n@-ms-viewport {\n width: device-width;\n}\n\n\n// Visibility utilities\n// Note: Deprecated .visible-xs, .visible-sm, .visible-md, and .visible-lg as of v3.2.0\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n .responsive-invisibility();\n}\n\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n\n.visible-xs {\n @media (max-width: @screen-xs-max) {\n .responsive-visibility();\n }\n}\n.visible-xs-block {\n @media (max-width: @screen-xs-max) {\n display: block !important;\n }\n}\n.visible-xs-inline {\n @media (max-width: @screen-xs-max) {\n display: inline !important;\n }\n}\n.visible-xs-inline-block {\n @media (max-width: @screen-xs-max) {\n display: inline-block !important;\n }\n}\n\n.visible-sm {\n @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {\n .responsive-visibility();\n }\n}\n.visible-sm-block {\n @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {\n display: block !important;\n }\n}\n.visible-sm-inline {\n @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {\n display: inline !important;\n }\n}\n.visible-sm-inline-block {\n @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {\n display: inline-block !important;\n }\n}\n\n.visible-md {\n @media (min-width: @screen-md-min) and (max-width: @screen-md-max) {\n .responsive-visibility();\n }\n}\n.visible-md-block {\n @media (min-width: @screen-md-min) and (max-width: @screen-md-max) {\n display: block !important;\n }\n}\n.visible-md-inline {\n @media (min-width: @screen-md-min) and (max-width: @screen-md-max) {\n display: inline !important;\n }\n}\n.visible-md-inline-block {\n @media (min-width: @screen-md-min) and (max-width: @screen-md-max) {\n display: inline-block !important;\n }\n}\n\n.visible-lg {\n @media (min-width: @screen-lg-min) {\n .responsive-visibility();\n }\n}\n.visible-lg-block {\n @media (min-width: @screen-lg-min) {\n display: block !important;\n }\n}\n.visible-lg-inline {\n @media (min-width: @screen-lg-min) {\n display: inline !important;\n }\n}\n.visible-lg-inline-block {\n @media (min-width: @screen-lg-min) {\n display: inline-block !important;\n }\n}\n\n.hidden-xs {\n @media (max-width: @screen-xs-max) {\n .responsive-invisibility();\n }\n}\n.hidden-sm {\n @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {\n .responsive-invisibility();\n }\n}\n.hidden-md {\n @media (min-width: @screen-md-min) and (max-width: @screen-md-max) {\n .responsive-invisibility();\n }\n}\n.hidden-lg {\n @media (min-width: @screen-lg-min) {\n .responsive-invisibility();\n }\n}\n\n\n// Print utilities\n//\n// Media queries are placed on the inside to be mixin-friendly.\n\n// Note: Deprecated .visible-print as of v3.2.0\n.visible-print {\n .responsive-invisibility();\n\n @media print {\n .responsive-visibility();\n }\n}\n.visible-print-block {\n display: none !important;\n\n @media print {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n\n @media print {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n\n @media print {\n display: inline-block !important;\n }\n}\n\n.hidden-print {\n @media print {\n .responsive-invisibility();\n }\n}\n","// Responsive utilities\n\n//\n// More easily include all the states for responsive-utilities.less.\n.responsive-visibility() {\n display: block !important;\n table& { display: table; }\n tr& { display: table-row !important; }\n th&,\n td& { display: table-cell !important; }\n}\n\n.responsive-invisibility() {\n display: none !important;\n}\n"]}
\ No newline at end of file
diff --git a/v2/css/bootstrap.min.css b/v2/css/bootstrap.min.css
new file mode 100644
index 00000000..b6fe4e0f
--- /dev/null
+++ b/v2/css/bootstrap.min.css
@@ -0,0 +1,5 @@
+/*!
+ * Bootstrap v3.3.1 (http://getbootstrap.com)
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ *//*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:before,:after{color:#000!important;text-shadow:none!important;background:transparent!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date],input[type=time],input[type=datetime-local],input[type=month]{line-height:34px}input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{position:absolute;margin-top:4px \9;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=checkbox][disabled],input[type=radio].disabled,input[type=checkbox].disabled,fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm,.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm,select.form-group-sm .form-control{height:30px;line-height:30px}textarea.input-sm,textarea.form-group-sm .form-control,select[multiple].input-sm,select[multiple].form-group-sm .form-control{height:auto}.input-lg,.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg,select.form-group-lg .form-control{height:46px;line-height:46px}textarea.input-lg,textarea.form-group-lg .form-control,select[multiple].input-lg,select[multiple].form-group-lg .form-control{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.3px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default.focus,.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary:hover,.btn-primary:focus,.btn-primary.focus,.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success.focus,.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info.focus,.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning.focus,.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger.focus,.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none;visibility:hidden}.collapse.in{display:block;visibility:visible}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#777}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px solid}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=radio],[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none;visibility:hidden}.tab-content>.active{display:block;visibility:visible}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important;visibility:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type=radio],.navbar-form .checkbox input[type=checkbox]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:hover,.label-default[href]:focus{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px 15px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding:48px 0}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{color:#555;text-decoration:none;background-color:#f5f5f5}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-right:15px;padding-left:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:absolute;top:0;right:0;left:0;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.43px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-weight:400;line-height:1.4;visibility:visible;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.42857143;text-align:left;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000;perspective:1000}.carousel-inner>.item.next,.carousel-inner>.item.active.right{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.prev,.carousel-inner>.item.active.left{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right,.carousel-inner>.item.active{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{display:table;content:" "}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none!important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}
\ No newline at end of file
diff --git a/v2/css/carousel.css b/v2/css/carousel.css
new file mode 100644
index 00000000..f513e3f5
--- /dev/null
+++ b/v2/css/carousel.css
@@ -0,0 +1,142 @@
+/* GLOBAL STYLES
+-------------------------------------------------- */
+/* Padding below the footer and lighter body text */
+
+body {
+ padding-bottom: 40px;
+ color: #5a5a5a;
+}
+
+
+/* CUSTOMIZE THE NAVBAR
+-------------------------------------------------- */
+
+/* Special class on .container surrounding .navbar, used for positioning it into place. */
+.navbar-wrapper {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: 20;
+}
+
+/* Flip around the padding for proper display in narrow viewports */
+.navbar-wrapper > .container {
+ padding-right: 0;
+ padding-left: 0;
+}
+.navbar-wrapper .navbar {
+ padding-right: 15px;
+ padding-left: 15px;
+}
+.navbar-wrapper .navbar .container {
+ width: auto;
+}
+
+
+/* CUSTOMIZE THE CAROUSEL
+-------------------------------------------------- */
+
+/* Carousel base class */
+.carousel {
+ height: 500px;
+ margin-bottom: 60px;
+}
+/* Since positioning the image, we need to help out the caption */
+.carousel-caption {
+ z-index: 10;
+ /*background-color:rgba(0,0,0,0.2);*/
+ /*padding-left: 20px;*/
+ /*padding-right:20px;*/
+ /*border-radius:30px;*/
+}
+
+/* Declare heights because of positioning of img element */
+.carousel .item {
+ height: 500px;
+ background-color: #064880;
+ }
+.carousel .item.dark {
+ background-color: #06132c;
+ }
+.carousel-inner > .item > img {
+ position: absolute;
+ top: 0;
+ left: 0;
+ min-width: 100%;
+ height: 800px;
+}
+
+
+/* MARKETING CONTENT
+-------------------------------------------------- */
+
+/* Center align the text within the three columns below the carousel */
+.marketing .col-lg-3 {
+ margin-bottom: 20px;
+ text-align: center;
+}
+.marketing h2 {
+ font-weight: normal;
+}
+.marketing .col-lg-3 p {
+ margin-right: 10px;
+ margin-left: 10px;
+}
+
+
+/* Featurettes
+------------------------- */
+
+.featurette-divider {
+ margin: 80px 0; /* Space out the Bootstrap
more */
+ border-top: 1px solid #d2d2d2;
+}
+
+/* Thin out the marketing headings */
+.featurette-heading {
+ font-weight: 300;
+ line-height: 1;
+ letter-spacing: -1px;
+}
+
+
+/* RESPONSIVE CSS
+-------------------------------------------------- */
+
+@media (min-width: 768px) {
+ /* Navbar positioning foo */
+ .navbar-wrapper {
+ margin-top: 20px;
+ }
+ .navbar-wrapper .container {
+ padding-right: 15px;
+ padding-left: 15px;
+ }
+ .navbar-wrapper .navbar {
+ padding-right: 0;
+ padding-left: 0;
+ }
+
+ /* The navbar becomes detached from the top, so we round the corners */
+ .navbar-wrapper .navbar {
+ border-radius: 4px;
+ }
+
+ /* Bump up size of carousel content */
+ .carousel-caption p {
+ margin-bottom: 20px;
+ font-size: 21px;
+ line-height: 1.4;
+ }
+
+ .featurette-heading {
+ font-size: 50px;
+ }
+}
+
+@media (min-width: 992px) {
+ .featurette-heading {
+ margin-top: 120px;
+ }
+}
\ No newline at end of file
diff --git a/v2/css/prettify.css b/v2/css/prettify.css
new file mode 100644
index 00000000..3c7acd2e
--- /dev/null
+++ b/v2/css/prettify.css
@@ -0,0 +1,87 @@
+.com {
+ color: gray;
+}
+
+.lit {
+ color: red;
+}
+
+.pun {
+ color: gray;
+}
+
+.pln {
+ color: #333333;
+}
+
+pre.prettyprint {
+ border: 1px solid lightgray;
+ background-color: #fcfcfc;
+ padding: 5px;
+
+ font-size: 10pt;
+ line-height: 1.5em;
+ font-family: monospace;
+}
+
+ol.linenums {
+ margin-top:0;
+ margin-bottom:0;
+}
+
+li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8 {
+ list-style:none;
+}
+
+li.L1,li.L3,li.L5,li.L7,li.L9 {
+ background:#eee;
+}
+
+.str,.atv {
+ color: green;
+}
+
+.kwd,.tag {
+ color:#2B7CE9;
+}
+
+.typ,.atn,.dec {
+ color: darkorange;
+}
+
+@media print {
+ .com {
+ color:#600;
+ font-style:italic;
+ }
+
+ .typ {
+ color:#404;
+ font-weight:700;
+ }
+
+ .lit {
+ color:#044;
+ }
+
+ .pun {
+ color:#440;
+ }
+
+ .pln {
+ color:#000;
+ }
+
+ .atn {
+ color:#404;
+ }
+
+ .str,.atv {
+ color:#060;
+ }
+
+ .kwd,.tag {
+ color:#006;
+ font-weight:700;
+ }
+}
\ No newline at end of file
diff --git a/v2/dist/img/network/acceptDeleteIcon.png b/v2/dist/img/network/acceptDeleteIcon.png
new file mode 100644
index 00000000..02a06285
Binary files /dev/null and b/v2/dist/img/network/acceptDeleteIcon.png differ
diff --git a/v2/dist/img/network/addNodeIcon.png b/v2/dist/img/network/addNodeIcon.png
new file mode 100644
index 00000000..6fa30613
Binary files /dev/null and b/v2/dist/img/network/addNodeIcon.png differ
diff --git a/v2/dist/img/network/backIcon.png b/v2/dist/img/network/backIcon.png
new file mode 100644
index 00000000..e2f99126
Binary files /dev/null and b/v2/dist/img/network/backIcon.png differ
diff --git a/v2/dist/img/network/connectIcon.png b/v2/dist/img/network/connectIcon.png
new file mode 100644
index 00000000..4164da1f
Binary files /dev/null and b/v2/dist/img/network/connectIcon.png differ
diff --git a/v2/dist/img/network/cross.png b/v2/dist/img/network/cross.png
new file mode 100644
index 00000000..9cbd189a
Binary files /dev/null and b/v2/dist/img/network/cross.png differ
diff --git a/v2/dist/img/network/cross2.png b/v2/dist/img/network/cross2.png
new file mode 100644
index 00000000..9fc4b95c
Binary files /dev/null and b/v2/dist/img/network/cross2.png differ
diff --git a/v2/dist/img/network/deleteIcon.png b/v2/dist/img/network/deleteIcon.png
new file mode 100644
index 00000000..54025647
Binary files /dev/null and b/v2/dist/img/network/deleteIcon.png differ
diff --git a/v2/dist/img/network/downArrow.png b/v2/dist/img/network/downArrow.png
new file mode 100644
index 00000000..e77d5e6d
Binary files /dev/null and b/v2/dist/img/network/downArrow.png differ
diff --git a/v2/dist/img/network/editIcon.png b/v2/dist/img/network/editIcon.png
new file mode 100644
index 00000000..494d0f00
Binary files /dev/null and b/v2/dist/img/network/editIcon.png differ
diff --git a/v2/dist/img/network/leftArrow.png b/v2/dist/img/network/leftArrow.png
new file mode 100644
index 00000000..3823536e
Binary files /dev/null and b/v2/dist/img/network/leftArrow.png differ
diff --git a/v2/dist/img/network/minus.png b/v2/dist/img/network/minus.png
new file mode 100644
index 00000000..30698076
Binary files /dev/null and b/v2/dist/img/network/minus.png differ
diff --git a/v2/dist/img/network/plus.png b/v2/dist/img/network/plus.png
new file mode 100644
index 00000000..f7ab2a33
Binary files /dev/null and b/v2/dist/img/network/plus.png differ
diff --git a/v2/dist/img/network/rightArrow.png b/v2/dist/img/network/rightArrow.png
new file mode 100644
index 00000000..c3a209d8
Binary files /dev/null and b/v2/dist/img/network/rightArrow.png differ
diff --git a/v2/dist/img/network/upArrow.png b/v2/dist/img/network/upArrow.png
new file mode 100644
index 00000000..8aedced7
Binary files /dev/null and b/v2/dist/img/network/upArrow.png differ
diff --git a/v2/dist/img/network/zoomExtends.png b/v2/dist/img/network/zoomExtends.png
new file mode 100644
index 00000000..74595c63
Binary files /dev/null and b/v2/dist/img/network/zoomExtends.png differ
diff --git a/v2/dist/img/timeline/delete.png b/v2/dist/img/timeline/delete.png
new file mode 100644
index 00000000..d54d0e06
Binary files /dev/null and b/v2/dist/img/timeline/delete.png differ
diff --git a/v2/dist/vis.css b/v2/dist/vis.css
new file mode 100644
index 00000000..529a17fb
--- /dev/null
+++ b/v2/dist/vis.css
@@ -0,0 +1,795 @@
+.vis .overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+
+ /* Must be displayed above for example selected Timeline items */
+ z-index: 10;
+}
+
+.vis-active {
+ box-shadow: 0 0 10px #86d5f8;
+}
+
+/* override some bootstrap styles screwing up the timelines css */
+
+.vis [class*="span"] {
+ min-height: 0;
+ width: auto;
+}
+
+.vis.timeline {
+}
+
+
+.vis.timeline.root {
+ position: relative;
+ border: 1px solid #bfbfbf;
+
+ overflow: hidden;
+ padding: 0;
+ margin: 0;
+
+ box-sizing: border-box;
+}
+
+.vis.timeline .vispanel {
+ position: absolute;
+
+ padding: 0;
+ margin: 0;
+
+ box-sizing: border-box;
+}
+
+.vis.timeline .vispanel.center,
+.vis.timeline .vispanel.left,
+.vis.timeline .vispanel.right,
+.vis.timeline .vispanel.top,
+.vis.timeline .vispanel.bottom {
+ border: 1px #bfbfbf;
+}
+
+.vis.timeline .vispanel.center,
+.vis.timeline .vispanel.left,
+.vis.timeline .vispanel.right {
+ border-top-style: solid;
+ border-bottom-style: solid;
+ overflow: hidden;
+}
+
+.vis.timeline .vispanel.center,
+.vis.timeline .vispanel.top,
+.vis.timeline .vispanel.bottom {
+ border-left-style: solid;
+ border-right-style: solid;
+}
+
+.vis.timeline .background {
+ overflow: hidden;
+}
+
+.vis.timeline .vispanel > .content {
+ position: relative;
+}
+
+.vis.timeline .vispanel .shadow {
+ position: absolute;
+ width: 100%;
+ height: 1px;
+ box-shadow: 0 0 10px rgba(0,0,0,0.8);
+ /* TODO: find a nice way to ensure shadows are drawn on top of items
+ z-index: 1;
+ */
+}
+
+.vis.timeline .vispanel .shadow.top {
+ top: -1px;
+ left: 0;
+}
+
+.vis.timeline .vispanel .shadow.bottom {
+ bottom: -1px;
+ left: 0;
+}
+
+.vis.timeline .labelset {
+ position: relative;
+
+ overflow: hidden;
+
+ box-sizing: border-box;
+}
+
+.vis.timeline .labelset .vlabel {
+ position: relative;
+ left: 0;
+ top: 0;
+ width: 100%;
+ color: #4d4d4d;
+
+ box-sizing: border-box;
+}
+
+.vis.timeline .labelset .vlabel {
+ border-bottom: 1px solid #bfbfbf;
+}
+
+.vis.timeline .labelset .vlabel:last-child {
+ border-bottom: none;
+}
+
+.vis.timeline .labelset .vlabel .inner {
+ display: inline-block;
+ padding: 5px;
+}
+
+.vis.timeline .labelset .vlabel .inner.hidden {
+ padding: 0;
+}
+
+
+.vis.timeline .itemset {
+ position: relative;
+ padding: 0;
+ margin: 0;
+
+ box-sizing: border-box;
+}
+
+.vis.timeline .itemset .background,
+.vis.timeline .itemset .foreground {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ overflow: visible;
+}
+
+.vis.timeline .axis {
+ position: absolute;
+ width: 100%;
+ height: 0;
+ left: 0;
+ z-index: 1;
+}
+
+.vis.timeline .foreground .group {
+ position: relative;
+ box-sizing: border-box;
+ border-bottom: 1px solid #bfbfbf;
+}
+
+.vis.timeline .foreground .group:last-child {
+ border-bottom: none;
+}
+
+
+.vis.timeline .item {
+ position: absolute;
+ color: #1A1A1A;
+ border-color: #97B0F8;
+ border-width: 1px;
+ background-color: #D5DDF6;
+ display: inline-block;
+ padding: 5px;
+}
+
+.vis.timeline .item.selected {
+ border-color: #FFC200;
+ background-color: #FFF785;
+
+ /* z-index must be higher than the z-index of custom time bar and current time bar */
+ z-index: 2;
+}
+
+.vis.timeline .editable .item.selected {
+ cursor: move;
+}
+
+.vis.timeline .item.point.selected {
+ background-color: #FFF785;
+}
+
+.vis.timeline .item.box {
+ text-align: center;
+ border-style: solid;
+ border-radius: 2px;
+}
+
+.vis.timeline .item.point {
+ background: none;
+}
+
+.vis.timeline .item.dot {
+ position: absolute;
+ padding: 0;
+ border-width: 4px;
+ border-style: solid;
+ border-radius: 4px;
+}
+
+.vis.timeline .item.range {
+ border-style: solid;
+ border-radius: 2px;
+ box-sizing: border-box;
+}
+
+.vis.timeline .item.background {
+ overflow: hidden;
+ border: none;
+ background-color: rgba(213, 221, 246, 0.4);
+ box-sizing: border-box;
+ padding: 0;
+ margin: 0;
+}
+
+.vis.timeline .item.range .content {
+ position: relative;
+ display: inline-block;
+ max-width: 100%;
+ overflow: hidden;
+}
+
+.vis.timeline .item.background .content {
+ position: absolute;
+ display: inline-block;
+ overflow: hidden;
+ max-width: 100%;
+ margin: 5px;
+}
+
+.vis.timeline .item.line {
+ padding: 0;
+ position: absolute;
+ width: 0;
+ border-left-width: 1px;
+ border-left-style: solid;
+}
+
+.vis.timeline .item .content {
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.vis.timeline .item .delete {
+ background: url('img/timeline/delete.png') no-repeat top center;
+ position: absolute;
+ width: 24px;
+ height: 24px;
+ top: 0;
+ right: -24px;
+ cursor: pointer;
+}
+
+.vis.timeline .item.range .drag-left {
+ position: absolute;
+ width: 24px;
+ height: 100%;
+ top: 0;
+ left: -4px;
+
+ cursor: w-resize;
+}
+
+.vis.timeline .item.range .drag-right {
+ position: absolute;
+ width: 24px;
+ height: 100%;
+ top: 0;
+ right: -4px;
+
+ cursor: e-resize;
+}
+
+.vis.timeline .timeaxis {
+ position: relative;
+ overflow: hidden;
+}
+
+.vis.timeline .timeaxis.foreground {
+ top: 0;
+ left: 0;
+ width: 100%;
+}
+
+.vis.timeline .timeaxis.background {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.vis.timeline .timeaxis .text {
+ position: absolute;
+ color: #4d4d4d;
+ padding: 3px;
+ white-space: nowrap;
+}
+
+.vis.timeline .timeaxis .text.measure {
+ position: absolute;
+ padding-left: 0;
+ padding-right: 0;
+ margin-left: 0;
+ margin-right: 0;
+ visibility: hidden;
+}
+
+.vis.timeline .timeaxis .grid.vertical {
+ position: absolute;
+ width: 0;
+ border-right: 1px solid;
+}
+
+.vis.timeline .timeaxis .grid.minor {
+ border-color: #e5e5e5;
+}
+
+.vis.timeline .timeaxis .grid.major {
+ border-color: #bfbfbf;
+}
+
+.vis.timeline .currenttime {
+ background-color: #FF7F6E;
+ width: 2px;
+ z-index: 1;
+}
+.vis.timeline .customtime {
+ background-color: #6E94FF;
+ width: 2px;
+ cursor: move;
+ z-index: 1;
+}
+.vis.timeline.root {
+ /*
+ -webkit-transition: height .4s ease-in-out;
+ transition: height .4s ease-in-out;
+ */
+}
+
+.vis.timeline .vispanel {
+ /*
+ -webkit-transition: height .4s ease-in-out, top .4s ease-in-out;
+ transition: height .4s ease-in-out, top .4s ease-in-out;
+ */
+}
+
+.vis.timeline .axis {
+ /*
+ -webkit-transition: top .4s ease-in-out;
+ transition: top .4s ease-in-out;
+ */
+}
+
+/* TODO: get animation working nicely
+
+.vis.timeline .item {
+ -webkit-transition: top .4s ease-in-out;
+ transition: top .4s ease-in-out;
+}
+
+.vis.timeline .item.line {
+ -webkit-transition: height .4s ease-in-out, top .4s ease-in-out;
+ transition: height .4s ease-in-out, top .4s ease-in-out;
+}
+/**/
+
+.vis.timeline .vispanel.background.horizontal .grid.horizontal {
+ position: absolute;
+ width: 100%;
+ height: 0;
+ border-bottom: 1px solid;
+}
+
+.vis.timeline .vispanel.background.horizontal .grid.minor {
+ border-color: #e5e5e5;
+}
+
+.vis.timeline .vispanel.background.horizontal .grid.major {
+ border-color: #bfbfbf;
+}
+
+
+.vis.timeline .dataaxis .yAxis.major {
+ width: 100%;
+ position: absolute;
+ color: #4d4d4d;
+ white-space: nowrap;
+}
+
+.vis.timeline .dataaxis .yAxis.major.measure{
+ padding: 0px 0px 0px 0px;
+ margin: 0px 0px 0px 0px;
+ border: 0px;
+ visibility: hidden;
+ width: auto;
+}
+
+
+.vis.timeline .dataaxis .yAxis.minor{
+ position: absolute;
+ width: 100%;
+ color: #bebebe;
+ white-space: nowrap;
+}
+
+.vis.timeline .dataaxis .yAxis.minor.measure{
+ padding: 0px 0px 0px 0px;
+ margin: 0px 0px 0px 0px;
+ border: 0px;
+ visibility: hidden;
+ width: auto;
+}
+
+.vis.timeline .dataaxis .yAxis.title{
+ position: absolute;
+ color: #4d4d4d;
+ white-space: nowrap;
+ bottom: 20px;
+ text-align: center;
+}
+
+.vis.timeline .dataaxis .yAxis.title.measure{
+ padding: 0px 0px 0px 0px;
+ margin: 0px 0px 0px 0px;
+ visibility: hidden;
+ width: auto;
+}
+
+.vis.timeline .dataaxis .yAxis.title.left {
+ bottom: 0px;
+ -webkit-transform-origin: left top;
+ -moz-transform-origin: left top;
+ -ms-transform-origin: left top;
+ -o-transform-origin: left top;
+ transform-origin: left bottom;
+ -webkit-transform: rotate(-90deg);
+ -moz-transform: rotate(-90deg);
+ -ms-transform: rotate(-90deg);
+ -o-transform: rotate(-90deg);
+ transform: rotate(-90deg);
+}
+
+.vis.timeline .dataaxis .yAxis.title.right {
+ bottom: 0px;
+ -webkit-transform-origin: right bottom;
+ -moz-transform-origin: right bottom;
+ -ms-transform-origin: right bottom;
+ -o-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate(90deg);
+ -moz-transform: rotate(90deg);
+ -ms-transform: rotate(90deg);
+ -o-transform: rotate(90deg);
+ transform: rotate(90deg);
+}
+
+.vis.timeline .legend {
+ background-color: rgba(247, 252, 255, 0.65);
+ padding: 5px;
+ border-color: #b3b3b3;
+ border-style:solid;
+ border-width: 1px;
+ box-shadow: 2px 2px 10px rgba(154, 154, 154, 0.55);
+}
+
+.vis.timeline .legendText {
+ /*font-size: 10px;*/
+ white-space: nowrap;
+ display: inline-block
+}
+.vis.timeline .graphGroup0 {
+ fill:#4f81bd;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #4f81bd;
+}
+
+.vis.timeline .graphGroup1 {
+ fill:#f79646;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #f79646;
+}
+
+.vis.timeline .graphGroup2 {
+ fill: #8c51cf;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #8c51cf;
+}
+
+.vis.timeline .graphGroup3 {
+ fill: #75c841;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #75c841;
+}
+
+.vis.timeline .graphGroup4 {
+ fill: #ff0100;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #ff0100;
+}
+
+.vis.timeline .graphGroup5 {
+ fill: #37d8e6;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #37d8e6;
+}
+
+.vis.timeline .graphGroup6 {
+ fill: #042662;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #042662;
+}
+
+.vis.timeline .graphGroup7 {
+ fill:#00ff26;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #00ff26;
+}
+
+.vis.timeline .graphGroup8 {
+ fill:#ff00ff;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #ff00ff;
+}
+
+.vis.timeline .graphGroup9 {
+ fill: #8f3938;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #8f3938;
+}
+
+.vis.timeline .fill {
+ fill-opacity:0.1;
+ stroke: none;
+}
+
+
+.vis.timeline .bar {
+ fill-opacity:0.5;
+ stroke-width:1px;
+}
+
+.vis.timeline .point {
+ stroke-width:2px;
+ fill-opacity:1.0;
+}
+
+
+.vis.timeline .legendBackground {
+ stroke-width:1px;
+ fill-opacity:0.9;
+ fill: #ffffff;
+ stroke: #c2c2c2;
+}
+
+
+.vis.timeline .outline {
+ stroke-width:1px;
+ fill-opacity:1;
+ fill: #ffffff;
+ stroke: #e5e5e5;
+}
+
+.vis.timeline .iconFill {
+ fill-opacity:0.3;
+ stroke: none;
+}
+
+
+
+div.network-manipulationDiv {
+ border-width: 0;
+ border-bottom: 1px;
+ border-style:solid;
+ border-color: #d6d9d8;
+ background: #ffffff; /* Old browsers */
+ background: -moz-linear-gradient(top, #ffffff 0%, #fcfcfc 48%, #fafafa 50%, #fcfcfc 100%); /* FF3.6+ */
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(48%,#fcfcfc), color-stop(50%,#fafafa), color-stop(100%,#fcfcfc)); /* Chrome,Safari4+ */
+ background: -webkit-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Chrome10+,Safari5.1+ */
+ background: -o-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* IE10+ */
+ background: linear-gradient(to bottom, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* W3C */
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fcfcfc',GradientType=0 ); /* IE6-9 */
+
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 30px;
+}
+
+div.network-manipulation-editMode {
+ position:absolute;
+ left: 0;
+ top: 0;
+ height: 30px;
+ margin-top:20px;
+}
+
+div.network-manipulation-closeDiv {
+ position:absolute;
+ right: 0;
+ top: 0;
+ width: 30px;
+ height: 30px;
+
+ background-position: 20px 3px;
+ background-repeat: no-repeat;
+ background-image: url("img/network/cross.png");
+ cursor: pointer;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+div.network-manipulation-closeDiv:hover {
+ opacity: 0.6;
+}
+
+span.network-manipulationUI {
+ font-family: verdana;
+ font-size: 12px;
+ -moz-border-radius: 15px;
+ border-radius: 15px;
+ display:inline-block;
+ background-position: 0px 0px;
+ background-repeat:no-repeat;
+ height:24px;
+ margin: -14px 0px 0px 10px;
+ vertical-align:middle;
+ cursor: pointer;
+ padding: 0px 8px 0px 8px;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+span.network-manipulationUI:hover {
+ box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20);
+}
+
+span.network-manipulationUI:active {
+ box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.50);
+}
+
+span.network-manipulationUI.back {
+ background-image: url("img/network/backIcon.png");
+}
+
+span.network-manipulationUI.none:hover {
+ box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
+ cursor: default;
+}
+span.network-manipulationUI.none:active {
+ box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
+}
+span.network-manipulationUI.none {
+ padding: 0;
+}
+span.network-manipulationUI.notification{
+ margin: 2px;
+ font-weight: bold;
+}
+
+span.network-manipulationUI.add {
+ background-image: url("img/network/addNodeIcon.png");
+}
+
+span.network-manipulationUI.edit {
+ background-image: url("img/network/editIcon.png");
+}
+
+span.network-manipulationUI.edit.editmode {
+ background-color: #fcfcfc;
+ border-style:solid;
+ border-width:1px;
+ border-color: #cccccc;
+}
+
+span.network-manipulationUI.connect {
+ background-image: url("img/network/connectIcon.png");
+}
+
+span.network-manipulationUI.delete {
+ background-image: url("img/network/deleteIcon.png");
+}
+/* top right bottom left */
+span.network-manipulationLabel {
+ margin: 0px 0px 0px 23px;
+ line-height: 25px;
+}
+div.network-seperatorLine {
+ display:inline-block;
+ width:1px;
+ height:20px;
+ background-color: #bdbdbd;
+ margin: 5px 7px 0px 15px;
+}
+
+div.network-navigation_wrapper {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+}
+div.network-navigation {
+ width:34px;
+ height:34px;
+ -moz-border-radius: 17px;
+ border-radius: 17px;
+ position:absolute;
+ display:inline-block;
+ background-position: 2px 2px;
+ background-repeat:no-repeat;
+ cursor: pointer;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+div.network-navigation:hover {
+ box-shadow: 0px 0px 3px 3px rgba(56, 207, 21, 0.30);
+}
+
+div.network-navigation:active {
+ box-shadow: 0px 0px 1px 3px rgba(56, 207, 21, 0.95);
+}
+
+div.network-navigation.up {
+ background-image: url("img/network/upArrow.png");
+ bottom:50px;
+ left:55px;
+}
+div.network-navigation.down {
+ background-image: url("img/network/downArrow.png");
+ bottom:10px;
+ left:55px;
+}
+div.network-navigation.left {
+ background-image: url("img/network/leftArrow.png");
+ bottom:10px;
+ left:15px;
+}
+div.network-navigation.right {
+ background-image: url("img/network/rightArrow.png");
+ bottom:10px;
+ left:95px;
+}
+div.network-navigation.zoomIn {
+ background-image: url("img/network/plus.png");
+ bottom:10px;
+ right:15px;
+}
+div.network-navigation.zoomOut {
+ background-image: url("img/network/minus.png");
+ bottom:10px;
+ right:55px;
+}
+div.network-navigation.zoomExtends {
+ background-image: url("img/network/zoomExtends.png");
+ bottom:50px;
+ right:15px;
+}
\ No newline at end of file
diff --git a/v2/dist/vis.js b/v2/dist/vis.js
new file mode 100644
index 00000000..516d408f
--- /dev/null
+++ b/v2/dist/vis.js
@@ -0,0 +1,34028 @@
+/**
+ * vis.js
+ * https://github.com/almende/vis
+ *
+ * A dynamic, browser-based visualization library.
+ *
+ * @version 3.7.2-SNAPSHOT
+ * @date 2014-12-24
+ *
+ * @license
+ * Copyright (C) 2011-2014 Almende B.V, http://almende.com
+ *
+ * Vis.js is dual licensed under both
+ *
+ * * The Apache 2.0 License
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * and
+ *
+ * * The MIT License
+ * http://opensource.org/licenses/MIT
+ *
+ * Vis.js may be distributed under either license.
+ */
+
+"use strict";
+
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define(factory);
+ else if(typeof exports === 'object')
+ exports["vis"] = factory();
+ else
+ root["vis"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+/******/
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // utils
+ exports.util = __webpack_require__(1);
+ exports.DOMutil = __webpack_require__(6);
+
+ // data
+ exports.DataSet = __webpack_require__(7);
+ exports.DataView = __webpack_require__(9);
+ exports.Queue = __webpack_require__(8);
+
+ // Graph3d
+ exports.Graph3d = __webpack_require__(10);
+ exports.graph3d = {
+ Camera: __webpack_require__(14),
+ Filter: __webpack_require__(15),
+ Point2d: __webpack_require__(13),
+ Point3d: __webpack_require__(12),
+ Slider: __webpack_require__(16),
+ StepNumber: __webpack_require__(17)
+ };
+
+ // Timeline
+ exports.Timeline = __webpack_require__(18);
+ exports.Graph2d = __webpack_require__(42);
+ exports.timeline = {
+ DateUtil: __webpack_require__(24),
+ DataStep: __webpack_require__(45),
+ Range: __webpack_require__(21),
+ stack: __webpack_require__(28),
+ TimeStep: __webpack_require__(38),
+
+ components: {
+ items: {
+ Item: __webpack_require__(30),
+ BackgroundItem: __webpack_require__(34),
+ BoxItem: __webpack_require__(32),
+ PointItem: __webpack_require__(33),
+ RangeItem: __webpack_require__(29)
+ },
+
+ Component: __webpack_require__(23),
+ CurrentTime: __webpack_require__(39),
+ CustomTime: __webpack_require__(41),
+ DataAxis: __webpack_require__(44),
+ GraphGroup: __webpack_require__(46),
+ Group: __webpack_require__(27),
+ BackgroundGroup: __webpack_require__(31),
+ ItemSet: __webpack_require__(26),
+ Legend: __webpack_require__(50),
+ LineGraph: __webpack_require__(43),
+ TimeAxis: __webpack_require__(37)
+ }
+ };
+
+ // Network
+ exports.Network = __webpack_require__(51);
+ exports.network = {
+ Edge: __webpack_require__(52),
+ Groups: __webpack_require__(54),
+ Images: __webpack_require__(55),
+ Node: __webpack_require__(53),
+ Popup: __webpack_require__(56),
+ dotparser: __webpack_require__(57),
+ gephiParser: __webpack_require__(58)
+ };
+
+ // Deprecated since v3.0.0
+ exports.Graph = function () {
+ throw new Error('Graph is renamed to Network. Please create a graph as new vis.Network(...)');
+ };
+
+ // bundled external libraries
+ exports.moment = __webpack_require__(2);
+ exports.hammer = __webpack_require__(19);
+
+
+/***/ },
+/* 1 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // utility functions
+
+ // first check if moment.js is already loaded in the browser window, if so,
+ // use this instance. Else, load via commonjs.
+ var moment = __webpack_require__(2);
+
+ /**
+ * Test whether given object is a number
+ * @param {*} object
+ * @return {Boolean} isNumber
+ */
+ exports.isNumber = function(object) {
+ return (object instanceof Number || typeof object == 'number');
+ };
+
+ /**
+ * Test whether given object is a string
+ * @param {*} object
+ * @return {Boolean} isString
+ */
+ exports.isString = function(object) {
+ return (object instanceof String || typeof object == 'string');
+ };
+
+ /**
+ * Test whether given object is a Date, or a String containing a Date
+ * @param {Date | String} object
+ * @return {Boolean} isDate
+ */
+ exports.isDate = function(object) {
+ if (object instanceof Date) {
+ return true;
+ }
+ else if (exports.isString(object)) {
+ // test whether this string contains a date
+ var match = ASPDateRegex.exec(object);
+ if (match) {
+ return true;
+ }
+ else if (!isNaN(Date.parse(object))) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Test whether given object is an instance of google.visualization.DataTable
+ * @param {*} object
+ * @return {Boolean} isDataTable
+ */
+ exports.isDataTable = function(object) {
+ return (typeof (google) !== 'undefined') &&
+ (google.visualization) &&
+ (google.visualization.DataTable) &&
+ (object instanceof google.visualization.DataTable);
+ };
+
+ /**
+ * Create a semi UUID
+ * source: http://stackoverflow.com/a/105074/1262753
+ * @return {String} uuid
+ */
+ exports.randomUUID = function() {
+ var S4 = function () {
+ return Math.floor(
+ Math.random() * 0x10000 /* 65536 */
+ ).toString(16);
+ };
+
+ return (
+ S4() + S4() + '-' +
+ S4() + '-' +
+ S4() + '-' +
+ S4() + '-' +
+ S4() + S4() + S4()
+ );
+ };
+
+ /**
+ * Extend object a with the properties of object b or a series of objects
+ * Only properties with defined values are copied
+ * @param {Object} a
+ * @param {... Object} b
+ * @return {Object} a
+ */
+ exports.extend = function (a, b) {
+ for (var i = 1, len = arguments.length; i < len; i++) {
+ var other = arguments[i];
+ for (var prop in other) {
+ if (other.hasOwnProperty(prop)) {
+ a[prop] = other[prop];
+ }
+ }
+ }
+
+ return a;
+ };
+
+ /**
+ * Extend object a with selected properties of object b or a series of objects
+ * Only properties with defined values are copied
+ * @param {Array.
} props
+ * @param {Object} a
+ * @param {... Object} b
+ * @return {Object} a
+ */
+ exports.selectiveExtend = function (props, a, b) {
+ if (!Array.isArray(props)) {
+ throw new Error('Array with property names expected as first argument');
+ }
+
+ for (var i = 2; i < arguments.length; i++) {
+ var other = arguments[i];
+
+ for (var p = 0; p < props.length; p++) {
+ var prop = props[p];
+ if (other.hasOwnProperty(prop)) {
+ a[prop] = other[prop];
+ }
+ }
+ }
+ return a;
+ };
+
+ /**
+ * Extend object a with selected properties of object b or a series of objects
+ * Only properties with defined values are copied
+ * @param {Array.} props
+ * @param {Object} a
+ * @param {... Object} b
+ * @return {Object} a
+ */
+ exports.selectiveDeepExtend = function (props, a, b) {
+ // TODO: add support for Arrays to deepExtend
+ if (Array.isArray(b)) {
+ throw new TypeError('Arrays are not supported by deepExtend');
+ }
+ for (var i = 2; i < arguments.length; i++) {
+ var other = arguments[i];
+ for (var p = 0; p < props.length; p++) {
+ var prop = props[p];
+ if (other.hasOwnProperty(prop)) {
+ if (b[prop] && b[prop].constructor === Object) {
+ if (a[prop] === undefined) {
+ a[prop] = {};
+ }
+ if (a[prop].constructor === Object) {
+ exports.deepExtend(a[prop], b[prop]);
+ }
+ else {
+ a[prop] = b[prop];
+ }
+ } else if (Array.isArray(b[prop])) {
+ throw new TypeError('Arrays are not supported by deepExtend');
+ } else {
+ a[prop] = b[prop];
+ }
+
+ }
+ }
+ }
+ return a;
+ };
+
+ /**
+ * Extend object a with selected properties of object b or a series of objects
+ * Only properties with defined values are copied
+ * @param {Array.} props
+ * @param {Object} a
+ * @param {... Object} b
+ * @return {Object} a
+ */
+ exports.selectiveNotDeepExtend = function (props, a, b) {
+ // TODO: add support for Arrays to deepExtend
+ if (Array.isArray(b)) {
+ throw new TypeError('Arrays are not supported by deepExtend');
+ }
+ for (var prop in b) {
+ if (b.hasOwnProperty(prop)) {
+ if (props.indexOf(prop) == -1) {
+ if (b[prop] && b[prop].constructor === Object) {
+ if (a[prop] === undefined) {
+ a[prop] = {};
+ }
+ if (a[prop].constructor === Object) {
+ exports.deepExtend(a[prop], b[prop]);
+ }
+ else {
+ a[prop] = b[prop];
+ }
+ } else if (Array.isArray(b[prop])) {
+ throw new TypeError('Arrays are not supported by deepExtend');
+ } else {
+ a[prop] = b[prop];
+ }
+ }
+ }
+ }
+ return a;
+ };
+
+ /**
+ * Deep extend an object a with the properties of object b
+ * @param {Object} a
+ * @param {Object} b
+ * @returns {Object}
+ */
+ exports.deepExtend = function(a, b) {
+ // TODO: add support for Arrays to deepExtend
+ if (Array.isArray(b)) {
+ throw new TypeError('Arrays are not supported by deepExtend');
+ }
+
+ for (var prop in b) {
+ if (b.hasOwnProperty(prop)) {
+ if (b[prop] && b[prop].constructor === Object) {
+ if (a[prop] === undefined) {
+ a[prop] = {};
+ }
+ if (a[prop].constructor === Object) {
+ exports.deepExtend(a[prop], b[prop]);
+ }
+ else {
+ a[prop] = b[prop];
+ }
+ } else if (Array.isArray(b[prop])) {
+ throw new TypeError('Arrays are not supported by deepExtend');
+ } else {
+ a[prop] = b[prop];
+ }
+ }
+ }
+ return a;
+ };
+
+ /**
+ * Test whether all elements in two arrays are equal.
+ * @param {Array} a
+ * @param {Array} b
+ * @return {boolean} Returns true if both arrays have the same length and same
+ * elements.
+ */
+ exports.equalArray = function (a, b) {
+ if (a.length != b.length) return false;
+
+ for (var i = 0, len = a.length; i < len; i++) {
+ if (a[i] != b[i]) return false;
+ }
+
+ return true;
+ };
+
+ /**
+ * Convert an object to another type
+ * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
+ * @param {String | undefined} type Name of the type. Available types:
+ * 'Boolean', 'Number', 'String',
+ * 'Date', 'Moment', ISODate', 'ASPDate'.
+ * @return {*} object
+ * @throws Error
+ */
+ exports.convert = function(object, type) {
+ var match;
+
+ if (object === undefined) {
+ return undefined;
+ }
+ if (object === null) {
+ return null;
+ }
+
+ if (!type) {
+ return object;
+ }
+ if (!(typeof type === 'string') && !(type instanceof String)) {
+ throw new Error('Type must be a string');
+ }
+
+ //noinspection FallthroughInSwitchStatementJS
+ switch (type) {
+ case 'boolean':
+ case 'Boolean':
+ return Boolean(object);
+
+ case 'number':
+ case 'Number':
+ return Number(object.valueOf());
+
+ case 'string':
+ case 'String':
+ return String(object);
+
+ case 'Date':
+ if (exports.isNumber(object)) {
+ return new Date(object);
+ }
+ if (object instanceof Date) {
+ return new Date(object.valueOf());
+ }
+ else if (moment.isMoment(object)) {
+ return new Date(object.valueOf());
+ }
+ if (exports.isString(object)) {
+ match = ASPDateRegex.exec(object);
+ if (match) {
+ // object is an ASP date
+ return new Date(Number(match[1])); // parse number
+ }
+ else {
+ return moment(object).toDate(); // parse string
+ }
+ }
+ else {
+ throw new Error(
+ 'Cannot convert object of type ' + exports.getType(object) +
+ ' to type Date');
+ }
+
+ case 'Moment':
+ if (exports.isNumber(object)) {
+ return moment(object);
+ }
+ if (object instanceof Date) {
+ return moment(object.valueOf());
+ }
+ else if (moment.isMoment(object)) {
+ return moment(object);
+ }
+ if (exports.isString(object)) {
+ match = ASPDateRegex.exec(object);
+ if (match) {
+ // object is an ASP date
+ return moment(Number(match[1])); // parse number
+ }
+ else {
+ return moment(object); // parse string
+ }
+ }
+ else {
+ throw new Error(
+ 'Cannot convert object of type ' + exports.getType(object) +
+ ' to type Date');
+ }
+
+ case 'ISODate':
+ if (exports.isNumber(object)) {
+ return new Date(object);
+ }
+ else if (object instanceof Date) {
+ return object.toISOString();
+ }
+ else if (moment.isMoment(object)) {
+ return object.toDate().toISOString();
+ }
+ else if (exports.isString(object)) {
+ match = ASPDateRegex.exec(object);
+ if (match) {
+ // object is an ASP date
+ return new Date(Number(match[1])).toISOString(); // parse number
+ }
+ else {
+ return new Date(object).toISOString(); // parse string
+ }
+ }
+ else {
+ throw new Error(
+ 'Cannot convert object of type ' + exports.getType(object) +
+ ' to type ISODate');
+ }
+
+ case 'ASPDate':
+ if (exports.isNumber(object)) {
+ return '/Date(' + object + ')/';
+ }
+ else if (object instanceof Date) {
+ return '/Date(' + object.valueOf() + ')/';
+ }
+ else if (exports.isString(object)) {
+ match = ASPDateRegex.exec(object);
+ var value;
+ if (match) {
+ // object is an ASP date
+ value = new Date(Number(match[1])).valueOf(); // parse number
+ }
+ else {
+ value = new Date(object).valueOf(); // parse string
+ }
+ return '/Date(' + value + ')/';
+ }
+ else {
+ throw new Error(
+ 'Cannot convert object of type ' + exports.getType(object) +
+ ' to type ASPDate');
+ }
+
+ default:
+ throw new Error('Unknown type "' + type + '"');
+ }
+ };
+
+ // parse ASP.Net Date pattern,
+ // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
+ // code from http://momentjs.com/
+ var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
+
+ /**
+ * Get the type of an object, for example exports.getType([]) returns 'Array'
+ * @param {*} object
+ * @return {String} type
+ */
+ exports.getType = function(object) {
+ var type = typeof object;
+
+ if (type == 'object') {
+ if (object == null) {
+ return 'null';
+ }
+ if (object instanceof Boolean) {
+ return 'Boolean';
+ }
+ if (object instanceof Number) {
+ return 'Number';
+ }
+ if (object instanceof String) {
+ return 'String';
+ }
+ if (Array.isArray(object)) {
+ return 'Array';
+ }
+ if (object instanceof Date) {
+ return 'Date';
+ }
+ return 'Object';
+ }
+ else if (type == 'number') {
+ return 'Number';
+ }
+ else if (type == 'boolean') {
+ return 'Boolean';
+ }
+ else if (type == 'string') {
+ return 'String';
+ }
+
+ return type;
+ };
+
+ /**
+ * Retrieve the absolute left value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {number} left The absolute left position of this element
+ * in the browser page.
+ */
+ exports.getAbsoluteLeft = function(elem) {
+ return elem.getBoundingClientRect().left + window.pageXOffset;
+ };
+
+ /**
+ * Retrieve the absolute top value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {number} top The absolute top position of this element
+ * in the browser page.
+ */
+ exports.getAbsoluteTop = function(elem) {
+ return elem.getBoundingClientRect().top + window.pageYOffset;
+ };
+
+ /**
+ * add a className to the given elements style
+ * @param {Element} elem
+ * @param {String} className
+ */
+ exports.addClassName = function(elem, className) {
+ var classes = elem.className.split(' ');
+ if (classes.indexOf(className) == -1) {
+ classes.push(className); // add the class to the array
+ elem.className = classes.join(' ');
+ }
+ };
+
+ /**
+ * add a className to the given elements style
+ * @param {Element} elem
+ * @param {String} className
+ */
+ exports.removeClassName = function(elem, className) {
+ var classes = elem.className.split(' ');
+ var index = classes.indexOf(className);
+ if (index != -1) {
+ classes.splice(index, 1); // remove the class from the array
+ elem.className = classes.join(' ');
+ }
+ };
+
+ /**
+ * For each method for both arrays and objects.
+ * In case of an array, the built-in Array.forEach() is applied.
+ * In case of an Object, the method loops over all properties of the object.
+ * @param {Object | Array} object An Object or Array
+ * @param {function} callback Callback method, called for each item in
+ * the object or array with three parameters:
+ * callback(value, index, object)
+ */
+ exports.forEach = function(object, callback) {
+ var i,
+ len;
+ if (Array.isArray(object)) {
+ // array
+ for (i = 0, len = object.length; i < len; i++) {
+ callback(object[i], i, object);
+ }
+ }
+ else {
+ // object
+ for (i in object) {
+ if (object.hasOwnProperty(i)) {
+ callback(object[i], i, object);
+ }
+ }
+ }
+ };
+
+ /**
+ * Convert an object into an array: all objects properties are put into the
+ * array. The resulting array is unordered.
+ * @param {Object} object
+ * @param {Array} array
+ */
+ exports.toArray = function(object) {
+ var array = [];
+
+ for (var prop in object) {
+ if (object.hasOwnProperty(prop)) array.push(object[prop]);
+ }
+
+ return array;
+ }
+
+ /**
+ * Update a property in an object
+ * @param {Object} object
+ * @param {String} key
+ * @param {*} value
+ * @return {Boolean} changed
+ */
+ exports.updateProperty = function(object, key, value) {
+ if (object[key] !== value) {
+ object[key] = value;
+ return true;
+ }
+ else {
+ return false;
+ }
+ };
+
+ /**
+ * Add and event listener. Works for all browsers
+ * @param {Element} element An html element
+ * @param {string} action The action, for example "click",
+ * without the prefix "on"
+ * @param {function} listener The callback function to be executed
+ * @param {boolean} [useCapture]
+ */
+ exports.addEventListener = function(element, action, listener, useCapture) {
+ if (element.addEventListener) {
+ if (useCapture === undefined)
+ useCapture = false;
+
+ if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
+ action = "DOMMouseScroll"; // For Firefox
+ }
+
+ element.addEventListener(action, listener, useCapture);
+ } else {
+ element.attachEvent("on" + action, listener); // IE browsers
+ }
+ };
+
+ /**
+ * Remove an event listener from an element
+ * @param {Element} element An html dom element
+ * @param {string} action The name of the event, for example "mousedown"
+ * @param {function} listener The listener function
+ * @param {boolean} [useCapture]
+ */
+ exports.removeEventListener = function(element, action, listener, useCapture) {
+ if (element.removeEventListener) {
+ // non-IE browsers
+ if (useCapture === undefined)
+ useCapture = false;
+
+ if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
+ action = "DOMMouseScroll"; // For Firefox
+ }
+
+ element.removeEventListener(action, listener, useCapture);
+ } else {
+ // IE browsers
+ element.detachEvent("on" + action, listener);
+ }
+ };
+
+ /**
+ * Cancels the event if it is cancelable, without stopping further propagation of the event.
+ */
+ exports.preventDefault = function (event) {
+ if (!event)
+ event = window.event;
+
+ if (event.preventDefault) {
+ event.preventDefault(); // non-IE browsers
+ }
+ else {
+ event.returnValue = false; // IE browsers
+ }
+ };
+
+ /**
+ * Get HTML element which is the target of the event
+ * @param {Event} event
+ * @return {Element} target element
+ */
+ exports.getTarget = function(event) {
+ // code from http://www.quirksmode.org/js/events_properties.html
+ if (!event) {
+ event = window.event;
+ }
+
+ var target;
+
+ if (event.target) {
+ target = event.target;
+ }
+ else if (event.srcElement) {
+ target = event.srcElement;
+ }
+
+ if (target.nodeType != undefined && target.nodeType == 3) {
+ // defeat Safari bug
+ target = target.parentNode;
+ }
+
+ return target;
+ };
+
+ exports.option = {};
+
+ /**
+ * Convert a value into a boolean
+ * @param {Boolean | function | undefined} value
+ * @param {Boolean} [defaultValue]
+ * @returns {Boolean} bool
+ */
+ exports.option.asBoolean = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ if (value != null) {
+ return (value != false);
+ }
+
+ return defaultValue || null;
+ };
+
+ /**
+ * Convert a value into a number
+ * @param {Boolean | function | undefined} value
+ * @param {Number} [defaultValue]
+ * @returns {Number} number
+ */
+ exports.option.asNumber = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ if (value != null) {
+ return Number(value) || defaultValue || null;
+ }
+
+ return defaultValue || null;
+ };
+
+ /**
+ * Convert a value into a string
+ * @param {String | function | undefined} value
+ * @param {String} [defaultValue]
+ * @returns {String} str
+ */
+ exports.option.asString = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ if (value != null) {
+ return String(value);
+ }
+
+ return defaultValue || null;
+ };
+
+ /**
+ * Convert a size or location into a string with pixels or a percentage
+ * @param {String | Number | function | undefined} value
+ * @param {String} [defaultValue]
+ * @returns {String} size
+ */
+ exports.option.asSize = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ if (exports.isString(value)) {
+ return value;
+ }
+ else if (exports.isNumber(value)) {
+ return value + 'px';
+ }
+ else {
+ return defaultValue || null;
+ }
+ };
+
+ /**
+ * Convert a value into a DOM element
+ * @param {HTMLElement | function | undefined} value
+ * @param {HTMLElement} [defaultValue]
+ * @returns {HTMLElement | null} dom
+ */
+ exports.option.asElement = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ return value || defaultValue || null;
+ };
+
+
+
+ exports.GiveDec = function(Hex) {
+ var Value;
+
+ if (Hex == "A")
+ Value = 10;
+ else if (Hex == "B")
+ Value = 11;
+ else if (Hex == "C")
+ Value = 12;
+ else if (Hex == "D")
+ Value = 13;
+ else if (Hex == "E")
+ Value = 14;
+ else if (Hex == "F")
+ Value = 15;
+ else
+ Value = eval(Hex);
+
+ return Value;
+ };
+
+ exports.GiveHex = function(Dec) {
+ var Value;
+
+ if(Dec == 10)
+ Value = "A";
+ else if (Dec == 11)
+ Value = "B";
+ else if (Dec == 12)
+ Value = "C";
+ else if (Dec == 13)
+ Value = "D";
+ else if (Dec == 14)
+ Value = "E";
+ else if (Dec == 15)
+ Value = "F";
+ else
+ Value = "" + Dec;
+
+ return Value;
+ };
+
+ /**
+ * Parse a color property into an object with border, background, and
+ * highlight colors
+ * @param {Object | String} color
+ * @return {Object} colorObject
+ */
+ exports.parseColor = function(color) {
+ var c;
+ if (exports.isString(color)) {
+ if (exports.isValidRGB(color)) {
+ var rgb = color.substr(4).substr(0,color.length-5).split(',');
+ color = exports.RGBToHex(rgb[0],rgb[1],rgb[2]);
+ }
+ if (exports.isValidHex(color)) {
+ var hsv = exports.hexToHSV(color);
+ var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)};
+ var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6};
+ var darkerColorHex = exports.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v);
+ var lighterColorHex = exports.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v);
+
+ c = {
+ background: color,
+ border:darkerColorHex,
+ highlight: {
+ background:lighterColorHex,
+ border:darkerColorHex
+ },
+ hover: {
+ background:lighterColorHex,
+ border:darkerColorHex
+ }
+ };
+ }
+ else {
+ c = {
+ background:color,
+ border:color,
+ highlight: {
+ background:color,
+ border:color
+ },
+ hover: {
+ background:color,
+ border:color
+ }
+ };
+ }
+ }
+ else {
+ c = {};
+ c.background = color.background || 'white';
+ c.border = color.border || c.background;
+
+ if (exports.isString(color.highlight)) {
+ c.highlight = {
+ border: color.highlight,
+ background: color.highlight
+ }
+ }
+ else {
+ c.highlight = {};
+ c.highlight.background = color.highlight && color.highlight.background || c.background;
+ c.highlight.border = color.highlight && color.highlight.border || c.border;
+ }
+
+ if (exports.isString(color.hover)) {
+ c.hover = {
+ border: color.hover,
+ background: color.hover
+ }
+ }
+ else {
+ c.hover = {};
+ c.hover.background = color.hover && color.hover.background || c.background;
+ c.hover.border = color.hover && color.hover.border || c.border;
+ }
+ }
+
+ return c;
+ };
+
+ /**
+ * http://www.yellowpipe.com/yis/tools/hex-to-rgb/color-converter.php
+ *
+ * @param {String} hex
+ * @returns {{r: *, g: *, b: *}}
+ */
+ exports.hexToRGB = function(hex) {
+ hex = hex.replace("#","").toUpperCase();
+
+ var a = exports.GiveDec(hex.substring(0, 1));
+ var b = exports.GiveDec(hex.substring(1, 2));
+ var c = exports.GiveDec(hex.substring(2, 3));
+ var d = exports.GiveDec(hex.substring(3, 4));
+ var e = exports.GiveDec(hex.substring(4, 5));
+ var f = exports.GiveDec(hex.substring(5, 6));
+
+ var r = (a * 16) + b;
+ var g = (c * 16) + d;
+ var b = (e * 16) + f;
+
+ return {r:r,g:g,b:b};
+ };
+
+ exports.RGBToHex = function(red,green,blue) {
+ var a = exports.GiveHex(Math.floor(red / 16));
+ var b = exports.GiveHex(red % 16);
+ var c = exports.GiveHex(Math.floor(green / 16));
+ var d = exports.GiveHex(green % 16);
+ var e = exports.GiveHex(Math.floor(blue / 16));
+ var f = exports.GiveHex(blue % 16);
+
+ var hex = a + b + c + d + e + f;
+ return "#" + hex;
+ };
+
+
+ /**
+ * http://www.javascripter.net/faq/rgb2hsv.htm
+ *
+ * @param red
+ * @param green
+ * @param blue
+ * @returns {*}
+ * @constructor
+ */
+ exports.RGBToHSV = function(red,green,blue) {
+ red=red/255; green=green/255; blue=blue/255;
+ var minRGB = Math.min(red,Math.min(green,blue));
+ var maxRGB = Math.max(red,Math.max(green,blue));
+
+ // Black-gray-white
+ if (minRGB == maxRGB) {
+ return {h:0,s:0,v:minRGB};
+ }
+
+ // Colors other than black-gray-white:
+ var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red);
+ var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5);
+ var hue = 60*(h - d/(maxRGB - minRGB))/360;
+ var saturation = (maxRGB - minRGB)/maxRGB;
+ var value = maxRGB;
+ return {h:hue,s:saturation,v:value};
+ };
+
+ var cssUtil = {
+ // split a string with css styles into an object with key/values
+ split: function (cssText) {
+ var styles = {};
+
+ cssText.split(';').forEach(function (style) {
+ if (style.trim() != '') {
+ var parts = style.split(':');
+ var key = parts[0].trim();
+ var value = parts[1].trim();
+ styles[key] = value;
+ }
+ });
+
+ return styles;
+ },
+
+ // build a css text string from an object with key/values
+ join: function (styles) {
+ return Object.keys(styles)
+ .map(function (key) {
+ return key + ': ' + styles[key];
+ })
+ .join('; ');
+ }
+ };
+
+ /**
+ * Append a string with css styles to an element
+ * @param {Element} element
+ * @param {String} cssText
+ */
+ exports.addCssText = function (element, cssText) {
+ var currentStyles = cssUtil.split(element.style.cssText);
+ var newStyles = cssUtil.split(cssText);
+ var styles = exports.extend(currentStyles, newStyles);
+
+ element.style.cssText = cssUtil.join(styles);
+ };
+
+ /**
+ * Remove a string with css styles from an element
+ * @param {Element} element
+ * @param {String} cssText
+ */
+ exports.removeCssText = function (element, cssText) {
+ var styles = cssUtil.split(element.style.cssText);
+ var removeStyles = cssUtil.split(cssText);
+
+ for (var key in removeStyles) {
+ if (removeStyles.hasOwnProperty(key)) {
+ delete styles[key];
+ }
+ }
+
+ element.style.cssText = cssUtil.join(styles);
+ };
+
+ /**
+ * https://gist.github.com/mjijackson/5311256
+ * @param h
+ * @param s
+ * @param v
+ * @returns {{r: number, g: number, b: number}}
+ * @constructor
+ */
+ exports.HSVToRGB = function(h, s, v) {
+ var r, g, b;
+
+ var i = Math.floor(h * 6);
+ var f = h * 6 - i;
+ var p = v * (1 - s);
+ var q = v * (1 - f * s);
+ var t = v * (1 - (1 - f) * s);
+
+ switch (i % 6) {
+ case 0: r = v, g = t, b = p; break;
+ case 1: r = q, g = v, b = p; break;
+ case 2: r = p, g = v, b = t; break;
+ case 3: r = p, g = q, b = v; break;
+ case 4: r = t, g = p, b = v; break;
+ case 5: r = v, g = p, b = q; break;
+ }
+
+ return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) };
+ };
+
+ exports.HSVToHex = function(h, s, v) {
+ var rgb = exports.HSVToRGB(h, s, v);
+ return exports.RGBToHex(rgb.r, rgb.g, rgb.b);
+ };
+
+ exports.hexToHSV = function(hex) {
+ var rgb = exports.hexToRGB(hex);
+ return exports.RGBToHSV(rgb.r, rgb.g, rgb.b);
+ };
+
+ exports.isValidHex = function(hex) {
+ var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
+ return isOk;
+ };
+
+ exports.isValidRGB = function(rgb) {
+ rgb = rgb.replace(" ","");
+ var isOk = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/i.test(rgb);
+ return isOk;
+ }
+
+ /**
+ * This recursively redirects the prototype of JSON objects to the referenceObject
+ * This is used for default options.
+ *
+ * @param referenceObject
+ * @returns {*}
+ */
+ exports.selectiveBridgeObject = function(fields, referenceObject) {
+ if (typeof referenceObject == "object") {
+ var objectTo = Object.create(referenceObject);
+ for (var i = 0; i < fields.length; i++) {
+ if (referenceObject.hasOwnProperty(fields[i])) {
+ if (typeof referenceObject[fields[i]] == "object") {
+ objectTo[fields[i]] = exports.bridgeObject(referenceObject[fields[i]]);
+ }
+ }
+ }
+ return objectTo;
+ }
+ else {
+ return null;
+ }
+ };
+
+ /**
+ * This recursively redirects the prototype of JSON objects to the referenceObject
+ * This is used for default options.
+ *
+ * @param referenceObject
+ * @returns {*}
+ */
+ exports.bridgeObject = function(referenceObject) {
+ if (typeof referenceObject == "object") {
+ var objectTo = Object.create(referenceObject);
+ for (var i in referenceObject) {
+ if (referenceObject.hasOwnProperty(i)) {
+ if (typeof referenceObject[i] == "object") {
+ objectTo[i] = exports.bridgeObject(referenceObject[i]);
+ }
+ }
+ }
+ return objectTo;
+ }
+ else {
+ return null;
+ }
+ };
+
+
+ /**
+ * this is used to set the options of subobjects in the options object. A requirement of these subobjects
+ * is that they have an 'enabled' element which is optional for the user but mandatory for the program.
+ *
+ * @param [object] mergeTarget | this is either this.options or the options used for the groups.
+ * @param [object] options | options
+ * @param [String] option | this is the option key in the options argument
+ * @private
+ */
+ exports.mergeOptions = function (mergeTarget, options, option) {
+ if (options[option] !== undefined) {
+ if (typeof options[option] == 'boolean') {
+ mergeTarget[option].enabled = options[option];
+ }
+ else {
+ mergeTarget[option].enabled = true;
+ for (var prop in options[option]) {
+ if (options[option].hasOwnProperty(prop)) {
+ mergeTarget[option][prop] = options[option][prop];
+ }
+ }
+ }
+ }
+ }
+
+
+ /**
+ * This function does a binary search for a visible item in a sorted list. If we find a visible item, the code that uses
+ * this function will then iterate in both directions over this sorted list to find all visible items.
+ *
+ * @param {Item[]} orderedItems | Items ordered by start
+ * @param {function} searchFunction | -1 is lower, 0 is found, 1 is higher
+ * @param {String} field
+ * @param {String} field2
+ * @returns {number}
+ * @private
+ */
+ exports.binarySearchCustom = function(orderedItems, searchFunction, field, field2) {
+ var maxIterations = 10000;
+ var iteration = 0;
+ var low = 0;
+ var high = orderedItems.length - 1;
+
+ while (low <= high && iteration < maxIterations) {
+ var middle = Math.floor((low + high) / 2);
+
+ var item = orderedItems[middle];
+ var value = (field2 === undefined) ? item[field] : item[field][field2];
+
+ var searchResult = searchFunction(value);
+ if (searchResult == 0) { // jihaa, found a visible item!
+ return middle;
+ }
+ else if (searchResult == -1) { // it is too small --> increase low
+ low = middle + 1;
+ }
+ else { // it is too big --> decrease high
+ high = middle - 1;
+ }
+
+ iteration++;
+ }
+
+ return -1;
+ };
+
+ /**
+ * This function does a binary search for a specific value in a sorted array. If it does not exist but is in between of
+ * two values, we return either the one before or the one after, depending on user input
+ * If it is found, we return the index, else -1.
+ *
+ * @param {Array} orderedItems
+ * @param {{start: number, end: number}} target
+ * @param {String} field
+ * @param {String} sidePreference 'before' or 'after'
+ * @returns {number}
+ * @private
+ */
+ exports.binarySearchValue = function(orderedItems, target, field, sidePreference) {
+ var maxIterations = 10000;
+ var iteration = 0;
+ var low = 0;
+ var high = orderedItems.length - 1;
+ var prevValue, value, nextValue, middle;
+
+ while (low <= high && iteration < maxIterations) {
+ // get a new guess
+ middle = Math.floor(0.5*(high+low));
+ prevValue = orderedItems[Math.max(0,middle - 1)][field];
+ value = orderedItems[middle][field];
+ nextValue = orderedItems[Math.min(orderedItems.length-1,middle + 1)][field];
+
+ if (value == target) { // we found the target
+ return middle;
+ }
+ else if (prevValue < target && value > target) { // target is in between of the previous and the current
+ return sidePreference == 'before' ? Math.max(0,middle - 1) : middle;
+ }
+ else if (value < target && nextValue > target) { // target is in between of the current and the next
+ return sidePreference == 'before' ? middle : Math.min(orderedItems.length-1,middle + 1);
+ }
+ else { // didnt find the target, we need to change our boundaries.
+ if (value < target) { // it is too small --> increase low
+ low = middle + 1;
+ }
+ else { // it is too big --> decrease high
+ high = middle - 1;
+ }
+ }
+ iteration++;
+ }
+
+ // didnt find anything. Return -1.
+ return -1;
+ };
+
+ /**
+ * Quadratic ease-in-out
+ * http://gizma.com/easing/
+ * @param {number} t Current time
+ * @param {number} start Start value
+ * @param {number} end End value
+ * @param {number} duration Duration
+ * @returns {number} Value corresponding with current time
+ */
+ exports.easeInOutQuad = function (t, start, end, duration) {
+ var change = end - start;
+ t /= duration/2;
+ if (t < 1) return change/2*t*t + start;
+ t--;
+ return -change/2 * (t*(t-2) - 1) + start;
+ };
+
+
+
+ /*
+ * Easing Functions - inspired from http://gizma.com/easing/
+ * only considering the t value for the range [0, 1] => [0, 1]
+ * https://gist.github.com/gre/1650294
+ */
+ exports.easingFunctions = {
+ // no easing, no acceleration
+ linear: function (t) {
+ return t
+ },
+ // accelerating from zero velocity
+ easeInQuad: function (t) {
+ return t * t
+ },
+ // decelerating to zero velocity
+ easeOutQuad: function (t) {
+ return t * (2 - t)
+ },
+ // acceleration until halfway, then deceleration
+ easeInOutQuad: function (t) {
+ return t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t
+ },
+ // accelerating from zero velocity
+ easeInCubic: function (t) {
+ return t * t * t
+ },
+ // decelerating to zero velocity
+ easeOutCubic: function (t) {
+ return (--t) * t * t + 1
+ },
+ // acceleration until halfway, then deceleration
+ easeInOutCubic: function (t) {
+ return t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
+ },
+ // accelerating from zero velocity
+ easeInQuart: function (t) {
+ return t * t * t * t
+ },
+ // decelerating to zero velocity
+ easeOutQuart: function (t) {
+ return 1 - (--t) * t * t * t
+ },
+ // acceleration until halfway, then deceleration
+ easeInOutQuart: function (t) {
+ return t < .5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t
+ },
+ // accelerating from zero velocity
+ easeInQuint: function (t) {
+ return t * t * t * t * t
+ },
+ // decelerating to zero velocity
+ easeOutQuint: function (t) {
+ return 1 + (--t) * t * t * t * t
+ },
+ // acceleration until halfway, then deceleration
+ easeInOutQuint: function (t) {
+ return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t
+ }
+ };
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // first check if moment.js is already loaded in the browser window, if so,
+ // use this instance. Else, load via commonjs.
+ module.exports = (typeof window !== 'undefined') && window['moment'] || __webpack_require__(3);
+
+
+/***/ },
+/* 3 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(global, module) {//! moment.js
+ //! version : 2.8.4
+ //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
+ //! license : MIT
+ //! momentjs.com
+
+ (function (undefined) {
+ /************************************
+ Constants
+ ************************************/
+
+ var moment,
+ VERSION = '2.8.4',
+ // the global-scope this is NOT the global object in Node.js
+ globalScope = typeof global !== 'undefined' ? global : this,
+ oldGlobalMoment,
+ round = Math.round,
+ hasOwnProperty = Object.prototype.hasOwnProperty,
+ i,
+
+ YEAR = 0,
+ MONTH = 1,
+ DATE = 2,
+ HOUR = 3,
+ MINUTE = 4,
+ SECOND = 5,
+ MILLISECOND = 6,
+
+ // internal storage for locale config files
+ locales = {},
+
+ // extra moment internal properties (plugins register props here)
+ momentProperties = [],
+
+ // check for nodeJS
+ hasModule = (typeof module !== 'undefined' && module && module.exports),
+
+ // ASP.NET json date format regex
+ aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
+ aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
+
+ // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
+ // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
+ isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
+
+ // format tokens
+ formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|x|X|zz?|ZZ?|.)/g,
+ localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,
+
+ // parsing token regexes
+ parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
+ parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
+ parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
+ parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
+ parseTokenDigits = /\d+/, // nonzero number of digits
+ parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic.
+ parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
+ parseTokenT = /T/i, // T (ISO separator)
+ parseTokenOffsetMs = /[\+\-]?\d+/, // 1234567890123
+ parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
+
+ //strict parsing regexes
+ parseTokenOneDigit = /\d/, // 0 - 9
+ parseTokenTwoDigits = /\d\d/, // 00 - 99
+ parseTokenThreeDigits = /\d{3}/, // 000 - 999
+ parseTokenFourDigits = /\d{4}/, // 0000 - 9999
+ parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999
+ parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf
+
+ // iso 8601 regex
+ // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00)
+ isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,
+
+ isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
+
+ isoDates = [
+ ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
+ ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
+ ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
+ ['GGGG-[W]WW', /\d{4}-W\d{2}/],
+ ['YYYY-DDD', /\d{4}-\d{3}/]
+ ],
+
+ // iso time formats and regexes
+ isoTimes = [
+ ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
+ ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
+ ['HH:mm', /(T| )\d\d:\d\d/],
+ ['HH', /(T| )\d\d/]
+ ],
+
+ // timezone chunker '+10:00' > ['10', '00'] or '-1530' > ['-15', '30']
+ parseTimezoneChunker = /([\+\-]|\d\d)/gi,
+
+ // getter and setter names
+ proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
+ unitMillisecondFactors = {
+ 'Milliseconds' : 1,
+ 'Seconds' : 1e3,
+ 'Minutes' : 6e4,
+ 'Hours' : 36e5,
+ 'Days' : 864e5,
+ 'Months' : 2592e6,
+ 'Years' : 31536e6
+ },
+
+ unitAliases = {
+ ms : 'millisecond',
+ s : 'second',
+ m : 'minute',
+ h : 'hour',
+ d : 'day',
+ D : 'date',
+ w : 'week',
+ W : 'isoWeek',
+ M : 'month',
+ Q : 'quarter',
+ y : 'year',
+ DDD : 'dayOfYear',
+ e : 'weekday',
+ E : 'isoWeekday',
+ gg: 'weekYear',
+ GG: 'isoWeekYear'
+ },
+
+ camelFunctions = {
+ dayofyear : 'dayOfYear',
+ isoweekday : 'isoWeekday',
+ isoweek : 'isoWeek',
+ weekyear : 'weekYear',
+ isoweekyear : 'isoWeekYear'
+ },
+
+ // format function strings
+ formatFunctions = {},
+
+ // default relative time thresholds
+ relativeTimeThresholds = {
+ s: 45, // seconds to minute
+ m: 45, // minutes to hour
+ h: 22, // hours to day
+ d: 26, // days to month
+ M: 11 // months to year
+ },
+
+ // tokens to ordinalize and pad
+ ordinalizeTokens = 'DDD w W M D d'.split(' '),
+ paddedTokens = 'M D H h m s w W'.split(' '),
+
+ formatTokenFunctions = {
+ M : function () {
+ return this.month() + 1;
+ },
+ MMM : function (format) {
+ return this.localeData().monthsShort(this, format);
+ },
+ MMMM : function (format) {
+ return this.localeData().months(this, format);
+ },
+ D : function () {
+ return this.date();
+ },
+ DDD : function () {
+ return this.dayOfYear();
+ },
+ d : function () {
+ return this.day();
+ },
+ dd : function (format) {
+ return this.localeData().weekdaysMin(this, format);
+ },
+ ddd : function (format) {
+ return this.localeData().weekdaysShort(this, format);
+ },
+ dddd : function (format) {
+ return this.localeData().weekdays(this, format);
+ },
+ w : function () {
+ return this.week();
+ },
+ W : function () {
+ return this.isoWeek();
+ },
+ YY : function () {
+ return leftZeroFill(this.year() % 100, 2);
+ },
+ YYYY : function () {
+ return leftZeroFill(this.year(), 4);
+ },
+ YYYYY : function () {
+ return leftZeroFill(this.year(), 5);
+ },
+ YYYYYY : function () {
+ var y = this.year(), sign = y >= 0 ? '+' : '-';
+ return sign + leftZeroFill(Math.abs(y), 6);
+ },
+ gg : function () {
+ return leftZeroFill(this.weekYear() % 100, 2);
+ },
+ gggg : function () {
+ return leftZeroFill(this.weekYear(), 4);
+ },
+ ggggg : function () {
+ return leftZeroFill(this.weekYear(), 5);
+ },
+ GG : function () {
+ return leftZeroFill(this.isoWeekYear() % 100, 2);
+ },
+ GGGG : function () {
+ return leftZeroFill(this.isoWeekYear(), 4);
+ },
+ GGGGG : function () {
+ return leftZeroFill(this.isoWeekYear(), 5);
+ },
+ e : function () {
+ return this.weekday();
+ },
+ E : function () {
+ return this.isoWeekday();
+ },
+ a : function () {
+ return this.localeData().meridiem(this.hours(), this.minutes(), true);
+ },
+ A : function () {
+ return this.localeData().meridiem(this.hours(), this.minutes(), false);
+ },
+ H : function () {
+ return this.hours();
+ },
+ h : function () {
+ return this.hours() % 12 || 12;
+ },
+ m : function () {
+ return this.minutes();
+ },
+ s : function () {
+ return this.seconds();
+ },
+ S : function () {
+ return toInt(this.milliseconds() / 100);
+ },
+ SS : function () {
+ return leftZeroFill(toInt(this.milliseconds() / 10), 2);
+ },
+ SSS : function () {
+ return leftZeroFill(this.milliseconds(), 3);
+ },
+ SSSS : function () {
+ return leftZeroFill(this.milliseconds(), 3);
+ },
+ Z : function () {
+ var a = -this.zone(),
+ b = '+';
+ if (a < 0) {
+ a = -a;
+ b = '-';
+ }
+ return b + leftZeroFill(toInt(a / 60), 2) + ':' + leftZeroFill(toInt(a) % 60, 2);
+ },
+ ZZ : function () {
+ var a = -this.zone(),
+ b = '+';
+ if (a < 0) {
+ a = -a;
+ b = '-';
+ }
+ return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
+ },
+ z : function () {
+ return this.zoneAbbr();
+ },
+ zz : function () {
+ return this.zoneName();
+ },
+ x : function () {
+ return this.valueOf();
+ },
+ X : function () {
+ return this.unix();
+ },
+ Q : function () {
+ return this.quarter();
+ }
+ },
+
+ deprecations = {},
+
+ lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
+
+ // Pick the first defined of two or three arguments. dfl comes from
+ // default.
+ function dfl(a, b, c) {
+ switch (arguments.length) {
+ case 2: return a != null ? a : b;
+ case 3: return a != null ? a : b != null ? b : c;
+ default: throw new Error('Implement me');
+ }
+ }
+
+ function hasOwnProp(a, b) {
+ return hasOwnProperty.call(a, b);
+ }
+
+ function defaultParsingFlags() {
+ // We need to deep clone this object, and es5 standard is not very
+ // helpful.
+ return {
+ empty : false,
+ unusedTokens : [],
+ unusedInput : [],
+ overflow : -2,
+ charsLeftOver : 0,
+ nullInput : false,
+ invalidMonth : null,
+ invalidFormat : false,
+ userInvalidated : false,
+ iso: false
+ };
+ }
+
+ function printMsg(msg) {
+ if (moment.suppressDeprecationWarnings === false &&
+ typeof console !== 'undefined' && console.warn) {
+ console.warn('Deprecation warning: ' + msg);
+ }
+ }
+
+ function deprecate(msg, fn) {
+ var firstTime = true;
+ return extend(function () {
+ if (firstTime) {
+ printMsg(msg);
+ firstTime = false;
+ }
+ return fn.apply(this, arguments);
+ }, fn);
+ }
+
+ function deprecateSimple(name, msg) {
+ if (!deprecations[name]) {
+ printMsg(msg);
+ deprecations[name] = true;
+ }
+ }
+
+ function padToken(func, count) {
+ return function (a) {
+ return leftZeroFill(func.call(this, a), count);
+ };
+ }
+ function ordinalizeToken(func, period) {
+ return function (a) {
+ return this.localeData().ordinal(func.call(this, a), period);
+ };
+ }
+
+ while (ordinalizeTokens.length) {
+ i = ordinalizeTokens.pop();
+ formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
+ }
+ while (paddedTokens.length) {
+ i = paddedTokens.pop();
+ formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
+ }
+ formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
+
+
+ /************************************
+ Constructors
+ ************************************/
+
+ function Locale() {
+ }
+
+ // Moment prototype object
+ function Moment(config, skipOverflow) {
+ if (skipOverflow !== false) {
+ checkOverflow(config);
+ }
+ copyConfig(this, config);
+ this._d = new Date(+config._d);
+ }
+
+ // Duration Constructor
+ function Duration(duration) {
+ var normalizedInput = normalizeObjectUnits(duration),
+ years = normalizedInput.year || 0,
+ quarters = normalizedInput.quarter || 0,
+ months = normalizedInput.month || 0,
+ weeks = normalizedInput.week || 0,
+ days = normalizedInput.day || 0,
+ hours = normalizedInput.hour || 0,
+ minutes = normalizedInput.minute || 0,
+ seconds = normalizedInput.second || 0,
+ milliseconds = normalizedInput.millisecond || 0;
+
+ // representation for dateAddRemove
+ this._milliseconds = +milliseconds +
+ seconds * 1e3 + // 1000
+ minutes * 6e4 + // 1000 * 60
+ hours * 36e5; // 1000 * 60 * 60
+ // Because of dateAddRemove treats 24 hours as different from a
+ // day when working around DST, we need to store them separately
+ this._days = +days +
+ weeks * 7;
+ // It is impossible translate months into days without knowing
+ // which months you are are talking about, so we have to store
+ // it separately.
+ this._months = +months +
+ quarters * 3 +
+ years * 12;
+
+ this._data = {};
+
+ this._locale = moment.localeData();
+
+ this._bubble();
+ }
+
+ /************************************
+ Helpers
+ ************************************/
+
+
+ function extend(a, b) {
+ for (var i in b) {
+ if (hasOwnProp(b, i)) {
+ a[i] = b[i];
+ }
+ }
+
+ if (hasOwnProp(b, 'toString')) {
+ a.toString = b.toString;
+ }
+
+ if (hasOwnProp(b, 'valueOf')) {
+ a.valueOf = b.valueOf;
+ }
+
+ return a;
+ }
+
+ function copyConfig(to, from) {
+ var i, prop, val;
+
+ if (typeof from._isAMomentObject !== 'undefined') {
+ to._isAMomentObject = from._isAMomentObject;
+ }
+ if (typeof from._i !== 'undefined') {
+ to._i = from._i;
+ }
+ if (typeof from._f !== 'undefined') {
+ to._f = from._f;
+ }
+ if (typeof from._l !== 'undefined') {
+ to._l = from._l;
+ }
+ if (typeof from._strict !== 'undefined') {
+ to._strict = from._strict;
+ }
+ if (typeof from._tzm !== 'undefined') {
+ to._tzm = from._tzm;
+ }
+ if (typeof from._isUTC !== 'undefined') {
+ to._isUTC = from._isUTC;
+ }
+ if (typeof from._offset !== 'undefined') {
+ to._offset = from._offset;
+ }
+ if (typeof from._pf !== 'undefined') {
+ to._pf = from._pf;
+ }
+ if (typeof from._locale !== 'undefined') {
+ to._locale = from._locale;
+ }
+
+ if (momentProperties.length > 0) {
+ for (i in momentProperties) {
+ prop = momentProperties[i];
+ val = from[prop];
+ if (typeof val !== 'undefined') {
+ to[prop] = val;
+ }
+ }
+ }
+
+ return to;
+ }
+
+ function absRound(number) {
+ if (number < 0) {
+ return Math.ceil(number);
+ } else {
+ return Math.floor(number);
+ }
+ }
+
+ // left zero fill a number
+ // see http://jsperf.com/left-zero-filling for performance comparison
+ function leftZeroFill(number, targetLength, forceSign) {
+ var output = '' + Math.abs(number),
+ sign = number >= 0;
+
+ while (output.length < targetLength) {
+ output = '0' + output;
+ }
+ return (sign ? (forceSign ? '+' : '') : '-') + output;
+ }
+
+ function positiveMomentsDifference(base, other) {
+ var res = {milliseconds: 0, months: 0};
+
+ res.months = other.month() - base.month() +
+ (other.year() - base.year()) * 12;
+ if (base.clone().add(res.months, 'M').isAfter(other)) {
+ --res.months;
+ }
+
+ res.milliseconds = +other - +(base.clone().add(res.months, 'M'));
+
+ return res;
+ }
+
+ function momentsDifference(base, other) {
+ var res;
+ other = makeAs(other, base);
+ if (base.isBefore(other)) {
+ res = positiveMomentsDifference(base, other);
+ } else {
+ res = positiveMomentsDifference(other, base);
+ res.milliseconds = -res.milliseconds;
+ res.months = -res.months;
+ }
+
+ return res;
+ }
+
+ // TODO: remove 'name' arg after deprecation is removed
+ function createAdder(direction, name) {
+ return function (val, period) {
+ var dur, tmp;
+ //invert the arguments, but complain about it
+ if (period !== null && !isNaN(+period)) {
+ deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).');
+ tmp = val; val = period; period = tmp;
+ }
+
+ val = typeof val === 'string' ? +val : val;
+ dur = moment.duration(val, period);
+ addOrSubtractDurationFromMoment(this, dur, direction);
+ return this;
+ };
+ }
+
+ function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) {
+ var milliseconds = duration._milliseconds,
+ days = duration._days,
+ months = duration._months;
+ updateOffset = updateOffset == null ? true : updateOffset;
+
+ if (milliseconds) {
+ mom._d.setTime(+mom._d + milliseconds * isAdding);
+ }
+ if (days) {
+ rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding);
+ }
+ if (months) {
+ rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding);
+ }
+ if (updateOffset) {
+ moment.updateOffset(mom, days || months);
+ }
+ }
+
+ // check if is an array
+ function isArray(input) {
+ return Object.prototype.toString.call(input) === '[object Array]';
+ }
+
+ function isDate(input) {
+ return Object.prototype.toString.call(input) === '[object Date]' ||
+ input instanceof Date;
+ }
+
+ // compare two arrays, return the number of differences
+ function compareArrays(array1, array2, dontConvert) {
+ var len = Math.min(array1.length, array2.length),
+ lengthDiff = Math.abs(array1.length - array2.length),
+ diffs = 0,
+ i;
+ for (i = 0; i < len; i++) {
+ if ((dontConvert && array1[i] !== array2[i]) ||
+ (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
+ diffs++;
+ }
+ }
+ return diffs + lengthDiff;
+ }
+
+ function normalizeUnits(units) {
+ if (units) {
+ var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
+ units = unitAliases[units] || camelFunctions[lowered] || lowered;
+ }
+ return units;
+ }
+
+ function normalizeObjectUnits(inputObject) {
+ var normalizedInput = {},
+ normalizedProp,
+ prop;
+
+ for (prop in inputObject) {
+ if (hasOwnProp(inputObject, prop)) {
+ normalizedProp = normalizeUnits(prop);
+ if (normalizedProp) {
+ normalizedInput[normalizedProp] = inputObject[prop];
+ }
+ }
+ }
+
+ return normalizedInput;
+ }
+
+ function makeList(field) {
+ var count, setter;
+
+ if (field.indexOf('week') === 0) {
+ count = 7;
+ setter = 'day';
+ }
+ else if (field.indexOf('month') === 0) {
+ count = 12;
+ setter = 'month';
+ }
+ else {
+ return;
+ }
+
+ moment[field] = function (format, index) {
+ var i, getter,
+ method = moment._locale[field],
+ results = [];
+
+ if (typeof format === 'number') {
+ index = format;
+ format = undefined;
+ }
+
+ getter = function (i) {
+ var m = moment().utc().set(setter, i);
+ return method.call(moment._locale, m, format || '');
+ };
+
+ if (index != null) {
+ return getter(index);
+ }
+ else {
+ for (i = 0; i < count; i++) {
+ results.push(getter(i));
+ }
+ return results;
+ }
+ };
+ }
+
+ function toInt(argumentForCoercion) {
+ var coercedNumber = +argumentForCoercion,
+ value = 0;
+
+ if (coercedNumber !== 0 && isFinite(coercedNumber)) {
+ if (coercedNumber >= 0) {
+ value = Math.floor(coercedNumber);
+ } else {
+ value = Math.ceil(coercedNumber);
+ }
+ }
+
+ return value;
+ }
+
+ function daysInMonth(year, month) {
+ return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
+ }
+
+ function weeksInYear(year, dow, doy) {
+ return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week;
+ }
+
+ function daysInYear(year) {
+ return isLeapYear(year) ? 366 : 365;
+ }
+
+ function isLeapYear(year) {
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+ }
+
+ function checkOverflow(m) {
+ var overflow;
+ if (m._a && m._pf.overflow === -2) {
+ overflow =
+ m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
+ m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
+ m._a[HOUR] < 0 || m._a[HOUR] > 24 ||
+ (m._a[HOUR] === 24 && (m._a[MINUTE] !== 0 ||
+ m._a[SECOND] !== 0 ||
+ m._a[MILLISECOND] !== 0)) ? HOUR :
+ m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
+ m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
+ m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
+ -1;
+
+ if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
+ overflow = DATE;
+ }
+
+ m._pf.overflow = overflow;
+ }
+ }
+
+ function isValid(m) {
+ if (m._isValid == null) {
+ m._isValid = !isNaN(m._d.getTime()) &&
+ m._pf.overflow < 0 &&
+ !m._pf.empty &&
+ !m._pf.invalidMonth &&
+ !m._pf.nullInput &&
+ !m._pf.invalidFormat &&
+ !m._pf.userInvalidated;
+
+ if (m._strict) {
+ m._isValid = m._isValid &&
+ m._pf.charsLeftOver === 0 &&
+ m._pf.unusedTokens.length === 0 &&
+ m._pf.bigHour === undefined;
+ }
+ }
+ return m._isValid;
+ }
+
+ function normalizeLocale(key) {
+ return key ? key.toLowerCase().replace('_', '-') : key;
+ }
+
+ // pick the locale from the array
+ // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
+ // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
+ function chooseLocale(names) {
+ var i = 0, j, next, locale, split;
+
+ while (i < names.length) {
+ split = normalizeLocale(names[i]).split('-');
+ j = split.length;
+ next = normalizeLocale(names[i + 1]);
+ next = next ? next.split('-') : null;
+ while (j > 0) {
+ locale = loadLocale(split.slice(0, j).join('-'));
+ if (locale) {
+ return locale;
+ }
+ if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
+ //the next array item is better than a shallower substring of this one
+ break;
+ }
+ j--;
+ }
+ i++;
+ }
+ return null;
+ }
+
+ function loadLocale(name) {
+ var oldLocale = null;
+ if (!locales[name] && hasModule) {
+ try {
+ oldLocale = moment.locale();
+ !(function webpackMissingModule() { var e = new Error("Cannot find module \"./locale\""); e.code = 'MODULE_NOT_FOUND'; throw e; }());
+ // because defineLocale currently also sets the global locale, we want to undo that for lazy loaded locales
+ moment.locale(oldLocale);
+ } catch (e) { }
+ }
+ return locales[name];
+ }
+
+ // Return a moment from input, that is local/utc/zone equivalent to model.
+ function makeAs(input, model) {
+ var res, diff;
+ if (model._isUTC) {
+ res = model.clone();
+ diff = (moment.isMoment(input) || isDate(input) ?
+ +input : +moment(input)) - (+res);
+ // Use low-level api, because this fn is low-level api.
+ res._d.setTime(+res._d + diff);
+ moment.updateOffset(res, false);
+ return res;
+ } else {
+ return moment(input).local();
+ }
+ }
+
+ /************************************
+ Locale
+ ************************************/
+
+
+ extend(Locale.prototype, {
+
+ set : function (config) {
+ var prop, i;
+ for (i in config) {
+ prop = config[i];
+ if (typeof prop === 'function') {
+ this[i] = prop;
+ } else {
+ this['_' + i] = prop;
+ }
+ }
+ // Lenient ordinal parsing accepts just a number in addition to
+ // number + (possibly) stuff coming from _ordinalParseLenient.
+ this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + /\d{1,2}/.source);
+ },
+
+ _months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'),
+ months : function (m) {
+ return this._months[m.month()];
+ },
+
+ _monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'),
+ monthsShort : function (m) {
+ return this._monthsShort[m.month()];
+ },
+
+ monthsParse : function (monthName, format, strict) {
+ var i, mom, regex;
+
+ if (!this._monthsParse) {
+ this._monthsParse = [];
+ this._longMonthsParse = [];
+ this._shortMonthsParse = [];
+ }
+
+ for (i = 0; i < 12; i++) {
+ // make the regex if we don't have it already
+ mom = moment.utc([2000, i]);
+ if (strict && !this._longMonthsParse[i]) {
+ this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i');
+ this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i');
+ }
+ if (!strict && !this._monthsParse[i]) {
+ regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
+ this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
+ }
+ // test the regex
+ if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) {
+ return i;
+ } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) {
+ return i;
+ } else if (!strict && this._monthsParse[i].test(monthName)) {
+ return i;
+ }
+ }
+ },
+
+ _weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'),
+ weekdays : function (m) {
+ return this._weekdays[m.day()];
+ },
+
+ _weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'),
+ weekdaysShort : function (m) {
+ return this._weekdaysShort[m.day()];
+ },
+
+ _weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'),
+ weekdaysMin : function (m) {
+ return this._weekdaysMin[m.day()];
+ },
+
+ weekdaysParse : function (weekdayName) {
+ var i, mom, regex;
+
+ if (!this._weekdaysParse) {
+ this._weekdaysParse = [];
+ }
+
+ for (i = 0; i < 7; i++) {
+ // make the regex if we don't have it already
+ if (!this._weekdaysParse[i]) {
+ mom = moment([2000, 1]).day(i);
+ regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
+ this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
+ }
+ // test the regex
+ if (this._weekdaysParse[i].test(weekdayName)) {
+ return i;
+ }
+ }
+ },
+
+ _longDateFormat : {
+ LTS : 'h:mm:ss A',
+ LT : 'h:mm A',
+ L : 'MM/DD/YYYY',
+ LL : 'MMMM D, YYYY',
+ LLL : 'MMMM D, YYYY LT',
+ LLLL : 'dddd, MMMM D, YYYY LT'
+ },
+ longDateFormat : function (key) {
+ var output = this._longDateFormat[key];
+ if (!output && this._longDateFormat[key.toUpperCase()]) {
+ output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
+ return val.slice(1);
+ });
+ this._longDateFormat[key] = output;
+ }
+ return output;
+ },
+
+ isPM : function (input) {
+ // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
+ // Using charAt should be more compatible.
+ return ((input + '').toLowerCase().charAt(0) === 'p');
+ },
+
+ _meridiemParse : /[ap]\.?m?\.?/i,
+ meridiem : function (hours, minutes, isLower) {
+ if (hours > 11) {
+ return isLower ? 'pm' : 'PM';
+ } else {
+ return isLower ? 'am' : 'AM';
+ }
+ },
+
+ _calendar : {
+ sameDay : '[Today at] LT',
+ nextDay : '[Tomorrow at] LT',
+ nextWeek : 'dddd [at] LT',
+ lastDay : '[Yesterday at] LT',
+ lastWeek : '[Last] dddd [at] LT',
+ sameElse : 'L'
+ },
+ calendar : function (key, mom, now) {
+ var output = this._calendar[key];
+ return typeof output === 'function' ? output.apply(mom, [now]) : output;
+ },
+
+ _relativeTime : {
+ future : 'in %s',
+ past : '%s ago',
+ s : 'a few seconds',
+ m : 'a minute',
+ mm : '%d minutes',
+ h : 'an hour',
+ hh : '%d hours',
+ d : 'a day',
+ dd : '%d days',
+ M : 'a month',
+ MM : '%d months',
+ y : 'a year',
+ yy : '%d years'
+ },
+
+ relativeTime : function (number, withoutSuffix, string, isFuture) {
+ var output = this._relativeTime[string];
+ return (typeof output === 'function') ?
+ output(number, withoutSuffix, string, isFuture) :
+ output.replace(/%d/i, number);
+ },
+
+ pastFuture : function (diff, output) {
+ var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
+ return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
+ },
+
+ ordinal : function (number) {
+ return this._ordinal.replace('%d', number);
+ },
+ _ordinal : '%d',
+ _ordinalParse : /\d{1,2}/,
+
+ preparse : function (string) {
+ return string;
+ },
+
+ postformat : function (string) {
+ return string;
+ },
+
+ week : function (mom) {
+ return weekOfYear(mom, this._week.dow, this._week.doy).week;
+ },
+
+ _week : {
+ dow : 0, // Sunday is the first day of the week.
+ doy : 6 // The week that contains Jan 1st is the first week of the year.
+ },
+
+ _invalidDate: 'Invalid date',
+ invalidDate: function () {
+ return this._invalidDate;
+ }
+ });
+
+ /************************************
+ Formatting
+ ************************************/
+
+
+ function removeFormattingTokens(input) {
+ if (input.match(/\[[\s\S]/)) {
+ return input.replace(/^\[|\]$/g, '');
+ }
+ return input.replace(/\\/g, '');
+ }
+
+ function makeFormatFunction(format) {
+ var array = format.match(formattingTokens), i, length;
+
+ for (i = 0, length = array.length; i < length; i++) {
+ if (formatTokenFunctions[array[i]]) {
+ array[i] = formatTokenFunctions[array[i]];
+ } else {
+ array[i] = removeFormattingTokens(array[i]);
+ }
+ }
+
+ return function (mom) {
+ var output = '';
+ for (i = 0; i < length; i++) {
+ output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
+ }
+ return output;
+ };
+ }
+
+ // format date using native date object
+ function formatMoment(m, format) {
+ if (!m.isValid()) {
+ return m.localeData().invalidDate();
+ }
+
+ format = expandFormat(format, m.localeData());
+
+ if (!formatFunctions[format]) {
+ formatFunctions[format] = makeFormatFunction(format);
+ }
+
+ return formatFunctions[format](m);
+ }
+
+ function expandFormat(format, locale) {
+ var i = 5;
+
+ function replaceLongDateFormatTokens(input) {
+ return locale.longDateFormat(input) || input;
+ }
+
+ localFormattingTokens.lastIndex = 0;
+ while (i >= 0 && localFormattingTokens.test(format)) {
+ format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
+ localFormattingTokens.lastIndex = 0;
+ i -= 1;
+ }
+
+ return format;
+ }
+
+
+ /************************************
+ Parsing
+ ************************************/
+
+
+ // get the regex to find the next token
+ function getParseRegexForToken(token, config) {
+ var a, strict = config._strict;
+ switch (token) {
+ case 'Q':
+ return parseTokenOneDigit;
+ case 'DDDD':
+ return parseTokenThreeDigits;
+ case 'YYYY':
+ case 'GGGG':
+ case 'gggg':
+ return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
+ case 'Y':
+ case 'G':
+ case 'g':
+ return parseTokenSignedNumber;
+ case 'YYYYYY':
+ case 'YYYYY':
+ case 'GGGGG':
+ case 'ggggg':
+ return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
+ case 'S':
+ if (strict) {
+ return parseTokenOneDigit;
+ }
+ /* falls through */
+ case 'SS':
+ if (strict) {
+ return parseTokenTwoDigits;
+ }
+ /* falls through */
+ case 'SSS':
+ if (strict) {
+ return parseTokenThreeDigits;
+ }
+ /* falls through */
+ case 'DDD':
+ return parseTokenOneToThreeDigits;
+ case 'MMM':
+ case 'MMMM':
+ case 'dd':
+ case 'ddd':
+ case 'dddd':
+ return parseTokenWord;
+ case 'a':
+ case 'A':
+ return config._locale._meridiemParse;
+ case 'x':
+ return parseTokenOffsetMs;
+ case 'X':
+ return parseTokenTimestampMs;
+ case 'Z':
+ case 'ZZ':
+ return parseTokenTimezone;
+ case 'T':
+ return parseTokenT;
+ case 'SSSS':
+ return parseTokenDigits;
+ case 'MM':
+ case 'DD':
+ case 'YY':
+ case 'GG':
+ case 'gg':
+ case 'HH':
+ case 'hh':
+ case 'mm':
+ case 'ss':
+ case 'ww':
+ case 'WW':
+ return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
+ case 'M':
+ case 'D':
+ case 'd':
+ case 'H':
+ case 'h':
+ case 'm':
+ case 's':
+ case 'w':
+ case 'W':
+ case 'e':
+ case 'E':
+ return parseTokenOneOrTwoDigits;
+ case 'Do':
+ return strict ? config._locale._ordinalParse : config._locale._ordinalParseLenient;
+ default :
+ a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), 'i'));
+ return a;
+ }
+ }
+
+ function timezoneMinutesFromString(string) {
+ string = string || '';
+ var possibleTzMatches = (string.match(parseTokenTimezone) || []),
+ tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
+ parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
+ minutes = +(parts[1] * 60) + toInt(parts[2]);
+
+ return parts[0] === '+' ? -minutes : minutes;
+ }
+
+ // function to convert string input to date
+ function addTimeToArrayFromToken(token, input, config) {
+ var a, datePartArray = config._a;
+
+ switch (token) {
+ // QUARTER
+ case 'Q':
+ if (input != null) {
+ datePartArray[MONTH] = (toInt(input) - 1) * 3;
+ }
+ break;
+ // MONTH
+ case 'M' : // fall through to MM
+ case 'MM' :
+ if (input != null) {
+ datePartArray[MONTH] = toInt(input) - 1;
+ }
+ break;
+ case 'MMM' : // fall through to MMMM
+ case 'MMMM' :
+ a = config._locale.monthsParse(input, token, config._strict);
+ // if we didn't find a month name, mark the date as invalid.
+ if (a != null) {
+ datePartArray[MONTH] = a;
+ } else {
+ config._pf.invalidMonth = input;
+ }
+ break;
+ // DAY OF MONTH
+ case 'D' : // fall through to DD
+ case 'DD' :
+ if (input != null) {
+ datePartArray[DATE] = toInt(input);
+ }
+ break;
+ case 'Do' :
+ if (input != null) {
+ datePartArray[DATE] = toInt(parseInt(
+ input.match(/\d{1,2}/)[0], 10));
+ }
+ break;
+ // DAY OF YEAR
+ case 'DDD' : // fall through to DDDD
+ case 'DDDD' :
+ if (input != null) {
+ config._dayOfYear = toInt(input);
+ }
+
+ break;
+ // YEAR
+ case 'YY' :
+ datePartArray[YEAR] = moment.parseTwoDigitYear(input);
+ break;
+ case 'YYYY' :
+ case 'YYYYY' :
+ case 'YYYYYY' :
+ datePartArray[YEAR] = toInt(input);
+ break;
+ // AM / PM
+ case 'a' : // fall through to A
+ case 'A' :
+ config._isPm = config._locale.isPM(input);
+ break;
+ // HOUR
+ case 'h' : // fall through to hh
+ case 'hh' :
+ config._pf.bigHour = true;
+ /* falls through */
+ case 'H' : // fall through to HH
+ case 'HH' :
+ datePartArray[HOUR] = toInt(input);
+ break;
+ // MINUTE
+ case 'm' : // fall through to mm
+ case 'mm' :
+ datePartArray[MINUTE] = toInt(input);
+ break;
+ // SECOND
+ case 's' : // fall through to ss
+ case 'ss' :
+ datePartArray[SECOND] = toInt(input);
+ break;
+ // MILLISECOND
+ case 'S' :
+ case 'SS' :
+ case 'SSS' :
+ case 'SSSS' :
+ datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
+ break;
+ // UNIX OFFSET (MILLISECONDS)
+ case 'x':
+ config._d = new Date(toInt(input));
+ break;
+ // UNIX TIMESTAMP WITH MS
+ case 'X':
+ config._d = new Date(parseFloat(input) * 1000);
+ break;
+ // TIMEZONE
+ case 'Z' : // fall through to ZZ
+ case 'ZZ' :
+ config._useUTC = true;
+ config._tzm = timezoneMinutesFromString(input);
+ break;
+ // WEEKDAY - human
+ case 'dd':
+ case 'ddd':
+ case 'dddd':
+ a = config._locale.weekdaysParse(input);
+ // if we didn't get a weekday name, mark the date as invalid
+ if (a != null) {
+ config._w = config._w || {};
+ config._w['d'] = a;
+ } else {
+ config._pf.invalidWeekday = input;
+ }
+ break;
+ // WEEK, WEEK DAY - numeric
+ case 'w':
+ case 'ww':
+ case 'W':
+ case 'WW':
+ case 'd':
+ case 'e':
+ case 'E':
+ token = token.substr(0, 1);
+ /* falls through */
+ case 'gggg':
+ case 'GGGG':
+ case 'GGGGG':
+ token = token.substr(0, 2);
+ if (input) {
+ config._w = config._w || {};
+ config._w[token] = toInt(input);
+ }
+ break;
+ case 'gg':
+ case 'GG':
+ config._w = config._w || {};
+ config._w[token] = moment.parseTwoDigitYear(input);
+ }
+ }
+
+ function dayOfYearFromWeekInfo(config) {
+ var w, weekYear, week, weekday, dow, doy, temp;
+
+ w = config._w;
+ if (w.GG != null || w.W != null || w.E != null) {
+ dow = 1;
+ doy = 4;
+
+ // TODO: We need to take the current isoWeekYear, but that depends on
+ // how we interpret now (local, utc, fixed offset). So create
+ // a now version of current config (take local/utc/offset flags, and
+ // create now).
+ weekYear = dfl(w.GG, config._a[YEAR], weekOfYear(moment(), 1, 4).year);
+ week = dfl(w.W, 1);
+ weekday = dfl(w.E, 1);
+ } else {
+ dow = config._locale._week.dow;
+ doy = config._locale._week.doy;
+
+ weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year);
+ week = dfl(w.w, 1);
+
+ if (w.d != null) {
+ // weekday -- low day numbers are considered next week
+ weekday = w.d;
+ if (weekday < dow) {
+ ++week;
+ }
+ } else if (w.e != null) {
+ // local weekday -- counting starts from begining of week
+ weekday = w.e + dow;
+ } else {
+ // default to begining of week
+ weekday = dow;
+ }
+ }
+ temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow);
+
+ config._a[YEAR] = temp.year;
+ config._dayOfYear = temp.dayOfYear;
+ }
+
+ // convert an array to a date.
+ // the array should mirror the parameters below
+ // note: all values past the year are optional and will default to the lowest possible value.
+ // [year, month, day , hour, minute, second, millisecond]
+ function dateFromConfig(config) {
+ var i, date, input = [], currentDate, yearToUse;
+
+ if (config._d) {
+ return;
+ }
+
+ currentDate = currentDateArray(config);
+
+ //compute day of the year from weeks and weekdays
+ if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
+ dayOfYearFromWeekInfo(config);
+ }
+
+ //if the day of the year is set, figure out what it is
+ if (config._dayOfYear) {
+ yearToUse = dfl(config._a[YEAR], currentDate[YEAR]);
+
+ if (config._dayOfYear > daysInYear(yearToUse)) {
+ config._pf._overflowDayOfYear = true;
+ }
+
+ date = makeUTCDate(yearToUse, 0, config._dayOfYear);
+ config._a[MONTH] = date.getUTCMonth();
+ config._a[DATE] = date.getUTCDate();
+ }
+
+ // Default to current date.
+ // * if no year, month, day of month are given, default to today
+ // * if day of month is given, default month and year
+ // * if month is given, default only year
+ // * if year is given, don't default anything
+ for (i = 0; i < 3 && config._a[i] == null; ++i) {
+ config._a[i] = input[i] = currentDate[i];
+ }
+
+ // Zero out whatever was not defaulted, including time
+ for (; i < 7; i++) {
+ config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
+ }
+
+ // Check for 24:00:00.000
+ if (config._a[HOUR] === 24 &&
+ config._a[MINUTE] === 0 &&
+ config._a[SECOND] === 0 &&
+ config._a[MILLISECOND] === 0) {
+ config._nextDay = true;
+ config._a[HOUR] = 0;
+ }
+
+ config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
+ // Apply timezone offset from input. The actual zone can be changed
+ // with parseZone.
+ if (config._tzm != null) {
+ config._d.setUTCMinutes(config._d.getUTCMinutes() + config._tzm);
+ }
+
+ if (config._nextDay) {
+ config._a[HOUR] = 24;
+ }
+ }
+
+ function dateFromObject(config) {
+ var normalizedInput;
+
+ if (config._d) {
+ return;
+ }
+
+ normalizedInput = normalizeObjectUnits(config._i);
+ config._a = [
+ normalizedInput.year,
+ normalizedInput.month,
+ normalizedInput.day || normalizedInput.date,
+ normalizedInput.hour,
+ normalizedInput.minute,
+ normalizedInput.second,
+ normalizedInput.millisecond
+ ];
+
+ dateFromConfig(config);
+ }
+
+ function currentDateArray(config) {
+ var now = new Date();
+ if (config._useUTC) {
+ return [
+ now.getUTCFullYear(),
+ now.getUTCMonth(),
+ now.getUTCDate()
+ ];
+ } else {
+ return [now.getFullYear(), now.getMonth(), now.getDate()];
+ }
+ }
+
+ // date from string and format string
+ function makeDateFromStringAndFormat(config) {
+ if (config._f === moment.ISO_8601) {
+ parseISO(config);
+ return;
+ }
+
+ config._a = [];
+ config._pf.empty = true;
+
+ // This array is used to make a Date, either with `new Date` or `Date.UTC`
+ var string = '' + config._i,
+ i, parsedInput, tokens, token, skipped,
+ stringLength = string.length,
+ totalParsedInputLength = 0;
+
+ tokens = expandFormat(config._f, config._locale).match(formattingTokens) || [];
+
+ for (i = 0; i < tokens.length; i++) {
+ token = tokens[i];
+ parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
+ if (parsedInput) {
+ skipped = string.substr(0, string.indexOf(parsedInput));
+ if (skipped.length > 0) {
+ config._pf.unusedInput.push(skipped);
+ }
+ string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
+ totalParsedInputLength += parsedInput.length;
+ }
+ // don't parse if it's not a known token
+ if (formatTokenFunctions[token]) {
+ if (parsedInput) {
+ config._pf.empty = false;
+ }
+ else {
+ config._pf.unusedTokens.push(token);
+ }
+ addTimeToArrayFromToken(token, parsedInput, config);
+ }
+ else if (config._strict && !parsedInput) {
+ config._pf.unusedTokens.push(token);
+ }
+ }
+
+ // add remaining unparsed input length to the string
+ config._pf.charsLeftOver = stringLength - totalParsedInputLength;
+ if (string.length > 0) {
+ config._pf.unusedInput.push(string);
+ }
+
+ // clear _12h flag if hour is <= 12
+ if (config._pf.bigHour === true && config._a[HOUR] <= 12) {
+ config._pf.bigHour = undefined;
+ }
+ // handle am pm
+ if (config._isPm && config._a[HOUR] < 12) {
+ config._a[HOUR] += 12;
+ }
+ // if is 12 am, change hours to 0
+ if (config._isPm === false && config._a[HOUR] === 12) {
+ config._a[HOUR] = 0;
+ }
+ dateFromConfig(config);
+ checkOverflow(config);
+ }
+
+ function unescapeFormat(s) {
+ return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
+ return p1 || p2 || p3 || p4;
+ });
+ }
+
+ // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
+ function regexpEscape(s) {
+ return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+ }
+
+ // date from string and array of format strings
+ function makeDateFromStringAndArray(config) {
+ var tempConfig,
+ bestMoment,
+
+ scoreToBeat,
+ i,
+ currentScore;
+
+ if (config._f.length === 0) {
+ config._pf.invalidFormat = true;
+ config._d = new Date(NaN);
+ return;
+ }
+
+ for (i = 0; i < config._f.length; i++) {
+ currentScore = 0;
+ tempConfig = copyConfig({}, config);
+ if (config._useUTC != null) {
+ tempConfig._useUTC = config._useUTC;
+ }
+ tempConfig._pf = defaultParsingFlags();
+ tempConfig._f = config._f[i];
+ makeDateFromStringAndFormat(tempConfig);
+
+ if (!isValid(tempConfig)) {
+ continue;
+ }
+
+ // if there is any input that was not parsed add a penalty for that format
+ currentScore += tempConfig._pf.charsLeftOver;
+
+ //or tokens
+ currentScore += tempConfig._pf.unusedTokens.length * 10;
+
+ tempConfig._pf.score = currentScore;
+
+ if (scoreToBeat == null || currentScore < scoreToBeat) {
+ scoreToBeat = currentScore;
+ bestMoment = tempConfig;
+ }
+ }
+
+ extend(config, bestMoment || tempConfig);
+ }
+
+ // date from iso format
+ function parseISO(config) {
+ var i, l,
+ string = config._i,
+ match = isoRegex.exec(string);
+
+ if (match) {
+ config._pf.iso = true;
+ for (i = 0, l = isoDates.length; i < l; i++) {
+ if (isoDates[i][1].exec(string)) {
+ // match[5] should be 'T' or undefined
+ config._f = isoDates[i][0] + (match[6] || ' ');
+ break;
+ }
+ }
+ for (i = 0, l = isoTimes.length; i < l; i++) {
+ if (isoTimes[i][1].exec(string)) {
+ config._f += isoTimes[i][0];
+ break;
+ }
+ }
+ if (string.match(parseTokenTimezone)) {
+ config._f += 'Z';
+ }
+ makeDateFromStringAndFormat(config);
+ } else {
+ config._isValid = false;
+ }
+ }
+
+ // date from iso format or fallback
+ function makeDateFromString(config) {
+ parseISO(config);
+ if (config._isValid === false) {
+ delete config._isValid;
+ moment.createFromInputFallback(config);
+ }
+ }
+
+ function map(arr, fn) {
+ var res = [], i;
+ for (i = 0; i < arr.length; ++i) {
+ res.push(fn(arr[i], i));
+ }
+ return res;
+ }
+
+ function makeDateFromInput(config) {
+ var input = config._i, matched;
+ if (input === undefined) {
+ config._d = new Date();
+ } else if (isDate(input)) {
+ config._d = new Date(+input);
+ } else if ((matched = aspNetJsonRegex.exec(input)) !== null) {
+ config._d = new Date(+matched[1]);
+ } else if (typeof input === 'string') {
+ makeDateFromString(config);
+ } else if (isArray(input)) {
+ config._a = map(input.slice(0), function (obj) {
+ return parseInt(obj, 10);
+ });
+ dateFromConfig(config);
+ } else if (typeof(input) === 'object') {
+ dateFromObject(config);
+ } else if (typeof(input) === 'number') {
+ // from milliseconds
+ config._d = new Date(input);
+ } else {
+ moment.createFromInputFallback(config);
+ }
+ }
+
+ function makeDate(y, m, d, h, M, s, ms) {
+ //can't just apply() to create a date:
+ //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
+ var date = new Date(y, m, d, h, M, s, ms);
+
+ //the date constructor doesn't accept years < 1970
+ if (y < 1970) {
+ date.setFullYear(y);
+ }
+ return date;
+ }
+
+ function makeUTCDate(y) {
+ var date = new Date(Date.UTC.apply(null, arguments));
+ if (y < 1970) {
+ date.setUTCFullYear(y);
+ }
+ return date;
+ }
+
+ function parseWeekday(input, locale) {
+ if (typeof input === 'string') {
+ if (!isNaN(input)) {
+ input = parseInt(input, 10);
+ }
+ else {
+ input = locale.weekdaysParse(input);
+ if (typeof input !== 'number') {
+ return null;
+ }
+ }
+ }
+ return input;
+ }
+
+ /************************************
+ Relative Time
+ ************************************/
+
+
+ // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
+ function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) {
+ return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
+ }
+
+ function relativeTime(posNegDuration, withoutSuffix, locale) {
+ var duration = moment.duration(posNegDuration).abs(),
+ seconds = round(duration.as('s')),
+ minutes = round(duration.as('m')),
+ hours = round(duration.as('h')),
+ days = round(duration.as('d')),
+ months = round(duration.as('M')),
+ years = round(duration.as('y')),
+
+ args = seconds < relativeTimeThresholds.s && ['s', seconds] ||
+ minutes === 1 && ['m'] ||
+ minutes < relativeTimeThresholds.m && ['mm', minutes] ||
+ hours === 1 && ['h'] ||
+ hours < relativeTimeThresholds.h && ['hh', hours] ||
+ days === 1 && ['d'] ||
+ days < relativeTimeThresholds.d && ['dd', days] ||
+ months === 1 && ['M'] ||
+ months < relativeTimeThresholds.M && ['MM', months] ||
+ years === 1 && ['y'] || ['yy', years];
+
+ args[2] = withoutSuffix;
+ args[3] = +posNegDuration > 0;
+ args[4] = locale;
+ return substituteTimeAgo.apply({}, args);
+ }
+
+
+ /************************************
+ Week of Year
+ ************************************/
+
+
+ // firstDayOfWeek 0 = sun, 6 = sat
+ // the day of the week that starts the week
+ // (usually sunday or monday)
+ // firstDayOfWeekOfYear 0 = sun, 6 = sat
+ // the first week is the week that contains the first
+ // of this day of the week
+ // (eg. ISO weeks use thursday (4))
+ function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
+ var end = firstDayOfWeekOfYear - firstDayOfWeek,
+ daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
+ adjustedMoment;
+
+
+ if (daysToDayOfWeek > end) {
+ daysToDayOfWeek -= 7;
+ }
+
+ if (daysToDayOfWeek < end - 7) {
+ daysToDayOfWeek += 7;
+ }
+
+ adjustedMoment = moment(mom).add(daysToDayOfWeek, 'd');
+ return {
+ week: Math.ceil(adjustedMoment.dayOfYear() / 7),
+ year: adjustedMoment.year()
+ };
+ }
+
+ //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
+ function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
+ var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear;
+
+ d = d === 0 ? 7 : d;
+ weekday = weekday != null ? weekday : firstDayOfWeek;
+ daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0);
+ dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
+
+ return {
+ year: dayOfYear > 0 ? year : year - 1,
+ dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
+ };
+ }
+
+ /************************************
+ Top Level Functions
+ ************************************/
+
+ function makeMoment(config) {
+ var input = config._i,
+ format = config._f,
+ res;
+
+ config._locale = config._locale || moment.localeData(config._l);
+
+ if (input === null || (format === undefined && input === '')) {
+ return moment.invalid({nullInput: true});
+ }
+
+ if (typeof input === 'string') {
+ config._i = input = config._locale.preparse(input);
+ }
+
+ if (moment.isMoment(input)) {
+ return new Moment(input, true);
+ } else if (format) {
+ if (isArray(format)) {
+ makeDateFromStringAndArray(config);
+ } else {
+ makeDateFromStringAndFormat(config);
+ }
+ } else {
+ makeDateFromInput(config);
+ }
+
+ res = new Moment(config);
+ if (res._nextDay) {
+ // Adding is smart enough around DST
+ res.add(1, 'd');
+ res._nextDay = undefined;
+ }
+
+ return res;
+ }
+
+ moment = function (input, format, locale, strict) {
+ var c;
+
+ if (typeof(locale) === 'boolean') {
+ strict = locale;
+ locale = undefined;
+ }
+ // object construction must be done this way.
+ // https://github.com/moment/moment/issues/1423
+ c = {};
+ c._isAMomentObject = true;
+ c._i = input;
+ c._f = format;
+ c._l = locale;
+ c._strict = strict;
+ c._isUTC = false;
+ c._pf = defaultParsingFlags();
+
+ return makeMoment(c);
+ };
+
+ moment.suppressDeprecationWarnings = false;
+
+ moment.createFromInputFallback = deprecate(
+ 'moment construction falls back to js Date. This is ' +
+ 'discouraged and will be removed in upcoming major ' +
+ 'release. Please refer to ' +
+ 'https://github.com/moment/moment/issues/1407 for more info.',
+ function (config) {
+ config._d = new Date(config._i + (config._useUTC ? ' UTC' : ''));
+ }
+ );
+
+ // Pick a moment m from moments so that m[fn](other) is true for all
+ // other. This relies on the function fn to be transitive.
+ //
+ // moments should either be an array of moment objects or an array, whose
+ // first element is an array of moment objects.
+ function pickBy(fn, moments) {
+ var res, i;
+ if (moments.length === 1 && isArray(moments[0])) {
+ moments = moments[0];
+ }
+ if (!moments.length) {
+ return moment();
+ }
+ res = moments[0];
+ for (i = 1; i < moments.length; ++i) {
+ if (moments[i][fn](res)) {
+ res = moments[i];
+ }
+ }
+ return res;
+ }
+
+ moment.min = function () {
+ var args = [].slice.call(arguments, 0);
+
+ return pickBy('isBefore', args);
+ };
+
+ moment.max = function () {
+ var args = [].slice.call(arguments, 0);
+
+ return pickBy('isAfter', args);
+ };
+
+ // creating with utc
+ moment.utc = function (input, format, locale, strict) {
+ var c;
+
+ if (typeof(locale) === 'boolean') {
+ strict = locale;
+ locale = undefined;
+ }
+ // object construction must be done this way.
+ // https://github.com/moment/moment/issues/1423
+ c = {};
+ c._isAMomentObject = true;
+ c._useUTC = true;
+ c._isUTC = true;
+ c._l = locale;
+ c._i = input;
+ c._f = format;
+ c._strict = strict;
+ c._pf = defaultParsingFlags();
+
+ return makeMoment(c).utc();
+ };
+
+ // creating with unix timestamp (in seconds)
+ moment.unix = function (input) {
+ return moment(input * 1000);
+ };
+
+ // duration
+ moment.duration = function (input, key) {
+ var duration = input,
+ // matching against regexp is expensive, do it on demand
+ match = null,
+ sign,
+ ret,
+ parseIso,
+ diffRes;
+
+ if (moment.isDuration(input)) {
+ duration = {
+ ms: input._milliseconds,
+ d: input._days,
+ M: input._months
+ };
+ } else if (typeof input === 'number') {
+ duration = {};
+ if (key) {
+ duration[key] = input;
+ } else {
+ duration.milliseconds = input;
+ }
+ } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
+ sign = (match[1] === '-') ? -1 : 1;
+ duration = {
+ y: 0,
+ d: toInt(match[DATE]) * sign,
+ h: toInt(match[HOUR]) * sign,
+ m: toInt(match[MINUTE]) * sign,
+ s: toInt(match[SECOND]) * sign,
+ ms: toInt(match[MILLISECOND]) * sign
+ };
+ } else if (!!(match = isoDurationRegex.exec(input))) {
+ sign = (match[1] === '-') ? -1 : 1;
+ parseIso = function (inp) {
+ // We'd normally use ~~inp for this, but unfortunately it also
+ // converts floats to ints.
+ // inp may be undefined, so careful calling replace on it.
+ var res = inp && parseFloat(inp.replace(',', '.'));
+ // apply sign while we're at it
+ return (isNaN(res) ? 0 : res) * sign;
+ };
+ duration = {
+ y: parseIso(match[2]),
+ M: parseIso(match[3]),
+ d: parseIso(match[4]),
+ h: parseIso(match[5]),
+ m: parseIso(match[6]),
+ s: parseIso(match[7]),
+ w: parseIso(match[8])
+ };
+ } else if (typeof duration === 'object' &&
+ ('from' in duration || 'to' in duration)) {
+ diffRes = momentsDifference(moment(duration.from), moment(duration.to));
+
+ duration = {};
+ duration.ms = diffRes.milliseconds;
+ duration.M = diffRes.months;
+ }
+
+ ret = new Duration(duration);
+
+ if (moment.isDuration(input) && hasOwnProp(input, '_locale')) {
+ ret._locale = input._locale;
+ }
+
+ return ret;
+ };
+
+ // version number
+ moment.version = VERSION;
+
+ // default format
+ moment.defaultFormat = isoFormat;
+
+ // constant that refers to the ISO standard
+ moment.ISO_8601 = function () {};
+
+ // Plugins that add properties should also add the key here (null value),
+ // so we can properly clone ourselves.
+ moment.momentProperties = momentProperties;
+
+ // This function will be called whenever a moment is mutated.
+ // It is intended to keep the offset in sync with the timezone.
+ moment.updateOffset = function () {};
+
+ // This function allows you to set a threshold for relative time strings
+ moment.relativeTimeThreshold = function (threshold, limit) {
+ if (relativeTimeThresholds[threshold] === undefined) {
+ return false;
+ }
+ if (limit === undefined) {
+ return relativeTimeThresholds[threshold];
+ }
+ relativeTimeThresholds[threshold] = limit;
+ return true;
+ };
+
+ moment.lang = deprecate(
+ 'moment.lang is deprecated. Use moment.locale instead.',
+ function (key, value) {
+ return moment.locale(key, value);
+ }
+ );
+
+ // This function will load locale and then set the global locale. If
+ // no arguments are passed in, it will simply return the current global
+ // locale key.
+ moment.locale = function (key, values) {
+ var data;
+ if (key) {
+ if (typeof(values) !== 'undefined') {
+ data = moment.defineLocale(key, values);
+ }
+ else {
+ data = moment.localeData(key);
+ }
+
+ if (data) {
+ moment.duration._locale = moment._locale = data;
+ }
+ }
+
+ return moment._locale._abbr;
+ };
+
+ moment.defineLocale = function (name, values) {
+ if (values !== null) {
+ values.abbr = name;
+ if (!locales[name]) {
+ locales[name] = new Locale();
+ }
+ locales[name].set(values);
+
+ // backwards compat for now: also set the locale
+ moment.locale(name);
+
+ return locales[name];
+ } else {
+ // useful for testing
+ delete locales[name];
+ return null;
+ }
+ };
+
+ moment.langData = deprecate(
+ 'moment.langData is deprecated. Use moment.localeData instead.',
+ function (key) {
+ return moment.localeData(key);
+ }
+ );
+
+ // returns locale data
+ moment.localeData = function (key) {
+ var locale;
+
+ if (key && key._locale && key._locale._abbr) {
+ key = key._locale._abbr;
+ }
+
+ if (!key) {
+ return moment._locale;
+ }
+
+ if (!isArray(key)) {
+ //short-circuit everything else
+ locale = loadLocale(key);
+ if (locale) {
+ return locale;
+ }
+ key = [key];
+ }
+
+ return chooseLocale(key);
+ };
+
+ // compare moment object
+ moment.isMoment = function (obj) {
+ return obj instanceof Moment ||
+ (obj != null && hasOwnProp(obj, '_isAMomentObject'));
+ };
+
+ // for typechecking Duration objects
+ moment.isDuration = function (obj) {
+ return obj instanceof Duration;
+ };
+
+ for (i = lists.length - 1; i >= 0; --i) {
+ makeList(lists[i]);
+ }
+
+ moment.normalizeUnits = function (units) {
+ return normalizeUnits(units);
+ };
+
+ moment.invalid = function (flags) {
+ var m = moment.utc(NaN);
+ if (flags != null) {
+ extend(m._pf, flags);
+ }
+ else {
+ m._pf.userInvalidated = true;
+ }
+
+ return m;
+ };
+
+ moment.parseZone = function () {
+ return moment.apply(null, arguments).parseZone();
+ };
+
+ moment.parseTwoDigitYear = function (input) {
+ return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
+ };
+
+ /************************************
+ Moment Prototype
+ ************************************/
+
+
+ extend(moment.fn = Moment.prototype, {
+
+ clone : function () {
+ return moment(this);
+ },
+
+ valueOf : function () {
+ return +this._d + ((this._offset || 0) * 60000);
+ },
+
+ unix : function () {
+ return Math.floor(+this / 1000);
+ },
+
+ toString : function () {
+ return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ');
+ },
+
+ toDate : function () {
+ return this._offset ? new Date(+this) : this._d;
+ },
+
+ toISOString : function () {
+ var m = moment(this).utc();
+ if (0 < m.year() && m.year() <= 9999) {
+ if ('function' === typeof Date.prototype.toISOString) {
+ // native implementation is ~50x faster, use it when we can
+ return this.toDate().toISOString();
+ } else {
+ return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+ }
+ } else {
+ return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+ }
+ },
+
+ toArray : function () {
+ var m = this;
+ return [
+ m.year(),
+ m.month(),
+ m.date(),
+ m.hours(),
+ m.minutes(),
+ m.seconds(),
+ m.milliseconds()
+ ];
+ },
+
+ isValid : function () {
+ return isValid(this);
+ },
+
+ isDSTShifted : function () {
+ if (this._a) {
+ return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
+ }
+
+ return false;
+ },
+
+ parsingFlags : function () {
+ return extend({}, this._pf);
+ },
+
+ invalidAt: function () {
+ return this._pf.overflow;
+ },
+
+ utc : function (keepLocalTime) {
+ return this.zone(0, keepLocalTime);
+ },
+
+ local : function (keepLocalTime) {
+ if (this._isUTC) {
+ this.zone(0, keepLocalTime);
+ this._isUTC = false;
+
+ if (keepLocalTime) {
+ this.add(this._dateTzOffset(), 'm');
+ }
+ }
+ return this;
+ },
+
+ format : function (inputString) {
+ var output = formatMoment(this, inputString || moment.defaultFormat);
+ return this.localeData().postformat(output);
+ },
+
+ add : createAdder(1, 'add'),
+
+ subtract : createAdder(-1, 'subtract'),
+
+ diff : function (input, units, asFloat) {
+ var that = makeAs(input, this),
+ zoneDiff = (this.zone() - that.zone()) * 6e4,
+ diff, output, daysAdjust;
+
+ units = normalizeUnits(units);
+
+ if (units === 'year' || units === 'month') {
+ // average number of days in the months in the given dates
+ diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
+ // difference in months
+ output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
+ // adjust by taking difference in days, average number of days
+ // and dst in the given months.
+ daysAdjust = (this - moment(this).startOf('month')) -
+ (that - moment(that).startOf('month'));
+ // same as above but with zones, to negate all dst
+ daysAdjust -= ((this.zone() - moment(this).startOf('month').zone()) -
+ (that.zone() - moment(that).startOf('month').zone())) * 6e4;
+ output += daysAdjust / diff;
+ if (units === 'year') {
+ output = output / 12;
+ }
+ } else {
+ diff = (this - that);
+ output = units === 'second' ? diff / 1e3 : // 1000
+ units === 'minute' ? diff / 6e4 : // 1000 * 60
+ units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
+ units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
+ units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
+ diff;
+ }
+ return asFloat ? output : absRound(output);
+ },
+
+ from : function (time, withoutSuffix) {
+ return moment.duration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix);
+ },
+
+ fromNow : function (withoutSuffix) {
+ return this.from(moment(), withoutSuffix);
+ },
+
+ calendar : function (time) {
+ // We want to compare the start of today, vs this.
+ // Getting start-of-today depends on whether we're zone'd or not.
+ var now = time || moment(),
+ sod = makeAs(now, this).startOf('day'),
+ diff = this.diff(sod, 'days', true),
+ format = diff < -6 ? 'sameElse' :
+ diff < -1 ? 'lastWeek' :
+ diff < 0 ? 'lastDay' :
+ diff < 1 ? 'sameDay' :
+ diff < 2 ? 'nextDay' :
+ diff < 7 ? 'nextWeek' : 'sameElse';
+ return this.format(this.localeData().calendar(format, this, moment(now)));
+ },
+
+ isLeapYear : function () {
+ return isLeapYear(this.year());
+ },
+
+ isDST : function () {
+ return (this.zone() < this.clone().month(0).zone() ||
+ this.zone() < this.clone().month(5).zone());
+ },
+
+ day : function (input) {
+ var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
+ if (input != null) {
+ input = parseWeekday(input, this.localeData());
+ return this.add(input - day, 'd');
+ } else {
+ return day;
+ }
+ },
+
+ month : makeAccessor('Month', true),
+
+ startOf : function (units) {
+ units = normalizeUnits(units);
+ // the following switch intentionally omits break keywords
+ // to utilize falling through the cases.
+ switch (units) {
+ case 'year':
+ this.month(0);
+ /* falls through */
+ case 'quarter':
+ case 'month':
+ this.date(1);
+ /* falls through */
+ case 'week':
+ case 'isoWeek':
+ case 'day':
+ this.hours(0);
+ /* falls through */
+ case 'hour':
+ this.minutes(0);
+ /* falls through */
+ case 'minute':
+ this.seconds(0);
+ /* falls through */
+ case 'second':
+ this.milliseconds(0);
+ /* falls through */
+ }
+
+ // weeks are a special case
+ if (units === 'week') {
+ this.weekday(0);
+ } else if (units === 'isoWeek') {
+ this.isoWeekday(1);
+ }
+
+ // quarters are also special
+ if (units === 'quarter') {
+ this.month(Math.floor(this.month() / 3) * 3);
+ }
+
+ return this;
+ },
+
+ endOf: function (units) {
+ units = normalizeUnits(units);
+ if (units === undefined || units === 'millisecond') {
+ return this;
+ }
+ return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms');
+ },
+
+ isAfter: function (input, units) {
+ var inputMs;
+ units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond');
+ if (units === 'millisecond') {
+ input = moment.isMoment(input) ? input : moment(input);
+ return +this > +input;
+ } else {
+ inputMs = moment.isMoment(input) ? +input : +moment(input);
+ return inputMs < +this.clone().startOf(units);
+ }
+ },
+
+ isBefore: function (input, units) {
+ var inputMs;
+ units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond');
+ if (units === 'millisecond') {
+ input = moment.isMoment(input) ? input : moment(input);
+ return +this < +input;
+ } else {
+ inputMs = moment.isMoment(input) ? +input : +moment(input);
+ return +this.clone().endOf(units) < inputMs;
+ }
+ },
+
+ isSame: function (input, units) {
+ var inputMs;
+ units = normalizeUnits(units || 'millisecond');
+ if (units === 'millisecond') {
+ input = moment.isMoment(input) ? input : moment(input);
+ return +this === +input;
+ } else {
+ inputMs = +moment(input);
+ return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units));
+ }
+ },
+
+ min: deprecate(
+ 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548',
+ function (other) {
+ other = moment.apply(null, arguments);
+ return other < this ? this : other;
+ }
+ ),
+
+ max: deprecate(
+ 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548',
+ function (other) {
+ other = moment.apply(null, arguments);
+ return other > this ? this : other;
+ }
+ ),
+
+ // keepLocalTime = true means only change the timezone, without
+ // affecting the local hour. So 5:31:26 +0300 --[zone(2, true)]-->
+ // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist int zone
+ // +0200, so we adjust the time as needed, to be valid.
+ //
+ // Keeping the time actually adds/subtracts (one hour)
+ // from the actual represented time. That is why we call updateOffset
+ // a second time. In case it wants us to change the offset again
+ // _changeInProgress == true case, then we have to adjust, because
+ // there is no such time in the given timezone.
+ zone : function (input, keepLocalTime) {
+ var offset = this._offset || 0,
+ localAdjust;
+ if (input != null) {
+ if (typeof input === 'string') {
+ input = timezoneMinutesFromString(input);
+ }
+ if (Math.abs(input) < 16) {
+ input = input * 60;
+ }
+ if (!this._isUTC && keepLocalTime) {
+ localAdjust = this._dateTzOffset();
+ }
+ this._offset = input;
+ this._isUTC = true;
+ if (localAdjust != null) {
+ this.subtract(localAdjust, 'm');
+ }
+ if (offset !== input) {
+ if (!keepLocalTime || this._changeInProgress) {
+ addOrSubtractDurationFromMoment(this,
+ moment.duration(offset - input, 'm'), 1, false);
+ } else if (!this._changeInProgress) {
+ this._changeInProgress = true;
+ moment.updateOffset(this, true);
+ this._changeInProgress = null;
+ }
+ }
+ } else {
+ return this._isUTC ? offset : this._dateTzOffset();
+ }
+ return this;
+ },
+
+ zoneAbbr : function () {
+ return this._isUTC ? 'UTC' : '';
+ },
+
+ zoneName : function () {
+ return this._isUTC ? 'Coordinated Universal Time' : '';
+ },
+
+ parseZone : function () {
+ if (this._tzm) {
+ this.zone(this._tzm);
+ } else if (typeof this._i === 'string') {
+ this.zone(this._i);
+ }
+ return this;
+ },
+
+ hasAlignedHourOffset : function (input) {
+ if (!input) {
+ input = 0;
+ }
+ else {
+ input = moment(input).zone();
+ }
+
+ return (this.zone() - input) % 60 === 0;
+ },
+
+ daysInMonth : function () {
+ return daysInMonth(this.year(), this.month());
+ },
+
+ dayOfYear : function (input) {
+ var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
+ return input == null ? dayOfYear : this.add((input - dayOfYear), 'd');
+ },
+
+ quarter : function (input) {
+ return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
+ },
+
+ weekYear : function (input) {
+ var year = weekOfYear(this, this.localeData()._week.dow, this.localeData()._week.doy).year;
+ return input == null ? year : this.add((input - year), 'y');
+ },
+
+ isoWeekYear : function (input) {
+ var year = weekOfYear(this, 1, 4).year;
+ return input == null ? year : this.add((input - year), 'y');
+ },
+
+ week : function (input) {
+ var week = this.localeData().week(this);
+ return input == null ? week : this.add((input - week) * 7, 'd');
+ },
+
+ isoWeek : function (input) {
+ var week = weekOfYear(this, 1, 4).week;
+ return input == null ? week : this.add((input - week) * 7, 'd');
+ },
+
+ weekday : function (input) {
+ var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7;
+ return input == null ? weekday : this.add(input - weekday, 'd');
+ },
+
+ isoWeekday : function (input) {
+ // behaves the same as moment#day except
+ // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
+ // as a setter, sunday should belong to the previous week.
+ return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
+ },
+
+ isoWeeksInYear : function () {
+ return weeksInYear(this.year(), 1, 4);
+ },
+
+ weeksInYear : function () {
+ var weekInfo = this.localeData()._week;
+ return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
+ },
+
+ get : function (units) {
+ units = normalizeUnits(units);
+ return this[units]();
+ },
+
+ set : function (units, value) {
+ units = normalizeUnits(units);
+ if (typeof this[units] === 'function') {
+ this[units](value);
+ }
+ return this;
+ },
+
+ // If passed a locale key, it will set the locale for this
+ // instance. Otherwise, it will return the locale configuration
+ // variables for this instance.
+ locale : function (key) {
+ var newLocaleData;
+
+ if (key === undefined) {
+ return this._locale._abbr;
+ } else {
+ newLocaleData = moment.localeData(key);
+ if (newLocaleData != null) {
+ this._locale = newLocaleData;
+ }
+ return this;
+ }
+ },
+
+ lang : deprecate(
+ 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.',
+ function (key) {
+ if (key === undefined) {
+ return this.localeData();
+ } else {
+ return this.locale(key);
+ }
+ }
+ ),
+
+ localeData : function () {
+ return this._locale;
+ },
+
+ _dateTzOffset : function () {
+ // On Firefox.24 Date#getTimezoneOffset returns a floating point.
+ // https://github.com/moment/moment/pull/1871
+ return Math.round(this._d.getTimezoneOffset() / 15) * 15;
+ }
+ });
+
+ function rawMonthSetter(mom, value) {
+ var dayOfMonth;
+
+ // TODO: Move this out of here!
+ if (typeof value === 'string') {
+ value = mom.localeData().monthsParse(value);
+ // TODO: Another silent failure?
+ if (typeof value !== 'number') {
+ return mom;
+ }
+ }
+
+ dayOfMonth = Math.min(mom.date(),
+ daysInMonth(mom.year(), value));
+ mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
+ return mom;
+ }
+
+ function rawGetter(mom, unit) {
+ return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
+ }
+
+ function rawSetter(mom, unit, value) {
+ if (unit === 'Month') {
+ return rawMonthSetter(mom, value);
+ } else {
+ return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
+ }
+ }
+
+ function makeAccessor(unit, keepTime) {
+ return function (value) {
+ if (value != null) {
+ rawSetter(this, unit, value);
+ moment.updateOffset(this, keepTime);
+ return this;
+ } else {
+ return rawGetter(this, unit);
+ }
+ };
+ }
+
+ moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false);
+ moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false);
+ moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false);
+ // Setting the hour should keep the time, because the user explicitly
+ // specified which hour he wants. So trying to maintain the same hour (in
+ // a new timezone) makes sense. Adding/subtracting hours does not follow
+ // this rule.
+ moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true);
+ // moment.fn.month is defined separately
+ moment.fn.date = makeAccessor('Date', true);
+ moment.fn.dates = deprecate('dates accessor is deprecated. Use date instead.', makeAccessor('Date', true));
+ moment.fn.year = makeAccessor('FullYear', true);
+ moment.fn.years = deprecate('years accessor is deprecated. Use year instead.', makeAccessor('FullYear', true));
+
+ // add plural methods
+ moment.fn.days = moment.fn.day;
+ moment.fn.months = moment.fn.month;
+ moment.fn.weeks = moment.fn.week;
+ moment.fn.isoWeeks = moment.fn.isoWeek;
+ moment.fn.quarters = moment.fn.quarter;
+
+ // add aliased format methods
+ moment.fn.toJSON = moment.fn.toISOString;
+
+ /************************************
+ Duration Prototype
+ ************************************/
+
+
+ function daysToYears (days) {
+ // 400 years have 146097 days (taking into account leap year rules)
+ return days * 400 / 146097;
+ }
+
+ function yearsToDays (years) {
+ // years * 365 + absRound(years / 4) -
+ // absRound(years / 100) + absRound(years / 400);
+ return years * 146097 / 400;
+ }
+
+ extend(moment.duration.fn = Duration.prototype, {
+
+ _bubble : function () {
+ var milliseconds = this._milliseconds,
+ days = this._days,
+ months = this._months,
+ data = this._data,
+ seconds, minutes, hours, years = 0;
+
+ // The following code bubbles up values, see the tests for
+ // examples of what that means.
+ data.milliseconds = milliseconds % 1000;
+
+ seconds = absRound(milliseconds / 1000);
+ data.seconds = seconds % 60;
+
+ minutes = absRound(seconds / 60);
+ data.minutes = minutes % 60;
+
+ hours = absRound(minutes / 60);
+ data.hours = hours % 24;
+
+ days += absRound(hours / 24);
+
+ // Accurately convert days to years, assume start from year 0.
+ years = absRound(daysToYears(days));
+ days -= absRound(yearsToDays(years));
+
+ // 30 days to a month
+ // TODO (iskren): Use anchor date (like 1st Jan) to compute this.
+ months += absRound(days / 30);
+ days %= 30;
+
+ // 12 months -> 1 year
+ years += absRound(months / 12);
+ months %= 12;
+
+ data.days = days;
+ data.months = months;
+ data.years = years;
+ },
+
+ abs : function () {
+ this._milliseconds = Math.abs(this._milliseconds);
+ this._days = Math.abs(this._days);
+ this._months = Math.abs(this._months);
+
+ this._data.milliseconds = Math.abs(this._data.milliseconds);
+ this._data.seconds = Math.abs(this._data.seconds);
+ this._data.minutes = Math.abs(this._data.minutes);
+ this._data.hours = Math.abs(this._data.hours);
+ this._data.months = Math.abs(this._data.months);
+ this._data.years = Math.abs(this._data.years);
+
+ return this;
+ },
+
+ weeks : function () {
+ return absRound(this.days() / 7);
+ },
+
+ valueOf : function () {
+ return this._milliseconds +
+ this._days * 864e5 +
+ (this._months % 12) * 2592e6 +
+ toInt(this._months / 12) * 31536e6;
+ },
+
+ humanize : function (withSuffix) {
+ var output = relativeTime(this, !withSuffix, this.localeData());
+
+ if (withSuffix) {
+ output = this.localeData().pastFuture(+this, output);
+ }
+
+ return this.localeData().postformat(output);
+ },
+
+ add : function (input, val) {
+ // supports only 2.0-style add(1, 's') or add(moment)
+ var dur = moment.duration(input, val);
+
+ this._milliseconds += dur._milliseconds;
+ this._days += dur._days;
+ this._months += dur._months;
+
+ this._bubble();
+
+ return this;
+ },
+
+ subtract : function (input, val) {
+ var dur = moment.duration(input, val);
+
+ this._milliseconds -= dur._milliseconds;
+ this._days -= dur._days;
+ this._months -= dur._months;
+
+ this._bubble();
+
+ return this;
+ },
+
+ get : function (units) {
+ units = normalizeUnits(units);
+ return this[units.toLowerCase() + 's']();
+ },
+
+ as : function (units) {
+ var days, months;
+ units = normalizeUnits(units);
+
+ if (units === 'month' || units === 'year') {
+ days = this._days + this._milliseconds / 864e5;
+ months = this._months + daysToYears(days) * 12;
+ return units === 'month' ? months : months / 12;
+ } else {
+ // handle milliseconds separately because of floating point math errors (issue #1867)
+ days = this._days + Math.round(yearsToDays(this._months / 12));
+ switch (units) {
+ case 'week': return days / 7 + this._milliseconds / 6048e5;
+ case 'day': return days + this._milliseconds / 864e5;
+ case 'hour': return days * 24 + this._milliseconds / 36e5;
+ case 'minute': return days * 24 * 60 + this._milliseconds / 6e4;
+ case 'second': return days * 24 * 60 * 60 + this._milliseconds / 1000;
+ // Math.floor prevents floating point math errors here
+ case 'millisecond': return Math.floor(days * 24 * 60 * 60 * 1000) + this._milliseconds;
+ default: throw new Error('Unknown unit ' + units);
+ }
+ }
+ },
+
+ lang : moment.fn.lang,
+ locale : moment.fn.locale,
+
+ toIsoString : deprecate(
+ 'toIsoString() is deprecated. Please use toISOString() instead ' +
+ '(notice the capitals)',
+ function () {
+ return this.toISOString();
+ }
+ ),
+
+ toISOString : function () {
+ // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
+ var years = Math.abs(this.years()),
+ months = Math.abs(this.months()),
+ days = Math.abs(this.days()),
+ hours = Math.abs(this.hours()),
+ minutes = Math.abs(this.minutes()),
+ seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
+
+ if (!this.asSeconds()) {
+ // this is the same as C#'s (Noda) and python (isodate)...
+ // but not other JS (goog.date)
+ return 'P0D';
+ }
+
+ return (this.asSeconds() < 0 ? '-' : '') +
+ 'P' +
+ (years ? years + 'Y' : '') +
+ (months ? months + 'M' : '') +
+ (days ? days + 'D' : '') +
+ ((hours || minutes || seconds) ? 'T' : '') +
+ (hours ? hours + 'H' : '') +
+ (minutes ? minutes + 'M' : '') +
+ (seconds ? seconds + 'S' : '');
+ },
+
+ localeData : function () {
+ return this._locale;
+ }
+ });
+
+ moment.duration.fn.toString = moment.duration.fn.toISOString;
+
+ function makeDurationGetter(name) {
+ moment.duration.fn[name] = function () {
+ return this._data[name];
+ };
+ }
+
+ for (i in unitMillisecondFactors) {
+ if (hasOwnProp(unitMillisecondFactors, i)) {
+ makeDurationGetter(i.toLowerCase());
+ }
+ }
+
+ moment.duration.fn.asMilliseconds = function () {
+ return this.as('ms');
+ };
+ moment.duration.fn.asSeconds = function () {
+ return this.as('s');
+ };
+ moment.duration.fn.asMinutes = function () {
+ return this.as('m');
+ };
+ moment.duration.fn.asHours = function () {
+ return this.as('h');
+ };
+ moment.duration.fn.asDays = function () {
+ return this.as('d');
+ };
+ moment.duration.fn.asWeeks = function () {
+ return this.as('weeks');
+ };
+ moment.duration.fn.asMonths = function () {
+ return this.as('M');
+ };
+ moment.duration.fn.asYears = function () {
+ return this.as('y');
+ };
+
+ /************************************
+ Default Locale
+ ************************************/
+
+
+ // Set default locale, other locale will inherit from English.
+ moment.locale('en', {
+ ordinalParse: /\d{1,2}(th|st|nd|rd)/,
+ ordinal : function (number) {
+ var b = number % 10,
+ output = (toInt(number % 100 / 10) === 1) ? 'th' :
+ (b === 1) ? 'st' :
+ (b === 2) ? 'nd' :
+ (b === 3) ? 'rd' : 'th';
+ return number + output;
+ }
+ });
+
+ /* EMBED_LOCALES */
+
+ /************************************
+ Exposing Moment
+ ************************************/
+
+ function makeGlobal(shouldDeprecate) {
+ /*global ender:false */
+ if (typeof ender !== 'undefined') {
+ return;
+ }
+ oldGlobalMoment = globalScope.moment;
+ if (shouldDeprecate) {
+ globalScope.moment = deprecate(
+ 'Accessing Moment through the global scope is ' +
+ 'deprecated, and will be removed in an upcoming ' +
+ 'release.',
+ moment);
+ } else {
+ globalScope.moment = moment;
+ }
+ }
+
+ // CommonJS module is defined
+ if (hasModule) {
+ module.exports = moment;
+ } else if (true) {
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
+ if (module.config && module.config() && module.config().noGlobal === true) {
+ // release the global variable
+ globalScope.moment = oldGlobalMoment;
+ }
+
+ return moment;
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ makeGlobal(true);
+ } else {
+ makeGlobal();
+ }
+ }).call(this);
+
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()), __webpack_require__(5)(module)))
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+ function webpackContext(req) {
+ throw new Error("Cannot find module '" + req + "'.");
+ }
+ webpackContext.keys = function() { return []; };
+ webpackContext.resolve = webpackContext;
+ module.exports = webpackContext;
+ webpackContext.id = 4;
+
+
+/***/ },
+/* 5 */
+/***/ function(module, exports, __webpack_require__) {
+
+ module.exports = function(module) {
+ if(!module.webpackPolyfill) {
+ module.deprecate = function() {};
+ module.paths = [];
+ // module.parent = undefined by default
+ module.children = [];
+ module.webpackPolyfill = 1;
+ }
+ return module;
+ }
+
+
+/***/ },
+/* 6 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // DOM utility methods
+
+ /**
+ * this prepares the JSON container for allocating SVG elements
+ * @param JSONcontainer
+ * @private
+ */
+ exports.prepareElements = function(JSONcontainer) {
+ // cleanup the redundant svgElements;
+ for (var elementType in JSONcontainer) {
+ if (JSONcontainer.hasOwnProperty(elementType)) {
+ JSONcontainer[elementType].redundant = JSONcontainer[elementType].used;
+ JSONcontainer[elementType].used = [];
+ }
+ }
+ };
+
+ /**
+ * this cleans up all the unused SVG elements. By asking for the parentNode, we only need to supply the JSON container from
+ * which to remove the redundant elements.
+ *
+ * @param JSONcontainer
+ * @private
+ */
+ exports.cleanupElements = function(JSONcontainer) {
+ // cleanup the redundant svgElements;
+ for (var elementType in JSONcontainer) {
+ if (JSONcontainer.hasOwnProperty(elementType)) {
+ if (JSONcontainer[elementType].redundant) {
+ for (var i = 0; i < JSONcontainer[elementType].redundant.length; i++) {
+ JSONcontainer[elementType].redundant[i].parentNode.removeChild(JSONcontainer[elementType].redundant[i]);
+ }
+ JSONcontainer[elementType].redundant = [];
+ }
+ }
+ }
+ };
+
+ /**
+ * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer
+ * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.
+ *
+ * @param elementType
+ * @param JSONcontainer
+ * @param svgContainer
+ * @returns {*}
+ * @private
+ */
+ exports.getSVGElement = function (elementType, JSONcontainer, svgContainer) {
+ var element;
+ // allocate SVG element, if it doesnt yet exist, create one.
+ if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before
+ // check if there is an redundant element
+ if (JSONcontainer[elementType].redundant.length > 0) {
+ element = JSONcontainer[elementType].redundant[0];
+ JSONcontainer[elementType].redundant.shift();
+ }
+ else {
+ // create a new element and add it to the SVG
+ element = document.createElementNS('http://www.w3.org/2000/svg', elementType);
+ svgContainer.appendChild(element);
+ }
+ }
+ else {
+ // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.
+ element = document.createElementNS('http://www.w3.org/2000/svg', elementType);
+ JSONcontainer[elementType] = {used: [], redundant: []};
+ svgContainer.appendChild(element);
+ }
+ JSONcontainer[elementType].used.push(element);
+ return element;
+ };
+
+
+ /**
+ * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer
+ * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.
+ *
+ * @param elementType
+ * @param JSONcontainer
+ * @param DOMContainer
+ * @returns {*}
+ * @private
+ */
+ exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer, insertBefore) {
+ var element;
+ // allocate DOM element, if it doesnt yet exist, create one.
+ if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before
+ // check if there is an redundant element
+ if (JSONcontainer[elementType].redundant.length > 0) {
+ element = JSONcontainer[elementType].redundant[0];
+ JSONcontainer[elementType].redundant.shift();
+ }
+ else {
+ // create a new element and add it to the SVG
+ element = document.createElement(elementType);
+ if (insertBefore !== undefined) {
+ DOMContainer.insertBefore(element, insertBefore);
+ }
+ else {
+ DOMContainer.appendChild(element);
+ }
+ }
+ }
+ else {
+ // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.
+ element = document.createElement(elementType);
+ JSONcontainer[elementType] = {used: [], redundant: []};
+ if (insertBefore !== undefined) {
+ DOMContainer.insertBefore(element, insertBefore);
+ }
+ else {
+ DOMContainer.appendChild(element);
+ }
+ }
+ JSONcontainer[elementType].used.push(element);
+ return element;
+ };
+
+
+
+
+ /**
+ * draw a point object. this is a seperate function because it can also be called by the legend.
+ * The reason the JSONcontainer and the target SVG svgContainer have to be supplied is so the legend can use these functions
+ * as well.
+ *
+ * @param x
+ * @param y
+ * @param group
+ * @param JSONcontainer
+ * @param svgContainer
+ * @returns {*}
+ */
+ exports.drawPoint = function(x, y, group, JSONcontainer, svgContainer) {
+ var point;
+ if (group.options.drawPoints.style == 'circle') {
+ point = exports.getSVGElement('circle',JSONcontainer,svgContainer);
+ point.setAttributeNS(null, "cx", x);
+ point.setAttributeNS(null, "cy", y);
+ point.setAttributeNS(null, "r", 0.5 * group.options.drawPoints.size);
+ }
+ else {
+ point = exports.getSVGElement('rect',JSONcontainer,svgContainer);
+ point.setAttributeNS(null, "x", x - 0.5*group.options.drawPoints.size);
+ point.setAttributeNS(null, "y", y - 0.5*group.options.drawPoints.size);
+ point.setAttributeNS(null, "width", group.options.drawPoints.size);
+ point.setAttributeNS(null, "height", group.options.drawPoints.size);
+ }
+
+ if(group.options.drawPoints.styles !== undefined) {
+ point.setAttributeNS(null, "style", group.group.options.drawPoints.styles);
+ }
+ point.setAttributeNS(null, "class", group.className + " point");
+ return point;
+ };
+
+ /**
+ * draw a bar SVG element centered on the X coordinate
+ *
+ * @param x
+ * @param y
+ * @param className
+ */
+ exports.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer) {
+ if (height != 0) {
+ if (height < 0) {
+ height *= -1;
+ y -= height;
+ }
+ var rect = exports.getSVGElement('rect',JSONcontainer, svgContainer);
+ rect.setAttributeNS(null, "x", x - 0.5 * width);
+ rect.setAttributeNS(null, "y", y);
+ rect.setAttributeNS(null, "width", width);
+ rect.setAttributeNS(null, "height", height);
+ rect.setAttributeNS(null, "class", className);
+ }
+ };
+
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var util = __webpack_require__(1);
+ var Queue = __webpack_require__(8);
+
+ /**
+ * DataSet
+ *
+ * Usage:
+ * var dataSet = new DataSet({
+ * fieldId: '_id',
+ * type: {
+ * // ...
+ * }
+ * });
+ *
+ * dataSet.add(item);
+ * dataSet.add(data);
+ * dataSet.update(item);
+ * dataSet.update(data);
+ * dataSet.remove(id);
+ * dataSet.remove(ids);
+ * var data = dataSet.get();
+ * var data = dataSet.get(id);
+ * var data = dataSet.get(ids);
+ * var data = dataSet.get(ids, options, data);
+ * dataSet.clear();
+ *
+ * A data set can:
+ * - add/remove/update data
+ * - gives triggers upon changes in the data
+ * - can import/export data in various data formats
+ *
+ * @param {Array | DataTable} [data] Optional array with initial data
+ * @param {Object} [options] Available options:
+ * {String} fieldId Field name of the id in the
+ * items, 'id' by default.
+ * {Object.} [type]
+ * {String[]} [fields] field names to be returned
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * {Array | DataTable} [data] If provided, items will be appended to this
+ * array or table. Required in case of Google
+ * DataTable.
+ *
+ * @throws Error
+ */
+ DataSet.prototype.get = function (args) {
+ var me = this;
+
+ // parse the arguments
+ var id, ids, options, data;
+ var firstType = util.getType(arguments[0]);
+ if (firstType == 'String' || firstType == 'Number') {
+ // get(id [, options] [, data])
+ id = arguments[0];
+ options = arguments[1];
+ data = arguments[2];
+ }
+ else if (firstType == 'Array') {
+ // get(ids [, options] [, data])
+ ids = arguments[0];
+ options = arguments[1];
+ data = arguments[2];
+ }
+ else {
+ // get([, options] [, data])
+ options = arguments[0];
+ data = arguments[1];
+ }
+
+ // determine the return type
+ var returnType;
+ if (options && options.returnType) {
+ var allowedValues = ["DataTable", "Array", "Object"];
+ returnType = allowedValues.indexOf(options.returnType) == -1 ? "Array" : options.returnType;
+
+ if (data && (returnType != util.getType(data))) {
+ throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
+ 'does not correspond with specified options.type (' + options.type + ')');
+ }
+ if (returnType == 'DataTable' && !util.isDataTable(data)) {
+ throw new Error('Parameter "data" must be a DataTable ' +
+ 'when options.type is "DataTable"');
+ }
+ }
+ else if (data) {
+ returnType = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
+ }
+ else {
+ returnType = 'Array';
+ }
+
+ // build options
+ var type = options && options.type || this._options.type;
+ var filter = options && options.filter;
+ var items = [], item, itemId, i, len;
+
+ // convert items
+ if (id != undefined) {
+ // return a single item
+ item = me._getItem(id, type);
+ if (filter && !filter(item)) {
+ item = null;
+ }
+ }
+ else if (ids != undefined) {
+ // return a subset of items
+ for (i = 0, len = ids.length; i < len; i++) {
+ item = me._getItem(ids[i], type);
+ if (!filter || filter(item)) {
+ items.push(item);
+ }
+ }
+ }
+ else {
+ // return all items
+ for (itemId in this._data) {
+ if (this._data.hasOwnProperty(itemId)) {
+ item = me._getItem(itemId, type);
+ if (!filter || filter(item)) {
+ items.push(item);
+ }
+ }
+ }
+ }
+
+ // order the results
+ if (options && options.order && id == undefined) {
+ this._sort(items, options.order);
+ }
+
+ // filter fields of the items
+ if (options && options.fields) {
+ var fields = options.fields;
+ if (id != undefined) {
+ item = this._filterFields(item, fields);
+ }
+ else {
+ for (i = 0, len = items.length; i < len; i++) {
+ items[i] = this._filterFields(items[i], fields);
+ }
+ }
+ }
+
+ // return the results
+ if (returnType == 'DataTable') {
+ var columns = this._getColumnNames(data);
+ if (id != undefined) {
+ // append a single item to the data table
+ me._appendRow(data, columns, item);
+ }
+ else {
+ // copy the items to the provided data table
+ for (i = 0; i < items.length; i++) {
+ me._appendRow(data, columns, items[i]);
+ }
+ }
+ return data;
+ }
+ else if (returnType == "Object") {
+ var result = {};
+ for (i = 0; i < items.length; i++) {
+ result[items[i].id] = items[i];
+ }
+ return result;
+ }
+ else {
+ // return an array
+ if (id != undefined) {
+ // a single item
+ return item;
+ }
+ else {
+ // multiple items
+ if (data) {
+ // copy the items to the provided array
+ for (i = 0, len = items.length; i < len; i++) {
+ data.push(items[i]);
+ }
+ return data;
+ }
+ else {
+ // just return our array
+ return items;
+ }
+ }
+ }
+ };
+
+ /**
+ * Get ids of all items or from a filtered set of items.
+ * @param {Object} [options] An Object with options. Available options:
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * @return {Array} ids
+ */
+ DataSet.prototype.getIds = function (options) {
+ var data = this._data,
+ filter = options && options.filter,
+ order = options && options.order,
+ type = options && options.type || this._options.type,
+ i,
+ len,
+ id,
+ item,
+ items,
+ ids = [];
+
+ if (filter) {
+ // get filtered items
+ if (order) {
+ // create ordered list
+ items = [];
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, type);
+ if (filter(item)) {
+ items.push(item);
+ }
+ }
+ }
+
+ this._sort(items, order);
+
+ for (i = 0, len = items.length; i < len; i++) {
+ ids[i] = items[i][this._fieldId];
+ }
+ }
+ else {
+ // create unordered list
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, type);
+ if (filter(item)) {
+ ids.push(item[this._fieldId]);
+ }
+ }
+ }
+ }
+ }
+ else {
+ // get all items
+ if (order) {
+ // create an ordered list
+ items = [];
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ items.push(data[id]);
+ }
+ }
+
+ this._sort(items, order);
+
+ for (i = 0, len = items.length; i < len; i++) {
+ ids[i] = items[i][this._fieldId];
+ }
+ }
+ else {
+ // create unordered list
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = data[id];
+ ids.push(item[this._fieldId]);
+ }
+ }
+ }
+ }
+
+ return ids;
+ };
+
+ /**
+ * Returns the DataSet itself. Is overwritten for example by the DataView,
+ * which returns the DataSet it is connected to instead.
+ */
+ DataSet.prototype.getDataSet = function () {
+ return this;
+ };
+
+ /**
+ * Execute a callback function for every item in the dataset.
+ * @param {function} callback
+ * @param {Object} [options] Available options:
+ * {Object.} [type]
+ * {String[]} [fields] filter fields
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ */
+ DataSet.prototype.forEach = function (callback, options) {
+ var filter = options && options.filter,
+ type = options && options.type || this._options.type,
+ data = this._data,
+ item,
+ id;
+
+ if (options && options.order) {
+ // execute forEach on ordered list
+ var items = this.get(options);
+
+ for (var i = 0, len = items.length; i < len; i++) {
+ item = items[i];
+ id = item[this._fieldId];
+ callback(item, id);
+ }
+ }
+ else {
+ // unordered
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, type);
+ if (!filter || filter(item)) {
+ callback(item, id);
+ }
+ }
+ }
+ }
+ };
+
+ /**
+ * Map every item in the dataset.
+ * @param {function} callback
+ * @param {Object} [options] Available options:
+ * {Object.} [type]
+ * {String[]} [fields] filter fields
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * @return {Object[]} mappedItems
+ */
+ DataSet.prototype.map = function (callback, options) {
+ var filter = options && options.filter,
+ type = options && options.type || this._options.type,
+ mappedItems = [],
+ data = this._data,
+ item;
+
+ // convert and filter items
+ for (var id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, type);
+ if (!filter || filter(item)) {
+ mappedItems.push(callback(item, id));
+ }
+ }
+ }
+
+ // order items
+ if (options && options.order) {
+ this._sort(mappedItems, options.order);
+ }
+
+ return mappedItems;
+ };
+
+ /**
+ * Filter the fields of an item
+ * @param {Object} item
+ * @param {String[]} fields Field names
+ * @return {Object} filteredItem
+ * @private
+ */
+ DataSet.prototype._filterFields = function (item, fields) {
+ var filteredItem = {};
+
+ for (var field in item) {
+ if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
+ filteredItem[field] = item[field];
+ }
+ }
+
+ return filteredItem;
+ };
+
+ /**
+ * Sort the provided array with items
+ * @param {Object[]} items
+ * @param {String | function} order A field name or custom sort function.
+ * @private
+ */
+ DataSet.prototype._sort = function (items, order) {
+ if (util.isString(order)) {
+ // order by provided field name
+ var name = order; // field name
+ items.sort(function (a, b) {
+ var av = a[name];
+ var bv = b[name];
+ return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
+ });
+ }
+ else if (typeof order === 'function') {
+ // order by sort function
+ items.sort(order);
+ }
+ // TODO: extend order by an Object {field:String, direction:String}
+ // where direction can be 'asc' or 'desc'
+ else {
+ throw new TypeError('Order must be a function or a string');
+ }
+ };
+
+ /**
+ * Remove an object by pointer or by id
+ * @param {String | Number | Object | Array} id Object or id, or an array with
+ * objects or ids to be removed
+ * @param {String} [senderId] Optional sender id
+ * @return {Array} removedIds
+ */
+ DataSet.prototype.remove = function (id, senderId) {
+ var removedIds = [],
+ i, len, removedId;
+
+ if (Array.isArray(id)) {
+ for (i = 0, len = id.length; i < len; i++) {
+ removedId = this._remove(id[i]);
+ if (removedId != null) {
+ removedIds.push(removedId);
+ }
+ }
+ }
+ else {
+ removedId = this._remove(id);
+ if (removedId != null) {
+ removedIds.push(removedId);
+ }
+ }
+
+ if (removedIds.length) {
+ this._trigger('remove', {items: removedIds}, senderId);
+ }
+
+ return removedIds;
+ };
+
+ /**
+ * Remove an item by its id
+ * @param {Number | String | Object} id id or item
+ * @returns {Number | String | null} id
+ * @private
+ */
+ DataSet.prototype._remove = function (id) {
+ if (util.isNumber(id) || util.isString(id)) {
+ if (this._data[id]) {
+ delete this._data[id];
+ return id;
+ }
+ }
+ else if (id instanceof Object) {
+ var itemId = id[this._fieldId];
+ if (itemId && this._data[itemId]) {
+ delete this._data[itemId];
+ return itemId;
+ }
+ }
+ return null;
+ };
+
+ /**
+ * Clear the data
+ * @param {String} [senderId] Optional sender id
+ * @return {Array} removedIds The ids of all removed items
+ */
+ DataSet.prototype.clear = function (senderId) {
+ var ids = Object.keys(this._data);
+
+ this._data = {};
+
+ this._trigger('remove', {items: ids}, senderId);
+
+ return ids;
+ };
+
+ /**
+ * Find the item with maximum value of a specified field
+ * @param {String} field
+ * @return {Object | null} item Item containing max value, or null if no items
+ */
+ DataSet.prototype.max = function (field) {
+ var data = this._data,
+ max = null,
+ maxField = null;
+
+ for (var id in data) {
+ if (data.hasOwnProperty(id)) {
+ var item = data[id];
+ var itemField = item[field];
+ if (itemField != null && (!max || itemField > maxField)) {
+ max = item;
+ maxField = itemField;
+ }
+ }
+ }
+
+ return max;
+ };
+
+ /**
+ * Find the item with minimum value of a specified field
+ * @param {String} field
+ * @return {Object | null} item Item containing max value, or null if no items
+ */
+ DataSet.prototype.min = function (field) {
+ var data = this._data,
+ min = null,
+ minField = null;
+
+ for (var id in data) {
+ if (data.hasOwnProperty(id)) {
+ var item = data[id];
+ var itemField = item[field];
+ if (itemField != null && (!min || itemField < minField)) {
+ min = item;
+ minField = itemField;
+ }
+ }
+ }
+
+ return min;
+ };
+
+ /**
+ * Find all distinct values of a specified field
+ * @param {String} field
+ * @return {Array} values Array containing all distinct values. If data items
+ * do not contain the specified field are ignored.
+ * The returned array is unordered.
+ */
+ DataSet.prototype.distinct = function (field) {
+ var data = this._data;
+ var values = [];
+ var fieldType = this._options.type && this._options.type[field] || null;
+ var count = 0;
+ var i;
+
+ for (var prop in data) {
+ if (data.hasOwnProperty(prop)) {
+ var item = data[prop];
+ var value = item[field];
+ var exists = false;
+ for (i = 0; i < count; i++) {
+ if (values[i] == value) {
+ exists = true;
+ break;
+ }
+ }
+ if (!exists && (value !== undefined)) {
+ values[count] = value;
+ count++;
+ }
+ }
+ }
+
+ if (fieldType) {
+ for (i = 0; i < values.length; i++) {
+ values[i] = util.convert(values[i], fieldType);
+ }
+ }
+
+ return values;
+ };
+
+ /**
+ * Add a single item. Will fail when an item with the same id already exists.
+ * @param {Object} item
+ * @return {String} id
+ * @private
+ */
+ DataSet.prototype._addItem = function (item) {
+ var id = item[this._fieldId];
+
+ if (id != undefined) {
+ // check whether this id is already taken
+ if (this._data[id]) {
+ // item already exists
+ throw new Error('Cannot add item: item with id ' + id + ' already exists');
+ }
+ }
+ else {
+ // generate an id
+ id = util.randomUUID();
+ item[this._fieldId] = id;
+ }
+
+ var d = {};
+ for (var field in item) {
+ if (item.hasOwnProperty(field)) {
+ var fieldType = this._type[field]; // type may be undefined
+ d[field] = util.convert(item[field], fieldType);
+ }
+ }
+ this._data[id] = d;
+
+ return id;
+ };
+
+ /**
+ * Get an item. Fields can be converted to a specific type
+ * @param {String} id
+ * @param {Object.} [types] field types to convert
+ * @return {Object | null} item
+ * @private
+ */
+ DataSet.prototype._getItem = function (id, types) {
+ var field, value;
+
+ // get the item from the dataset
+ var raw = this._data[id];
+ if (!raw) {
+ return null;
+ }
+
+ // convert the items field types
+ var converted = {};
+ if (types) {
+ for (field in raw) {
+ if (raw.hasOwnProperty(field)) {
+ value = raw[field];
+ converted[field] = util.convert(value, types[field]);
+ }
+ }
+ }
+ else {
+ // no field types specified, no converting needed
+ for (field in raw) {
+ if (raw.hasOwnProperty(field)) {
+ value = raw[field];
+ converted[field] = value;
+ }
+ }
+ }
+ return converted;
+ };
+
+ /**
+ * Update a single item: merge with existing item.
+ * Will fail when the item has no id, or when there does not exist an item
+ * with the same id.
+ * @param {Object} item
+ * @return {String} id
+ * @private
+ */
+ DataSet.prototype._updateItem = function (item) {
+ var id = item[this._fieldId];
+ if (id == undefined) {
+ throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
+ }
+ var d = this._data[id];
+ if (!d) {
+ // item doesn't exist
+ throw new Error('Cannot update item: no item with id ' + id + ' found');
+ }
+
+ // merge with current item
+ for (var field in item) {
+ if (item.hasOwnProperty(field)) {
+ var fieldType = this._type[field]; // type may be undefined
+ d[field] = util.convert(item[field], fieldType);
+ }
+ }
+
+ return id;
+ };
+
+ /**
+ * Get an array with the column names of a Google DataTable
+ * @param {DataTable} dataTable
+ * @return {String[]} columnNames
+ * @private
+ */
+ DataSet.prototype._getColumnNames = function (dataTable) {
+ var columns = [];
+ for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
+ columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
+ }
+ return columns;
+ };
+
+ /**
+ * Append an item as a row to the dataTable
+ * @param dataTable
+ * @param columns
+ * @param item
+ * @private
+ */
+ DataSet.prototype._appendRow = function (dataTable, columns, item) {
+ var row = dataTable.addRow();
+
+ for (var col = 0, cols = columns.length; col < cols; col++) {
+ var field = columns[col];
+ dataTable.setValue(row, col, item[field]);
+ }
+ };
+
+ module.exports = DataSet;
+
+
+/***/ },
+/* 8 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * A queue
+ * @param {Object} options
+ * Available options:
+ * - delay: number When provided, the queue will be flushed
+ * automatically after an inactivity of this delay
+ * in milliseconds.
+ * Default value is null.
+ * - max: number When the queue exceeds the given maximum number
+ * of entries, the queue is flushed automatically.
+ * Default value of max is Infinity.
+ * @constructor
+ */
+ function Queue(options) {
+ // options
+ this.delay = null;
+ this.max = Infinity;
+
+ // properties
+ this._queue = [];
+ this._timeout = null;
+ this._extended = null;
+
+ this.setOptions(options);
+ }
+
+ /**
+ * Update the configuration of the queue
+ * @param {Object} options
+ * Available options:
+ * - delay: number When provided, the queue will be flushed
+ * automatically after an inactivity of this delay
+ * in milliseconds.
+ * Default value is null.
+ * - max: number When the queue exceeds the given maximum number
+ * of entries, the queue is flushed automatically.
+ * Default value of max is Infinity.
+ * @param options
+ */
+ Queue.prototype.setOptions = function (options) {
+ if (options && typeof options.delay !== 'undefined') {
+ this.delay = options.delay;
+ }
+ if (options && typeof options.max !== 'undefined') {
+ this.max = options.max;
+ }
+
+ this._flushIfNeeded();
+ };
+
+ /**
+ * Extend an object with queuing functionality.
+ * The object will be extended with a function flush, and the methods provided
+ * in options.replace will be replaced with queued ones.
+ * @param {Object} object
+ * @param {Object} options
+ * Available options:
+ * - replace: Array.
+ * A list with method names of the methods
+ * on the object to be replaced with queued ones.
+ * - delay: number When provided, the queue will be flushed
+ * automatically after an inactivity of this delay
+ * in milliseconds.
+ * Default value is null.
+ * - max: number When the queue exceeds the given maximum number
+ * of entries, the queue is flushed automatically.
+ * Default value of max is Infinity.
+ * @return {Queue} Returns the created queue
+ */
+ Queue.extend = function (object, options) {
+ var queue = new Queue(options);
+
+ if (object.flush !== undefined) {
+ throw new Error('Target object already has a property flush');
+ }
+ object.flush = function () {
+ queue.flush();
+ };
+
+ var methods = [{
+ name: 'flush',
+ original: undefined
+ }];
+
+ if (options && options.replace) {
+ for (var i = 0; i < options.replace.length; i++) {
+ var name = options.replace[i];
+ methods.push({
+ name: name,
+ original: object[name]
+ });
+ queue.replace(object, name);
+ }
+ }
+
+ queue._extended = {
+ object: object,
+ methods: methods
+ };
+
+ return queue;
+ };
+
+ /**
+ * Destroy the queue. The queue will first flush all queued actions, and in
+ * case it has extended an object, will restore the original object.
+ */
+ Queue.prototype.destroy = function () {
+ this.flush();
+
+ if (this._extended) {
+ var object = this._extended.object;
+ var methods = this._extended.methods;
+ for (var i = 0; i < methods.length; i++) {
+ var method = methods[i];
+ if (method.original) {
+ object[method.name] = method.original;
+ }
+ else {
+ delete object[method.name];
+ }
+ }
+ this._extended = null;
+ }
+ };
+
+ /**
+ * Replace a method on an object with a queued version
+ * @param {Object} object Object having the method
+ * @param {string} method The method name
+ */
+ Queue.prototype.replace = function(object, method) {
+ var me = this;
+ var original = object[method];
+ if (!original) {
+ throw new Error('Method ' + method + ' undefined');
+ }
+
+ object[method] = function () {
+ // create an Array with the arguments
+ var args = [];
+ for (var i = 0; i < arguments.length; i++) {
+ args[i] = arguments[i];
+ }
+
+ // add this call to the queue
+ me.queue({
+ args: args,
+ fn: original,
+ context: this
+ });
+ };
+ };
+
+ /**
+ * Queue a call
+ * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry
+ */
+ Queue.prototype.queue = function(entry) {
+ if (typeof entry === 'function') {
+ this._queue.push({fn: entry});
+ }
+ else {
+ this._queue.push(entry);
+ }
+
+ this._flushIfNeeded();
+ };
+
+ /**
+ * Check whether the queue needs to be flushed
+ * @private
+ */
+ Queue.prototype._flushIfNeeded = function () {
+ // flush when the maximum is exceeded.
+ if (this._queue.length > this.max) {
+ this.flush();
+ }
+
+ // flush after a period of inactivity when a delay is configured
+ clearTimeout(this._timeout);
+ if (this.queue.length > 0 && typeof this.delay === 'number') {
+ var me = this;
+ this._timeout = setTimeout(function () {
+ me.flush();
+ }, this.delay);
+ }
+ };
+
+ /**
+ * Flush all queued calls
+ */
+ Queue.prototype.flush = function () {
+ while (this._queue.length > 0) {
+ var entry = this._queue.shift();
+ entry.fn.apply(entry.context || entry.fn, entry.args || []);
+ }
+ };
+
+ module.exports = Queue;
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var util = __webpack_require__(1);
+ var DataSet = __webpack_require__(7);
+
+ /**
+ * DataView
+ *
+ * a dataview offers a filtered view on a dataset or an other dataview.
+ *
+ * @param {DataSet | DataView} data
+ * @param {Object} [options] Available options: see method get
+ *
+ * @constructor DataView
+ */
+ function DataView (data, options) {
+ this._data = null;
+ this._ids = {}; // ids of the items currently in memory (just contains a boolean true)
+ this._options = options || {};
+ this._fieldId = 'id'; // name of the field containing id
+ this._subscribers = {}; // event subscribers
+
+ var me = this;
+ this.listener = function () {
+ me._onEvent.apply(me, arguments);
+ };
+
+ this.setData(data);
+ }
+
+ // TODO: implement a function .config() to dynamically update things like configured filter
+ // and trigger changes accordingly
+
+ /**
+ * Set a data source for the view
+ * @param {DataSet | DataView} data
+ */
+ DataView.prototype.setData = function (data) {
+ var ids, i, len;
+
+ if (this._data) {
+ // unsubscribe from current dataset
+ if (this._data.unsubscribe) {
+ this._data.unsubscribe('*', this.listener);
+ }
+
+ // trigger a remove of all items in memory
+ ids = [];
+ for (var id in this._ids) {
+ if (this._ids.hasOwnProperty(id)) {
+ ids.push(id);
+ }
+ }
+ this._ids = {};
+ this._trigger('remove', {items: ids});
+ }
+
+ this._data = data;
+
+ if (this._data) {
+ // update fieldId
+ this._fieldId = this._options.fieldId ||
+ (this._data && this._data.options && this._data.options.fieldId) ||
+ 'id';
+
+ // trigger an add of all added items
+ ids = this._data.getIds({filter: this._options && this._options.filter});
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ this._ids[id] = true;
+ }
+ this._trigger('add', {items: ids});
+
+ // subscribe to new dataset
+ if (this._data.on) {
+ this._data.on('*', this.listener);
+ }
+ }
+ };
+
+ /**
+ * Get data from the data view
+ *
+ * Usage:
+ *
+ * get()
+ * get(options: Object)
+ * get(options: Object, data: Array | DataTable)
+ *
+ * get(id: Number)
+ * get(id: Number, options: Object)
+ * get(id: Number, options: Object, data: Array | DataTable)
+ *
+ * get(ids: Number[])
+ * get(ids: Number[], options: Object)
+ * get(ids: Number[], options: Object, data: Array | DataTable)
+ *
+ * Where:
+ *
+ * {Number | String} id The id of an item
+ * {Number[] | String{}} ids An array with ids of items
+ * {Object} options An Object with options. Available options:
+ * {String} [type] Type of data to be returned. Can
+ * be 'DataTable' or 'Array' (default)
+ * {Object.} [convert]
+ * {String[]} [fields] field names to be returned
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * {Array | DataTable} [data] If provided, items will be appended to this
+ * array or table. Required in case of Google
+ * DataTable.
+ * @param args
+ */
+ DataView.prototype.get = function (args) {
+ var me = this;
+
+ // parse the arguments
+ var ids, options, data;
+ var firstType = util.getType(arguments[0]);
+ if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
+ // get(id(s) [, options] [, data])
+ ids = arguments[0]; // can be a single id or an array with ids
+ options = arguments[1];
+ data = arguments[2];
+ }
+ else {
+ // get([, options] [, data])
+ options = arguments[0];
+ data = arguments[1];
+ }
+
+ // extend the options with the default options and provided options
+ var viewOptions = util.extend({}, this._options, options);
+
+ // create a combined filter method when needed
+ if (this._options.filter && options && options.filter) {
+ viewOptions.filter = function (item) {
+ return me._options.filter(item) && options.filter(item);
+ }
+ }
+
+ // build up the call to the linked data set
+ var getArguments = [];
+ if (ids != undefined) {
+ getArguments.push(ids);
+ }
+ getArguments.push(viewOptions);
+ getArguments.push(data);
+
+ return this._data && this._data.get.apply(this._data, getArguments);
+ };
+
+ /**
+ * Get ids of all items or from a filtered set of items.
+ * @param {Object} [options] An Object with options. Available options:
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * @return {Array} ids
+ */
+ DataView.prototype.getIds = function (options) {
+ var ids;
+
+ if (this._data) {
+ var defaultFilter = this._options.filter;
+ var filter;
+
+ if (options && options.filter) {
+ if (defaultFilter) {
+ filter = function (item) {
+ return defaultFilter(item) && options.filter(item);
+ }
+ }
+ else {
+ filter = options.filter;
+ }
+ }
+ else {
+ filter = defaultFilter;
+ }
+
+ ids = this._data.getIds({
+ filter: filter,
+ order: options && options.order
+ });
+ }
+ else {
+ ids = [];
+ }
+
+ return ids;
+ };
+
+ /**
+ * Get the DataSet to which this DataView is connected. In case there is a chain
+ * of multiple DataViews, the root DataSet of this chain is returned.
+ * @return {DataSet} dataSet
+ */
+ DataView.prototype.getDataSet = function () {
+ var dataSet = this;
+ while (dataSet instanceof DataView) {
+ dataSet = dataSet._data;
+ }
+ return dataSet || null;
+ };
+
+ /**
+ * Event listener. Will propagate all events from the connected data set to
+ * the subscribers of the DataView, but will filter the items and only trigger
+ * when there are changes in the filtered data set.
+ * @param {String} event
+ * @param {Object | null} params
+ * @param {String} senderId
+ * @private
+ */
+ DataView.prototype._onEvent = function (event, params, senderId) {
+ var i, len, id, item,
+ ids = params && params.items,
+ data = this._data,
+ added = [],
+ updated = [],
+ removed = [];
+
+ if (ids && data) {
+ switch (event) {
+ case 'add':
+ // filter the ids of the added items
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ item = this.get(id);
+ if (item) {
+ this._ids[id] = true;
+ added.push(id);
+ }
+ }
+
+ break;
+
+ case 'update':
+ // determine the event from the views viewpoint: an updated
+ // item can be added, updated, or removed from this view.
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ item = this.get(id);
+
+ if (item) {
+ if (this._ids[id]) {
+ updated.push(id);
+ }
+ else {
+ this._ids[id] = true;
+ added.push(id);
+ }
+ }
+ else {
+ if (this._ids[id]) {
+ delete this._ids[id];
+ removed.push(id);
+ }
+ else {
+ // nothing interesting for me :-(
+ }
+ }
+ }
+
+ break;
+
+ case 'remove':
+ // filter the ids of the removed items
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ if (this._ids[id]) {
+ delete this._ids[id];
+ removed.push(id);
+ }
+ }
+
+ break;
+ }
+
+ if (added.length) {
+ this._trigger('add', {items: added}, senderId);
+ }
+ if (updated.length) {
+ this._trigger('update', {items: updated}, senderId);
+ }
+ if (removed.length) {
+ this._trigger('remove', {items: removed}, senderId);
+ }
+ }
+ };
+
+ // copy subscription functionality from DataSet
+ DataView.prototype.on = DataSet.prototype.on;
+ DataView.prototype.off = DataSet.prototype.off;
+ DataView.prototype._trigger = DataSet.prototype._trigger;
+
+ // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
+ DataView.prototype.subscribe = DataView.prototype.on;
+ DataView.prototype.unsubscribe = DataView.prototype.off;
+
+ module.exports = DataView;
+
+/***/ },
+/* 10 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Emitter = __webpack_require__(11);
+ var DataSet = __webpack_require__(7);
+ var DataView = __webpack_require__(9);
+ var util = __webpack_require__(1);
+ var Point3d = __webpack_require__(12);
+ var Point2d = __webpack_require__(13);
+ var Camera = __webpack_require__(14);
+ var Filter = __webpack_require__(15);
+ var Slider = __webpack_require__(16);
+ var StepNumber = __webpack_require__(17);
+
+ /**
+ * @constructor Graph3d
+ * Graph3d displays data in 3d.
+ *
+ * Graph3d is developed in javascript as a Google Visualization Chart.
+ *
+ * @param {Element} container The DOM element in which the Graph3d will
+ * be created. Normally a div element.
+ * @param {DataSet | DataView | Array} [data]
+ * @param {Object} [options]
+ */
+ function Graph3d(container, data, options) {
+ if (!(this instanceof Graph3d)) {
+ throw new SyntaxError('Constructor must be called with the new operator');
+ }
+
+ // create variables and set default values
+ this.containerElement = container;
+ this.width = '400px';
+ this.height = '400px';
+ this.margin = 10; // px
+ this.defaultXCenter = '55%';
+ this.defaultYCenter = '50%';
+
+ this.xLabel = 'x';
+ this.yLabel = 'y';
+ this.zLabel = 'z';
+
+ var passValueFn = function(v) { return v; };
+ this.xValueLabel = passValueFn;
+ this.yValueLabel = passValueFn;
+ this.zValueLabel = passValueFn;
+
+ this.filterLabel = 'time';
+ this.legendLabel = 'value';
+
+ this.style = Graph3d.STYLE.DOT;
+ this.showPerspective = true;
+ this.showGrid = true;
+ this.keepAspectRatio = true;
+ this.showShadow = false;
+ this.showGrayBottom = false; // TODO: this does not work correctly
+ this.showTooltip = false;
+ this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube'
+
+ this.animationInterval = 1000; // milliseconds
+ this.animationPreload = false;
+
+ this.camera = new Camera();
+ this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window?
+
+ this.dataTable = null; // The original data table
+ this.dataPoints = null; // The table with point objects
+
+ // the column indexes
+ this.colX = undefined;
+ this.colY = undefined;
+ this.colZ = undefined;
+ this.colValue = undefined;
+ this.colFilter = undefined;
+
+ this.xMin = 0;
+ this.xStep = undefined; // auto by default
+ this.xMax = 1;
+ this.yMin = 0;
+ this.yStep = undefined; // auto by default
+ this.yMax = 1;
+ this.zMin = 0;
+ this.zStep = undefined; // auto by default
+ this.zMax = 1;
+ this.valueMin = 0;
+ this.valueMax = 1;
+ this.xBarWidth = 1;
+ this.yBarWidth = 1;
+ // TODO: customize axis range
+
+ // constants
+ this.colorAxis = '#4D4D4D';
+ this.colorGrid = '#D3D3D3';
+ this.colorDot = '#7DC1FF';
+ this.colorDotBorder = '#3267D2';
+
+ // create a frame and canvas
+ this.create();
+
+ // apply options (also when undefined)
+ this.setOptions(options);
+
+ // apply data
+ if (data) {
+ this.setData(data);
+ }
+ }
+
+ // Extend Graph3d with an Emitter mixin
+ Emitter(Graph3d.prototype);
+
+ /**
+ * Calculate the scaling values, dependent on the range in x, y, and z direction
+ */
+ Graph3d.prototype._setScale = function() {
+ this.scale = new Point3d(1 / (this.xMax - this.xMin),
+ 1 / (this.yMax - this.yMin),
+ 1 / (this.zMax - this.zMin));
+
+ // keep aspect ration between x and y scale if desired
+ if (this.keepAspectRatio) {
+ if (this.scale.x < this.scale.y) {
+ //noinspection JSSuspiciousNameCombination
+ this.scale.y = this.scale.x;
+ }
+ else {
+ //noinspection JSSuspiciousNameCombination
+ this.scale.x = this.scale.y;
+ }
+ }
+
+ // scale the vertical axis
+ this.scale.z *= this.verticalRatio;
+ // TODO: can this be automated? verticalRatio?
+
+ // determine scale for (optional) value
+ this.scale.value = 1 / (this.valueMax - this.valueMin);
+
+ // position the camera arm
+ var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x;
+ var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y;
+ var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z;
+ this.camera.setArmLocation(xCenter, yCenter, zCenter);
+ };
+
+
+ /**
+ * Convert a 3D location to a 2D location on screen
+ * http://en.wikipedia.org/wiki/3D_projection
+ * @param {Point3d} point3d A 3D point with parameters x, y, z
+ * @return {Point2d} point2d A 2D point with parameters x, y
+ */
+ Graph3d.prototype._convert3Dto2D = function(point3d) {
+ var translation = this._convertPointToTranslation(point3d);
+ return this._convertTranslationToScreen(translation);
+ };
+
+ /**
+ * Convert a 3D location its translation seen from the camera
+ * http://en.wikipedia.org/wiki/3D_projection
+ * @param {Point3d} point3d A 3D point with parameters x, y, z
+ * @return {Point3d} translation A 3D point with parameters x, y, z This is
+ * the translation of the point, seen from the
+ * camera
+ */
+ Graph3d.prototype._convertPointToTranslation = function(point3d) {
+ var ax = point3d.x * this.scale.x,
+ ay = point3d.y * this.scale.y,
+ az = point3d.z * this.scale.z,
+
+ cx = this.camera.getCameraLocation().x,
+ cy = this.camera.getCameraLocation().y,
+ cz = this.camera.getCameraLocation().z,
+
+ // calculate angles
+ sinTx = Math.sin(this.camera.getCameraRotation().x),
+ cosTx = Math.cos(this.camera.getCameraRotation().x),
+ sinTy = Math.sin(this.camera.getCameraRotation().y),
+ cosTy = Math.cos(this.camera.getCameraRotation().y),
+ sinTz = Math.sin(this.camera.getCameraRotation().z),
+ cosTz = Math.cos(this.camera.getCameraRotation().z),
+
+ // calculate translation
+ dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz),
+ dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax-cx)),
+ dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax-cx));
+
+ return new Point3d(dx, dy, dz);
+ };
+
+ /**
+ * Convert a translation point to a point on the screen
+ * @param {Point3d} translation A 3D point with parameters x, y, z This is
+ * the translation of the point, seen from the
+ * camera
+ * @return {Point2d} point2d A 2D point with parameters x, y
+ */
+ Graph3d.prototype._convertTranslationToScreen = function(translation) {
+ var ex = this.eye.x,
+ ey = this.eye.y,
+ ez = this.eye.z,
+ dx = translation.x,
+ dy = translation.y,
+ dz = translation.z;
+
+ // calculate position on screen from translation
+ var bx;
+ var by;
+ if (this.showPerspective) {
+ bx = (dx - ex) * (ez / dz);
+ by = (dy - ey) * (ez / dz);
+ }
+ else {
+ bx = dx * -(ez / this.camera.getArmLength());
+ by = dy * -(ez / this.camera.getArmLength());
+ }
+
+ // shift and scale the point to the center of the screen
+ // use the width of the graph to scale both horizontally and vertically.
+ return new Point2d(
+ this.xcenter + bx * this.frame.canvas.clientWidth,
+ this.ycenter - by * this.frame.canvas.clientWidth);
+ };
+
+ /**
+ * Set the background styling for the graph
+ * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor
+ */
+ Graph3d.prototype._setBackgroundColor = function(backgroundColor) {
+ var fill = 'white';
+ var stroke = 'gray';
+ var strokeWidth = 1;
+
+ if (typeof(backgroundColor) === 'string') {
+ fill = backgroundColor;
+ stroke = 'none';
+ strokeWidth = 0;
+ }
+ else if (typeof(backgroundColor) === 'object') {
+ if (backgroundColor.fill !== undefined) fill = backgroundColor.fill;
+ if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke;
+ if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth;
+ }
+ else if (backgroundColor === undefined) {
+ // use use defaults
+ }
+ else {
+ throw 'Unsupported type of backgroundColor';
+ }
+
+ this.frame.style.backgroundColor = fill;
+ this.frame.style.borderColor = stroke;
+ this.frame.style.borderWidth = strokeWidth + 'px';
+ this.frame.style.borderStyle = 'solid';
+ };
+
+
+ /// enumerate the available styles
+ Graph3d.STYLE = {
+ BAR: 0,
+ BARCOLOR: 1,
+ BARSIZE: 2,
+ DOT : 3,
+ DOTLINE : 4,
+ DOTCOLOR: 5,
+ DOTSIZE: 6,
+ GRID : 7,
+ LINE: 8,
+ SURFACE : 9
+ };
+
+ /**
+ * Retrieve the style index from given styleName
+ * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line'
+ * @return {Number} styleNumber Enumeration value representing the style, or -1
+ * when not found
+ */
+ Graph3d.prototype._getStyleNumber = function(styleName) {
+ switch (styleName) {
+ case 'dot': return Graph3d.STYLE.DOT;
+ case 'dot-line': return Graph3d.STYLE.DOTLINE;
+ case 'dot-color': return Graph3d.STYLE.DOTCOLOR;
+ case 'dot-size': return Graph3d.STYLE.DOTSIZE;
+ case 'line': return Graph3d.STYLE.LINE;
+ case 'grid': return Graph3d.STYLE.GRID;
+ case 'surface': return Graph3d.STYLE.SURFACE;
+ case 'bar': return Graph3d.STYLE.BAR;
+ case 'bar-color': return Graph3d.STYLE.BARCOLOR;
+ case 'bar-size': return Graph3d.STYLE.BARSIZE;
+ }
+
+ return -1;
+ };
+
+ /**
+ * Determine the indexes of the data columns, based on the given style and data
+ * @param {DataSet} data
+ * @param {Number} style
+ */
+ Graph3d.prototype._determineColumnIndexes = function(data, style) {
+ if (this.style === Graph3d.STYLE.DOT ||
+ this.style === Graph3d.STYLE.DOTLINE ||
+ this.style === Graph3d.STYLE.LINE ||
+ this.style === Graph3d.STYLE.GRID ||
+ this.style === Graph3d.STYLE.SURFACE ||
+ this.style === Graph3d.STYLE.BAR) {
+ // 3 columns expected, and optionally a 4th with filter values
+ this.colX = 0;
+ this.colY = 1;
+ this.colZ = 2;
+ this.colValue = undefined;
+
+ if (data.getNumberOfColumns() > 3) {
+ this.colFilter = 3;
+ }
+ }
+ else if (this.style === Graph3d.STYLE.DOTCOLOR ||
+ this.style === Graph3d.STYLE.DOTSIZE ||
+ this.style === Graph3d.STYLE.BARCOLOR ||
+ this.style === Graph3d.STYLE.BARSIZE) {
+ // 4 columns expected, and optionally a 5th with filter values
+ this.colX = 0;
+ this.colY = 1;
+ this.colZ = 2;
+ this.colValue = 3;
+
+ if (data.getNumberOfColumns() > 4) {
+ this.colFilter = 4;
+ }
+ }
+ else {
+ throw 'Unknown style "' + this.style + '"';
+ }
+ };
+
+ Graph3d.prototype.getNumberOfRows = function(data) {
+ return data.length;
+ }
+
+
+ Graph3d.prototype.getNumberOfColumns = function(data) {
+ var counter = 0;
+ for (var column in data[0]) {
+ if (data[0].hasOwnProperty(column)) {
+ counter++;
+ }
+ }
+ return counter;
+ }
+
+
+ Graph3d.prototype.getDistinctValues = function(data, column) {
+ var distinctValues = [];
+ for (var i = 0; i < data.length; i++) {
+ if (distinctValues.indexOf(data[i][column]) == -1) {
+ distinctValues.push(data[i][column]);
+ }
+ }
+ return distinctValues;
+ }
+
+
+ Graph3d.prototype.getColumnRange = function(data,column) {
+ var minMax = {min:data[0][column],max:data[0][column]};
+ for (var i = 0; i < data.length; i++) {
+ if (minMax.min > data[i][column]) { minMax.min = data[i][column]; }
+ if (minMax.max < data[i][column]) { minMax.max = data[i][column]; }
+ }
+ return minMax;
+ };
+
+ /**
+ * Initialize the data from the data table. Calculate minimum and maximum values
+ * and column index values
+ * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph.
+ * @param {Number} style Style Number
+ */
+ Graph3d.prototype._dataInitialize = function (rawData, style) {
+ var me = this;
+
+ // unsubscribe from the dataTable
+ if (this.dataSet) {
+ this.dataSet.off('*', this._onChange);
+ }
+
+ if (rawData === undefined)
+ return;
+
+ if (Array.isArray(rawData)) {
+ rawData = new DataSet(rawData);
+ }
+
+ var data;
+ if (rawData instanceof DataSet || rawData instanceof DataView) {
+ data = rawData.get();
+ }
+ else {
+ throw new Error('Array, DataSet, or DataView expected');
+ }
+
+ if (data.length == 0)
+ return;
+
+ this.dataSet = rawData;
+ this.dataTable = data;
+
+ // subscribe to changes in the dataset
+ this._onChange = function () {
+ me.setData(me.dataSet);
+ };
+ this.dataSet.on('*', this._onChange);
+
+ // _determineColumnIndexes
+ // getNumberOfRows (points)
+ // getNumberOfColumns (x,y,z,v,t,t1,t2...)
+ // getDistinctValues (unique values?)
+ // getColumnRange
+
+ // determine the location of x,y,z,value,filter columns
+ this.colX = 'x';
+ this.colY = 'y';
+ this.colZ = 'z';
+ this.colValue = 'style';
+ this.colFilter = 'filter';
+
+
+
+ // check if a filter column is provided
+ if (data[0].hasOwnProperty('filter')) {
+ if (this.dataFilter === undefined) {
+ this.dataFilter = new Filter(rawData, this.colFilter, this);
+ this.dataFilter.setOnLoadCallback(function() {me.redraw();});
+ }
+ }
+
+
+ var withBars = this.style == Graph3d.STYLE.BAR ||
+ this.style == Graph3d.STYLE.BARCOLOR ||
+ this.style == Graph3d.STYLE.BARSIZE;
+
+ // determine barWidth from data
+ if (withBars) {
+ if (this.defaultXBarWidth !== undefined) {
+ this.xBarWidth = this.defaultXBarWidth;
+ }
+ else {
+ var dataX = this.getDistinctValues(data,this.colX);
+ this.xBarWidth = (dataX[1] - dataX[0]) || 1;
+ }
+
+ if (this.defaultYBarWidth !== undefined) {
+ this.yBarWidth = this.defaultYBarWidth;
+ }
+ else {
+ var dataY = this.getDistinctValues(data,this.colY);
+ this.yBarWidth = (dataY[1] - dataY[0]) || 1;
+ }
+ }
+
+ // calculate minimums and maximums
+ var xRange = this.getColumnRange(data,this.colX);
+ if (withBars) {
+ xRange.min -= this.xBarWidth / 2;
+ xRange.max += this.xBarWidth / 2;
+ }
+ this.xMin = (this.defaultXMin !== undefined) ? this.defaultXMin : xRange.min;
+ this.xMax = (this.defaultXMax !== undefined) ? this.defaultXMax : xRange.max;
+ if (this.xMax <= this.xMin) this.xMax = this.xMin + 1;
+ this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : (this.xMax-this.xMin)/5;
+
+ var yRange = this.getColumnRange(data,this.colY);
+ if (withBars) {
+ yRange.min -= this.yBarWidth / 2;
+ yRange.max += this.yBarWidth / 2;
+ }
+ this.yMin = (this.defaultYMin !== undefined) ? this.defaultYMin : yRange.min;
+ this.yMax = (this.defaultYMax !== undefined) ? this.defaultYMax : yRange.max;
+ if (this.yMax <= this.yMin) this.yMax = this.yMin + 1;
+ this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : (this.yMax-this.yMin)/5;
+
+ var zRange = this.getColumnRange(data,this.colZ);
+ this.zMin = (this.defaultZMin !== undefined) ? this.defaultZMin : zRange.min;
+ this.zMax = (this.defaultZMax !== undefined) ? this.defaultZMax : zRange.max;
+ if (this.zMax <= this.zMin) this.zMax = this.zMin + 1;
+ this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5;
+
+ if (this.colValue !== undefined) {
+ var valueRange = this.getColumnRange(data,this.colValue);
+ this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min;
+ this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max;
+ if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1;
+ }
+
+ // set the scale dependent on the ranges.
+ this._setScale();
+ };
+
+
+
+ /**
+ * Filter the data based on the current filter
+ * @param {Array} data
+ * @return {Array} dataPoints Array with point objects which can be drawn on screen
+ */
+ Graph3d.prototype._getDataPoints = function (data) {
+ // TODO: store the created matrix dataPoints in the filters instead of reloading each time
+ var x, y, i, z, obj, point;
+
+ var dataPoints = [];
+
+ if (this.style === Graph3d.STYLE.GRID ||
+ this.style === Graph3d.STYLE.SURFACE) {
+ // copy all values from the google data table to a matrix
+ // the provided values are supposed to form a grid of (x,y) positions
+
+ // create two lists with all present x and y values
+ var dataX = [];
+ var dataY = [];
+ for (i = 0; i < this.getNumberOfRows(data); i++) {
+ x = data[i][this.colX] || 0;
+ y = data[i][this.colY] || 0;
+
+ if (dataX.indexOf(x) === -1) {
+ dataX.push(x);
+ }
+ if (dataY.indexOf(y) === -1) {
+ dataY.push(y);
+ }
+ }
+
+ var sortNumber = function (a, b) {
+ return a - b;
+ };
+ dataX.sort(sortNumber);
+ dataY.sort(sortNumber);
+
+ // create a grid, a 2d matrix, with all values.
+ var dataMatrix = []; // temporary data matrix
+ for (i = 0; i < data.length; i++) {
+ x = data[i][this.colX] || 0;
+ y = data[i][this.colY] || 0;
+ z = data[i][this.colZ] || 0;
+
+ var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer
+ var yIndex = dataY.indexOf(y);
+
+ if (dataMatrix[xIndex] === undefined) {
+ dataMatrix[xIndex] = [];
+ }
+
+ var point3d = new Point3d();
+ point3d.x = x;
+ point3d.y = y;
+ point3d.z = z;
+
+ obj = {};
+ obj.point = point3d;
+ obj.trans = undefined;
+ obj.screen = undefined;
+ obj.bottom = new Point3d(x, y, this.zMin);
+
+ dataMatrix[xIndex][yIndex] = obj;
+
+ dataPoints.push(obj);
+ }
+
+ // fill in the pointers to the neighbors.
+ for (x = 0; x < dataMatrix.length; x++) {
+ for (y = 0; y < dataMatrix[x].length; y++) {
+ if (dataMatrix[x][y]) {
+ dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined;
+ dataMatrix[x][y].pointTop = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined;
+ dataMatrix[x][y].pointCross =
+ (x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ?
+ dataMatrix[x+1][y+1] :
+ undefined;
+ }
+ }
+ }
+ }
+ else { // 'dot', 'dot-line', etc.
+ // copy all values from the google data table to a list with Point3d objects
+ for (i = 0; i < data.length; i++) {
+ point = new Point3d();
+ point.x = data[i][this.colX] || 0;
+ point.y = data[i][this.colY] || 0;
+ point.z = data[i][this.colZ] || 0;
+
+ if (this.colValue !== undefined) {
+ point.value = data[i][this.colValue] || 0;
+ }
+
+ obj = {};
+ obj.point = point;
+ obj.bottom = new Point3d(point.x, point.y, this.zMin);
+ obj.trans = undefined;
+ obj.screen = undefined;
+
+ dataPoints.push(obj);
+ }
+ }
+
+ return dataPoints;
+ };
+
+ /**
+ * Create the main frame for the Graph3d.
+ * This function is executed once when a Graph3d object is created. The frame
+ * contains a canvas, and this canvas contains all objects like the axis and
+ * nodes.
+ */
+ Graph3d.prototype.create = function () {
+ // remove all elements from the container element.
+ while (this.containerElement.hasChildNodes()) {
+ this.containerElement.removeChild(this.containerElement.firstChild);
+ }
+
+ this.frame = document.createElement('div');
+ this.frame.style.position = 'relative';
+ this.frame.style.overflow = 'hidden';
+
+ // create the graph canvas (HTML canvas element)
+ this.frame.canvas = document.createElement( 'canvas' );
+ this.frame.canvas.style.position = 'relative';
+ this.frame.appendChild(this.frame.canvas);
+ //if (!this.frame.canvas.getContext) {
+ {
+ var noCanvas = document.createElement( 'DIV' );
+ noCanvas.style.color = 'red';
+ noCanvas.style.fontWeight = 'bold' ;
+ noCanvas.style.padding = '10px';
+ noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
+ this.frame.canvas.appendChild(noCanvas);
+ }
+
+ this.frame.filter = document.createElement( 'div' );
+ this.frame.filter.style.position = 'absolute';
+ this.frame.filter.style.bottom = '0px';
+ this.frame.filter.style.left = '0px';
+ this.frame.filter.style.width = '100%';
+ this.frame.appendChild(this.frame.filter);
+
+ // add event listeners to handle moving and zooming the contents
+ var me = this;
+ var onmousedown = function (event) {me._onMouseDown(event);};
+ var ontouchstart = function (event) {me._onTouchStart(event);};
+ var onmousewheel = function (event) {me._onWheel(event);};
+ var ontooltip = function (event) {me._onTooltip(event);};
+ // TODO: these events are never cleaned up... can give a 'memory leakage'
+
+ util.addEventListener(this.frame.canvas, 'keydown', onkeydown);
+ util.addEventListener(this.frame.canvas, 'mousedown', onmousedown);
+ util.addEventListener(this.frame.canvas, 'touchstart', ontouchstart);
+ util.addEventListener(this.frame.canvas, 'mousewheel', onmousewheel);
+ util.addEventListener(this.frame.canvas, 'mousemove', ontooltip);
+
+ // add the new graph to the container element
+ this.containerElement.appendChild(this.frame);
+ };
+
+
+ /**
+ * Set a new size for the graph
+ * @param {string} width Width in pixels or percentage (for example '800px'
+ * or '50%')
+ * @param {string} height Height in pixels or percentage (for example '400px'
+ * or '30%')
+ */
+ Graph3d.prototype.setSize = function(width, height) {
+ this.frame.style.width = width;
+ this.frame.style.height = height;
+
+ this._resizeCanvas();
+ };
+
+ /**
+ * Resize the canvas to the current size of the frame
+ */
+ Graph3d.prototype._resizeCanvas = function() {
+ this.frame.canvas.style.width = '100%';
+ this.frame.canvas.style.height = '100%';
+
+ this.frame.canvas.width = this.frame.canvas.clientWidth;
+ this.frame.canvas.height = this.frame.canvas.clientHeight;
+
+ // adjust with for margin
+ this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + 'px';
+ };
+
+ /**
+ * Start animation
+ */
+ Graph3d.prototype.animationStart = function() {
+ if (!this.frame.filter || !this.frame.filter.slider)
+ throw 'No animation available';
+
+ this.frame.filter.slider.play();
+ };
+
+
+ /**
+ * Stop animation
+ */
+ Graph3d.prototype.animationStop = function() {
+ if (!this.frame.filter || !this.frame.filter.slider) return;
+
+ this.frame.filter.slider.stop();
+ };
+
+
+ /**
+ * Resize the center position based on the current values in this.defaultXCenter
+ * and this.defaultYCenter (which are strings with a percentage or a value
+ * in pixels). The center positions are the variables this.xCenter
+ * and this.yCenter
+ */
+ Graph3d.prototype._resizeCenter = function() {
+ // calculate the horizontal center position
+ if (this.defaultXCenter.charAt(this.defaultXCenter.length-1) === '%') {
+ this.xcenter =
+ parseFloat(this.defaultXCenter) / 100 *
+ this.frame.canvas.clientWidth;
+ }
+ else {
+ this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px
+ }
+
+ // calculate the vertical center position
+ if (this.defaultYCenter.charAt(this.defaultYCenter.length-1) === '%') {
+ this.ycenter =
+ parseFloat(this.defaultYCenter) / 100 *
+ (this.frame.canvas.clientHeight - this.frame.filter.clientHeight);
+ }
+ else {
+ this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px
+ }
+ };
+
+ /**
+ * Set the rotation and distance of the camera
+ * @param {Object} pos An object with the camera position. The object
+ * contains three parameters:
+ * - horizontal {Number}
+ * The horizontal rotation, between 0 and 2*PI.
+ * Optional, can be left undefined.
+ * - vertical {Number}
+ * The vertical rotation, between 0 and 0.5*PI
+ * if vertical=0.5*PI, the graph is shown from the
+ * top. Optional, can be left undefined.
+ * - distance {Number}
+ * The (normalized) distance of the camera to the
+ * center of the graph, a value between 0.71 and 5.0.
+ * Optional, can be left undefined.
+ */
+ Graph3d.prototype.setCameraPosition = function(pos) {
+ if (pos === undefined) {
+ return;
+ }
+
+ if (pos.horizontal !== undefined && pos.vertical !== undefined) {
+ this.camera.setArmRotation(pos.horizontal, pos.vertical);
+ }
+
+ if (pos.distance !== undefined) {
+ this.camera.setArmLength(pos.distance);
+ }
+
+ this.redraw();
+ };
+
+
+ /**
+ * Retrieve the current camera rotation
+ * @return {object} An object with parameters horizontal, vertical, and
+ * distance
+ */
+ Graph3d.prototype.getCameraPosition = function() {
+ var pos = this.camera.getArmRotation();
+ pos.distance = this.camera.getArmLength();
+ return pos;
+ };
+
+ /**
+ * Load data into the 3D Graph
+ */
+ Graph3d.prototype._readData = function(data) {
+ // read the data
+ this._dataInitialize(data, this.style);
+
+
+ if (this.dataFilter) {
+ // apply filtering
+ this.dataPoints = this.dataFilter._getDataPoints();
+ }
+ else {
+ // no filtering. load all data
+ this.dataPoints = this._getDataPoints(this.dataTable);
+ }
+
+ // draw the filter
+ this._redrawFilter();
+ };
+
+ /**
+ * Replace the dataset of the Graph3d
+ * @param {Array | DataSet | DataView} data
+ */
+ Graph3d.prototype.setData = function (data) {
+ this._readData(data);
+ this.redraw();
+
+ // start animation when option is true
+ if (this.animationAutoStart && this.dataFilter) {
+ this.animationStart();
+ }
+ };
+
+ /**
+ * Update the options. Options will be merged with current options
+ * @param {Object} options
+ */
+ Graph3d.prototype.setOptions = function (options) {
+ var cameraPosition = undefined;
+
+ this.animationStop();
+
+ if (options !== undefined) {
+ // retrieve parameter values
+ if (options.width !== undefined) this.width = options.width;
+ if (options.height !== undefined) this.height = options.height;
+
+ if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter;
+ if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter;
+
+ if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel;
+ if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel;
+ if (options.xLabel !== undefined) this.xLabel = options.xLabel;
+ if (options.yLabel !== undefined) this.yLabel = options.yLabel;
+ if (options.zLabel !== undefined) this.zLabel = options.zLabel;
+
+ if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel;
+ if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel;
+ if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel;
+
+ if (options.style !== undefined) {
+ var styleNumber = this._getStyleNumber(options.style);
+ if (styleNumber !== -1) {
+ this.style = styleNumber;
+ }
+ }
+ if (options.showGrid !== undefined) this.showGrid = options.showGrid;
+ if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective;
+ if (options.showShadow !== undefined) this.showShadow = options.showShadow;
+ if (options.tooltip !== undefined) this.showTooltip = options.tooltip;
+ if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls;
+ if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio;
+ if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio;
+
+ if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval;
+ if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload;
+ if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart;
+
+ if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth;
+ if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth;
+
+ if (options.xMin !== undefined) this.defaultXMin = options.xMin;
+ if (options.xStep !== undefined) this.defaultXStep = options.xStep;
+ if (options.xMax !== undefined) this.defaultXMax = options.xMax;
+ if (options.yMin !== undefined) this.defaultYMin = options.yMin;
+ if (options.yStep !== undefined) this.defaultYStep = options.yStep;
+ if (options.yMax !== undefined) this.defaultYMax = options.yMax;
+ if (options.zMin !== undefined) this.defaultZMin = options.zMin;
+ if (options.zStep !== undefined) this.defaultZStep = options.zStep;
+ if (options.zMax !== undefined) this.defaultZMax = options.zMax;
+ if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin;
+ if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax;
+
+ if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition;
+
+ if (cameraPosition !== undefined) {
+ this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical);
+ this.camera.setArmLength(cameraPosition.distance);
+ }
+ else {
+ this.camera.setArmRotation(1.0, 0.5);
+ this.camera.setArmLength(1.7);
+ }
+ }
+
+ this._setBackgroundColor(options && options.backgroundColor);
+
+ this.setSize(this.width, this.height);
+
+ // re-load the data
+ if (this.dataTable) {
+ this.setData(this.dataTable);
+ }
+
+ // start animation when option is true
+ if (this.animationAutoStart && this.dataFilter) {
+ this.animationStart();
+ }
+ };
+
+ /**
+ * Redraw the Graph.
+ */
+ Graph3d.prototype.redraw = function() {
+ if (this.dataPoints === undefined) {
+ throw 'Error: graph data not initialized';
+ }
+
+ this._resizeCanvas();
+ this._resizeCenter();
+ this._redrawSlider();
+ this._redrawClear();
+ this._redrawAxis();
+
+ if (this.style === Graph3d.STYLE.GRID ||
+ this.style === Graph3d.STYLE.SURFACE) {
+ this._redrawDataGrid();
+ }
+ else if (this.style === Graph3d.STYLE.LINE) {
+ this._redrawDataLine();
+ }
+ else if (this.style === Graph3d.STYLE.BAR ||
+ this.style === Graph3d.STYLE.BARCOLOR ||
+ this.style === Graph3d.STYLE.BARSIZE) {
+ this._redrawDataBar();
+ }
+ else {
+ // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE
+ this._redrawDataDot();
+ }
+
+ this._redrawInfo();
+ this._redrawLegend();
+ };
+
+ /**
+ * Clear the canvas before redrawing
+ */
+ Graph3d.prototype._redrawClear = function() {
+ var canvas = this.frame.canvas;
+ var ctx = canvas.getContext('2d');
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ };
+
+
+ /**
+ * Redraw the legend showing the colors
+ */
+ Graph3d.prototype._redrawLegend = function() {
+ var y;
+
+ if (this.style === Graph3d.STYLE.DOTCOLOR ||
+ this.style === Graph3d.STYLE.DOTSIZE) {
+
+ var dotSize = this.frame.clientWidth * 0.02;
+
+ var widthMin, widthMax;
+ if (this.style === Graph3d.STYLE.DOTSIZE) {
+ widthMin = dotSize / 2; // px
+ widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function
+ }
+ else {
+ widthMin = 20; // px
+ widthMax = 20; // px
+ }
+
+ var height = Math.max(this.frame.clientHeight * 0.25, 100);
+ var top = this.margin;
+ var right = this.frame.clientWidth - this.margin;
+ var left = right - widthMax;
+ var bottom = top + height;
+ }
+
+ var canvas = this.frame.canvas;
+ var ctx = canvas.getContext('2d');
+ ctx.lineWidth = 1;
+ ctx.font = '14px arial'; // TODO: put in options
+
+ if (this.style === Graph3d.STYLE.DOTCOLOR) {
+ // draw the color bar
+ var ymin = 0;
+ var ymax = height; // Todo: make height customizable
+ for (y = ymin; y < ymax; y++) {
+ var f = (y - ymin) / (ymax - ymin);
+
+ //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function
+ var hue = f * 240;
+ var color = this._hsv2rgb(hue, 1, 1);
+
+ ctx.strokeStyle = color;
+ ctx.beginPath();
+ ctx.moveTo(left, top + y);
+ ctx.lineTo(right, top + y);
+ ctx.stroke();
+ }
+
+ ctx.strokeStyle = this.colorAxis;
+ ctx.strokeRect(left, top, widthMax, height);
+ }
+
+ if (this.style === Graph3d.STYLE.DOTSIZE) {
+ // draw border around color bar
+ ctx.strokeStyle = this.colorAxis;
+ ctx.fillStyle = this.colorDot;
+ ctx.beginPath();
+ ctx.moveTo(left, top);
+ ctx.lineTo(right, top);
+ ctx.lineTo(right - widthMax + widthMin, bottom);
+ ctx.lineTo(left, bottom);
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+ }
+
+ if (this.style === Graph3d.STYLE.DOTCOLOR ||
+ this.style === Graph3d.STYLE.DOTSIZE) {
+ // print values along the color bar
+ var gridLineLen = 5; // px
+ var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax-this.valueMin)/5, true);
+ step.start();
+ if (step.getCurrent() < this.valueMin) {
+ step.next();
+ }
+ while (!step.end()) {
+ y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height;
+
+ ctx.beginPath();
+ ctx.moveTo(left - gridLineLen, y);
+ ctx.lineTo(left, y);
+ ctx.stroke();
+
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = this.colorAxis;
+ ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y);
+
+ step.next();
+ }
+
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'top';
+ var label = this.legendLabel;
+ ctx.fillText(label, right, bottom + this.margin);
+ }
+ };
+
+ /**
+ * Redraw the filter
+ */
+ Graph3d.prototype._redrawFilter = function() {
+ this.frame.filter.innerHTML = '';
+
+ if (this.dataFilter) {
+ var options = {
+ 'visible': this.showAnimationControls
+ };
+ var slider = new Slider(this.frame.filter, options);
+ this.frame.filter.slider = slider;
+
+ // TODO: css here is not nice here...
+ this.frame.filter.style.padding = '10px';
+ //this.frame.filter.style.backgroundColor = '#EFEFEF';
+
+ slider.setValues(this.dataFilter.values);
+ slider.setPlayInterval(this.animationInterval);
+
+ // create an event handler
+ var me = this;
+ var onchange = function () {
+ var index = slider.getIndex();
+
+ me.dataFilter.selectValue(index);
+ me.dataPoints = me.dataFilter._getDataPoints();
+
+ me.redraw();
+ };
+ slider.setOnChangeCallback(onchange);
+ }
+ else {
+ this.frame.filter.slider = undefined;
+ }
+ };
+
+ /**
+ * Redraw the slider
+ */
+ Graph3d.prototype._redrawSlider = function() {
+ if ( this.frame.filter.slider !== undefined) {
+ this.frame.filter.slider.redraw();
+ }
+ };
+
+
+ /**
+ * Redraw common information
+ */
+ Graph3d.prototype._redrawInfo = function() {
+ if (this.dataFilter) {
+ var canvas = this.frame.canvas;
+ var ctx = canvas.getContext('2d');
+
+ ctx.font = '14px arial'; // TODO: put in options
+ ctx.lineStyle = 'gray';
+ ctx.fillStyle = 'gray';
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'top';
+
+ var x = this.margin;
+ var y = this.margin;
+ ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y);
+ }
+ };
+
+
+ /**
+ * Redraw the axis
+ */
+ Graph3d.prototype._redrawAxis = function() {
+ var canvas = this.frame.canvas,
+ ctx = canvas.getContext('2d'),
+ from, to, step, prettyStep,
+ text, xText, yText, zText,
+ offset, xOffset, yOffset,
+ xMin2d, xMax2d;
+
+ // TODO: get the actual rendered style of the containerElement
+ //ctx.font = this.containerElement.style.font;
+ ctx.font = 24 / this.camera.getArmLength() + 'px arial';
+
+ // calculate the length for the short grid lines
+ var gridLenX = 0.025 / this.scale.x;
+ var gridLenY = 0.025 / this.scale.y;
+ var textMargin = 5 / this.camera.getArmLength(); // px
+ var armAngle = this.camera.getArmRotation().horizontal;
+
+ // draw x-grid lines
+ ctx.lineWidth = 1;
+ prettyStep = (this.defaultXStep === undefined);
+ step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep);
+ step.start();
+ if (step.getCurrent() < this.xMin) {
+ step.next();
+ }
+ while (!step.end()) {
+ var x = step.getCurrent();
+
+ if (this.showGrid) {
+ from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
+ to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
+ ctx.strokeStyle = this.colorGrid;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ }
+ else {
+ from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
+ to = this._convert3Dto2D(new Point3d(x, this.yMin+gridLenX, this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+
+ from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
+ to = this._convert3Dto2D(new Point3d(x, this.yMax-gridLenX, this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ }
+
+ yText = (Math.cos(armAngle) > 0) ? this.yMin : this.yMax;
+ text = this._convert3Dto2D(new Point3d(x, yText, this.zMin));
+ if (Math.cos(armAngle * 2) > 0) {
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ text.y += textMargin;
+ }
+ else if (Math.sin(armAngle * 2) < 0){
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ }
+ else {
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'middle';
+ }
+ ctx.fillStyle = this.colorAxis;
+ ctx.fillText(' ' + this.xValueLabel(step.getCurrent()) + ' ', text.x, text.y);
+
+ step.next();
+ }
+
+ // draw y-grid lines
+ ctx.lineWidth = 1;
+ prettyStep = (this.defaultYStep === undefined);
+ step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep);
+ step.start();
+ if (step.getCurrent() < this.yMin) {
+ step.next();
+ }
+ while (!step.end()) {
+ if (this.showGrid) {
+ from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
+ to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
+ ctx.strokeStyle = this.colorGrid;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ }
+ else {
+ from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
+ to = this._convert3Dto2D(new Point3d(this.xMin+gridLenY, step.getCurrent(), this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+
+ from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
+ to = this._convert3Dto2D(new Point3d(this.xMax-gridLenY, step.getCurrent(), this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ }
+
+ xText = (Math.sin(armAngle ) > 0) ? this.xMin : this.xMax;
+ text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin));
+ if (Math.cos(armAngle * 2) < 0) {
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ text.y += textMargin;
+ }
+ else if (Math.sin(armAngle * 2) > 0){
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ }
+ else {
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'middle';
+ }
+ ctx.fillStyle = this.colorAxis;
+ ctx.fillText(' ' + this.yValueLabel(step.getCurrent()) + ' ', text.x, text.y);
+
+ step.next();
+ }
+
+ // draw z-grid lines and axis
+ ctx.lineWidth = 1;
+ prettyStep = (this.defaultZStep === undefined);
+ step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep);
+ step.start();
+ if (step.getCurrent() < this.zMin) {
+ step.next();
+ }
+ xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
+ yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
+ while (!step.end()) {
+ // TODO: make z-grid lines really 3d?
+ from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent()));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(from.x - textMargin, from.y);
+ ctx.stroke();
+
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = this.colorAxis;
+ ctx.fillText(this.zValueLabel(step.getCurrent()) + ' ', from.x - 5, from.y);
+
+ step.next();
+ }
+ ctx.lineWidth = 1;
+ from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
+ to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+
+ // draw x-axis
+ ctx.lineWidth = 1;
+ // line at yMin
+ xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
+ xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(xMin2d.x, xMin2d.y);
+ ctx.lineTo(xMax2d.x, xMax2d.y);
+ ctx.stroke();
+ // line at ymax
+ xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
+ xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(xMin2d.x, xMin2d.y);
+ ctx.lineTo(xMax2d.x, xMax2d.y);
+ ctx.stroke();
+
+ // draw y-axis
+ ctx.lineWidth = 1;
+ // line at xMin
+ from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
+ to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ // line at xMax
+ from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
+ to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+
+ // draw x-label
+ var xLabel = this.xLabel;
+ if (xLabel.length > 0) {
+ yOffset = 0.1 / this.scale.y;
+ xText = (this.xMin + this.xMax) / 2;
+ yText = (Math.cos(armAngle) > 0) ? this.yMin - yOffset: this.yMax + yOffset;
+ text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
+ if (Math.cos(armAngle * 2) > 0) {
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ }
+ else if (Math.sin(armAngle * 2) < 0){
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ }
+ else {
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'middle';
+ }
+ ctx.fillStyle = this.colorAxis;
+ ctx.fillText(xLabel, text.x, text.y);
+ }
+
+ // draw y-label
+ var yLabel = this.yLabel;
+ if (yLabel.length > 0) {
+ xOffset = 0.1 / this.scale.x;
+ xText = (Math.sin(armAngle ) > 0) ? this.xMin - xOffset : this.xMax + xOffset;
+ yText = (this.yMin + this.yMax) / 2;
+ text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
+ if (Math.cos(armAngle * 2) < 0) {
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ }
+ else if (Math.sin(armAngle * 2) > 0){
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ }
+ else {
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'middle';
+ }
+ ctx.fillStyle = this.colorAxis;
+ ctx.fillText(yLabel, text.x, text.y);
+ }
+
+ // draw z-label
+ var zLabel = this.zLabel;
+ if (zLabel.length > 0) {
+ offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis?
+ xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
+ yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
+ zText = (this.zMin + this.zMax) / 2;
+ text = this._convert3Dto2D(new Point3d(xText, yText, zText));
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = this.colorAxis;
+ ctx.fillText(zLabel, text.x - offset, text.y);
+ }
+ };
+
+ /**
+ * Calculate the color based on the given value.
+ * @param {Number} H Hue, a value be between 0 and 360
+ * @param {Number} S Saturation, a value between 0 and 1
+ * @param {Number} V Value, a value between 0 and 1
+ */
+ Graph3d.prototype._hsv2rgb = function(H, S, V) {
+ var R, G, B, C, Hi, X;
+
+ C = V * S;
+ Hi = Math.floor(H/60); // hi = 0,1,2,3,4,5
+ X = C * (1 - Math.abs(((H/60) % 2) - 1));
+
+ switch (Hi) {
+ case 0: R = C; G = X; B = 0; break;
+ case 1: R = X; G = C; B = 0; break;
+ case 2: R = 0; G = C; B = X; break;
+ case 3: R = 0; G = X; B = C; break;
+ case 4: R = X; G = 0; B = C; break;
+ case 5: R = C; G = 0; B = X; break;
+
+ default: R = 0; G = 0; B = 0; break;
+ }
+
+ return 'RGB(' + parseInt(R*255) + ',' + parseInt(G*255) + ',' + parseInt(B*255) + ')';
+ };
+
+
+ /**
+ * Draw all datapoints as a grid
+ * This function can be used when the style is 'grid'
+ */
+ Graph3d.prototype._redrawDataGrid = function() {
+ var canvas = this.frame.canvas,
+ ctx = canvas.getContext('2d'),
+ point, right, top, cross,
+ i,
+ topSideVisible, fillStyle, strokeStyle, lineWidth,
+ h, s, v, zAvg;
+
+
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0)
+ return; // TODO: throw exception?
+
+ // calculate the translations and screen position of all points
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+ var screen = this._convertTranslationToScreen(trans);
+
+ this.dataPoints[i].trans = trans;
+ this.dataPoints[i].screen = screen;
+
+ // calculate the translation of the point at the bottom (needed for sorting)
+ var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
+ this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
+ }
+
+ // sort the points on depth of their (x,y) position (not on z)
+ var sortDepth = function (a, b) {
+ return b.dist - a.dist;
+ };
+ this.dataPoints.sort(sortDepth);
+
+ if (this.style === Graph3d.STYLE.SURFACE) {
+ for (i = 0; i < this.dataPoints.length; i++) {
+ point = this.dataPoints[i];
+ right = this.dataPoints[i].pointRight;
+ top = this.dataPoints[i].pointTop;
+ cross = this.dataPoints[i].pointCross;
+
+ if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) {
+
+ if (this.showGrayBottom || this.showShadow) {
+ // calculate the cross product of the two vectors from center
+ // to left and right, in order to know whether we are looking at the
+ // bottom or at the top side. We can also use the cross product
+ // for calculating light intensity
+ var aDiff = Point3d.subtract(cross.trans, point.trans);
+ var bDiff = Point3d.subtract(top.trans, right.trans);
+ var crossproduct = Point3d.crossProduct(aDiff, bDiff);
+ var len = crossproduct.length();
+ // FIXME: there is a bug with determining the surface side (shadow or colored)
+
+ topSideVisible = (crossproduct.z > 0);
+ }
+ else {
+ topSideVisible = true;
+ }
+
+ if (topSideVisible) {
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+ zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4;
+ h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+ s = 1; // saturation
+
+ if (this.showShadow) {
+ v = Math.min(1 + (crossproduct.x / len) / 2, 1); // value. TODO: scale
+ fillStyle = this._hsv2rgb(h, s, v);
+ strokeStyle = fillStyle;
+ }
+ else {
+ v = 1;
+ fillStyle = this._hsv2rgb(h, s, v);
+ strokeStyle = this.colorAxis;
+ }
+ }
+ else {
+ fillStyle = 'gray';
+ strokeStyle = this.colorAxis;
+ }
+ lineWidth = 0.5;
+
+ ctx.lineWidth = lineWidth;
+ ctx.fillStyle = fillStyle;
+ ctx.strokeStyle = strokeStyle;
+ ctx.beginPath();
+ ctx.moveTo(point.screen.x, point.screen.y);
+ ctx.lineTo(right.screen.x, right.screen.y);
+ ctx.lineTo(cross.screen.x, cross.screen.y);
+ ctx.lineTo(top.screen.x, top.screen.y);
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+ }
+ }
+ }
+ else { // grid style
+ for (i = 0; i < this.dataPoints.length; i++) {
+ point = this.dataPoints[i];
+ right = this.dataPoints[i].pointRight;
+ top = this.dataPoints[i].pointTop;
+
+ if (point !== undefined) {
+ if (this.showPerspective) {
+ lineWidth = 2 / -point.trans.z;
+ }
+ else {
+ lineWidth = 2 * -(this.eye.z / this.camera.getArmLength());
+ }
+ }
+
+ if (point !== undefined && right !== undefined) {
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+ zAvg = (point.point.z + right.point.z) / 2;
+ h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+
+ ctx.lineWidth = lineWidth;
+ ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
+ ctx.beginPath();
+ ctx.moveTo(point.screen.x, point.screen.y);
+ ctx.lineTo(right.screen.x, right.screen.y);
+ ctx.stroke();
+ }
+
+ if (point !== undefined && top !== undefined) {
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+ zAvg = (point.point.z + top.point.z) / 2;
+ h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+
+ ctx.lineWidth = lineWidth;
+ ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
+ ctx.beginPath();
+ ctx.moveTo(point.screen.x, point.screen.y);
+ ctx.lineTo(top.screen.x, top.screen.y);
+ ctx.stroke();
+ }
+ }
+ }
+ };
+
+
+ /**
+ * Draw all datapoints as dots.
+ * This function can be used when the style is 'dot' or 'dot-line'
+ */
+ Graph3d.prototype._redrawDataDot = function() {
+ var canvas = this.frame.canvas;
+ var ctx = canvas.getContext('2d');
+ var i;
+
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0)
+ return; // TODO: throw exception?
+
+ // calculate the translations of all points
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+ var screen = this._convertTranslationToScreen(trans);
+ this.dataPoints[i].trans = trans;
+ this.dataPoints[i].screen = screen;
+
+ // calculate the distance from the point at the bottom to the camera
+ var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
+ this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
+ }
+
+ // order the translated points by depth
+ var sortDepth = function (a, b) {
+ return b.dist - a.dist;
+ };
+ this.dataPoints.sort(sortDepth);
+
+ // draw the datapoints as colored circles
+ var dotSize = this.frame.clientWidth * 0.02; // px
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var point = this.dataPoints[i];
+
+ if (this.style === Graph3d.STYLE.DOTLINE) {
+ // draw a vertical line from the bottom to the graph value
+ //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin));
+ var from = this._convert3Dto2D(point.bottom);
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = this.colorGrid;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(point.screen.x, point.screen.y);
+ ctx.stroke();
+ }
+
+ // calculate radius for the circle
+ var size;
+ if (this.style === Graph3d.STYLE.DOTSIZE) {
+ size = dotSize/2 + 2*dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin);
+ }
+ else {
+ size = dotSize;
+ }
+
+ var radius;
+ if (this.showPerspective) {
+ radius = size / -point.trans.z;
+ }
+ else {
+ radius = size * -(this.eye.z / this.camera.getArmLength());
+ }
+ if (radius < 0) {
+ radius = 0;
+ }
+
+ var hue, color, borderColor;
+ if (this.style === Graph3d.STYLE.DOTCOLOR ) {
+ // calculate the color based on the value
+ hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
+ color = this._hsv2rgb(hue, 1, 1);
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
+ }
+ else if (this.style === Graph3d.STYLE.DOTSIZE) {
+ color = this.colorDot;
+ borderColor = this.colorDotBorder;
+ }
+ else {
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+ hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+ color = this._hsv2rgb(hue, 1, 1);
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
+ }
+
+ // draw the circle
+ ctx.lineWidth = 1.0;
+ ctx.strokeStyle = borderColor;
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true);
+ ctx.fill();
+ ctx.stroke();
+ }
+ };
+
+ /**
+ * Draw all datapoints as bars.
+ * This function can be used when the style is 'bar', 'bar-color', or 'bar-size'
+ */
+ Graph3d.prototype._redrawDataBar = function() {
+ var canvas = this.frame.canvas;
+ var ctx = canvas.getContext('2d');
+ var i, j, surface, corners;
+
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0)
+ return; // TODO: throw exception?
+
+ // calculate the translations of all points
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+ var screen = this._convertTranslationToScreen(trans);
+ this.dataPoints[i].trans = trans;
+ this.dataPoints[i].screen = screen;
+
+ // calculate the distance from the point at the bottom to the camera
+ var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
+ this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
+ }
+
+ // order the translated points by depth
+ var sortDepth = function (a, b) {
+ return b.dist - a.dist;
+ };
+ this.dataPoints.sort(sortDepth);
+
+ // draw the datapoints as bars
+ var xWidth = this.xBarWidth / 2;
+ var yWidth = this.yBarWidth / 2;
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var point = this.dataPoints[i];
+
+ // determine color
+ var hue, color, borderColor;
+ if (this.style === Graph3d.STYLE.BARCOLOR ) {
+ // calculate the color based on the value
+ hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
+ color = this._hsv2rgb(hue, 1, 1);
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
+ }
+ else if (this.style === Graph3d.STYLE.BARSIZE) {
+ color = this.colorDot;
+ borderColor = this.colorDotBorder;
+ }
+ else {
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+ hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+ color = this._hsv2rgb(hue, 1, 1);
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
+ }
+
+ // calculate size for the bar
+ if (this.style === Graph3d.STYLE.BARSIZE) {
+ xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
+ yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
+ }
+
+ // calculate all corner points
+ var me = this;
+ var point3d = point.point;
+ var top = [
+ {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)},
+ {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)},
+ {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)},
+ {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)}
+ ];
+ var bottom = [
+ {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)},
+ {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)},
+ {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)},
+ {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)}
+ ];
+
+ // calculate screen location of the points
+ top.forEach(function (obj) {
+ obj.screen = me._convert3Dto2D(obj.point);
+ });
+ bottom.forEach(function (obj) {
+ obj.screen = me._convert3Dto2D(obj.point);
+ });
+
+ // create five sides, calculate both corner points and center points
+ var surfaces = [
+ {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)},
+ {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)},
+ {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)},
+ {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)},
+ {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)}
+ ];
+ point.surfaces = surfaces;
+
+ // calculate the distance of each of the surface centers to the camera
+ for (j = 0; j < surfaces.length; j++) {
+ surface = surfaces[j];
+ var transCenter = this._convertPointToTranslation(surface.center);
+ surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z;
+ // TODO: this dept calculation doesn't work 100% of the cases due to perspective,
+ // but the current solution is fast/simple and works in 99.9% of all cases
+ // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9})
+ }
+
+ // order the surfaces by their (translated) depth
+ surfaces.sort(function (a, b) {
+ var diff = b.dist - a.dist;
+ if (diff) return diff;
+
+ // if equal depth, sort the top surface last
+ if (a.corners === top) return 1;
+ if (b.corners === top) return -1;
+
+ // both are equal
+ return 0;
+ });
+
+ // draw the ordered surfaces
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = borderColor;
+ ctx.fillStyle = color;
+ // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside
+ for (j = 2; j < surfaces.length; j++) {
+ surface = surfaces[j];
+ corners = surface.corners;
+ ctx.beginPath();
+ ctx.moveTo(corners[3].screen.x, corners[3].screen.y);
+ ctx.lineTo(corners[0].screen.x, corners[0].screen.y);
+ ctx.lineTo(corners[1].screen.x, corners[1].screen.y);
+ ctx.lineTo(corners[2].screen.x, corners[2].screen.y);
+ ctx.lineTo(corners[3].screen.x, corners[3].screen.y);
+ ctx.fill();
+ ctx.stroke();
+ }
+ }
+ };
+
+
+ /**
+ * Draw a line through all datapoints.
+ * This function can be used when the style is 'line'
+ */
+ Graph3d.prototype._redrawDataLine = function() {
+ var canvas = this.frame.canvas,
+ ctx = canvas.getContext('2d'),
+ point, i;
+
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0)
+ return; // TODO: throw exception?
+
+ // calculate the translations of all points
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+ var screen = this._convertTranslationToScreen(trans);
+
+ this.dataPoints[i].trans = trans;
+ this.dataPoints[i].screen = screen;
+ }
+
+ // start the line
+ if (this.dataPoints.length > 0) {
+ point = this.dataPoints[0];
+
+ ctx.lineWidth = 1; // TODO: make customizable
+ ctx.strokeStyle = 'blue'; // TODO: make customizable
+ ctx.beginPath();
+ ctx.moveTo(point.screen.x, point.screen.y);
+ }
+
+ // draw the datapoints as colored circles
+ for (i = 1; i < this.dataPoints.length; i++) {
+ point = this.dataPoints[i];
+ ctx.lineTo(point.screen.x, point.screen.y);
+ }
+
+ // finish the line
+ if (this.dataPoints.length > 0) {
+ ctx.stroke();
+ }
+ };
+
+ /**
+ * Start a moving operation inside the provided parent element
+ * @param {Event} event The event that occurred (required for
+ * retrieving the mouse position)
+ */
+ Graph3d.prototype._onMouseDown = function(event) {
+ event = event || window.event;
+
+ // check if mouse is still down (may be up when focus is lost for example
+ // in an iframe)
+ if (this.leftButtonDown) {
+ this._onMouseUp(event);
+ }
+
+ // only react on left mouse button down
+ this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
+ if (!this.leftButtonDown && !this.touchDown) return;
+
+ // get mouse position (different code for IE and all other browsers)
+ this.startMouseX = getMouseX(event);
+ this.startMouseY = getMouseY(event);
+
+ this.startStart = new Date(this.start);
+ this.startEnd = new Date(this.end);
+ this.startArmRotation = this.camera.getArmRotation();
+
+ this.frame.style.cursor = 'move';
+
+ // add event listeners to handle moving the contents
+ // we store the function onmousemove and onmouseup in the graph, so we can
+ // remove the eventlisteners lateron in the function mouseUp()
+ var me = this;
+ this.onmousemove = function (event) {me._onMouseMove(event);};
+ this.onmouseup = function (event) {me._onMouseUp(event);};
+ util.addEventListener(document, 'mousemove', me.onmousemove);
+ util.addEventListener(document, 'mouseup', me.onmouseup);
+ util.preventDefault(event);
+ };
+
+
+ /**
+ * Perform moving operating.
+ * This function activated from within the funcion Graph.mouseDown().
+ * @param {Event} event Well, eehh, the event
+ */
+ Graph3d.prototype._onMouseMove = function (event) {
+ event = event || window.event;
+
+ // calculate change in mouse position
+ var diffX = parseFloat(getMouseX(event)) - this.startMouseX;
+ var diffY = parseFloat(getMouseY(event)) - this.startMouseY;
+
+ var horizontalNew = this.startArmRotation.horizontal + diffX / 200;
+ var verticalNew = this.startArmRotation.vertical + diffY / 200;
+
+ var snapAngle = 4; // degrees
+ var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI);
+
+ // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc...
+ // the -0.001 is to take care that the vertical axis is always drawn at the left front corner
+ if (Math.abs(Math.sin(horizontalNew)) < snapValue) {
+ horizontalNew = Math.round((horizontalNew / Math.PI)) * Math.PI - 0.001;
+ }
+ if (Math.abs(Math.cos(horizontalNew)) < snapValue) {
+ horizontalNew = (Math.round((horizontalNew/ Math.PI - 0.5)) + 0.5) * Math.PI - 0.001;
+ }
+
+ // snap vertically to nice angles
+ if (Math.abs(Math.sin(verticalNew)) < snapValue) {
+ verticalNew = Math.round((verticalNew / Math.PI)) * Math.PI;
+ }
+ if (Math.abs(Math.cos(verticalNew)) < snapValue) {
+ verticalNew = (Math.round((verticalNew/ Math.PI - 0.5)) + 0.5) * Math.PI;
+ }
+
+ this.camera.setArmRotation(horizontalNew, verticalNew);
+ this.redraw();
+
+ // fire a cameraPositionChange event
+ var parameters = this.getCameraPosition();
+ this.emit('cameraPositionChange', parameters);
+
+ util.preventDefault(event);
+ };
+
+
+ /**
+ * Stop moving operating.
+ * This function activated from within the funcion Graph.mouseDown().
+ * @param {event} event The event
+ */
+ Graph3d.prototype._onMouseUp = function (event) {
+ this.frame.style.cursor = 'auto';
+ this.leftButtonDown = false;
+
+ // remove event listeners here
+ util.removeEventListener(document, 'mousemove', this.onmousemove);
+ util.removeEventListener(document, 'mouseup', this.onmouseup);
+ util.preventDefault(event);
+ };
+
+ /**
+ * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point
+ * @param {Event} event A mouse move event
+ */
+ Graph3d.prototype._onTooltip = function (event) {
+ var delay = 300; // ms
+ var mouseX = getMouseX(event) - util.getAbsoluteLeft(this.frame);
+ var mouseY = getMouseY(event) - util.getAbsoluteTop(this.frame);
+
+ if (!this.showTooltip) {
+ return;
+ }
+
+ if (this.tooltipTimeout) {
+ clearTimeout(this.tooltipTimeout);
+ }
+
+ // (delayed) display of a tooltip only if no mouse button is down
+ if (this.leftButtonDown) {
+ this._hideTooltip();
+ return;
+ }
+
+ if (this.tooltip && this.tooltip.dataPoint) {
+ // tooltip is currently visible
+ var dataPoint = this._dataPointFromXY(mouseX, mouseY);
+ if (dataPoint !== this.tooltip.dataPoint) {
+ // datapoint changed
+ if (dataPoint) {
+ this._showTooltip(dataPoint);
+ }
+ else {
+ this._hideTooltip();
+ }
+ }
+ }
+ else {
+ // tooltip is currently not visible
+ var me = this;
+ this.tooltipTimeout = setTimeout(function () {
+ me.tooltipTimeout = null;
+
+ // show a tooltip if we have a data point
+ var dataPoint = me._dataPointFromXY(mouseX, mouseY);
+ if (dataPoint) {
+ me._showTooltip(dataPoint);
+ }
+ }, delay);
+ }
+ };
+
+ /**
+ * Event handler for touchstart event on mobile devices
+ */
+ Graph3d.prototype._onTouchStart = function(event) {
+ this.touchDown = true;
+
+ var me = this;
+ this.ontouchmove = function (event) {me._onTouchMove(event);};
+ this.ontouchend = function (event) {me._onTouchEnd(event);};
+ util.addEventListener(document, 'touchmove', me.ontouchmove);
+ util.addEventListener(document, 'touchend', me.ontouchend);
+
+ this._onMouseDown(event);
+ };
+
+ /**
+ * Event handler for touchmove event on mobile devices
+ */
+ Graph3d.prototype._onTouchMove = function(event) {
+ this._onMouseMove(event);
+ };
+
+ /**
+ * Event handler for touchend event on mobile devices
+ */
+ Graph3d.prototype._onTouchEnd = function(event) {
+ this.touchDown = false;
+
+ util.removeEventListener(document, 'touchmove', this.ontouchmove);
+ util.removeEventListener(document, 'touchend', this.ontouchend);
+
+ this._onMouseUp(event);
+ };
+
+
+ /**
+ * Event handler for mouse wheel event, used to zoom the graph
+ * Code from http://adomas.org/javascript-mouse-wheel/
+ * @param {event} event The event
+ */
+ Graph3d.prototype._onWheel = function(event) {
+ if (!event) /* For IE. */
+ event = window.event;
+
+ // retrieve delta
+ var delta = 0;
+ if (event.wheelDelta) { /* IE/Opera. */
+ delta = event.wheelDelta/120;
+ } else if (event.detail) { /* Mozilla case. */
+ // In Mozilla, sign of delta is different than in IE.
+ // Also, delta is multiple of 3.
+ delta = -event.detail/3;
+ }
+
+ // If delta is nonzero, handle it.
+ // Basically, delta is now positive if wheel was scrolled up,
+ // and negative, if wheel was scrolled down.
+ if (delta) {
+ var oldLength = this.camera.getArmLength();
+ var newLength = oldLength * (1 - delta / 10);
+
+ this.camera.setArmLength(newLength);
+ this.redraw();
+
+ this._hideTooltip();
+ }
+
+ // fire a cameraPositionChange event
+ var parameters = this.getCameraPosition();
+ this.emit('cameraPositionChange', parameters);
+
+ // Prevent default actions caused by mouse wheel.
+ // That might be ugly, but we handle scrolls somehow
+ // anyway, so don't bother here..
+ util.preventDefault(event);
+ };
+
+ /**
+ * Test whether a point lies inside given 2D triangle
+ * @param {Point2d} point
+ * @param {Point2d[]} triangle
+ * @return {boolean} Returns true if given point lies inside or on the edge of the triangle
+ * @private
+ */
+ Graph3d.prototype._insideTriangle = function (point, triangle) {
+ var a = triangle[0],
+ b = triangle[1],
+ c = triangle[2];
+
+ function sign (x) {
+ return x > 0 ? 1 : x < 0 ? -1 : 0;
+ }
+
+ var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x));
+ var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x));
+ var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x));
+
+ // each of the three signs must be either equal to each other or zero
+ return (as == 0 || bs == 0 || as == bs) &&
+ (bs == 0 || cs == 0 || bs == cs) &&
+ (as == 0 || cs == 0 || as == cs);
+ };
+
+ /**
+ * Find a data point close to given screen position (x, y)
+ * @param {Number} x
+ * @param {Number} y
+ * @return {Object | null} The closest data point or null if not close to any data point
+ * @private
+ */
+ Graph3d.prototype._dataPointFromXY = function (x, y) {
+ var i,
+ distMax = 100, // px
+ dataPoint = null,
+ closestDataPoint = null,
+ closestDist = null,
+ center = new Point2d(x, y);
+
+ if (this.style === Graph3d.STYLE.BAR ||
+ this.style === Graph3d.STYLE.BARCOLOR ||
+ this.style === Graph3d.STYLE.BARSIZE) {
+ // the data points are ordered from far away to closest
+ for (i = this.dataPoints.length - 1; i >= 0; i--) {
+ dataPoint = this.dataPoints[i];
+ var surfaces = dataPoint.surfaces;
+ if (surfaces) {
+ for (var s = surfaces.length - 1; s >= 0; s--) {
+ // split each surface in two triangles, and see if the center point is inside one of these
+ var surface = surfaces[s];
+ var corners = surface.corners;
+ var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen];
+ var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen];
+ if (this._insideTriangle(center, triangle1) ||
+ this._insideTriangle(center, triangle2)) {
+ // return immediately at the first hit
+ return dataPoint;
+ }
+ }
+ }
+ }
+ }
+ else {
+ // find the closest data point, using distance to the center of the point on 2d screen
+ for (i = 0; i < this.dataPoints.length; i++) {
+ dataPoint = this.dataPoints[i];
+ var point = dataPoint.screen;
+ if (point) {
+ var distX = Math.abs(x - point.x);
+ var distY = Math.abs(y - point.y);
+ var dist = Math.sqrt(distX * distX + distY * distY);
+
+ if ((closestDist === null || dist < closestDist) && dist < distMax) {
+ closestDist = dist;
+ closestDataPoint = dataPoint;
+ }
+ }
+ }
+ }
+
+
+ return closestDataPoint;
+ };
+
+ /**
+ * Display a tooltip for given data point
+ * @param {Object} dataPoint
+ * @private
+ */
+ Graph3d.prototype._showTooltip = function (dataPoint) {
+ var content, line, dot;
+
+ if (!this.tooltip) {
+ content = document.createElement('div');
+ content.style.position = 'absolute';
+ content.style.padding = '10px';
+ content.style.border = '1px solid #4d4d4d';
+ content.style.color = '#1a1a1a';
+ content.style.background = 'rgba(255,255,255,0.7)';
+ content.style.borderRadius = '2px';
+ content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)';
+
+ line = document.createElement('div');
+ line.style.position = 'absolute';
+ line.style.height = '40px';
+ line.style.width = '0';
+ line.style.borderLeft = '1px solid #4d4d4d';
+
+ dot = document.createElement('div');
+ dot.style.position = 'absolute';
+ dot.style.height = '0';
+ dot.style.width = '0';
+ dot.style.border = '5px solid #4d4d4d';
+ dot.style.borderRadius = '5px';
+
+ this.tooltip = {
+ dataPoint: null,
+ dom: {
+ content: content,
+ line: line,
+ dot: dot
+ }
+ };
+ }
+ else {
+ content = this.tooltip.dom.content;
+ line = this.tooltip.dom.line;
+ dot = this.tooltip.dom.dot;
+ }
+
+ this._hideTooltip();
+
+ this.tooltip.dataPoint = dataPoint;
+ if (typeof this.showTooltip === 'function') {
+ content.innerHTML = this.showTooltip(dataPoint.point);
+ }
+ else {
+ content.innerHTML = '' +
+ 'x: | ' + dataPoint.point.x + ' |
' +
+ 'y: | ' + dataPoint.point.y + ' |
' +
+ 'z: | ' + dataPoint.point.z + ' |
' +
+ '
';
+ }
+
+ content.style.left = '0';
+ content.style.top = '0';
+ this.frame.appendChild(content);
+ this.frame.appendChild(line);
+ this.frame.appendChild(dot);
+
+ // calculate sizes
+ var contentWidth = content.offsetWidth;
+ var contentHeight = content.offsetHeight;
+ var lineHeight = line.offsetHeight;
+ var dotWidth = dot.offsetWidth;
+ var dotHeight = dot.offsetHeight;
+
+ var left = dataPoint.screen.x - contentWidth / 2;
+ left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth);
+
+ line.style.left = dataPoint.screen.x + 'px';
+ line.style.top = (dataPoint.screen.y - lineHeight) + 'px';
+ content.style.left = left + 'px';
+ content.style.top = (dataPoint.screen.y - lineHeight - contentHeight) + 'px';
+ dot.style.left = (dataPoint.screen.x - dotWidth / 2) + 'px';
+ dot.style.top = (dataPoint.screen.y - dotHeight / 2) + 'px';
+ };
+
+ /**
+ * Hide the tooltip when displayed
+ * @private
+ */
+ Graph3d.prototype._hideTooltip = function () {
+ if (this.tooltip) {
+ this.tooltip.dataPoint = null;
+
+ for (var prop in this.tooltip.dom) {
+ if (this.tooltip.dom.hasOwnProperty(prop)) {
+ var elem = this.tooltip.dom[prop];
+ if (elem && elem.parentNode) {
+ elem.parentNode.removeChild(elem);
+ }
+ }
+ }
+ }
+ };
+
+ /**--------------------------------------------------------------------------**/
+
+
+ /**
+ * Get the horizontal mouse position from a mouse event
+ * @param {Event} event
+ * @return {Number} mouse x
+ */
+ function getMouseX (event) {
+ if ('clientX' in event) return event.clientX;
+ return event.targetTouches[0] && event.targetTouches[0].clientX || 0;
+ }
+
+ /**
+ * Get the vertical mouse position from a mouse event
+ * @param {Event} event
+ * @return {Number} mouse y
+ */
+ function getMouseY (event) {
+ if ('clientY' in event) return event.clientY;
+ return event.targetTouches[0] && event.targetTouches[0].clientY || 0;
+ }
+
+ module.exports = Graph3d;
+
+
+/***/ },
+/* 11 */
+/***/ function(module, exports, __webpack_require__) {
+
+
+ /**
+ * Expose `Emitter`.
+ */
+
+ module.exports = Emitter;
+
+ /**
+ * Initialize a new `Emitter`.
+ *
+ * @api public
+ */
+
+ function Emitter(obj) {
+ if (obj) return mixin(obj);
+ };
+
+ /**
+ * Mixin the emitter properties.
+ *
+ * @param {Object} obj
+ * @return {Object}
+ * @api private
+ */
+
+ function mixin(obj) {
+ for (var key in Emitter.prototype) {
+ obj[key] = Emitter.prototype[key];
+ }
+ return obj;
+ }
+
+ /**
+ * Listen on the given `event` with `fn`.
+ *
+ * @param {String} event
+ * @param {Function} fn
+ * @return {Emitter}
+ * @api public
+ */
+
+ Emitter.prototype.on =
+ Emitter.prototype.addEventListener = function(event, fn){
+ this._callbacks = this._callbacks || {};
+ (this._callbacks[event] = this._callbacks[event] || [])
+ .push(fn);
+ return this;
+ };
+
+ /**
+ * Adds an `event` listener that will be invoked a single
+ * time then automatically removed.
+ *
+ * @param {String} event
+ * @param {Function} fn
+ * @return {Emitter}
+ * @api public
+ */
+
+ Emitter.prototype.once = function(event, fn){
+ var self = this;
+ this._callbacks = this._callbacks || {};
+
+ function on() {
+ self.off(event, on);
+ fn.apply(this, arguments);
+ }
+
+ on.fn = fn;
+ this.on(event, on);
+ return this;
+ };
+
+ /**
+ * Remove the given callback for `event` or all
+ * registered callbacks.
+ *
+ * @param {String} event
+ * @param {Function} fn
+ * @return {Emitter}
+ * @api public
+ */
+
+ Emitter.prototype.off =
+ Emitter.prototype.removeListener =
+ Emitter.prototype.removeAllListeners =
+ Emitter.prototype.removeEventListener = function(event, fn){
+ this._callbacks = this._callbacks || {};
+
+ // all
+ if (0 == arguments.length) {
+ this._callbacks = {};
+ return this;
+ }
+
+ // specific event
+ var callbacks = this._callbacks[event];
+ if (!callbacks) return this;
+
+ // remove all handlers
+ if (1 == arguments.length) {
+ delete this._callbacks[event];
+ return this;
+ }
+
+ // remove specific handler
+ var cb;
+ for (var i = 0; i < callbacks.length; i++) {
+ cb = callbacks[i];
+ if (cb === fn || cb.fn === fn) {
+ callbacks.splice(i, 1);
+ break;
+ }
+ }
+ return this;
+ };
+
+ /**
+ * Emit `event` with the given args.
+ *
+ * @param {String} event
+ * @param {Mixed} ...
+ * @return {Emitter}
+ */
+
+ Emitter.prototype.emit = function(event){
+ this._callbacks = this._callbacks || {};
+ var args = [].slice.call(arguments, 1)
+ , callbacks = this._callbacks[event];
+
+ if (callbacks) {
+ callbacks = callbacks.slice(0);
+ for (var i = 0, len = callbacks.length; i < len; ++i) {
+ callbacks[i].apply(this, args);
+ }
+ }
+
+ return this;
+ };
+
+ /**
+ * Return array of callbacks for `event`.
+ *
+ * @param {String} event
+ * @return {Array}
+ * @api public
+ */
+
+ Emitter.prototype.listeners = function(event){
+ this._callbacks = this._callbacks || {};
+ return this._callbacks[event] || [];
+ };
+
+ /**
+ * Check if this emitter has `event` handlers.
+ *
+ * @param {String} event
+ * @return {Boolean}
+ * @api public
+ */
+
+ Emitter.prototype.hasListeners = function(event){
+ return !! this.listeners(event).length;
+ };
+
+
+/***/ },
+/* 12 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * @prototype Point3d
+ * @param {Number} [x]
+ * @param {Number} [y]
+ * @param {Number} [z]
+ */
+ function Point3d(x, y, z) {
+ this.x = x !== undefined ? x : 0;
+ this.y = y !== undefined ? y : 0;
+ this.z = z !== undefined ? z : 0;
+ };
+
+ /**
+ * Subtract the two provided points, returns a-b
+ * @param {Point3d} a
+ * @param {Point3d} b
+ * @return {Point3d} a-b
+ */
+ Point3d.subtract = function(a, b) {
+ var sub = new Point3d();
+ sub.x = a.x - b.x;
+ sub.y = a.y - b.y;
+ sub.z = a.z - b.z;
+ return sub;
+ };
+
+ /**
+ * Add the two provided points, returns a+b
+ * @param {Point3d} a
+ * @param {Point3d} b
+ * @return {Point3d} a+b
+ */
+ Point3d.add = function(a, b) {
+ var sum = new Point3d();
+ sum.x = a.x + b.x;
+ sum.y = a.y + b.y;
+ sum.z = a.z + b.z;
+ return sum;
+ };
+
+ /**
+ * Calculate the average of two 3d points
+ * @param {Point3d} a
+ * @param {Point3d} b
+ * @return {Point3d} The average, (a+b)/2
+ */
+ Point3d.avg = function(a, b) {
+ return new Point3d(
+ (a.x + b.x) / 2,
+ (a.y + b.y) / 2,
+ (a.z + b.z) / 2
+ );
+ };
+
+ /**
+ * Calculate the cross product of the two provided points, returns axb
+ * Documentation: http://en.wikipedia.org/wiki/Cross_product
+ * @param {Point3d} a
+ * @param {Point3d} b
+ * @return {Point3d} cross product axb
+ */
+ Point3d.crossProduct = function(a, b) {
+ var crossproduct = new Point3d();
+
+ crossproduct.x = a.y * b.z - a.z * b.y;
+ crossproduct.y = a.z * b.x - a.x * b.z;
+ crossproduct.z = a.x * b.y - a.y * b.x;
+
+ return crossproduct;
+ };
+
+
+ /**
+ * Rtrieve the length of the vector (or the distance from this point to the origin
+ * @return {Number} length
+ */
+ Point3d.prototype.length = function() {
+ return Math.sqrt(
+ this.x * this.x +
+ this.y * this.y +
+ this.z * this.z
+ );
+ };
+
+ module.exports = Point3d;
+
+
+/***/ },
+/* 13 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * @prototype Point2d
+ * @param {Number} [x]
+ * @param {Number} [y]
+ */
+ function Point2d (x, y) {
+ this.x = x !== undefined ? x : 0;
+ this.y = y !== undefined ? y : 0;
+ }
+
+ module.exports = Point2d;
+
+
+/***/ },
+/* 14 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Point3d = __webpack_require__(12);
+
+ /**
+ * @class Camera
+ * The camera is mounted on a (virtual) camera arm. The camera arm can rotate
+ * The camera is always looking in the direction of the origin of the arm.
+ * This way, the camera always rotates around one fixed point, the location
+ * of the camera arm.
+ *
+ * Documentation:
+ * http://en.wikipedia.org/wiki/3D_projection
+ */
+ function Camera() {
+ this.armLocation = new Point3d();
+ this.armRotation = {};
+ this.armRotation.horizontal = 0;
+ this.armRotation.vertical = 0;
+ this.armLength = 1.7;
+
+ this.cameraLocation = new Point3d();
+ this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0);
+
+ this.calculateCameraOrientation();
+ }
+
+ /**
+ * Set the location (origin) of the arm
+ * @param {Number} x Normalized value of x
+ * @param {Number} y Normalized value of y
+ * @param {Number} z Normalized value of z
+ */
+ Camera.prototype.setArmLocation = function(x, y, z) {
+ this.armLocation.x = x;
+ this.armLocation.y = y;
+ this.armLocation.z = z;
+
+ this.calculateCameraOrientation();
+ };
+
+ /**
+ * Set the rotation of the camera arm
+ * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI.
+ * Optional, can be left undefined.
+ * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI
+ * if vertical=0.5*PI, the graph is shown from the
+ * top. Optional, can be left undefined.
+ */
+ Camera.prototype.setArmRotation = function(horizontal, vertical) {
+ if (horizontal !== undefined) {
+ this.armRotation.horizontal = horizontal;
+ }
+
+ if (vertical !== undefined) {
+ this.armRotation.vertical = vertical;
+ if (this.armRotation.vertical < 0) this.armRotation.vertical = 0;
+ if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI;
+ }
+
+ if (horizontal !== undefined || vertical !== undefined) {
+ this.calculateCameraOrientation();
+ }
+ };
+
+ /**
+ * Retrieve the current arm rotation
+ * @return {object} An object with parameters horizontal and vertical
+ */
+ Camera.prototype.getArmRotation = function() {
+ var rot = {};
+ rot.horizontal = this.armRotation.horizontal;
+ rot.vertical = this.armRotation.vertical;
+
+ return rot;
+ };
+
+ /**
+ * Set the (normalized) length of the camera arm.
+ * @param {Number} length A length between 0.71 and 5.0
+ */
+ Camera.prototype.setArmLength = function(length) {
+ if (length === undefined)
+ return;
+
+ this.armLength = length;
+
+ // Radius must be larger than the corner of the graph,
+ // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the
+ // graph
+ if (this.armLength < 0.71) this.armLength = 0.71;
+ if (this.armLength > 5.0) this.armLength = 5.0;
+
+ this.calculateCameraOrientation();
+ };
+
+ /**
+ * Retrieve the arm length
+ * @return {Number} length
+ */
+ Camera.prototype.getArmLength = function() {
+ return this.armLength;
+ };
+
+ /**
+ * Retrieve the camera location
+ * @return {Point3d} cameraLocation
+ */
+ Camera.prototype.getCameraLocation = function() {
+ return this.cameraLocation;
+ };
+
+ /**
+ * Retrieve the camera rotation
+ * @return {Point3d} cameraRotation
+ */
+ Camera.prototype.getCameraRotation = function() {
+ return this.cameraRotation;
+ };
+
+ /**
+ * Calculate the location and rotation of the camera based on the
+ * position and orientation of the camera arm
+ */
+ Camera.prototype.calculateCameraOrientation = function() {
+ // calculate location of the camera
+ this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
+ this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
+ this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical);
+
+ // calculate rotation of the camera
+ this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical;
+ this.cameraRotation.y = 0;
+ this.cameraRotation.z = -this.armRotation.horizontal;
+ };
+
+ module.exports = Camera;
+
+/***/ },
+/* 15 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var DataView = __webpack_require__(9);
+
+ /**
+ * @class Filter
+ *
+ * @param {DataSet} data The google data table
+ * @param {Number} column The index of the column to be filtered
+ * @param {Graph} graph The graph
+ */
+ function Filter (data, column, graph) {
+ this.data = data;
+ this.column = column;
+ this.graph = graph; // the parent graph
+
+ this.index = undefined;
+ this.value = undefined;
+
+ // read all distinct values and select the first one
+ this.values = graph.getDistinctValues(data.get(), this.column);
+
+ // sort both numeric and string values correctly
+ this.values.sort(function (a, b) {
+ return a > b ? 1 : a < b ? -1 : 0;
+ });
+
+ if (this.values.length > 0) {
+ this.selectValue(0);
+ }
+
+ // create an array with the filtered datapoints. this will be loaded afterwards
+ this.dataPoints = [];
+
+ this.loaded = false;
+ this.onLoadCallback = undefined;
+
+ if (graph.animationPreload) {
+ this.loaded = false;
+ this.loadInBackground();
+ }
+ else {
+ this.loaded = true;
+ }
+ };
+
+
+ /**
+ * Return the label
+ * @return {string} label
+ */
+ Filter.prototype.isLoaded = function() {
+ return this.loaded;
+ };
+
+
+ /**
+ * Return the loaded progress
+ * @return {Number} percentage between 0 and 100
+ */
+ Filter.prototype.getLoadedProgress = function() {
+ var len = this.values.length;
+
+ var i = 0;
+ while (this.dataPoints[i]) {
+ i++;
+ }
+
+ return Math.round(i / len * 100);
+ };
+
+
+ /**
+ * Return the label
+ * @return {string} label
+ */
+ Filter.prototype.getLabel = function() {
+ return this.graph.filterLabel;
+ };
+
+
+ /**
+ * Return the columnIndex of the filter
+ * @return {Number} columnIndex
+ */
+ Filter.prototype.getColumn = function() {
+ return this.column;
+ };
+
+ /**
+ * Return the currently selected value. Returns undefined if there is no selection
+ * @return {*} value
+ */
+ Filter.prototype.getSelectedValue = function() {
+ if (this.index === undefined)
+ return undefined;
+
+ return this.values[this.index];
+ };
+
+ /**
+ * Retrieve all values of the filter
+ * @return {Array} values
+ */
+ Filter.prototype.getValues = function() {
+ return this.values;
+ };
+
+ /**
+ * Retrieve one value of the filter
+ * @param {Number} index
+ * @return {*} value
+ */
+ Filter.prototype.getValue = function(index) {
+ if (index >= this.values.length)
+ throw 'Error: index out of range';
+
+ return this.values[index];
+ };
+
+
+ /**
+ * Retrieve the (filtered) dataPoints for the currently selected filter index
+ * @param {Number} [index] (optional)
+ * @return {Array} dataPoints
+ */
+ Filter.prototype._getDataPoints = function(index) {
+ if (index === undefined)
+ index = this.index;
+
+ if (index === undefined)
+ return [];
+
+ var dataPoints;
+ if (this.dataPoints[index]) {
+ dataPoints = this.dataPoints[index];
+ }
+ else {
+ var f = {};
+ f.column = this.column;
+ f.value = this.values[index];
+
+ var dataView = new DataView(this.data,{filter: function (item) {return (item[f.column] == f.value);}}).get();
+ dataPoints = this.graph._getDataPoints(dataView);
+
+ this.dataPoints[index] = dataPoints;
+ }
+
+ return dataPoints;
+ };
+
+
+
+ /**
+ * Set a callback function when the filter is fully loaded.
+ */
+ Filter.prototype.setOnLoadCallback = function(callback) {
+ this.onLoadCallback = callback;
+ };
+
+
+ /**
+ * Add a value to the list with available values for this filter
+ * No double entries will be created.
+ * @param {Number} index
+ */
+ Filter.prototype.selectValue = function(index) {
+ if (index >= this.values.length)
+ throw 'Error: index out of range';
+
+ this.index = index;
+ this.value = this.values[index];
+ };
+
+ /**
+ * Load all filtered rows in the background one by one
+ * Start this method without providing an index!
+ */
+ Filter.prototype.loadInBackground = function(index) {
+ if (index === undefined)
+ index = 0;
+
+ var frame = this.graph.frame;
+
+ if (index < this.values.length) {
+ var dataPointsTemp = this._getDataPoints(index);
+ //this.graph.redrawInfo(); // TODO: not neat
+
+ // create a progress box
+ if (frame.progress === undefined) {
+ frame.progress = document.createElement('DIV');
+ frame.progress.style.position = 'absolute';
+ frame.progress.style.color = 'gray';
+ frame.appendChild(frame.progress);
+ }
+ var progress = this.getLoadedProgress();
+ frame.progress.innerHTML = 'Loading animation... ' + progress + '%';
+ // TODO: this is no nice solution...
+ frame.progress.style.bottom = 60 + 'px'; // TODO: use height of slider
+ frame.progress.style.left = 10 + 'px';
+
+ var me = this;
+ setTimeout(function() {me.loadInBackground(index+1);}, 10);
+ this.loaded = false;
+ }
+ else {
+ this.loaded = true;
+
+ // remove the progress box
+ if (frame.progress !== undefined) {
+ frame.removeChild(frame.progress);
+ frame.progress = undefined;
+ }
+
+ if (this.onLoadCallback)
+ this.onLoadCallback();
+ }
+ };
+
+ module.exports = Filter;
+
+
+/***/ },
+/* 16 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var util = __webpack_require__(1);
+
+ /**
+ * @constructor Slider
+ *
+ * An html slider control with start/stop/prev/next buttons
+ * @param {Element} container The element where the slider will be created
+ * @param {Object} options Available options:
+ * {boolean} visible If true (default) the
+ * slider is visible.
+ */
+ function Slider(container, options) {
+ if (container === undefined) {
+ throw 'Error: No container element defined';
+ }
+ this.container = container;
+ this.visible = (options && options.visible != undefined) ? options.visible : true;
+
+ if (this.visible) {
+ this.frame = document.createElement('DIV');
+ //this.frame.style.backgroundColor = '#E5E5E5';
+ this.frame.style.width = '100%';
+ this.frame.style.position = 'relative';
+ this.container.appendChild(this.frame);
+
+ this.frame.prev = document.createElement('INPUT');
+ this.frame.prev.type = 'BUTTON';
+ this.frame.prev.value = 'Prev';
+ this.frame.appendChild(this.frame.prev);
+
+ this.frame.play = document.createElement('INPUT');
+ this.frame.play.type = 'BUTTON';
+ this.frame.play.value = 'Play';
+ this.frame.appendChild(this.frame.play);
+
+ this.frame.next = document.createElement('INPUT');
+ this.frame.next.type = 'BUTTON';
+ this.frame.next.value = 'Next';
+ this.frame.appendChild(this.frame.next);
+
+ this.frame.bar = document.createElement('INPUT');
+ this.frame.bar.type = 'BUTTON';
+ this.frame.bar.style.position = 'absolute';
+ this.frame.bar.style.border = '1px solid red';
+ this.frame.bar.style.width = '100px';
+ this.frame.bar.style.height = '6px';
+ this.frame.bar.style.borderRadius = '2px';
+ this.frame.bar.style.MozBorderRadius = '2px';
+ this.frame.bar.style.border = '1px solid #7F7F7F';
+ this.frame.bar.style.backgroundColor = '#E5E5E5';
+ this.frame.appendChild(this.frame.bar);
+
+ this.frame.slide = document.createElement('INPUT');
+ this.frame.slide.type = 'BUTTON';
+ this.frame.slide.style.margin = '0px';
+ this.frame.slide.value = ' ';
+ this.frame.slide.style.position = 'relative';
+ this.frame.slide.style.left = '-100px';
+ this.frame.appendChild(this.frame.slide);
+
+ // create events
+ var me = this;
+ this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);};
+ this.frame.prev.onclick = function (event) {me.prev(event);};
+ this.frame.play.onclick = function (event) {me.togglePlay(event);};
+ this.frame.next.onclick = function (event) {me.next(event);};
+ }
+
+ this.onChangeCallback = undefined;
+
+ this.values = [];
+ this.index = undefined;
+
+ this.playTimeout = undefined;
+ this.playInterval = 1000; // milliseconds
+ this.playLoop = true;
+ }
+
+ /**
+ * Select the previous index
+ */
+ Slider.prototype.prev = function() {
+ var index = this.getIndex();
+ if (index > 0) {
+ index--;
+ this.setIndex(index);
+ }
+ };
+
+ /**
+ * Select the next index
+ */
+ Slider.prototype.next = function() {
+ var index = this.getIndex();
+ if (index < this.values.length - 1) {
+ index++;
+ this.setIndex(index);
+ }
+ };
+
+ /**
+ * Select the next index
+ */
+ Slider.prototype.playNext = function() {
+ var start = new Date();
+
+ var index = this.getIndex();
+ if (index < this.values.length - 1) {
+ index++;
+ this.setIndex(index);
+ }
+ else if (this.playLoop) {
+ // jump to the start
+ index = 0;
+ this.setIndex(index);
+ }
+
+ var end = new Date();
+ var diff = (end - start);
+
+ // calculate how much time it to to set the index and to execute the callback
+ // function.
+ var interval = Math.max(this.playInterval - diff, 0);
+ // document.title = diff // TODO: cleanup
+
+ var me = this;
+ this.playTimeout = setTimeout(function() {me.playNext();}, interval);
+ };
+
+ /**
+ * Toggle start or stop playing
+ */
+ Slider.prototype.togglePlay = function() {
+ if (this.playTimeout === undefined) {
+ this.play();
+ } else {
+ this.stop();
+ }
+ };
+
+ /**
+ * Start playing
+ */
+ Slider.prototype.play = function() {
+ // Test whether already playing
+ if (this.playTimeout) return;
+
+ this.playNext();
+
+ if (this.frame) {
+ this.frame.play.value = 'Stop';
+ }
+ };
+
+ /**
+ * Stop playing
+ */
+ Slider.prototype.stop = function() {
+ clearInterval(this.playTimeout);
+ this.playTimeout = undefined;
+
+ if (this.frame) {
+ this.frame.play.value = 'Play';
+ }
+ };
+
+ /**
+ * Set a callback function which will be triggered when the value of the
+ * slider bar has changed.
+ */
+ Slider.prototype.setOnChangeCallback = function(callback) {
+ this.onChangeCallback = callback;
+ };
+
+ /**
+ * Set the interval for playing the list
+ * @param {Number} interval The interval in milliseconds
+ */
+ Slider.prototype.setPlayInterval = function(interval) {
+ this.playInterval = interval;
+ };
+
+ /**
+ * Retrieve the current play interval
+ * @return {Number} interval The interval in milliseconds
+ */
+ Slider.prototype.getPlayInterval = function(interval) {
+ return this.playInterval;
+ };
+
+ /**
+ * Set looping on or off
+ * @pararm {boolean} doLoop If true, the slider will jump to the start when
+ * the end is passed, and will jump to the end
+ * when the start is passed.
+ */
+ Slider.prototype.setPlayLoop = function(doLoop) {
+ this.playLoop = doLoop;
+ };
+
+
+ /**
+ * Execute the onchange callback function
+ */
+ Slider.prototype.onChange = function() {
+ if (this.onChangeCallback !== undefined) {
+ this.onChangeCallback();
+ }
+ };
+
+ /**
+ * redraw the slider on the correct place
+ */
+ Slider.prototype.redraw = function() {
+ if (this.frame) {
+ // resize the bar
+ this.frame.bar.style.top = (this.frame.clientHeight/2 -
+ this.frame.bar.offsetHeight/2) + 'px';
+ this.frame.bar.style.width = (this.frame.clientWidth -
+ this.frame.prev.clientWidth -
+ this.frame.play.clientWidth -
+ this.frame.next.clientWidth - 30) + 'px';
+
+ // position the slider button
+ var left = this.indexToLeft(this.index);
+ this.frame.slide.style.left = (left) + 'px';
+ }
+ };
+
+
+ /**
+ * Set the list with values for the slider
+ * @param {Array} values A javascript array with values (any type)
+ */
+ Slider.prototype.setValues = function(values) {
+ this.values = values;
+
+ if (this.values.length > 0)
+ this.setIndex(0);
+ else
+ this.index = undefined;
+ };
+
+ /**
+ * Select a value by its index
+ * @param {Number} index
+ */
+ Slider.prototype.setIndex = function(index) {
+ if (index < this.values.length) {
+ this.index = index;
+
+ this.redraw();
+ this.onChange();
+ }
+ else {
+ throw 'Error: index out of range';
+ }
+ };
+
+ /**
+ * retrieve the index of the currently selected vaue
+ * @return {Number} index
+ */
+ Slider.prototype.getIndex = function() {
+ return this.index;
+ };
+
+
+ /**
+ * retrieve the currently selected value
+ * @return {*} value
+ */
+ Slider.prototype.get = function() {
+ return this.values[this.index];
+ };
+
+
+ Slider.prototype._onMouseDown = function(event) {
+ // only react on left mouse button down
+ var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
+ if (!leftButtonDown) return;
+
+ this.startClientX = event.clientX;
+ this.startSlideX = parseFloat(this.frame.slide.style.left);
+
+ this.frame.style.cursor = 'move';
+
+ // add event listeners to handle moving the contents
+ // we store the function onmousemove and onmouseup in the graph, so we can
+ // remove the eventlisteners lateron in the function mouseUp()
+ var me = this;
+ this.onmousemove = function (event) {me._onMouseMove(event);};
+ this.onmouseup = function (event) {me._onMouseUp(event);};
+ util.addEventListener(document, 'mousemove', this.onmousemove);
+ util.addEventListener(document, 'mouseup', this.onmouseup);
+ util.preventDefault(event);
+ };
+
+
+ Slider.prototype.leftToIndex = function (left) {
+ var width = parseFloat(this.frame.bar.style.width) -
+ this.frame.slide.clientWidth - 10;
+ var x = left - 3;
+
+ var index = Math.round(x / width * (this.values.length-1));
+ if (index < 0) index = 0;
+ if (index > this.values.length-1) index = this.values.length-1;
+
+ return index;
+ };
+
+ Slider.prototype.indexToLeft = function (index) {
+ var width = parseFloat(this.frame.bar.style.width) -
+ this.frame.slide.clientWidth - 10;
+
+ var x = index / (this.values.length-1) * width;
+ var left = x + 3;
+
+ return left;
+ };
+
+
+
+ Slider.prototype._onMouseMove = function (event) {
+ var diff = event.clientX - this.startClientX;
+ var x = this.startSlideX + diff;
+
+ var index = this.leftToIndex(x);
+
+ this.setIndex(index);
+
+ util.preventDefault();
+ };
+
+
+ Slider.prototype._onMouseUp = function (event) {
+ this.frame.style.cursor = 'auto';
+
+ // remove event listeners
+ util.removeEventListener(document, 'mousemove', this.onmousemove);
+ util.removeEventListener(document, 'mouseup', this.onmouseup);
+
+ util.preventDefault();
+ };
+
+ module.exports = Slider;
+
+
+/***/ },
+/* 17 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * @prototype StepNumber
+ * The class StepNumber is an iterator for Numbers. You provide a start and end
+ * value, and a best step size. StepNumber itself rounds to fixed values and
+ * a finds the step that best fits the provided step.
+ *
+ * If prettyStep is true, the step size is chosen as close as possible to the
+ * provided step, but being a round value like 1, 2, 5, 10, 20, 50, ....
+ *
+ * Example usage:
+ * var step = new StepNumber(0, 10, 2.5, true);
+ * step.start();
+ * while (!step.end()) {
+ * alert(step.getCurrent());
+ * step.next();
+ * }
+ *
+ * Version: 1.0
+ *
+ * @param {Number} start The start value
+ * @param {Number} end The end value
+ * @param {Number} step Optional. Step size. Must be a positive value.
+ * @param {boolean} prettyStep Optional. If true, the step size is rounded
+ * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
+ */
+ function StepNumber(start, end, step, prettyStep) {
+ // set default values
+ this._start = 0;
+ this._end = 0;
+ this._step = 1;
+ this.prettyStep = true;
+ this.precision = 5;
+
+ this._current = 0;
+ this.setRange(start, end, step, prettyStep);
+ };
+
+ /**
+ * Set a new range: start, end and step.
+ *
+ * @param {Number} start The start value
+ * @param {Number} end The end value
+ * @param {Number} step Optional. Step size. Must be a positive value.
+ * @param {boolean} prettyStep Optional. If true, the step size is rounded
+ * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
+ */
+ StepNumber.prototype.setRange = function(start, end, step, prettyStep) {
+ this._start = start ? start : 0;
+ this._end = end ? end : 0;
+
+ this.setStep(step, prettyStep);
+ };
+
+ /**
+ * Set a new step size
+ * @param {Number} step New step size. Must be a positive value
+ * @param {boolean} prettyStep Optional. If true, the provided step is rounded
+ * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
+ */
+ StepNumber.prototype.setStep = function(step, prettyStep) {
+ if (step === undefined || step <= 0)
+ return;
+
+ if (prettyStep !== undefined)
+ this.prettyStep = prettyStep;
+
+ if (this.prettyStep === true)
+ this._step = StepNumber.calculatePrettyStep(step);
+ else
+ this._step = step;
+ };
+
+ /**
+ * Calculate a nice step size, closest to the desired step size.
+ * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an
+ * integer Number. For example 1, 2, 5, 10, 20, 50, etc...
+ * @param {Number} step Desired step size
+ * @return {Number} Nice step size
+ */
+ StepNumber.calculatePrettyStep = function (step) {
+ var log10 = function (x) {return Math.log(x) / Math.LN10;};
+
+ // try three steps (multiple of 1, 2, or 5
+ var step1 = Math.pow(10, Math.round(log10(step))),
+ step2 = 2 * Math.pow(10, Math.round(log10(step / 2))),
+ step5 = 5 * Math.pow(10, Math.round(log10(step / 5)));
+
+ // choose the best step (closest to minimum step)
+ var prettyStep = step1;
+ if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2;
+ if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5;
+
+ // for safety
+ if (prettyStep <= 0) {
+ prettyStep = 1;
+ }
+
+ return prettyStep;
+ };
+
+ /**
+ * returns the current value of the step
+ * @return {Number} current value
+ */
+ StepNumber.prototype.getCurrent = function () {
+ return parseFloat(this._current.toPrecision(this.precision));
+ };
+
+ /**
+ * returns the current step size
+ * @return {Number} current step size
+ */
+ StepNumber.prototype.getStep = function () {
+ return this._step;
+ };
+
+ /**
+ * Set the current value to the largest value smaller than start, which
+ * is a multiple of the step size
+ */
+ StepNumber.prototype.start = function() {
+ this._current = this._start - this._start % this._step;
+ };
+
+ /**
+ * Do a step, add the step size to the current value
+ */
+ StepNumber.prototype.next = function () {
+ this._current += this._step;
+ };
+
+ /**
+ * Returns true whether the end is reached
+ * @return {boolean} True if the current value has passed the end value.
+ */
+ StepNumber.prototype.end = function () {
+ return (this._current > this._end);
+ };
+
+ module.exports = StepNumber;
+
+
+/***/ },
+/* 18 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Emitter = __webpack_require__(11);
+ var Hammer = __webpack_require__(19);
+ var util = __webpack_require__(1);
+ var DataSet = __webpack_require__(7);
+ var DataView = __webpack_require__(9);
+ var Range = __webpack_require__(21);
+ var Core = __webpack_require__(25);
+ var TimeAxis = __webpack_require__(37);
+ var CurrentTime = __webpack_require__(39);
+ var CustomTime = __webpack_require__(41);
+ var ItemSet = __webpack_require__(26);
+
+ /**
+ * Create a timeline visualization
+ * @param {HTMLElement} container
+ * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
+ * @param {vis.DataSet | Array | google.visualization.DataTable} [groups]
+ * @param {Object} [options] See Timeline.setOptions for the available options.
+ * @constructor
+ * @extends Core
+ */
+ function Timeline (container, items, groups, options) {
+ if (!(this instanceof Timeline)) {
+ throw new SyntaxError('Constructor must be called with the new operator');
+ }
+
+ // if the third element is options, the forth is groups (optionally);
+ if (!(Array.isArray(groups) || groups instanceof DataSet) && groups instanceof Object) {
+ var forthArgument = options;
+ options = groups;
+ groups = forthArgument;
+ }
+
+ var me = this;
+ this.defaultOptions = {
+ start: null,
+ end: null,
+
+ autoResize: true,
+
+ orientation: 'bottom',
+ width: null,
+ height: null,
+ maxHeight: null,
+ minHeight: null
+ };
+ this.options = util.deepExtend({}, this.defaultOptions);
+
+ // Create the DOM, props, and emitter
+ this._create(container);
+
+ // all components listed here will be repainted automatically
+ this.components = [];
+
+ this.body = {
+ dom: this.dom,
+ domProps: this.props,
+ emitter: {
+ on: this.on.bind(this),
+ off: this.off.bind(this),
+ emit: this.emit.bind(this)
+ },
+ hiddenDates: [],
+ util: {
+ snap: null, // will be specified after TimeAxis is created
+ toScreen: me._toScreen.bind(me),
+ toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
+ toTime: me._toTime.bind(me),
+ toGlobalTime : me._toGlobalTime.bind(me)
+ }
+ };
+
+ // range
+ this.range = new Range(this.body);
+ this.components.push(this.range);
+ this.body.range = this.range;
+
+ // time axis
+ this.timeAxis = new TimeAxis(this.body);
+ this.components.push(this.timeAxis);
+ this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
+
+ // current time bar
+ this.currentTime = new CurrentTime(this.body);
+ this.components.push(this.currentTime);
+
+ // custom time bar
+ // Note: time bar will be attached in this.setOptions when selected
+ this.customTime = new CustomTime(this.body);
+ this.components.push(this.customTime);
+
+ // item set
+ this.itemSet = new ItemSet(this.body);
+ this.components.push(this.itemSet);
+
+ this.itemsData = null; // DataSet
+ this.groupsData = null; // DataSet
+
+ // apply options
+ if (options) {
+ this.setOptions(options);
+ }
+
+ // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
+ if (groups) {
+ this.setGroups(groups);
+ }
+
+ // create itemset
+ if (items) {
+ this.setItems(items);
+ }
+ else {
+ this.redraw();
+ }
+ }
+
+ // Extend the functionality from Core
+ Timeline.prototype = new Core();
+
+ /**
+ * Set items
+ * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
+ */
+ Timeline.prototype.setItems = function(items) {
+ var initialLoad = (this.itemsData == null);
+
+ // convert to type DataSet when needed
+ var newDataSet;
+ if (!items) {
+ newDataSet = null;
+ }
+ else if (items instanceof DataSet || items instanceof DataView) {
+ newDataSet = items;
+ }
+ else {
+ // turn an array into a dataset
+ newDataSet = new DataSet(items, {
+ type: {
+ start: 'Date',
+ end: 'Date'
+ }
+ });
+ }
+
+ // set items
+ this.itemsData = newDataSet;
+ this.itemSet && this.itemSet.setItems(newDataSet);
+
+ if (initialLoad) {
+ if (this.options.start != undefined || this.options.end != undefined) {
+ if (this.options.start == undefined || this.options.end == undefined) {
+ var dataRange = this._getDataRange();
+ }
+
+ var start = this.options.start != undefined ? this.options.start : dataRange.start;
+ var end = this.options.end != undefined ? this.options.end : dataRange.end;
+
+ this.setWindow(start, end, {animate: false});
+ }
+ else {
+ this.fit({animate: false});
+ }
+ }
+ };
+
+ /**
+ * Set groups
+ * @param {vis.DataSet | Array | google.visualization.DataTable} groups
+ */
+ Timeline.prototype.setGroups = function(groups) {
+ // convert to type DataSet when needed
+ var newDataSet;
+ if (!groups) {
+ newDataSet = null;
+ }
+ else if (groups instanceof DataSet || groups instanceof DataView) {
+ newDataSet = groups;
+ }
+ else {
+ // turn an array into a dataset
+ newDataSet = new DataSet(groups);
+ }
+
+ this.groupsData = newDataSet;
+ this.itemSet.setGroups(newDataSet);
+ };
+
+ /**
+ * Set selected items by their id. Replaces the current selection
+ * Unknown id's are silently ignored.
+ * @param {string[] | string} [ids] An array with zero or more id's of the items to be
+ * selected. If ids is an empty array, all items will be
+ * unselected.
+ * @param {Object} [options] Available options:
+ * `focus: boolean`
+ * If true, focus will be set to the selected item(s)
+ * `animate: boolean | number`
+ * If true (default), the range is animated
+ * smoothly to the new window.
+ * If a number, the number is taken as duration
+ * for the animation. Default duration is 500 ms.
+ * Only applicable when option focus is true.
+ */
+ Timeline.prototype.setSelection = function(ids, options) {
+ this.itemSet && this.itemSet.setSelection(ids);
+
+ if (options && options.focus) {
+ this.focus(ids, options);
+ }
+ };
+
+ /**
+ * Get the selected items by their id
+ * @return {Array} ids The ids of the selected items
+ */
+ Timeline.prototype.getSelection = function() {
+ return this.itemSet && this.itemSet.getSelection() || [];
+ };
+
+ /**
+ * Adjust the visible window such that the selected item (or multiple items)
+ * are centered on screen.
+ * @param {String | String[]} id An item id or array with item ids
+ * @param {Object} [options] Available options:
+ * `animate: boolean | number`
+ * If true (default), the range is animated
+ * smoothly to the new window.
+ * If a number, the number is taken as duration
+ * for the animation. Default duration is 500 ms.
+ * Only applicable when option focus is true
+ */
+ Timeline.prototype.focus = function(id, options) {
+ if (!this.itemsData || id == undefined) return;
+
+ var ids = Array.isArray(id) ? id : [id];
+
+ // get the specified item(s)
+ var itemsData = this.itemsData.getDataSet().get(ids, {
+ type: {
+ start: 'Date',
+ end: 'Date'
+ }
+ });
+
+ // calculate minimum start and maximum end of specified items
+ var start = null;
+ var end = null;
+ itemsData.forEach(function (itemData) {
+ var s = itemData.start.valueOf();
+ var e = 'end' in itemData ? itemData.end.valueOf() : itemData.start.valueOf();
+
+ if (start === null || s < start) {
+ start = s;
+ }
+
+ if (end === null || e > end) {
+ end = e;
+ }
+ });
+
+ if (start !== null && end !== null) {
+ // calculate the new middle and interval for the window
+ var middle = (start + end) / 2;
+ var interval = Math.max((this.range.end - this.range.start), (end - start) * 1.1);
+
+ var animate = (options && options.animate !== undefined) ? options.animate : true;
+ this.range.setRange(middle - interval / 2, middle + interval / 2, animate);
+ }
+ };
+
+ /**
+ * Get the data range of the item set.
+ * @returns {{min: Date, max: Date}} range A range with a start and end Date.
+ * When no minimum is found, min==null
+ * When no maximum is found, max==null
+ */
+ Timeline.prototype.getItemRange = function() {
+ // calculate min from start filed
+ var dataset = this.itemsData.getDataSet(),
+ min = null,
+ max = null;
+
+ if (dataset) {
+ // calculate the minimum value of the field 'start'
+ var minItem = dataset.min('start');
+ min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
+ // Note: we convert first to Date and then to number because else
+ // a conversion from ISODate to Number will fail
+
+ // calculate maximum value of fields 'start' and 'end'
+ var maxStartItem = dataset.max('start');
+ if (maxStartItem) {
+ max = util.convert(maxStartItem.start, 'Date').valueOf();
+ }
+ var maxEndItem = dataset.max('end');
+ if (maxEndItem) {
+ if (max == null) {
+ max = util.convert(maxEndItem.end, 'Date').valueOf();
+ }
+ else {
+ max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
+ }
+ }
+ }
+
+ return {
+ min: (min != null) ? new Date(min) : null,
+ max: (max != null) ? new Date(max) : null
+ };
+ };
+
+
+ module.exports = Timeline;
+
+
+/***/ },
+/* 19 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // Only load hammer.js when in a browser environment
+ // (loading hammer.js in a node.js environment gives errors)
+ if (typeof window !== 'undefined') {
+ module.exports = window['Hammer'] || __webpack_require__(20);
+ }
+ else {
+ module.exports = function () {
+ throw Error('hammer.js is only available in a browser, not in node.js.');
+ }
+ }
+
+
+/***/ },
+/* 20 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v1.1.3 - 2014-05-20
+ * http://eightmedia.github.io/hammer.js
+ *
+ * Copyright (c) 2014 Jorik Tangelder ;
+ * Licensed under the MIT license */
+
+ (function(window, undefined) {
+ 'use strict';
+
+ /**
+ * @main
+ * @module hammer
+ *
+ * @class Hammer
+ * @static
+ */
+
+ /**
+ * Hammer, use this to create instances
+ * ````
+ * var hammertime = new Hammer(myElement);
+ * ````
+ *
+ * @method Hammer
+ * @param {HTMLElement} element
+ * @param {Object} [options={}]
+ * @return {Hammer.Instance}
+ */
+ var Hammer = function Hammer(element, options) {
+ return new Hammer.Instance(element, options || {});
+ };
+
+ /**
+ * version, as defined in package.json
+ * the value will be set at each build
+ * @property VERSION
+ * @final
+ * @type {String}
+ */
+ Hammer.VERSION = '1.1.3';
+
+ /**
+ * default settings.
+ * more settings are defined per gesture at `/gestures`. Each gesture can be disabled/enabled
+ * by setting it's name (like `swipe`) to false.
+ * You can set the defaults for all instances by changing this object before creating an instance.
+ * @example
+ * ````
+ * Hammer.defaults.drag = false;
+ * Hammer.defaults.behavior.touchAction = 'pan-y';
+ * delete Hammer.defaults.behavior.userSelect;
+ * ````
+ * @property defaults
+ * @type {Object}
+ */
+ Hammer.defaults = {
+ /**
+ * this setting object adds styles and attributes to the element to prevent the browser from doing
+ * its native behavior. The css properties are auto prefixed for the browsers when needed.
+ * @property defaults.behavior
+ * @type {Object}
+ */
+ behavior: {
+ /**
+ * Disables text selection to improve the dragging gesture. When the value is `none` it also sets
+ * `onselectstart=false` for IE on the element. Mainly for desktop browsers.
+ * @property defaults.behavior.userSelect
+ * @type {String}
+ * @default 'none'
+ */
+ userSelect: 'none',
+
+ /**
+ * Specifies whether and how a given region can be manipulated by the user (for instance, by panning or zooming).
+ * Used by Chrome 35> and IE10>. By default this makes the element blocking any touch event.
+ * @property defaults.behavior.touchAction
+ * @type {String}
+ * @default: 'pan-y'
+ */
+ touchAction: 'pan-y',
+
+ /**
+ * Disables the default callout shown when you touch and hold a touch target.
+ * On iOS, when you touch and hold a touch target such as a link, Safari displays
+ * a callout containing information about the link. This property allows you to disable that callout.
+ * @property defaults.behavior.touchCallout
+ * @type {String}
+ * @default 'none'
+ */
+ touchCallout: 'none',
+
+ /**
+ * Specifies whether zooming is enabled. Used by IE10>
+ * @property defaults.behavior.contentZooming
+ * @type {String}
+ * @default 'none'
+ */
+ contentZooming: 'none',
+
+ /**
+ * Specifies that an entire element should be draggable instead of its contents.
+ * Mainly for desktop browsers.
+ * @property defaults.behavior.userDrag
+ * @type {String}
+ * @default 'none'
+ */
+ userDrag: 'none',
+
+ /**
+ * Overrides the highlight color shown when the user taps a link or a JavaScript
+ * clickable element in Safari on iPhone. This property obeys the alpha value, if specified.
+ *
+ * If you don't specify an alpha value, Safari on iPhone applies a default alpha value
+ * to the color. To disable tap highlighting, set the alpha value to 0 (invisible).
+ * If you set the alpha value to 1.0 (opaque), the element is not visible when tapped.
+ * @property defaults.behavior.tapHighlightColor
+ * @type {String}
+ * @default 'rgba(0,0,0,0)'
+ */
+ tapHighlightColor: 'rgba(0,0,0,0)'
+ }
+ };
+
+ /**
+ * hammer document where the base events are added at
+ * @property DOCUMENT
+ * @type {HTMLElement}
+ * @default window.document
+ */
+ Hammer.DOCUMENT = document;
+
+ /**
+ * detect support for pointer events
+ * @property HAS_POINTEREVENTS
+ * @type {Boolean}
+ */
+ Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
+
+ /**
+ * detect support for touch events
+ * @property HAS_TOUCHEVENTS
+ * @type {Boolean}
+ */
+ Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
+
+ /**
+ * detect mobile browsers
+ * @property IS_MOBILE
+ * @type {Boolean}
+ */
+ Hammer.IS_MOBILE = /mobile|tablet|ip(ad|hone|od)|android|silk/i.test(navigator.userAgent);
+
+ /**
+ * detect if we want to support mouseevents at all
+ * @property NO_MOUSEEVENTS
+ * @type {Boolean}
+ */
+ Hammer.NO_MOUSEEVENTS = (Hammer.HAS_TOUCHEVENTS && Hammer.IS_MOBILE) || Hammer.HAS_POINTEREVENTS;
+
+ /**
+ * interval in which Hammer recalculates current velocity/direction/angle in ms
+ * @property CALCULATE_INTERVAL
+ * @type {Number}
+ * @default 25
+ */
+ Hammer.CALCULATE_INTERVAL = 25;
+
+ /**
+ * eventtypes per touchevent (start, move, end) are filled by `Event.determineEventTypes` on `setup`
+ * the object contains the DOM event names per type (`EVENT_START`, `EVENT_MOVE`, `EVENT_END`)
+ * @property EVENT_TYPES
+ * @private
+ * @writeOnce
+ * @type {Object}
+ */
+ var EVENT_TYPES = {};
+
+ /**
+ * direction strings, for safe comparisons
+ * @property DIRECTION_DOWN|LEFT|UP|RIGHT
+ * @final
+ * @type {String}
+ * @default 'down' 'left' 'up' 'right'
+ */
+ var DIRECTION_DOWN = Hammer.DIRECTION_DOWN = 'down';
+ var DIRECTION_LEFT = Hammer.DIRECTION_LEFT = 'left';
+ var DIRECTION_UP = Hammer.DIRECTION_UP = 'up';
+ var DIRECTION_RIGHT = Hammer.DIRECTION_RIGHT = 'right';
+
+ /**
+ * pointertype strings, for safe comparisons
+ * @property POINTER_MOUSE|TOUCH|PEN
+ * @final
+ * @type {String}
+ * @default 'mouse' 'touch' 'pen'
+ */
+ var POINTER_MOUSE = Hammer.POINTER_MOUSE = 'mouse';
+ var POINTER_TOUCH = Hammer.POINTER_TOUCH = 'touch';
+ var POINTER_PEN = Hammer.POINTER_PEN = 'pen';
+
+ /**
+ * eventtypes
+ * @property EVENT_START|MOVE|END|RELEASE|TOUCH
+ * @final
+ * @type {String}
+ * @default 'start' 'change' 'move' 'end' 'release' 'touch'
+ */
+ var EVENT_START = Hammer.EVENT_START = 'start';
+ var EVENT_MOVE = Hammer.EVENT_MOVE = 'move';
+ var EVENT_END = Hammer.EVENT_END = 'end';
+ var EVENT_RELEASE = Hammer.EVENT_RELEASE = 'release';
+ var EVENT_TOUCH = Hammer.EVENT_TOUCH = 'touch';
+
+ /**
+ * if the window events are set...
+ * @property READY
+ * @writeOnce
+ * @type {Boolean}
+ * @default false
+ */
+ Hammer.READY = false;
+
+ /**
+ * plugins namespace
+ * @property plugins
+ * @type {Object}
+ */
+ Hammer.plugins = Hammer.plugins || {};
+
+ /**
+ * gestures namespace
+ * see `/gestures` for the definitions
+ * @property gestures
+ * @type {Object}
+ */
+ Hammer.gestures = Hammer.gestures || {};
+
+ /**
+ * setup events to detect gestures on the document
+ * this function is called when creating an new instance
+ * @private
+ */
+ function setup() {
+ if(Hammer.READY) {
+ return;
+ }
+
+ // find what eventtypes we add listeners to
+ Event.determineEventTypes();
+
+ // Register all gestures inside Hammer.gestures
+ Utils.each(Hammer.gestures, function(gesture) {
+ Detection.register(gesture);
+ });
+
+ // Add touch events on the document
+ Event.onTouch(Hammer.DOCUMENT, EVENT_MOVE, Detection.detect);
+ Event.onTouch(Hammer.DOCUMENT, EVENT_END, Detection.detect);
+
+ // Hammer is ready...!
+ Hammer.READY = true;
+ }
+
+ /**
+ * @module hammer
+ *
+ * @class Utils
+ * @static
+ */
+ var Utils = Hammer.utils = {
+ /**
+ * extend method, could also be used for cloning when `dest` is an empty object.
+ * changes the dest object
+ * @method extend
+ * @param {Object} dest
+ * @param {Object} src
+ * @param {Boolean} [merge=false] do a merge
+ * @return {Object} dest
+ */
+ extend: function extend(dest, src, merge) {
+ for(var key in src) {
+ if(!src.hasOwnProperty(key) || (dest[key] !== undefined && merge)) {
+ continue;
+ }
+ dest[key] = src[key];
+ }
+ return dest;
+ },
+
+ /**
+ * simple addEventListener wrapper
+ * @method on
+ * @param {HTMLElement} element
+ * @param {String} type
+ * @param {Function} handler
+ */
+ on: function on(element, type, handler) {
+ element.addEventListener(type, handler, false);
+ },
+
+ /**
+ * simple removeEventListener wrapper
+ * @method off
+ * @param {HTMLElement} element
+ * @param {String} type
+ * @param {Function} handler
+ */
+ off: function off(element, type, handler) {
+ element.removeEventListener(type, handler, false);
+ },
+
+ /**
+ * forEach over arrays and objects
+ * @method each
+ * @param {Object|Array} obj
+ * @param {Function} iterator
+ * @param {any} iterator.item
+ * @param {Number} iterator.index
+ * @param {Object|Array} iterator.obj the source object
+ * @param {Object} context value to use as `this` in the iterator
+ */
+ each: function each(obj, iterator, context) {
+ var i, len;
+
+ // native forEach on arrays
+ if('forEach' in obj) {
+ obj.forEach(iterator, context);
+ // arrays
+ } else if(obj.length !== undefined) {
+ for(i = 0, len = obj.length; i < len; i++) {
+ if(iterator.call(context, obj[i], i, obj) === false) {
+ return;
+ }
+ }
+ // objects
+ } else {
+ for(i in obj) {
+ if(obj.hasOwnProperty(i) &&
+ iterator.call(context, obj[i], i, obj) === false) {
+ return;
+ }
+ }
+ }
+ },
+
+ /**
+ * find if a string contains the string using indexOf
+ * @method inStr
+ * @param {String} src
+ * @param {String} find
+ * @return {Boolean} found
+ */
+ inStr: function inStr(src, find) {
+ return src.indexOf(find) > -1;
+ },
+
+ /**
+ * find if a array contains the object using indexOf or a simple polyfill
+ * @method inArray
+ * @param {String} src
+ * @param {String} find
+ * @return {Boolean|Number} false when not found, or the index
+ */
+ inArray: function inArray(src, find) {
+ if(src.indexOf) {
+ var index = src.indexOf(find);
+ return (index === -1) ? false : index;
+ } else {
+ for(var i = 0, len = src.length; i < len; i++) {
+ if(src[i] === find) {
+ return i;
+ }
+ }
+ return false;
+ }
+ },
+
+ /**
+ * convert an array-like object (`arguments`, `touchlist`) to an array
+ * @method toArray
+ * @param {Object} obj
+ * @return {Array}
+ */
+ toArray: function toArray(obj) {
+ return Array.prototype.slice.call(obj, 0);
+ },
+
+ /**
+ * find if a node is in the given parent
+ * @method hasParent
+ * @param {HTMLElement} node
+ * @param {HTMLElement} parent
+ * @return {Boolean} found
+ */
+ hasParent: function hasParent(node, parent) {
+ while(node) {
+ if(node == parent) {
+ return true;
+ }
+ node = node.parentNode;
+ }
+ return false;
+ },
+
+ /**
+ * get the center of all the touches
+ * @method getCenter
+ * @param {Array} touches
+ * @return {Object} center contains `pageX`, `pageY`, `clientX` and `clientY` properties
+ */
+ getCenter: function getCenter(touches) {
+ var pageX = [],
+ pageY = [],
+ clientX = [],
+ clientY = [],
+ min = Math.min,
+ max = Math.max;
+
+ // no need to loop when only one touch
+ if(touches.length === 1) {
+ return {
+ pageX: touches[0].pageX,
+ pageY: touches[0].pageY,
+ clientX: touches[0].clientX,
+ clientY: touches[0].clientY
+ };
+ }
+
+ Utils.each(touches, function(touch) {
+ pageX.push(touch.pageX);
+ pageY.push(touch.pageY);
+ clientX.push(touch.clientX);
+ clientY.push(touch.clientY);
+ });
+
+ return {
+ pageX: (min.apply(Math, pageX) + max.apply(Math, pageX)) / 2,
+ pageY: (min.apply(Math, pageY) + max.apply(Math, pageY)) / 2,
+ clientX: (min.apply(Math, clientX) + max.apply(Math, clientX)) / 2,
+ clientY: (min.apply(Math, clientY) + max.apply(Math, clientY)) / 2
+ };
+ },
+
+ /**
+ * calculate the velocity between two points. unit is in px per ms.
+ * @method getVelocity
+ * @param {Number} deltaTime
+ * @param {Number} deltaX
+ * @param {Number} deltaY
+ * @return {Object} velocity `x` and `y`
+ */
+ getVelocity: function getVelocity(deltaTime, deltaX, deltaY) {
+ return {
+ x: Math.abs(deltaX / deltaTime) || 0,
+ y: Math.abs(deltaY / deltaTime) || 0
+ };
+ },
+
+ /**
+ * calculate the angle between two coordinates
+ * @method getAngle
+ * @param {Touch} touch1
+ * @param {Touch} touch2
+ * @return {Number} angle
+ */
+ getAngle: function getAngle(touch1, touch2) {
+ var x = touch2.clientX - touch1.clientX,
+ y = touch2.clientY - touch1.clientY;
+
+ return Math.atan2(y, x) * 180 / Math.PI;
+ },
+
+ /**
+ * do a small comparision to get the direction between two touches.
+ * @method getDirection
+ * @param {Touch} touch1
+ * @param {Touch} touch2
+ * @return {String} direction matches `DIRECTION_LEFT|RIGHT|UP|DOWN`
+ */
+ getDirection: function getDirection(touch1, touch2) {
+ var x = Math.abs(touch1.clientX - touch2.clientX),
+ y = Math.abs(touch1.clientY - touch2.clientY);
+
+ if(x >= y) {
+ return touch1.clientX - touch2.clientX > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
+ }
+ return touch1.clientY - touch2.clientY > 0 ? DIRECTION_UP : DIRECTION_DOWN;
+ },
+
+ /**
+ * calculate the distance between two touches
+ * @method getDistance
+ * @param {Touch}touch1
+ * @param {Touch} touch2
+ * @return {Number} distance
+ */
+ getDistance: function getDistance(touch1, touch2) {
+ var x = touch2.clientX - touch1.clientX,
+ y = touch2.clientY - touch1.clientY;
+
+ return Math.sqrt((x * x) + (y * y));
+ },
+
+ /**
+ * calculate the scale factor between two touchLists
+ * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
+ * @method getScale
+ * @param {Array} start array of touches
+ * @param {Array} end array of touches
+ * @return {Number} scale
+ */
+ getScale: function getScale(start, end) {
+ // need two fingers...
+ if(start.length >= 2 && end.length >= 2) {
+ return this.getDistance(end[0], end[1]) / this.getDistance(start[0], start[1]);
+ }
+ return 1;
+ },
+
+ /**
+ * calculate the rotation degrees between two touchLists
+ * @method getRotation
+ * @param {Array} start array of touches
+ * @param {Array} end array of touches
+ * @return {Number} rotation
+ */
+ getRotation: function getRotation(start, end) {
+ // need two fingers
+ if(start.length >= 2 && end.length >= 2) {
+ return this.getAngle(end[1], end[0]) - this.getAngle(start[1], start[0]);
+ }
+ return 0;
+ },
+
+ /**
+ * find out if the direction is vertical *
+ * @method isVertical
+ * @param {String} direction matches `DIRECTION_UP|DOWN`
+ * @return {Boolean} is_vertical
+ */
+ isVertical: function isVertical(direction) {
+ return direction == DIRECTION_UP || direction == DIRECTION_DOWN;
+ },
+
+ /**
+ * set css properties with their prefixes
+ * @param {HTMLElement} element
+ * @param {String} prop
+ * @param {String} value
+ * @param {Boolean} [toggle=true]
+ * @return {Boolean}
+ */
+ setPrefixedCss: function setPrefixedCss(element, prop, value, toggle) {
+ var prefixes = ['', 'Webkit', 'Moz', 'O', 'ms'];
+ prop = Utils.toCamelCase(prop);
+
+ for(var i = 0; i < prefixes.length; i++) {
+ var p = prop;
+ // prefixes
+ if(prefixes[i]) {
+ p = prefixes[i] + p.slice(0, 1).toUpperCase() + p.slice(1);
+ }
+
+ // test the style
+ if(p in element.style) {
+ element.style[p] = (toggle == null || toggle) && value || '';
+ break;
+ }
+ }
+ },
+
+ /**
+ * toggle browser default behavior by setting css properties.
+ * `userSelect='none'` also sets `element.onselectstart` to false
+ * `userDrag='none'` also sets `element.ondragstart` to false
+ *
+ * @method toggleBehavior
+ * @param {HtmlElement} element
+ * @param {Object} props
+ * @param {Boolean} [toggle=true]
+ */
+ toggleBehavior: function toggleBehavior(element, props, toggle) {
+ if(!props || !element || !element.style) {
+ return;
+ }
+
+ // set the css properties
+ Utils.each(props, function(value, prop) {
+ Utils.setPrefixedCss(element, prop, value, toggle);
+ });
+
+ var falseFn = toggle && function() {
+ return false;
+ };
+
+ // also the disable onselectstart
+ if(props.userSelect == 'none') {
+ element.onselectstart = falseFn;
+ }
+ // and disable ondragstart
+ if(props.userDrag == 'none') {
+ element.ondragstart = falseFn;
+ }
+ },
+
+ /**
+ * convert a string with underscores to camelCase
+ * so prevent_default becomes preventDefault
+ * @param {String} str
+ * @return {String} camelCaseStr
+ */
+ toCamelCase: function toCamelCase(str) {
+ return str.replace(/[_-]([a-z])/g, function(s) {
+ return s[1].toUpperCase();
+ });
+ }
+ };
+
+
+ /**
+ * @module hammer
+ */
+ /**
+ * @class Event
+ * @static
+ */
+ var Event = Hammer.event = {
+ /**
+ * when touch events have been fired, this is true
+ * this is used to stop mouse events
+ * @property prevent_mouseevents
+ * @private
+ * @type {Boolean}
+ */
+ preventMouseEvents: false,
+
+ /**
+ * if EVENT_START has been fired
+ * @property started
+ * @private
+ * @type {Boolean}
+ */
+ started: false,
+
+ /**
+ * when the mouse is hold down, this is true
+ * @property should_detect
+ * @private
+ * @type {Boolean}
+ */
+ shouldDetect: false,
+
+ /**
+ * simple event binder with a hook and support for multiple types
+ * @method on
+ * @param {HTMLElement} element
+ * @param {String} type
+ * @param {Function} handler
+ * @param {Function} [hook]
+ * @param {Object} hook.type
+ */
+ on: function on(element, type, handler, hook) {
+ var types = type.split(' ');
+ Utils.each(types, function(type) {
+ Utils.on(element, type, handler);
+ hook && hook(type);
+ });
+ },
+
+ /**
+ * simple event unbinder with a hook and support for multiple types
+ * @method off
+ * @param {HTMLElement} element
+ * @param {String} type
+ * @param {Function} handler
+ * @param {Function} [hook]
+ * @param {Object} hook.type
+ */
+ off: function off(element, type, handler, hook) {
+ var types = type.split(' ');
+ Utils.each(types, function(type) {
+ Utils.off(element, type, handler);
+ hook && hook(type);
+ });
+ },
+
+ /**
+ * the core touch event handler.
+ * this finds out if we should to detect gestures
+ * @method onTouch
+ * @param {HTMLElement} element
+ * @param {String} eventType matches `EVENT_START|MOVE|END`
+ * @param {Function} handler
+ * @return onTouchHandler {Function} the core event handler
+ */
+ onTouch: function onTouch(element, eventType, handler) {
+ var self = this;
+
+ var onTouchHandler = function onTouchHandler(ev) {
+ var srcType = ev.type.toLowerCase(),
+ isPointer = Hammer.HAS_POINTEREVENTS,
+ isMouse = Utils.inStr(srcType, 'mouse'),
+ triggerType;
+
+ // if we are in a mouseevent, but there has been a touchevent triggered in this session
+ // we want to do nothing. simply break out of the event.
+ if(isMouse && self.preventMouseEvents) {
+ return;
+
+ // mousebutton must be down
+ } else if(isMouse && eventType == EVENT_START && ev.button === 0) {
+ self.preventMouseEvents = false;
+ self.shouldDetect = true;
+ } else if(isPointer && eventType == EVENT_START) {
+ self.shouldDetect = (ev.buttons === 1 || PointerEvent.matchType(POINTER_TOUCH, ev));
+ // just a valid start event, but no mouse
+ } else if(!isMouse && eventType == EVENT_START) {
+ self.preventMouseEvents = true;
+ self.shouldDetect = true;
+ }
+
+ // update the pointer event before entering the detection
+ if(isPointer && eventType != EVENT_END) {
+ PointerEvent.updatePointer(eventType, ev);
+ }
+
+ // we are in a touch/down state, so allowed detection of gestures
+ if(self.shouldDetect) {
+ triggerType = self.doDetect.call(self, ev, eventType, element, handler);
+ }
+
+ // ...and we are done with the detection
+ // so reset everything to start each detection totally fresh
+ if(triggerType == EVENT_END) {
+ self.preventMouseEvents = false;
+ self.shouldDetect = false;
+ PointerEvent.reset();
+ // update the pointerevent object after the detection
+ }
+
+ if(isPointer && eventType == EVENT_END) {
+ PointerEvent.updatePointer(eventType, ev);
+ }
+ };
+
+ this.on(element, EVENT_TYPES[eventType], onTouchHandler);
+ return onTouchHandler;
+ },
+
+ /**
+ * the core detection method
+ * this finds out what hammer-touch-events to trigger
+ * @method doDetect
+ * @param {Object} ev
+ * @param {String} eventType matches `EVENT_START|MOVE|END`
+ * @param {HTMLElement} element
+ * @param {Function} handler
+ * @return {String} triggerType matches `EVENT_START|MOVE|END`
+ */
+ doDetect: function doDetect(ev, eventType, element, handler) {
+ var touchList = this.getTouchList(ev, eventType);
+ var touchListLength = touchList.length;
+ var triggerType = eventType;
+ var triggerChange = touchList.trigger; // used by fakeMultitouch plugin
+ var changedLength = touchListLength;
+
+ // at each touchstart-like event we want also want to trigger a TOUCH event...
+ if(eventType == EVENT_START) {
+ triggerChange = EVENT_TOUCH;
+ // ...the same for a touchend-like event
+ } else if(eventType == EVENT_END) {
+ triggerChange = EVENT_RELEASE;
+
+ // keep track of how many touches have been removed
+ changedLength = touchList.length - ((ev.changedTouches) ? ev.changedTouches.length : 1);
+ }
+
+ // after there are still touches on the screen,
+ // we just want to trigger a MOVE event. so change the START or END to a MOVE
+ // but only after detection has been started, the first time we actualy want a START
+ if(changedLength > 0 && this.started) {
+ triggerType = EVENT_MOVE;
+ }
+
+ // detection has been started, we keep track of this, see above
+ this.started = true;
+
+ // generate some event data, some basic information
+ var evData = this.collectEventData(element, triggerType, touchList, ev);
+
+ // trigger the triggerType event before the change (TOUCH, RELEASE) events
+ // but the END event should be at last
+ if(eventType != EVENT_END) {
+ handler.call(Detection, evData);
+ }
+
+ // trigger a change (TOUCH, RELEASE) event, this means the length of the touches changed
+ if(triggerChange) {
+ evData.changedLength = changedLength;
+ evData.eventType = triggerChange;
+
+ handler.call(Detection, evData);
+
+ evData.eventType = triggerType;
+ delete evData.changedLength;
+ }
+
+ // trigger the END event
+ if(triggerType == EVENT_END) {
+ handler.call(Detection, evData);
+
+ // ...and we are done with the detection
+ // so reset everything to start each detection totally fresh
+ this.started = false;
+ }
+
+ return triggerType;
+ },
+
+ /**
+ * we have different events for each device/browser
+ * determine what we need and set them in the EVENT_TYPES constant
+ * the `onTouch` method is bind to these properties.
+ * @method determineEventTypes
+ * @return {Object} events
+ */
+ determineEventTypes: function determineEventTypes() {
+ var types;
+ if(Hammer.HAS_POINTEREVENTS) {
+ if(window.PointerEvent) {
+ types = [
+ 'pointerdown',
+ 'pointermove',
+ 'pointerup pointercancel lostpointercapture'
+ ];
+ } else {
+ types = [
+ 'MSPointerDown',
+ 'MSPointerMove',
+ 'MSPointerUp MSPointerCancel MSLostPointerCapture'
+ ];
+ }
+ } else if(Hammer.NO_MOUSEEVENTS) {
+ types = [
+ 'touchstart',
+ 'touchmove',
+ 'touchend touchcancel'
+ ];
+ } else {
+ types = [
+ 'touchstart mousedown',
+ 'touchmove mousemove',
+ 'touchend touchcancel mouseup'
+ ];
+ }
+
+ EVENT_TYPES[EVENT_START] = types[0];
+ EVENT_TYPES[EVENT_MOVE] = types[1];
+ EVENT_TYPES[EVENT_END] = types[2];
+ return EVENT_TYPES;
+ },
+
+ /**
+ * create touchList depending on the event
+ * @method getTouchList
+ * @param {Object} ev
+ * @param {String} eventType
+ * @return {Array} touches
+ */
+ getTouchList: function getTouchList(ev, eventType) {
+ // get the fake pointerEvent touchlist
+ if(Hammer.HAS_POINTEREVENTS) {
+ return PointerEvent.getTouchList();
+ }
+
+ // get the touchlist
+ if(ev.touches) {
+ if(eventType == EVENT_MOVE) {
+ return ev.touches;
+ }
+
+ var identifiers = [];
+ var concat = [].concat(Utils.toArray(ev.touches), Utils.toArray(ev.changedTouches));
+ var touchList = [];
+
+ Utils.each(concat, function(touch) {
+ if(Utils.inArray(identifiers, touch.identifier) === false) {
+ touchList.push(touch);
+ }
+ identifiers.push(touch.identifier);
+ });
+
+ return touchList;
+ }
+
+ // make fake touchList from mouse position
+ ev.identifier = 1;
+ return [ev];
+ },
+
+ /**
+ * collect basic event data
+ * @method collectEventData
+ * @param {HTMLElement} element
+ * @param {String} eventType matches `EVENT_START|MOVE|END`
+ * @param {Array} touches
+ * @param {Object} ev
+ * @return {Object} ev
+ */
+ collectEventData: function collectEventData(element, eventType, touches, ev) {
+ // find out pointerType
+ var pointerType = POINTER_TOUCH;
+ if(Utils.inStr(ev.type, 'mouse') || PointerEvent.matchType(POINTER_MOUSE, ev)) {
+ pointerType = POINTER_MOUSE;
+ } else if(PointerEvent.matchType(POINTER_PEN, ev)) {
+ pointerType = POINTER_PEN;
+ }
+
+ return {
+ center: Utils.getCenter(touches),
+ timeStamp: Date.now(),
+ target: ev.target,
+ touches: touches,
+ eventType: eventType,
+ pointerType: pointerType,
+ srcEvent: ev,
+
+ /**
+ * prevent the browser default actions
+ * mostly used to disable scrolling of the browser
+ */
+ preventDefault: function() {
+ var srcEvent = this.srcEvent;
+ srcEvent.preventManipulation && srcEvent.preventManipulation();
+ srcEvent.preventDefault && srcEvent.preventDefault();
+ },
+
+ /**
+ * stop bubbling the event up to its parents
+ */
+ stopPropagation: function() {
+ this.srcEvent.stopPropagation();
+ },
+
+ /**
+ * immediately stop gesture detection
+ * might be useful after a swipe was detected
+ * @return {*}
+ */
+ stopDetect: function() {
+ return Detection.stopDetect();
+ }
+ };
+ }
+ };
+
+
+ /**
+ * @module hammer
+ *
+ * @class PointerEvent
+ * @static
+ */
+ var PointerEvent = Hammer.PointerEvent = {
+ /**
+ * holds all pointers, by `identifier`
+ * @property pointers
+ * @type {Object}
+ */
+ pointers: {},
+
+ /**
+ * get the pointers as an array
+ * @method getTouchList
+ * @return {Array} touchlist
+ */
+ getTouchList: function getTouchList() {
+ var touchlist = [];
+ // we can use forEach since pointerEvents only is in IE10
+ Utils.each(this.pointers, function(pointer) {
+ touchlist.push(pointer);
+ });
+ return touchlist;
+ },
+
+ /**
+ * update the position of a pointer
+ * @method updatePointer
+ * @param {String} eventType matches `EVENT_START|MOVE|END`
+ * @param {Object} pointerEvent
+ */
+ updatePointer: function updatePointer(eventType, pointerEvent) {
+ if(eventType == EVENT_END || (eventType != EVENT_END && pointerEvent.buttons !== 1)) {
+ delete this.pointers[pointerEvent.pointerId];
+ } else {
+ pointerEvent.identifier = pointerEvent.pointerId;
+ this.pointers[pointerEvent.pointerId] = pointerEvent;
+ }
+ },
+
+ /**
+ * check if ev matches pointertype
+ * @method matchType
+ * @param {String} pointerType matches `POINTER_MOUSE|TOUCH|PEN`
+ * @param {PointerEvent} ev
+ */
+ matchType: function matchType(pointerType, ev) {
+ if(!ev.pointerType) {
+ return false;
+ }
+
+ var pt = ev.pointerType,
+ types = {};
+
+ types[POINTER_MOUSE] = (pt === (ev.MSPOINTER_TYPE_MOUSE || POINTER_MOUSE));
+ types[POINTER_TOUCH] = (pt === (ev.MSPOINTER_TYPE_TOUCH || POINTER_TOUCH));
+ types[POINTER_PEN] = (pt === (ev.MSPOINTER_TYPE_PEN || POINTER_PEN));
+ return types[pointerType];
+ },
+
+ /**
+ * reset the stored pointers
+ * @method reset
+ */
+ reset: function resetList() {
+ this.pointers = {};
+ }
+ };
+
+
+ /**
+ * @module hammer
+ *
+ * @class Detection
+ * @static
+ */
+ var Detection = Hammer.detection = {
+ // contains all registred Hammer.gestures in the correct order
+ gestures: [],
+
+ // data of the current Hammer.gesture detection session
+ current: null,
+
+ // the previous Hammer.gesture session data
+ // is a full clone of the previous gesture.current object
+ previous: null,
+
+ // when this becomes true, no gestures are fired
+ stopped: false,
+
+ /**
+ * start Hammer.gesture detection
+ * @method startDetect
+ * @param {Hammer.Instance} inst
+ * @param {Object} eventData
+ */
+ startDetect: function startDetect(inst, eventData) {
+ // already busy with a Hammer.gesture detection on an element
+ if(this.current) {
+ return;
+ }
+
+ this.stopped = false;
+
+ // holds current session
+ this.current = {
+ inst: inst, // reference to HammerInstance we're working for
+ startEvent: Utils.extend({}, eventData), // start eventData for distances, timing etc
+ lastEvent: false, // last eventData
+ lastCalcEvent: false, // last eventData for calculations.
+ futureCalcEvent: false, // last eventData for calculations.
+ lastCalcData: {}, // last lastCalcData
+ name: '' // current gesture we're in/detected, can be 'tap', 'hold' etc
+ };
+
+ this.detect(eventData);
+ },
+
+ /**
+ * Hammer.gesture detection
+ * @method detect
+ * @param {Object} eventData
+ * @return {any}
+ */
+ detect: function detect(eventData) {
+ if(!this.current || this.stopped) {
+ return;
+ }
+
+ // extend event data with calculations about scale, distance etc
+ eventData = this.extendEventData(eventData);
+
+ // hammer instance and instance options
+ var inst = this.current.inst,
+ instOptions = inst.options;
+
+ // call Hammer.gesture handlers
+ Utils.each(this.gestures, function triggerGesture(gesture) {
+ // only when the instance options have enabled this gesture
+ if(!this.stopped && inst.enabled && instOptions[gesture.name]) {
+ gesture.handler.call(gesture, eventData, inst);
+ }
+ }, this);
+
+ // store as previous event event
+ if(this.current) {
+ this.current.lastEvent = eventData;
+ }
+
+ if(eventData.eventType == EVENT_END) {
+ this.stopDetect();
+ }
+
+ return eventData;
+ },
+
+ /**
+ * clear the Hammer.gesture vars
+ * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
+ * to stop other Hammer.gestures from being fired
+ * @method stopDetect
+ */
+ stopDetect: function stopDetect() {
+ // clone current data to the store as the previous gesture
+ // used for the double tap gesture, since this is an other gesture detect session
+ this.previous = Utils.extend({}, this.current);
+
+ // reset the current
+ this.current = null;
+ this.stopped = true;
+ },
+
+ /**
+ * calculate velocity, angle and direction
+ * @method getVelocityData
+ * @param {Object} ev
+ * @param {Object} center
+ * @param {Number} deltaTime
+ * @param {Number} deltaX
+ * @param {Number} deltaY
+ */
+ getCalculatedData: function getCalculatedData(ev, center, deltaTime, deltaX, deltaY) {
+ var cur = this.current,
+ recalc = false,
+ calcEv = cur.lastCalcEvent,
+ calcData = cur.lastCalcData;
+
+ if(calcEv && ev.timeStamp - calcEv.timeStamp > Hammer.CALCULATE_INTERVAL) {
+ center = calcEv.center;
+ deltaTime = ev.timeStamp - calcEv.timeStamp;
+ deltaX = ev.center.clientX - calcEv.center.clientX;
+ deltaY = ev.center.clientY - calcEv.center.clientY;
+ recalc = true;
+ }
+
+ if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) {
+ cur.futureCalcEvent = ev;
+ }
+
+ if(!cur.lastCalcEvent || recalc) {
+ calcData.velocity = Utils.getVelocity(deltaTime, deltaX, deltaY);
+ calcData.angle = Utils.getAngle(center, ev.center);
+ calcData.direction = Utils.getDirection(center, ev.center);
+
+ cur.lastCalcEvent = cur.futureCalcEvent || ev;
+ cur.futureCalcEvent = ev;
+ }
+
+ ev.velocityX = calcData.velocity.x;
+ ev.velocityY = calcData.velocity.y;
+ ev.interimAngle = calcData.angle;
+ ev.interimDirection = calcData.direction;
+ },
+
+ /**
+ * extend eventData for Hammer.gestures
+ * @method extendEventData
+ * @param {Object} ev
+ * @return {Object} ev
+ */
+ extendEventData: function extendEventData(ev) {
+ var cur = this.current,
+ startEv = cur.startEvent,
+ lastEv = cur.lastEvent || startEv;
+
+ // update the start touchlist to calculate the scale/rotation
+ if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) {
+ startEv.touches = [];
+ Utils.each(ev.touches, function(touch) {
+ startEv.touches.push({
+ clientX: touch.clientX,
+ clientY: touch.clientY
+ });
+ });
+ }
+
+ var deltaTime = ev.timeStamp - startEv.timeStamp,
+ deltaX = ev.center.clientX - startEv.center.clientX,
+ deltaY = ev.center.clientY - startEv.center.clientY;
+
+ this.getCalculatedData(ev, lastEv.center, deltaTime, deltaX, deltaY);
+
+ Utils.extend(ev, {
+ startEvent: startEv,
+
+ deltaTime: deltaTime,
+ deltaX: deltaX,
+ deltaY: deltaY,
+
+ distance: Utils.getDistance(startEv.center, ev.center),
+ angle: Utils.getAngle(startEv.center, ev.center),
+ direction: Utils.getDirection(startEv.center, ev.center),
+ scale: Utils.getScale(startEv.touches, ev.touches),
+ rotation: Utils.getRotation(startEv.touches, ev.touches)
+ });
+
+ return ev;
+ },
+
+ /**
+ * register new gesture
+ * @method register
+ * @param {Object} gesture object, see `gestures/` for documentation
+ * @return {Array} gestures
+ */
+ register: function register(gesture) {
+ // add an enable gesture options if there is no given
+ var options = gesture.defaults || {};
+ if(options[gesture.name] === undefined) {
+ options[gesture.name] = true;
+ }
+
+ // extend Hammer default options with the Hammer.gesture options
+ Utils.extend(Hammer.defaults, options, true);
+
+ // set its index
+ gesture.index = gesture.index || 1000;
+
+ // add Hammer.gesture to the list
+ this.gestures.push(gesture);
+
+ // sort the list by index
+ this.gestures.sort(function(a, b) {
+ if(a.index < b.index) {
+ return -1;
+ }
+ if(a.index > b.index) {
+ return 1;
+ }
+ return 0;
+ });
+
+ return this.gestures;
+ }
+ };
+
+
+ /**
+ * @module hammer
+ */
+
+ /**
+ * create new hammer instance
+ * all methods should return the instance itself, so it is chainable.
+ *
+ * @class Instance
+ * @constructor
+ * @param {HTMLElement} element
+ * @param {Object} [options={}] options are merged with `Hammer.defaults`
+ * @return {Hammer.Instance}
+ */
+ Hammer.Instance = function(element, options) {
+ var self = this;
+
+ // setup HammerJS window events and register all gestures
+ // this also sets up the default options
+ setup();
+
+ /**
+ * @property element
+ * @type {HTMLElement}
+ */
+ this.element = element;
+
+ /**
+ * @property enabled
+ * @type {Boolean}
+ * @protected
+ */
+ this.enabled = true;
+
+ /**
+ * options, merged with the defaults
+ * options with an _ are converted to camelCase
+ * @property options
+ * @type {Object}
+ */
+ Utils.each(options, function(value, name) {
+ delete options[name];
+ options[Utils.toCamelCase(name)] = value;
+ });
+
+ this.options = Utils.extend(Utils.extend({}, Hammer.defaults), options || {});
+
+ // add some css to the element to prevent the browser from doing its native behavoir
+ if(this.options.behavior) {
+ Utils.toggleBehavior(this.element, this.options.behavior, true);
+ }
+
+ /**
+ * event start handler on the element to start the detection
+ * @property eventStartHandler
+ * @type {Object}
+ */
+ this.eventStartHandler = Event.onTouch(element, EVENT_START, function(ev) {
+ if(self.enabled && ev.eventType == EVENT_START) {
+ Detection.startDetect(self, ev);
+ } else if(ev.eventType == EVENT_TOUCH) {
+ Detection.detect(ev);
+ }
+ });
+
+ /**
+ * keep a list of user event handlers which needs to be removed when calling 'dispose'
+ * @property eventHandlers
+ * @type {Array}
+ */
+ this.eventHandlers = [];
+ };
+
+ Hammer.Instance.prototype = {
+ /**
+ * bind events to the instance
+ * @method on
+ * @chainable
+ * @param {String} gestures multiple gestures by splitting with a space
+ * @param {Function} handler
+ * @param {Object} handler.ev event object
+ */
+ on: function onEvent(gestures, handler) {
+ var self = this;
+ Event.on(self.element, gestures, handler, function(type) {
+ self.eventHandlers.push({ gesture: type, handler: handler });
+ });
+ return self;
+ },
+
+ /**
+ * unbind events to the instance
+ * @method off
+ * @chainable
+ * @param {String} gestures
+ * @param {Function} handler
+ */
+ off: function offEvent(gestures, handler) {
+ var self = this;
+
+ Event.off(self.element, gestures, handler, function(type) {
+ var index = Utils.inArray({ gesture: type, handler: handler });
+ if(index !== false) {
+ self.eventHandlers.splice(index, 1);
+ }
+ });
+ return self;
+ },
+
+ /**
+ * trigger gesture event
+ * @method trigger
+ * @chainable
+ * @param {String} gesture
+ * @param {Object} [eventData]
+ */
+ trigger: function triggerEvent(gesture, eventData) {
+ // optional
+ if(!eventData) {
+ eventData = {};
+ }
+
+ // create DOM event
+ var event = Hammer.DOCUMENT.createEvent('Event');
+ event.initEvent(gesture, true, true);
+ event.gesture = eventData;
+
+ // trigger on the target if it is in the instance element,
+ // this is for event delegation tricks
+ var element = this.element;
+ if(Utils.hasParent(eventData.target, element)) {
+ element = eventData.target;
+ }
+
+ element.dispatchEvent(event);
+ return this;
+ },
+
+ /**
+ * enable of disable hammer.js detection
+ * @method enable
+ * @chainable
+ * @param {Boolean} state
+ */
+ enable: function enable(state) {
+ this.enabled = state;
+ return this;
+ },
+
+ /**
+ * dispose this hammer instance
+ * @method dispose
+ * @return {Null}
+ */
+ dispose: function dispose() {
+ var i, eh;
+
+ // undo all changes made by stop_browser_behavior
+ Utils.toggleBehavior(this.element, this.options.behavior, false);
+
+ // unbind all custom event handlers
+ for(i = -1; (eh = this.eventHandlers[++i]);) {
+ Utils.off(this.element, eh.gesture, eh.handler);
+ }
+
+ this.eventHandlers = [];
+
+ // unbind the start event listener
+ Event.off(this.element, EVENT_TYPES[EVENT_START], this.eventStartHandler);
+
+ return null;
+ }
+ };
+
+
+ /**
+ * @module gestures
+ */
+ /**
+ * Move with x fingers (default 1) around on the page.
+ * Preventing the default browser behavior is a good way to improve feel and working.
+ * ````
+ * hammertime.on("drag", function(ev) {
+ * console.log(ev);
+ * ev.gesture.preventDefault();
+ * });
+ * ````
+ *
+ * @class Drag
+ * @static
+ */
+ /**
+ * @event drag
+ * @param {Object} ev
+ */
+ /**
+ * @event dragstart
+ * @param {Object} ev
+ */
+ /**
+ * @event dragend
+ * @param {Object} ev
+ */
+ /**
+ * @event drapleft
+ * @param {Object} ev
+ */
+ /**
+ * @event dragright
+ * @param {Object} ev
+ */
+ /**
+ * @event dragup
+ * @param {Object} ev
+ */
+ /**
+ * @event dragdown
+ * @param {Object} ev
+ */
+
+ /**
+ * @param {String} name
+ */
+ (function(name) {
+ var triggered = false;
+
+ function dragGesture(ev, inst) {
+ var cur = Detection.current;
+
+ // max touches
+ if(inst.options.dragMaxTouches > 0 &&
+ ev.touches.length > inst.options.dragMaxTouches) {
+ return;
+ }
+
+ switch(ev.eventType) {
+ case EVENT_START:
+ triggered = false;
+ break;
+
+ case EVENT_MOVE:
+ // when the distance we moved is too small we skip this gesture
+ // or we can be already in dragging
+ if(ev.distance < inst.options.dragMinDistance &&
+ cur.name != name) {
+ return;
+ }
+
+ var startCenter = cur.startEvent.center;
+
+ // we are dragging!
+ if(cur.name != name) {
+ cur.name = name;
+ if(inst.options.dragDistanceCorrection && ev.distance > 0) {
+ // When a drag is triggered, set the event center to dragMinDistance pixels from the original event center.
+ // Without this correction, the dragged distance would jumpstart at dragMinDistance pixels instead of at 0.
+ // It might be useful to save the original start point somewhere
+ var factor = Math.abs(inst.options.dragMinDistance / ev.distance);
+ startCenter.pageX += ev.deltaX * factor;
+ startCenter.pageY += ev.deltaY * factor;
+ startCenter.clientX += ev.deltaX * factor;
+ startCenter.clientY += ev.deltaY * factor;
+
+ // recalculate event data using new start point
+ ev = Detection.extendEventData(ev);
+ }
+ }
+
+ // lock drag to axis?
+ if(cur.lastEvent.dragLockToAxis ||
+ ( inst.options.dragLockToAxis &&
+ inst.options.dragLockMinDistance <= ev.distance
+ )) {
+ ev.dragLockToAxis = true;
+ }
+
+ // keep direction on the axis that the drag gesture started on
+ var lastDirection = cur.lastEvent.direction;
+ if(ev.dragLockToAxis && lastDirection !== ev.direction) {
+ if(Utils.isVertical(lastDirection)) {
+ ev.direction = (ev.deltaY < 0) ? DIRECTION_UP : DIRECTION_DOWN;
+ } else {
+ ev.direction = (ev.deltaX < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT;
+ }
+ }
+
+ // first time, trigger dragstart event
+ if(!triggered) {
+ inst.trigger(name + 'start', ev);
+ triggered = true;
+ }
+
+ // trigger events
+ inst.trigger(name, ev);
+ inst.trigger(name + ev.direction, ev);
+
+ var isVertical = Utils.isVertical(ev.direction);
+
+ // block the browser events
+ if((inst.options.dragBlockVertical && isVertical) ||
+ (inst.options.dragBlockHorizontal && !isVertical)) {
+ ev.preventDefault();
+ }
+ break;
+
+ case EVENT_RELEASE:
+ if(triggered && ev.changedLength <= inst.options.dragMaxTouches) {
+ inst.trigger(name + 'end', ev);
+ triggered = false;
+ }
+ break;
+
+ case EVENT_END:
+ triggered = false;
+ break;
+ }
+ }
+
+ Hammer.gestures.Drag = {
+ name: name,
+ index: 50,
+ handler: dragGesture,
+ defaults: {
+ /**
+ * minimal movement that have to be made before the drag event gets triggered
+ * @property dragMinDistance
+ * @type {Number}
+ * @default 10
+ */
+ dragMinDistance: 10,
+
+ /**
+ * Set dragDistanceCorrection to true to make the starting point of the drag
+ * be calculated from where the drag was triggered, not from where the touch started.
+ * Useful to avoid a jerk-starting drag, which can make fine-adjustments
+ * through dragging difficult, and be visually unappealing.
+ * @property dragDistanceCorrection
+ * @type {Boolean}
+ * @default true
+ */
+ dragDistanceCorrection: true,
+
+ /**
+ * set 0 for unlimited, but this can conflict with transform
+ * @property dragMaxTouches
+ * @type {Number}
+ * @default 1
+ */
+ dragMaxTouches: 1,
+
+ /**
+ * prevent default browser behavior when dragging occurs
+ * be careful with it, it makes the element a blocking element
+ * when you are using the drag gesture, it is a good practice to set this true
+ * @property dragBlockHorizontal
+ * @type {Boolean}
+ * @default false
+ */
+ dragBlockHorizontal: false,
+
+ /**
+ * same as `dragBlockHorizontal`, but for vertical movement
+ * @property dragBlockVertical
+ * @type {Boolean}
+ * @default false
+ */
+ dragBlockVertical: false,
+
+ /**
+ * dragLockToAxis keeps the drag gesture on the axis that it started on,
+ * It disallows vertical directions if the initial direction was horizontal, and vice versa.
+ * @property dragLockToAxis
+ * @type {Boolean}
+ * @default false
+ */
+ dragLockToAxis: false,
+
+ /**
+ * drag lock only kicks in when distance > dragLockMinDistance
+ * This way, locking occurs only when the distance has become large enough to reliably determine the direction
+ * @property dragLockMinDistance
+ * @type {Number}
+ * @default 25
+ */
+ dragLockMinDistance: 25
+ }
+ };
+ })('drag');
+
+ /**
+ * @module gestures
+ */
+ /**
+ * trigger a simple gesture event, so you can do anything in your handler.
+ * only usable if you know what your doing...
+ *
+ * @class Gesture
+ * @static
+ */
+ /**
+ * @event gesture
+ * @param {Object} ev
+ */
+ Hammer.gestures.Gesture = {
+ name: 'gesture',
+ index: 1337,
+ handler: function releaseGesture(ev, inst) {
+ inst.trigger(this.name, ev);
+ }
+ };
+
+ /**
+ * @module gestures
+ */
+ /**
+ * Touch stays at the same place for x time
+ *
+ * @class Hold
+ * @static
+ */
+ /**
+ * @event hold
+ * @param {Object} ev
+ */
+
+ /**
+ * @param {String} name
+ */
+ (function(name) {
+ var timer;
+
+ function holdGesture(ev, inst) {
+ var options = inst.options,
+ current = Detection.current;
+
+ switch(ev.eventType) {
+ case EVENT_START:
+ clearTimeout(timer);
+
+ // set the gesture so we can check in the timeout if it still is
+ current.name = name;
+
+ // set timer and if after the timeout it still is hold,
+ // we trigger the hold event
+ timer = setTimeout(function() {
+ if(current && current.name == name) {
+ inst.trigger(name, ev);
+ }
+ }, options.holdTimeout);
+ break;
+
+ case EVENT_MOVE:
+ if(ev.distance > options.holdThreshold) {
+ clearTimeout(timer);
+ }
+ break;
+
+ case EVENT_RELEASE:
+ clearTimeout(timer);
+ break;
+ }
+ }
+
+ Hammer.gestures.Hold = {
+ name: name,
+ index: 10,
+ defaults: {
+ /**
+ * @property holdTimeout
+ * @type {Number}
+ * @default 500
+ */
+ holdTimeout: 500,
+
+ /**
+ * movement allowed while holding
+ * @property holdThreshold
+ * @type {Number}
+ * @default 2
+ */
+ holdThreshold: 2
+ },
+ handler: holdGesture
+ };
+ })('hold');
+
+ /**
+ * @module gestures
+ */
+ /**
+ * when a touch is being released from the page
+ *
+ * @class Release
+ * @static
+ */
+ /**
+ * @event release
+ * @param {Object} ev
+ */
+ Hammer.gestures.Release = {
+ name: 'release',
+ index: Infinity,
+ handler: function releaseGesture(ev, inst) {
+ if(ev.eventType == EVENT_RELEASE) {
+ inst.trigger(this.name, ev);
+ }
+ }
+ };
+
+ /**
+ * @module gestures
+ */
+ /**
+ * triggers swipe events when the end velocity is above the threshold
+ * for best usage, set `preventDefault` (on the drag gesture) to `true`
+ * ````
+ * hammertime.on("dragleft swipeleft", function(ev) {
+ * console.log(ev);
+ * ev.gesture.preventDefault();
+ * });
+ * ````
+ *
+ * @class Swipe
+ * @static
+ */
+ /**
+ * @event swipe
+ * @param {Object} ev
+ */
+ /**
+ * @event swipeleft
+ * @param {Object} ev
+ */
+ /**
+ * @event swiperight
+ * @param {Object} ev
+ */
+ /**
+ * @event swipeup
+ * @param {Object} ev
+ */
+ /**
+ * @event swipedown
+ * @param {Object} ev
+ */
+ Hammer.gestures.Swipe = {
+ name: 'swipe',
+ index: 40,
+ defaults: {
+ /**
+ * @property swipeMinTouches
+ * @type {Number}
+ * @default 1
+ */
+ swipeMinTouches: 1,
+
+ /**
+ * @property swipeMaxTouches
+ * @type {Number}
+ * @default 1
+ */
+ swipeMaxTouches: 1,
+
+ /**
+ * horizontal swipe velocity
+ * @property swipeVelocityX
+ * @type {Number}
+ * @default 0.6
+ */
+ swipeVelocityX: 0.6,
+
+ /**
+ * vertical swipe velocity
+ * @property swipeVelocityY
+ * @type {Number}
+ * @default 0.6
+ */
+ swipeVelocityY: 0.6
+ },
+
+ handler: function swipeGesture(ev, inst) {
+ if(ev.eventType == EVENT_RELEASE) {
+ var touches = ev.touches.length,
+ options = inst.options;
+
+ // max touches
+ if(touches < options.swipeMinTouches ||
+ touches > options.swipeMaxTouches) {
+ return;
+ }
+
+ // when the distance we moved is too small we skip this gesture
+ // or we can be already in dragging
+ if(ev.velocityX > options.swipeVelocityX ||
+ ev.velocityY > options.swipeVelocityY) {
+ // trigger swipe events
+ inst.trigger(this.name, ev);
+ inst.trigger(this.name + ev.direction, ev);
+ }
+ }
+ }
+ };
+
+ /**
+ * @module gestures
+ */
+ /**
+ * Single tap and a double tap on a place
+ *
+ * @class Tap
+ * @static
+ */
+ /**
+ * @event tap
+ * @param {Object} ev
+ */
+ /**
+ * @event doubletap
+ * @param {Object} ev
+ */
+
+ /**
+ * @param {String} name
+ */
+ (function(name) {
+ var hasMoved = false;
+
+ function tapGesture(ev, inst) {
+ var options = inst.options,
+ current = Detection.current,
+ prev = Detection.previous,
+ sincePrev,
+ didDoubleTap;
+
+ switch(ev.eventType) {
+ case EVENT_START:
+ hasMoved = false;
+ break;
+
+ case EVENT_MOVE:
+ hasMoved = hasMoved || (ev.distance > options.tapMaxDistance);
+ break;
+
+ case EVENT_END:
+ if(!Utils.inStr(ev.srcEvent.type, 'cancel') && ev.deltaTime < options.tapMaxTime && !hasMoved) {
+ // previous gesture, for the double tap since these are two different gesture detections
+ sincePrev = prev && prev.lastEvent && ev.timeStamp - prev.lastEvent.timeStamp;
+ didDoubleTap = false;
+
+ // check if double tap
+ if(prev && prev.name == name &&
+ (sincePrev && sincePrev < options.doubleTapInterval) &&
+ ev.distance < options.doubleTapDistance) {
+ inst.trigger('doubletap', ev);
+ didDoubleTap = true;
+ }
+
+ // do a single tap
+ if(!didDoubleTap || options.tapAlways) {
+ current.name = name;
+ inst.trigger(current.name, ev);
+ }
+ }
+ break;
+ }
+ }
+
+ Hammer.gestures.Tap = {
+ name: name,
+ index: 100,
+ handler: tapGesture,
+ defaults: {
+ /**
+ * max time of a tap, this is for the slow tappers
+ * @property tapMaxTime
+ * @type {Number}
+ * @default 250
+ */
+ tapMaxTime: 250,
+
+ /**
+ * max distance of movement of a tap, this is for the slow tappers
+ * @property tapMaxDistance
+ * @type {Number}
+ * @default 10
+ */
+ tapMaxDistance: 10,
+
+ /**
+ * always trigger the `tap` event, even while double-tapping
+ * @property tapAlways
+ * @type {Boolean}
+ * @default true
+ */
+ tapAlways: true,
+
+ /**
+ * max distance between two taps
+ * @property doubleTapDistance
+ * @type {Number}
+ * @default 20
+ */
+ doubleTapDistance: 20,
+
+ /**
+ * max time between two taps
+ * @property doubleTapInterval
+ * @type {Number}
+ * @default 300
+ */
+ doubleTapInterval: 300
+ }
+ };
+ })('tap');
+
+ /**
+ * @module gestures
+ */
+ /**
+ * when a touch is being touched at the page
+ *
+ * @class Touch
+ * @static
+ */
+ /**
+ * @event touch
+ * @param {Object} ev
+ */
+ Hammer.gestures.Touch = {
+ name: 'touch',
+ index: -Infinity,
+ defaults: {
+ /**
+ * call preventDefault at touchstart, and makes the element blocking by disabling the scrolling of the page,
+ * but it improves gestures like transforming and dragging.
+ * be careful with using this, it can be very annoying for users to be stuck on the page
+ * @property preventDefault
+ * @type {Boolean}
+ * @default false
+ */
+ preventDefault: false,
+
+ /**
+ * disable mouse events, so only touch (or pen!) input triggers events
+ * @property preventMouse
+ * @type {Boolean}
+ * @default false
+ */
+ preventMouse: false
+ },
+ handler: function touchGesture(ev, inst) {
+ if(inst.options.preventMouse && ev.pointerType == POINTER_MOUSE) {
+ ev.stopDetect();
+ return;
+ }
+
+ if(inst.options.preventDefault) {
+ ev.preventDefault();
+ }
+
+ if(ev.eventType == EVENT_TOUCH) {
+ inst.trigger('touch', ev);
+ }
+ }
+ };
+
+ /**
+ * @module gestures
+ */
+ /**
+ * User want to scale or rotate with 2 fingers
+ * Preventing the default browser behavior is a good way to improve feel and working. This can be done with the
+ * `preventDefault` option.
+ *
+ * @class Transform
+ * @static
+ */
+ /**
+ * @event transform
+ * @param {Object} ev
+ */
+ /**
+ * @event transformstart
+ * @param {Object} ev
+ */
+ /**
+ * @event transformend
+ * @param {Object} ev
+ */
+ /**
+ * @event pinchin
+ * @param {Object} ev
+ */
+ /**
+ * @event pinchout
+ * @param {Object} ev
+ */
+ /**
+ * @event rotate
+ * @param {Object} ev
+ */
+
+ /**
+ * @param {String} name
+ */
+ (function(name) {
+ var triggered = false;
+
+ function transformGesture(ev, inst) {
+ switch(ev.eventType) {
+ case EVENT_START:
+ triggered = false;
+ break;
+
+ case EVENT_MOVE:
+ // at least multitouch
+ if(ev.touches.length < 2) {
+ return;
+ }
+
+ var scaleThreshold = Math.abs(1 - ev.scale);
+ var rotationThreshold = Math.abs(ev.rotation);
+
+ // when the distance we moved is too small we skip this gesture
+ // or we can be already in dragging
+ if(scaleThreshold < inst.options.transformMinScale &&
+ rotationThreshold < inst.options.transformMinRotation) {
+ return;
+ }
+
+ // we are transforming!
+ Detection.current.name = name;
+
+ // first time, trigger dragstart event
+ if(!triggered) {
+ inst.trigger(name + 'start', ev);
+ triggered = true;
+ }
+
+ inst.trigger(name, ev); // basic transform event
+
+ // trigger rotate event
+ if(rotationThreshold > inst.options.transformMinRotation) {
+ inst.trigger('rotate', ev);
+ }
+
+ // trigger pinch event
+ if(scaleThreshold > inst.options.transformMinScale) {
+ inst.trigger('pinch', ev);
+ inst.trigger('pinch' + (ev.scale < 1 ? 'in' : 'out'), ev);
+ }
+ break;
+
+ case EVENT_RELEASE:
+ if(triggered && ev.changedLength < 2) {
+ inst.trigger(name + 'end', ev);
+ triggered = false;
+ }
+ break;
+ }
+ }
+
+ Hammer.gestures.Transform = {
+ name: name,
+ index: 45,
+ defaults: {
+ /**
+ * minimal scale factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
+ * @property transformMinScale
+ * @type {Number}
+ * @default 0.01
+ */
+ transformMinScale: 0.01,
+
+ /**
+ * rotation in degrees
+ * @property transformMinRotation
+ * @type {Number}
+ * @default 1
+ */
+ transformMinRotation: 1
+ },
+
+ handler: transformGesture
+ };
+ })('transform');
+
+ /**
+ * @module hammer
+ */
+
+ // AMD export
+ if(true) {
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = function() {
+ return Hammer;
+ }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ // commonjs export
+ } else if(typeof module !== 'undefined' && module.exports) {
+ module.exports = Hammer;
+ // browser export
+ } else {
+ window.Hammer = Hammer;
+ }
+
+ })(window);
+
+/***/ },
+/* 21 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var util = __webpack_require__(1);
+ var hammerUtil = __webpack_require__(22);
+ var moment = __webpack_require__(2);
+ var Component = __webpack_require__(23);
+ var DateUtil = __webpack_require__(24);
+
+ /**
+ * @constructor Range
+ * A Range controls a numeric range with a start and end value.
+ * The Range adjusts the range based on mouse events or programmatic changes,
+ * and triggers events when the range is changing or has been changed.
+ * @param {{dom: Object, domProps: Object, emitter: Emitter}} body
+ * @param {Object} [options] See description at Range.setOptions
+ */
+ function Range(body, options) {
+ var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
+ this.start = now.clone().add(-3, 'days').valueOf(); // Number
+ this.end = now.clone().add(4, 'days').valueOf(); // Number
+
+ this.body = body;
+ this.deltaDifference = 0;
+ this.scaleOffset = 0;
+ this.startToFront = false;
+ this.endToFront = true;
+
+ // default options
+ this.defaultOptions = {
+ start: null,
+ end: null,
+ direction: 'horizontal', // 'horizontal' or 'vertical'
+ moveable: true,
+ zoomable: true,
+ min: null,
+ max: null,
+ zoomMin: 10, // milliseconds
+ zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds
+ };
+ this.options = util.extend({}, this.defaultOptions);
+
+ this.props = {
+ touch: {}
+ };
+ this.animateTimer = null;
+
+ // drag listeners for dragging
+ this.body.emitter.on('dragstart', this._onDragStart.bind(this));
+ this.body.emitter.on('drag', this._onDrag.bind(this));
+ this.body.emitter.on('dragend', this._onDragEnd.bind(this));
+
+ // ignore dragging when holding
+ this.body.emitter.on('hold', this._onHold.bind(this));
+
+ // mouse wheel for zooming
+ this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this));
+ this.body.emitter.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
+
+ // pinch to zoom
+ this.body.emitter.on('touch', this._onTouch.bind(this));
+ this.body.emitter.on('pinch', this._onPinch.bind(this));
+
+ this.setOptions(options);
+ }
+
+ Range.prototype = new Component();
+
+ /**
+ * Set options for the range controller
+ * @param {Object} options Available options:
+ * {Number | Date | String} start Start date for the range
+ * {Number | Date | String} end End date for the range
+ * {Number} min Minimum value for start
+ * {Number} max Maximum value for end
+ * {Number} zoomMin Set a minimum value for
+ * (end - start).
+ * {Number} zoomMax Set a maximum value for
+ * (end - start).
+ * {Boolean} moveable Enable moving of the range
+ * by dragging. True by default
+ * {Boolean} zoomable Enable zooming of the range
+ * by pinching/scrolling. True by default
+ */
+ Range.prototype.setOptions = function (options) {
+ if (options) {
+ // copy the options that we know
+ var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', 'activate', 'hiddenDates'];
+ util.selectiveExtend(fields, this.options, options);
+
+ if ('start' in options || 'end' in options) {
+ // apply a new range. both start and end are optional
+ this.setRange(options.start, options.end);
+ }
+ }
+ };
+
+ /**
+ * Test whether direction has a valid value
+ * @param {String} direction 'horizontal' or 'vertical'
+ */
+ function validateDirection (direction) {
+ if (direction != 'horizontal' && direction != 'vertical') {
+ throw new TypeError('Unknown direction "' + direction + '". ' +
+ 'Choose "horizontal" or "vertical".');
+ }
+ }
+
+ /**
+ * Set a new start and end range
+ * @param {Date | Number | String} [start]
+ * @param {Date | Number | String} [end]
+ * @param {boolean | number} [animate=false] If true, the range is animated
+ * smoothly to the new window.
+ * If animate is a number, the
+ * number is taken as duration
+ * Default duration is 500 ms.
+ *
+ */
+ Range.prototype.setRange = function(start, end, animate) {
+ var _start = start != undefined ? util.convert(start, 'Date').valueOf() : null;
+ var _end = end != undefined ? util.convert(end, 'Date').valueOf() : null;
+ this._cancelAnimation();
+
+ if (animate) {
+ var me = this;
+ var initStart = this.start;
+ var initEnd = this.end;
+ var duration = typeof animate === 'number' ? animate : 500;
+ var initTime = new Date().valueOf();
+ var anyChanged = false;
+
+ var next = function () {
+ if (!me.props.touch.dragging) {
+ var now = new Date().valueOf();
+ var time = now - initTime;
+ var done = time > duration;
+ var s = (done || _start === null) ? _start : util.easeInOutQuad(time, initStart, _start, duration);
+ var e = (done || _end === null) ? _end : util.easeInOutQuad(time, initEnd, _end, duration);
+
+ changed = me._applyRange(s, e);
+ DateUtil.updateHiddenDates(me.body, me.options.hiddenDates);
+ anyChanged = anyChanged || changed;
+ if (changed) {
+ me.body.emitter.emit('rangechange', {start: new Date(me.start), end: new Date(me.end)});
+ }
+
+ if (done) {
+ if (anyChanged) {
+ me.body.emitter.emit('rangechanged', {start: new Date(me.start), end: new Date(me.end)});
+ }
+ }
+ else {
+ // animate with as high as possible frame rate, leave 20 ms in between
+ // each to prevent the browser from blocking
+ me.animateTimer = setTimeout(next, 20);
+ }
+ }
+ }
+
+ return next();
+ }
+ else {
+ var changed = this._applyRange(_start, _end);
+ DateUtil.updateHiddenDates(this.body, this.options.hiddenDates);
+ if (changed) {
+ var params = {start: new Date(this.start), end: new Date(this.end)};
+ this.body.emitter.emit('rangechange', params);
+ this.body.emitter.emit('rangechanged', params);
+ }
+ }
+ };
+
+ /**
+ * Stop an animation
+ * @private
+ */
+ Range.prototype._cancelAnimation = function () {
+ if (this.animateTimer) {
+ clearTimeout(this.animateTimer);
+ this.animateTimer = null;
+ }
+ };
+
+ /**
+ * Set a new start and end range. This method is the same as setRange, but
+ * does not trigger a range change and range changed event, and it returns
+ * true when the range is changed
+ * @param {Number} [start]
+ * @param {Number} [end]
+ * @return {Boolean} changed
+ * @private
+ */
+ Range.prototype._applyRange = function(start, end) {
+ var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
+ newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
+ max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
+ min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
+ diff;
+
+ // check for valid number
+ if (isNaN(newStart) || newStart === null) {
+ throw new Error('Invalid start "' + start + '"');
+ }
+ if (isNaN(newEnd) || newEnd === null) {
+ throw new Error('Invalid end "' + end + '"');
+ }
+
+ // prevent start < end
+ if (newEnd < newStart) {
+ newEnd = newStart;
+ }
+
+ // prevent start < min
+ if (min !== null) {
+ if (newStart < min) {
+ diff = (min - newStart);
+ newStart += diff;
+ newEnd += diff;
+
+ // prevent end > max
+ if (max != null) {
+ if (newEnd > max) {
+ newEnd = max;
+ }
+ }
+ }
+ }
+
+ // prevent end > max
+ if (max !== null) {
+ if (newEnd > max) {
+ diff = (newEnd - max);
+ newStart -= diff;
+ newEnd -= diff;
+
+ // prevent start < min
+ if (min != null) {
+ if (newStart < min) {
+ newStart = min;
+ }
+ }
+ }
+ }
+
+ // prevent (end-start) < zoomMin
+ if (this.options.zoomMin !== null) {
+ var zoomMin = parseFloat(this.options.zoomMin);
+ if (zoomMin < 0) {
+ zoomMin = 0;
+ }
+ if ((newEnd - newStart) < zoomMin) {
+ if ((this.end - this.start) === zoomMin) {
+ // ignore this action, we are already zoomed to the minimum
+ newStart = this.start;
+ newEnd = this.end;
+ }
+ else {
+ // zoom to the minimum
+ diff = (zoomMin - (newEnd - newStart));
+ newStart -= diff / 2;
+ newEnd += diff / 2;
+ }
+ }
+ }
+
+ // prevent (end-start) > zoomMax
+ if (this.options.zoomMax !== null) {
+ var zoomMax = parseFloat(this.options.zoomMax);
+ if (zoomMax < 0) {
+ zoomMax = 0;
+ }
+ if ((newEnd - newStart) > zoomMax) {
+ if ((this.end - this.start) === zoomMax) {
+ // ignore this action, we are already zoomed to the maximum
+ newStart = this.start;
+ newEnd = this.end;
+ }
+ else {
+ // zoom to the maximum
+ diff = ((newEnd - newStart) - zoomMax);
+ newStart += diff / 2;
+ newEnd -= diff / 2;
+ }
+ }
+ }
+
+ var changed = (this.start != newStart || this.end != newEnd);
+
+ // if the new range does NOT overlap with the old range, emit checkRangedItems to avoid not showing ranged items (ranged meaning has end time, not neccesarily of type Range)
+ if (!((newStart >= this.start && newStart <= this.end) || (newEnd >= this.start && newEnd <= this.end)) &&
+ !((this.start >= newStart && this.start <= newEnd) || (this.end >= newStart && this.end <= newEnd) )) {
+ this.body.emitter.emit('checkRangedItems');
+ }
+
+ this.start = newStart;
+ this.end = newEnd;
+ return changed;
+ };
+
+ /**
+ * Retrieve the current range.
+ * @return {Object} An object with start and end properties
+ */
+ Range.prototype.getRange = function() {
+ return {
+ start: this.start,
+ end: this.end
+ };
+ };
+
+ /**
+ * Calculate the conversion offset and scale for current range, based on
+ * the provided width
+ * @param {Number} width
+ * @returns {{offset: number, scale: number}} conversion
+ */
+ Range.prototype.conversion = function (width, totalHidden) {
+ return Range.conversion(this.start, this.end, width, totalHidden);
+ };
+
+ /**
+ * Static method to calculate the conversion offset and scale for a range,
+ * based on the provided start, end, and width
+ * @param {Number} start
+ * @param {Number} end
+ * @param {Number} width
+ * @returns {{offset: number, scale: number}} conversion
+ */
+ Range.conversion = function (start, end, width, totalHidden) {
+ if (totalHidden === undefined) {
+ totalHidden = 0;
+ }
+ if (width != 0 && (end - start != 0)) {
+ return {
+ offset: start,
+ scale: width / (end - start - totalHidden)
+ }
+ }
+ else {
+ return {
+ offset: 0,
+ scale: 1
+ };
+ }
+ };
+
+ /**
+ * Start dragging horizontally or vertically
+ * @param {Event} event
+ * @private
+ */
+ Range.prototype._onDragStart = function(event) {
+ this.deltaDifference = 0;
+ this.previousDelta = 0;
+ // only allow dragging when configured as movable
+ if (!this.options.moveable) return;
+
+ // refuse to drag when we where pinching to prevent the timeline make a jump
+ // when releasing the fingers in opposite order from the touch screen
+ if (!this.props.touch.allowDragging) return;
+
+ this.props.touch.start = this.start;
+ this.props.touch.end = this.end;
+ this.props.touch.dragging = true;
+
+ if (this.body.dom.root) {
+ this.body.dom.root.style.cursor = 'move';
+ }
+ };
+
+ /**
+ * Perform dragging operation
+ * @param {Event} event
+ * @private
+ */
+ Range.prototype._onDrag = function (event) {
+ // only allow dragging when configured as movable
+ if (!this.options.moveable) return;
+ // refuse to drag when we where pinching to prevent the timeline make a jump
+ // when releasing the fingers in opposite order from the touch screen
+ if (!this.props.touch.allowDragging) return;
+
+ var direction = this.options.direction;
+ validateDirection(direction);
+
+ var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY;
+ delta -= this.deltaDifference;
+ var interval = (this.props.touch.end - this.props.touch.start);
+
+ // normalize dragging speed if cutout is in between.
+ var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end);
+ interval -= duration;
+
+ var width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height;
+ var diffRange = -delta / width * interval;
+ var newStart = this.props.touch.start + diffRange;
+ var newEnd = this.props.touch.end + diffRange;
+
+
+ // snapping times away from hidden zones
+ var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, this.previousDelta-delta, true);
+ var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, this.previousDelta-delta, true);
+ if (safeStart != newStart || safeEnd != newEnd) {
+ this.deltaDifference += delta;
+ this.props.touch.start = safeStart;
+ this.props.touch.end = safeEnd;
+ this._onDrag(event);
+ return;
+ }
+
+ this.previousDelta = delta;
+ this._applyRange(newStart, newEnd);
+
+ // fire a rangechange event
+ this.body.emitter.emit('rangechange', {
+ start: new Date(this.start),
+ end: new Date(this.end)
+ });
+ };
+
+ /**
+ * Stop dragging operation
+ * @param {event} event
+ * @private
+ */
+ Range.prototype._onDragEnd = function (event) {
+ // only allow dragging when configured as movable
+ if (!this.options.moveable) return;
+
+ // refuse to drag when we where pinching to prevent the timeline make a jump
+ // when releasing the fingers in opposite order from the touch screen
+ if (!this.props.touch.allowDragging) return;
+
+ this.props.touch.dragging = false;
+ if (this.body.dom.root) {
+ this.body.dom.root.style.cursor = 'auto';
+ }
+
+ // fire a rangechanged event
+ this.body.emitter.emit('rangechanged', {
+ start: new Date(this.start),
+ end: new Date(this.end)
+ });
+ };
+
+ /**
+ * Event handler for mouse wheel event, used to zoom
+ * Code from http://adomas.org/javascript-mouse-wheel/
+ * @param {Event} event
+ * @private
+ */
+ Range.prototype._onMouseWheel = function(event) {
+ // only allow zooming when configured as zoomable and moveable
+ if (!(this.options.zoomable && this.options.moveable)) return;
+
+ // retrieve delta
+ var delta = 0;
+ if (event.wheelDelta) { /* IE/Opera. */
+ delta = event.wheelDelta / 120;
+ } else if (event.detail) { /* Mozilla case. */
+ // In Mozilla, sign of delta is different than in IE.
+ // Also, delta is multiple of 3.
+ delta = -event.detail / 3;
+ }
+
+ // If delta is nonzero, handle it.
+ // Basically, delta is now positive if wheel was scrolled up,
+ // and negative, if wheel was scrolled down.
+ if (delta) {
+ // perform the zoom action. Delta is normally 1 or -1
+
+ // adjust a negative delta such that zooming in with delta 0.1
+ // equals zooming out with a delta -0.1
+ var scale;
+ if (delta < 0) {
+ scale = 1 - (delta / 5);
+ }
+ else {
+ scale = 1 / (1 + (delta / 5)) ;
+ }
+
+ // calculate center, the date to zoom around
+ var gesture = hammerUtil.fakeGesture(this, event),
+ pointer = getPointer(gesture.center, this.body.dom.center),
+ pointerDate = this._pointerToDate(pointer);
+
+ this.zoom(scale, pointerDate, delta);
+ }
+
+ // Prevent default actions caused by mouse wheel
+ // (else the page and timeline both zoom and scroll)
+ event.preventDefault();
+ };
+
+ /**
+ * Start of a touch gesture
+ * @private
+ */
+ Range.prototype._onTouch = function (event) {
+ this.props.touch.start = this.start;
+ this.props.touch.end = this.end;
+ this.props.touch.allowDragging = true;
+ this.props.touch.center = null;
+ this.scaleOffset = 0;
+ this.deltaDifference = 0;
+ };
+
+ /**
+ * On start of a hold gesture
+ * @private
+ */
+ Range.prototype._onHold = function () {
+ this.props.touch.allowDragging = false;
+ };
+
+ /**
+ * Handle pinch event
+ * @param {Event} event
+ * @private
+ */
+ Range.prototype._onPinch = function (event) {
+ // only allow zooming when configured as zoomable and moveable
+ if (!(this.options.zoomable && this.options.moveable)) return;
+
+ this.props.touch.allowDragging = false;
+
+ if (event.gesture.touches.length > 1) {
+ if (!this.props.touch.center) {
+ this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center);
+ }
+
+ var scale = 1 / (event.gesture.scale + this.scaleOffset);
+ var centerDate = this._pointerToDate(this.props.touch.center);
+
+ var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end);
+ var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, centerDate);
+ var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore;
+
+ // calculate new start and end
+ var newStart = (centerDate - hiddenDurationBefore) + (this.props.touch.start - (centerDate - hiddenDurationBefore)) * scale;
+ var newEnd = (centerDate + hiddenDurationAfter) + (this.props.touch.end - (centerDate + hiddenDurationAfter)) * scale;
+
+ // snapping times away from hidden zones
+ this.startToFront = 1 - scale > 0 ? false : true; // used to do the right autocorrection with periodic hidden times
+ this.endToFront = scale - 1 > 0 ? false : true; // used to do the right autocorrection with periodic hidden times
+
+ var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, 1 - scale, true);
+ var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, scale - 1, true);
+ if (safeStart != newStart || safeEnd != newEnd) {
+ this.props.touch.start = safeStart;
+ this.props.touch.end = safeEnd;
+ this.scaleOffset = 1 - event.gesture.scale;
+ newStart = safeStart;
+ newEnd = safeEnd;
+ }
+
+ this.setRange(newStart, newEnd);
+
+ this.startToFront = false; // revert to default
+ this.endToFront = true; // revert to default
+ }
+ };
+
+ /**
+ * Helper function to calculate the center date for zooming
+ * @param {{x: Number, y: Number}} pointer
+ * @return {number} date
+ * @private
+ */
+ Range.prototype._pointerToDate = function (pointer) {
+ var conversion;
+ var direction = this.options.direction;
+
+ validateDirection(direction);
+
+ if (direction == 'horizontal') {
+ return this.body.util.toTime(pointer.x).valueOf();
+ }
+ else {
+ var height = this.body.domProps.center.height;
+ conversion = this.conversion(height);
+ return pointer.y / conversion.scale + conversion.offset;
+ }
+ };
+
+ /**
+ * Get the pointer location relative to the location of the dom element
+ * @param {{pageX: Number, pageY: Number}} touch
+ * @param {Element} element HTML DOM element
+ * @return {{x: Number, y: Number}} pointer
+ * @private
+ */
+ function getPointer (touch, element) {
+ return {
+ x: touch.pageX - util.getAbsoluteLeft(element),
+ y: touch.pageY - util.getAbsoluteTop(element)
+ };
+ }
+
+ /**
+ * Zoom the range the given scale in or out. Start and end date will
+ * be adjusted, and the timeline will be redrawn. You can optionally give a
+ * date around which to zoom.
+ * For example, try scale = 0.9 or 1.1
+ * @param {Number} scale Scaling factor. Values above 1 will zoom out,
+ * values below 1 will zoom in.
+ * @param {Number} [center] Value representing a date around which will
+ * be zoomed.
+ */
+ Range.prototype.zoom = function(scale, center, delta) {
+ // if centerDate is not provided, take it half between start Date and end Date
+ if (center == null) {
+ center = (this.start + this.end) / 2;
+ }
+
+ var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end);
+ var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, center);
+ var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore;
+
+ // calculate new start and end
+ var newStart = (center-hiddenDurationBefore) + (this.start - (center-hiddenDurationBefore)) * scale;
+ var newEnd = (center+hiddenDurationAfter) + (this.end - (center+hiddenDurationAfter)) * scale;
+
+ // snapping times away from hidden zones
+ this.startToFront = delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times
+ this.endToFront = -delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times
+ var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, delta, true);
+ var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, -delta, true);
+ if (safeStart != newStart || safeEnd != newEnd) {
+ newStart = safeStart;
+ newEnd = safeEnd;
+ }
+
+ this.setRange(newStart, newEnd);
+
+ this.startToFront = false; // revert to default
+ this.endToFront = true; // revert to default
+ };
+
+
+
+ /**
+ * Move the range with a given delta to the left or right. Start and end
+ * value will be adjusted. For example, try delta = 0.1 or -0.1
+ * @param {Number} delta Moving amount. Positive value will move right,
+ * negative value will move left
+ */
+ Range.prototype.move = function(delta) {
+ // zoom start Date and end Date relative to the centerDate
+ var diff = (this.end - this.start);
+
+ // apply new values
+ var newStart = this.start + diff * delta;
+ var newEnd = this.end + diff * delta;
+
+ // TODO: reckon with min and max range
+
+ this.start = newStart;
+ this.end = newEnd;
+ };
+
+ /**
+ * Move the range to a new center point
+ * @param {Number} moveTo New center point of the range
+ */
+ Range.prototype.moveTo = function(moveTo) {
+ var center = (this.start + this.end) / 2;
+
+ var diff = center - moveTo;
+
+ // calculate new start and end
+ var newStart = this.start - diff;
+ var newEnd = this.end - diff;
+
+ this.setRange(newStart, newEnd);
+ };
+
+ module.exports = Range;
+
+
+/***/ },
+/* 22 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Hammer = __webpack_require__(19);
+
+ /**
+ * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent
+ * @param {Element} element
+ * @param {Event} event
+ */
+ exports.fakeGesture = function(element, event) {
+ var eventType = null;
+
+ // for hammer.js 1.0.5
+ // var gesture = Hammer.event.collectEventData(this, eventType, event);
+
+ // for hammer.js 1.0.6+
+ var touches = Hammer.event.getTouchList(event, eventType);
+ var gesture = Hammer.event.collectEventData(this, eventType, touches, event);
+
+ // on IE in standards mode, no touches are recognized by hammer.js,
+ // resulting in NaN values for center.pageX and center.pageY
+ if (isNaN(gesture.center.pageX)) {
+ gesture.center.pageX = event.pageX;
+ }
+ if (isNaN(gesture.center.pageY)) {
+ gesture.center.pageY = event.pageY;
+ }
+
+ return gesture;
+ };
+
+
+/***/ },
+/* 23 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Prototype for visual components
+ * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body]
+ * @param {Object} [options]
+ */
+ function Component (body, options) {
+ this.options = null;
+ this.props = null;
+ }
+
+ /**
+ * Set options for the component. The new options will be merged into the
+ * current options.
+ * @param {Object} options
+ */
+ Component.prototype.setOptions = function(options) {
+ if (options) {
+ util.extend(this.options, options);
+ }
+ };
+
+ /**
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
+ */
+ Component.prototype.redraw = function() {
+ // should be implemented by the component
+ return false;
+ };
+
+ /**
+ * Destroy the component. Cleanup DOM and event listeners
+ */
+ Component.prototype.destroy = function() {
+ // should be implemented by the component
+ };
+
+ /**
+ * Test whether the component is resized since the last time _isResized() was
+ * called.
+ * @return {Boolean} Returns true if the component is resized
+ * @protected
+ */
+ Component.prototype._isResized = function() {
+ var resized = (this.props._previousWidth !== this.props.width ||
+ this.props._previousHeight !== this.props.height);
+
+ this.props._previousWidth = this.props.width;
+ this.props._previousHeight = this.props.height;
+
+ return resized;
+ };
+
+ module.exports = Component;
+
+
+/***/ },
+/* 24 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Created by Alex on 10/3/2014.
+ */
+ var moment = __webpack_require__(2);
+
+
+ /**
+ * used in Core to convert the options into a volatile variable
+ *
+ * @param Core
+ */
+ exports.convertHiddenOptions = function(body, hiddenDates) {
+ body.hiddenDates = [];
+ if (hiddenDates) {
+ if (Array.isArray(hiddenDates) == true) {
+ for (var i = 0; i < hiddenDates.length; i++) {
+ if (hiddenDates[i].repeat === undefined) {
+ var dateItem = {};
+ dateItem.start = moment(hiddenDates[i].start).toDate().valueOf();
+ dateItem.end = moment(hiddenDates[i].end).toDate().valueOf();
+ body.hiddenDates.push(dateItem);
+ }
+ }
+ body.hiddenDates.sort(function (a, b) {
+ return a.start - b.start;
+ }); // sort by start time
+ }
+ }
+ };
+
+
+ /**
+ * create new entrees for the repeating hidden dates
+ * @param body
+ * @param hiddenDates
+ */
+ exports.updateHiddenDates = function (body, hiddenDates) {
+ if (hiddenDates && body.domProps.centerContainer.width !== undefined) {
+ exports.convertHiddenOptions(body, hiddenDates);
+
+ var start = moment(body.range.start);
+ var end = moment(body.range.end);
+
+ var totalRange = (body.range.end - body.range.start);
+ var pixelTime = totalRange / body.domProps.centerContainer.width;
+
+ for (var i = 0; i < hiddenDates.length; i++) {
+ if (hiddenDates[i].repeat !== undefined) {
+ var startDate = moment(hiddenDates[i].start);
+ var endDate = moment(hiddenDates[i].end);
+
+ if (startDate._d == "Invalid Date") {
+ throw new Error("Supplied start date is not valid: " + hiddenDates[i].start);
+ }
+ if (endDate._d == "Invalid Date") {
+ throw new Error("Supplied end date is not valid: " + hiddenDates[i].end);
+ }
+
+ var duration = endDate - startDate;
+ if (duration >= 4 * pixelTime) {
+
+ var offset = 0;
+ var runUntil = end.clone();
+ switch (hiddenDates[i].repeat) {
+ case "daily": // case of time
+ if (startDate.day() != endDate.day()) {
+ offset = 1;
+ }
+ startDate.dayOfYear(start.dayOfYear());
+ startDate.year(start.year());
+ startDate.subtract(7,'days');
+
+ endDate.dayOfYear(start.dayOfYear());
+ endDate.year(start.year());
+ endDate.subtract(7 - offset,'days');
+
+ runUntil.add(1, 'weeks');
+ break;
+ case "weekly":
+ var dayOffset = endDate.diff(startDate,'days')
+ var day = startDate.day();
+
+ // set the start date to the range.start
+ startDate.date(start.date());
+ startDate.month(start.month());
+ startDate.year(start.year());
+ endDate = startDate.clone();
+
+ // force
+ startDate.day(day);
+ endDate.day(day);
+ endDate.add(dayOffset,'days');
+
+ startDate.subtract(1,'weeks');
+ endDate.subtract(1,'weeks');
+
+ runUntil.add(1, 'weeks');
+ break
+ case "monthly":
+ if (startDate.month() != endDate.month()) {
+ offset = 1;
+ }
+ startDate.month(start.month());
+ startDate.year(start.year());
+ startDate.subtract(1,'months');
+
+ endDate.month(start.month());
+ endDate.year(start.year());
+ endDate.subtract(1,'months');
+ endDate.add(offset,'months');
+
+ runUntil.add(1, 'months');
+ break;
+ case "yearly":
+ if (startDate.year() != endDate.year()) {
+ offset = 1;
+ }
+ startDate.year(start.year());
+ startDate.subtract(1,'years');
+ endDate.year(start.year());
+ endDate.subtract(1,'years');
+ endDate.add(offset,'years');
+
+ runUntil.add(1, 'years');
+ break;
+ default:
+ console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat);
+ return;
+ }
+ while (startDate < runUntil) {
+ body.hiddenDates.push({start: startDate.valueOf(), end: endDate.valueOf()});
+ switch (hiddenDates[i].repeat) {
+ case "daily":
+ startDate.add(1, 'days');
+ endDate.add(1, 'days');
+ break;
+ case "weekly":
+ startDate.add(1, 'weeks');
+ endDate.add(1, 'weeks');
+ break
+ case "monthly":
+ startDate.add(1, 'months');
+ endDate.add(1, 'months');
+ break;
+ case "yearly":
+ startDate.add(1, 'y');
+ endDate.add(1, 'y');
+ break;
+ default:
+ console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat);
+ return;
+ }
+ }
+ body.hiddenDates.push({start: startDate.valueOf(), end: endDate.valueOf()});
+ }
+ }
+ }
+ // remove duplicates, merge where possible
+ exports.removeDuplicates(body);
+ // ensure the new positions are not on hidden dates
+ var startHidden = exports.isHidden(body.range.start, body.hiddenDates);
+ var endHidden = exports.isHidden(body.range.end,body.hiddenDates);
+ var rangeStart = body.range.start;
+ var rangeEnd = body.range.end;
+ if (startHidden.hidden == true) {rangeStart = body.range.startToFront == true ? startHidden.startDate - 1 : startHidden.endDate + 1;}
+ if (endHidden.hidden == true) {rangeEnd = body.range.endToFront == true ? endHidden.startDate - 1 : endHidden.endDate + 1;}
+ if (startHidden.hidden == true || endHidden.hidden == true) {
+ body.range._applyRange(rangeStart, rangeEnd);
+ }
+ }
+
+ }
+
+
+ /**
+ * remove duplicates from the hidden dates list. Duplicates are evil. They mess everything up.
+ * Scales with N^2
+ * @param body
+ */
+ exports.removeDuplicates = function(body) {
+ var hiddenDates = body.hiddenDates;
+ var safeDates = [];
+ for (var i = 0; i < hiddenDates.length; i++) {
+ for (var j = 0; j < hiddenDates.length; j++) {
+ if (i != j && hiddenDates[j].remove != true && hiddenDates[i].remove != true) {
+ // j inside i
+ if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) {
+ hiddenDates[j].remove = true;
+ }
+ // j start inside i
+ else if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].start <= hiddenDates[i].end) {
+ hiddenDates[i].end = hiddenDates[j].end;
+ hiddenDates[j].remove = true;
+ }
+ // j end inside i
+ else if (hiddenDates[j].end >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) {
+ hiddenDates[i].start = hiddenDates[j].start;
+ hiddenDates[j].remove = true;
+ }
+ }
+ }
+ }
+
+ for (var i = 0; i < hiddenDates.length; i++) {
+ if (hiddenDates[i].remove !== true) {
+ safeDates.push(hiddenDates[i]);
+ }
+ }
+
+ body.hiddenDates = safeDates;
+ body.hiddenDates.sort(function (a, b) {
+ return a.start - b.start;
+ }); // sort by start time
+ }
+
+ exports.printDates = function(dates) {
+ for (var i =0; i < dates.length; i++) {
+ console.log(i, new Date(dates[i].start),new Date(dates[i].end), dates[i].start, dates[i].end, dates[i].remove);
+ }
+ }
+
+ /**
+ * Used in TimeStep to avoid the hidden times.
+ * @param timeStep
+ * @param previousTime
+ */
+ exports.stepOverHiddenDates = function(timeStep, previousTime) {
+ var stepInHidden = false;
+ var currentValue = timeStep.current.valueOf();
+ for (var i = 0; i < timeStep.hiddenDates.length; i++) {
+ var startDate = timeStep.hiddenDates[i].start;
+ var endDate = timeStep.hiddenDates[i].end;
+ if (currentValue >= startDate && currentValue < endDate) {
+ stepInHidden = true;
+ break;
+ }
+ }
+
+ if (stepInHidden == true && currentValue < timeStep._end.valueOf() && currentValue != previousTime) {
+ var prevValue = moment(previousTime);
+ var newValue = moment(endDate);
+ //check if the next step should be major
+ if (prevValue.year() != newValue.year()) {timeStep.switchedYear = true;}
+ else if (prevValue.month() != newValue.month()) {timeStep.switchedMonth = true;}
+ else if (prevValue.dayOfYear() != newValue.dayOfYear()) {timeStep.switchedDay = true;}
+
+ timeStep.current = newValue.toDate();
+ }
+ };
+
+
+ ///**
+ // * Used in TimeStep to avoid the hidden times.
+ // * @param timeStep
+ // * @param previousTime
+ // */
+ //exports.checkFirstStep = function(timeStep) {
+ // var stepInHidden = false;
+ // var currentValue = timeStep.current.valueOf();
+ // for (var i = 0; i < timeStep.hiddenDates.length; i++) {
+ // var startDate = timeStep.hiddenDates[i].start;
+ // var endDate = timeStep.hiddenDates[i].end;
+ // if (currentValue >= startDate && currentValue < endDate) {
+ // stepInHidden = true;
+ // break;
+ // }
+ // }
+ //
+ // if (stepInHidden == true && currentValue <= timeStep._end.valueOf()) {
+ // var newValue = moment(endDate);
+ // timeStep.current = newValue.toDate();
+ // }
+ //};
+
+ /**
+ * replaces the Core toScreen methods
+ * @param Core
+ * @param time
+ * @param width
+ * @returns {number}
+ */
+ exports.toScreen = function(Core, time, width) {
+ if (Core.body.hiddenDates.length == 0) {
+ var conversion = Core.range.conversion(width);
+ return (time.valueOf() - conversion.offset) * conversion.scale;
+ }
+ else {
+ var hidden = exports.isHidden(time, Core.body.hiddenDates)
+ if (hidden.hidden == true) {
+ time = hidden.startDate;
+ }
+
+ var duration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end);
+ time = exports.correctTimeForHidden(Core.body.hiddenDates, Core.range, time);
+
+ var conversion = Core.range.conversion(width, duration);
+ return (time.valueOf() - conversion.offset) * conversion.scale;
+ }
+ };
+
+
+ /**
+ * Replaces the core toTime methods
+ * @param body
+ * @param range
+ * @param x
+ * @param width
+ * @returns {Date}
+ */
+ exports.toTime = function(Core, x, width) {
+ if (Core.body.hiddenDates.length == 0) {
+ var conversion = Core.range.conversion(width);
+ return new Date(x / conversion.scale + conversion.offset);
+ }
+ else {
+ var hiddenDuration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end);
+ var totalDuration = Core.range.end - Core.range.start - hiddenDuration;
+ var partialDuration = totalDuration * x / width;
+ var accumulatedHiddenDuration = exports.getAccumulatedHiddenDuration(Core.body.hiddenDates, Core.range, partialDuration);
+
+ var newTime = new Date(accumulatedHiddenDuration + partialDuration + Core.range.start);
+ return newTime;
+ }
+ };
+
+
+ /**
+ * Support function
+ *
+ * @param hiddenDates
+ * @param range
+ * @returns {number}
+ */
+ exports.getHiddenDurationBetween = function(hiddenDates, start, end) {
+ var duration = 0;
+ for (var i = 0; i < hiddenDates.length; i++) {
+ var startDate = hiddenDates[i].start;
+ var endDate = hiddenDates[i].end;
+ // if time after the cutout, and the
+ if (startDate >= start && endDate < end) {
+ duration += endDate - startDate;
+ }
+ }
+ return duration;
+ };
+
+
+ /**
+ * Support function
+ * @param hiddenDates
+ * @param range
+ * @param time
+ * @returns {{duration: number, time: *, offset: number}}
+ */
+ exports.correctTimeForHidden = function(hiddenDates, range, time) {
+ time = moment(time).toDate().valueOf();
+ time -= exports.getHiddenDurationBefore(hiddenDates,range,time);
+ return time;
+ };
+
+ exports.getHiddenDurationBefore = function(hiddenDates, range, time) {
+ var timeOffset = 0;
+ time = moment(time).toDate().valueOf();
+
+ for (var i = 0; i < hiddenDates.length; i++) {
+ var startDate = hiddenDates[i].start;
+ var endDate = hiddenDates[i].end;
+ // if time after the cutout, and the
+ if (startDate >= range.start && endDate < range.end) {
+ if (time >= endDate) {
+ timeOffset += (endDate - startDate);
+ }
+ }
+ }
+ return timeOffset;
+ }
+
+ /**
+ * sum the duration from start to finish, including the hidden duration,
+ * until the required amount has been reached, return the accumulated hidden duration
+ * @param hiddenDates
+ * @param range
+ * @param time
+ * @returns {{duration: number, time: *, offset: number}}
+ */
+ exports.getAccumulatedHiddenDuration = function(hiddenDates, range, requiredDuration) {
+ var hiddenDuration = 0;
+ var duration = 0;
+ var previousPoint = range.start;
+ //exports.printDates(hiddenDates)
+ for (var i = 0; i < hiddenDates.length; i++) {
+ var startDate = hiddenDates[i].start;
+ var endDate = hiddenDates[i].end;
+ // if time after the cutout, and the
+ if (startDate >= range.start && endDate < range.end) {
+ duration += startDate - previousPoint;
+ previousPoint = endDate;
+ if (duration >= requiredDuration) {
+ break;
+ }
+ else {
+ hiddenDuration += endDate - startDate;
+ }
+ }
+ }
+
+ return hiddenDuration;
+ };
+
+
+
+ /**
+ * used to step over to either side of a hidden block. Correction is disabled on tablets, might be set to true
+ * @param hiddenDates
+ * @param time
+ * @param direction
+ * @param correctionEnabled
+ * @returns {*}
+ */
+ exports.snapAwayFromHidden = function(hiddenDates, time, direction, correctionEnabled) {
+ var isHidden = exports.isHidden(time, hiddenDates);
+ if (isHidden.hidden == true) {
+ if (direction < 0) {
+ if (correctionEnabled == true) {
+ return isHidden.startDate - (isHidden.endDate - time) - 1;
+ }
+ else {
+ return isHidden.startDate - 1;
+ }
+ }
+ else {
+ if (correctionEnabled == true) {
+ return isHidden.endDate + (time - isHidden.startDate) + 1;
+ }
+ else {
+ return isHidden.endDate + 1;
+ }
+ }
+ }
+ else {
+ return time;
+ }
+
+ }
+
+
+ /**
+ * Check if a time is hidden
+ *
+ * @param time
+ * @param hiddenDates
+ * @returns {{hidden: boolean, startDate: Window.start, endDate: *}}
+ */
+ exports.isHidden = function(time, hiddenDates) {
+ for (var i = 0; i < hiddenDates.length; i++) {
+ var startDate = hiddenDates[i].start;
+ var endDate = hiddenDates[i].end;
+
+ if (time >= startDate && time < endDate) { // if the start is entering a hidden zone
+ return {hidden: true, startDate: startDate, endDate: endDate};
+ break;
+ }
+ }
+ return {hidden: false, startDate: startDate, endDate: endDate};
+ }
+
+/***/ },
+/* 25 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Emitter = __webpack_require__(11);
+ var Hammer = __webpack_require__(19);
+ var util = __webpack_require__(1);
+ var DataSet = __webpack_require__(7);
+ var DataView = __webpack_require__(9);
+ var Range = __webpack_require__(21);
+ var ItemSet = __webpack_require__(26);
+ var Activator = __webpack_require__(35);
+ var DateUtil = __webpack_require__(24);
+
+ /**
+ * Create a timeline visualization
+ * @param {HTMLElement} container
+ * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
+ * @param {Object} [options] See Core.setOptions for the available options.
+ * @constructor
+ */
+ function Core () {}
+
+ // turn Core into an event emitter
+ Emitter(Core.prototype);
+
+ /**
+ * Create the main DOM for the Core: a root panel containing left, right,
+ * top, bottom, content, and background panel.
+ * @param {Element} container The container element where the Core will
+ * be attached.
+ * @private
+ */
+ Core.prototype._create = function (container) {
+ this.dom = {};
+
+ this.dom.root = document.createElement('div');
+ this.dom.background = document.createElement('div');
+ this.dom.backgroundVertical = document.createElement('div');
+ this.dom.backgroundHorizontal = document.createElement('div');
+ this.dom.centerContainer = document.createElement('div');
+ this.dom.leftContainer = document.createElement('div');
+ this.dom.rightContainer = document.createElement('div');
+ this.dom.center = document.createElement('div');
+ this.dom.left = document.createElement('div');
+ this.dom.right = document.createElement('div');
+ this.dom.top = document.createElement('div');
+ this.dom.bottom = document.createElement('div');
+ this.dom.shadowTop = document.createElement('div');
+ this.dom.shadowBottom = document.createElement('div');
+ this.dom.shadowTopLeft = document.createElement('div');
+ this.dom.shadowBottomLeft = document.createElement('div');
+ this.dom.shadowTopRight = document.createElement('div');
+ this.dom.shadowBottomRight = document.createElement('div');
+
+ this.dom.root.className = 'vis timeline root';
+ this.dom.background.className = 'vispanel background';
+ this.dom.backgroundVertical.className = 'vispanel background vertical';
+ this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
+ this.dom.centerContainer.className = 'vispanel center';
+ this.dom.leftContainer.className = 'vispanel left';
+ this.dom.rightContainer.className = 'vispanel right';
+ this.dom.top.className = 'vispanel top';
+ this.dom.bottom.className = 'vispanel bottom';
+ this.dom.left.className = 'content';
+ this.dom.center.className = 'content';
+ this.dom.right.className = 'content';
+ this.dom.shadowTop.className = 'shadow top';
+ this.dom.shadowBottom.className = 'shadow bottom';
+ this.dom.shadowTopLeft.className = 'shadow top';
+ this.dom.shadowBottomLeft.className = 'shadow bottom';
+ this.dom.shadowTopRight.className = 'shadow top';
+ this.dom.shadowBottomRight.className = 'shadow bottom';
+
+ this.dom.root.appendChild(this.dom.background);
+ this.dom.root.appendChild(this.dom.backgroundVertical);
+ this.dom.root.appendChild(this.dom.backgroundHorizontal);
+ this.dom.root.appendChild(this.dom.centerContainer);
+ this.dom.root.appendChild(this.dom.leftContainer);
+ this.dom.root.appendChild(this.dom.rightContainer);
+ this.dom.root.appendChild(this.dom.top);
+ this.dom.root.appendChild(this.dom.bottom);
+
+ this.dom.centerContainer.appendChild(this.dom.center);
+ this.dom.leftContainer.appendChild(this.dom.left);
+ this.dom.rightContainer.appendChild(this.dom.right);
+
+ this.dom.centerContainer.appendChild(this.dom.shadowTop);
+ this.dom.centerContainer.appendChild(this.dom.shadowBottom);
+ this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
+ this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
+ this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
+ this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
+
+ this.on('rangechange', this.redraw.bind(this));
+ this.on('touch', this._onTouch.bind(this));
+ this.on('pinch', this._onPinch.bind(this));
+ this.on('dragstart', this._onDragStart.bind(this));
+ this.on('drag', this._onDrag.bind(this));
+
+ var me = this;
+ this.on('change', function (properties) {
+ if (properties && properties.queue == true) {
+ // redraw once on next tick
+ if (!me._redrawTimer) {
+ me._redrawTimer = setTimeout(function () {
+ me._redrawTimer = null;
+ me.redraw();
+ }, 0)
+ }
+ }
+ else {
+ // redraw immediately
+ me.redraw();
+ }
+ });
+
+ // create event listeners for all interesting events, these events will be
+ // emitted via emitter
+ this.hammer = Hammer(this.dom.root, {
+ preventDefault: true
+ });
+ this.listeners = {};
+
+ var events = [
+ 'touch', 'pinch',
+ 'tap', 'doubletap', 'hold',
+ 'dragstart', 'drag', 'dragend',
+ 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
+ ];
+ events.forEach(function (event) {
+ var listener = function () {
+ var args = [event].concat(Array.prototype.slice.call(arguments, 0));
+ if (me.isActive()) {
+ me.emit.apply(me, args);
+ }
+ };
+ me.hammer.on(event, listener);
+ me.listeners[event] = listener;
+ });
+
+ // size properties of each of the panels
+ this.props = {
+ root: {},
+ background: {},
+ centerContainer: {},
+ leftContainer: {},
+ rightContainer: {},
+ center: {},
+ left: {},
+ right: {},
+ top: {},
+ bottom: {},
+ border: {},
+ scrollTop: 0,
+ scrollTopMin: 0
+ };
+ this.touch = {}; // store state information needed for touch events
+
+ this.redrawCount = 0;
+
+ // attach the root panel to the provided container
+ if (!container) throw new Error('No container provided');
+ container.appendChild(this.dom.root);
+ };
+
+ /**
+ * Set options. Options will be passed to all components loaded in the Timeline.
+ * @param {Object} [options]
+ * {String} orientation
+ * Vertical orientation for the Timeline,
+ * can be 'bottom' (default) or 'top'.
+ * {String | Number} width
+ * Width for the timeline, a number in pixels or
+ * a css string like '1000px' or '75%'. '100%' by default.
+ * {String | Number} height
+ * Fixed height for the Timeline, a number in pixels or
+ * a css string like '400px' or '75%'. If undefined,
+ * The Timeline will automatically size such that
+ * its contents fit.
+ * {String | Number} minHeight
+ * Minimum height for the Timeline, a number in pixels or
+ * a css string like '400px' or '75%'.
+ * {String | Number} maxHeight
+ * Maximum height for the Timeline, a number in pixels or
+ * a css string like '400px' or '75%'.
+ * {Number | Date | String} start
+ * Start date for the visible window
+ * {Number | Date | String} end
+ * End date for the visible window
+ */
+ Core.prototype.setOptions = function (options) {
+ if (options) {
+ // copy the known options
+ var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation', 'clickToUse', 'dataAttributes', 'hiddenDates'];
+ util.selectiveExtend(fields, this.options, options);
+
+ if ('hiddenDates' in this.options) {
+ DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates);
+ }
+
+ if ('clickToUse' in options) {
+ if (options.clickToUse) {
+ this.activator = new Activator(this.dom.root);
+ }
+ else {
+ if (this.activator) {
+ this.activator.destroy();
+ delete this.activator;
+ }
+ }
+ }
+
+ // enable/disable autoResize
+ this._initAutoResize();
+ }
+
+ // propagate options to all components
+ this.components.forEach(function (component) {
+ component.setOptions(options);
+ });
+
+ // TODO: remove deprecation error one day (deprecated since version 0.8.0)
+ if (options && options.order) {
+ throw new Error('Option order is deprecated. There is no replacement for this feature.');
+ }
+
+ // redraw everything
+ this.redraw();
+ };
+
+ /**
+ * Returns true when the Timeline is active.
+ * @returns {boolean}
+ */
+ Core.prototype.isActive = function () {
+ return !this.activator || this.activator.active;
+ };
+
+ /**
+ * Destroy the Core, clean up all DOM elements and event listeners.
+ */
+ Core.prototype.destroy = function () {
+ // unbind datasets
+ this.clear();
+
+ // remove all event listeners
+ this.off();
+
+ // stop checking for changed size
+ this._stopAutoResize();
+
+ // remove from DOM
+ if (this.dom.root.parentNode) {
+ this.dom.root.parentNode.removeChild(this.dom.root);
+ }
+ this.dom = null;
+
+ // remove Activator
+ if (this.activator) {
+ this.activator.destroy();
+ delete this.activator;
+ }
+
+ // cleanup hammer touch events
+ for (var event in this.listeners) {
+ if (this.listeners.hasOwnProperty(event)) {
+ delete this.listeners[event];
+ }
+ }
+ this.listeners = null;
+ this.hammer = null;
+
+ // give all components the opportunity to cleanup
+ this.components.forEach(function (component) {
+ component.destroy();
+ });
+
+ this.body = null;
+ };
+
+
+ /**
+ * Set a custom time bar
+ * @param {Date} time
+ */
+ Core.prototype.setCustomTime = function (time) {
+ if (!this.customTime) {
+ throw new Error('Cannot get custom time: Custom time bar is not enabled');
+ }
+
+ this.customTime.setCustomTime(time);
+ };
+
+ /**
+ * Retrieve the current custom time.
+ * @return {Date} customTime
+ */
+ Core.prototype.getCustomTime = function() {
+ if (!this.customTime) {
+ throw new Error('Cannot get custom time: Custom time bar is not enabled');
+ }
+
+ return this.customTime.getCustomTime();
+ };
+
+
+ /**
+ * Get the id's of the currently visible items.
+ * @returns {Array} The ids of the visible items
+ */
+ Core.prototype.getVisibleItems = function() {
+ return this.itemSet && this.itemSet.getVisibleItems() || [];
+ };
+
+
+
+ /**
+ * Clear the Core. By Default, items, groups and options are cleared.
+ * Example usage:
+ *
+ * timeline.clear(); // clear items, groups, and options
+ * timeline.clear({options: true}); // clear options only
+ *
+ * @param {Object} [what] Optionally specify what to clear. By default:
+ * {items: true, groups: true, options: true}
+ */
+ Core.prototype.clear = function(what) {
+ // clear items
+ if (!what || what.items) {
+ this.setItems(null);
+ }
+
+ // clear groups
+ if (!what || what.groups) {
+ this.setGroups(null);
+ }
+
+ // clear options of timeline and of each of the components
+ if (!what || what.options) {
+ this.components.forEach(function (component) {
+ component.setOptions(component.defaultOptions);
+ });
+
+ this.setOptions(this.defaultOptions); // this will also do a redraw
+ }
+ };
+
+ /**
+ * Set Core window such that it fits all items
+ * @param {Object} [options] Available options:
+ * `animate: boolean | number`
+ * If true (default), the range is animated
+ * smoothly to the new window.
+ * If a number, the number is taken as duration
+ * for the animation. Default duration is 500 ms.
+ */
+ Core.prototype.fit = function(options) {
+ var range = this._getDataRange();
+
+ // skip range set if there is no start and end date
+ if (range.start === null && range.end === null) {
+ return;
+ }
+
+ var animate = (options && options.animate !== undefined) ? options.animate : true;
+ this.range.setRange(range.start, range.end, animate);
+ };
+
+ /**
+ * Calculate the data range of the items and applies a 5% window around it.
+ * @returns {{start: Date | null, end: Date | null}}
+ * @protected
+ */
+ Core.prototype._getDataRange = function() {
+ // apply the data range as range
+ var dataRange = this.getItemRange();
+
+ // add 5% space on both sides
+ var start = dataRange.min;
+ var end = dataRange.max;
+ if (start != null && end != null) {
+ var interval = (end.valueOf() - start.valueOf());
+ if (interval <= 0) {
+ // prevent an empty interval
+ interval = 24 * 60 * 60 * 1000; // 1 day
+ }
+ start = new Date(start.valueOf() - interval * 0.05);
+ end = new Date(end.valueOf() + interval * 0.05);
+ }
+
+ return {
+ start: start,
+ end: end
+ }
+ };
+
+ /**
+ * Set the visible window. Both parameters are optional, you can change only
+ * start or only end. Syntax:
+ *
+ * TimeLine.setWindow(start, end)
+ * TimeLine.setWindow(range)
+ *
+ * Where start and end can be a Date, number, or string, and range is an
+ * object with properties start and end.
+ *
+ * @param {Date | Number | String | Object} [start] Start date of visible window
+ * @param {Date | Number | String} [end] End date of visible window
+ * @param {Object} [options] Available options:
+ * `animate: boolean | number`
+ * If true (default), the range is animated
+ * smoothly to the new window.
+ * If a number, the number is taken as duration
+ * for the animation. Default duration is 500 ms.
+ */
+ Core.prototype.setWindow = function(start, end, options) {
+ var animate = (options && options.animate !== undefined) ? options.animate : true;
+ if (arguments.length == 1) {
+ var range = arguments[0];
+ this.range.setRange(range.start, range.end, animate);
+ }
+ else {
+ this.range.setRange(start, end, animate);
+ }
+ };
+
+ /**
+ * Move the window such that given time is centered on screen.
+ * @param {Date | Number | String} time
+ * @param {Object} [options] Available options:
+ * `animate: boolean | number`
+ * If true (default), the range is animated
+ * smoothly to the new window.
+ * If a number, the number is taken as duration
+ * for the animation. Default duration is 500 ms.
+ */
+ Core.prototype.moveTo = function(time, options) {
+ var interval = this.range.end - this.range.start;
+ var t = util.convert(time, 'Date').valueOf();
+
+ var start = t - interval / 2;
+ var end = t + interval / 2;
+ var animate = (options && options.animate !== undefined) ? options.animate : true;
+
+ this.range.setRange(start, end, animate);
+ };
+
+ /**
+ * Get the visible window
+ * @return {{start: Date, end: Date}} Visible range
+ */
+ Core.prototype.getWindow = function() {
+ var range = this.range.getRange();
+ return {
+ start: new Date(range.start),
+ end: new Date(range.end)
+ };
+ };
+
+ /**
+ * Force a redraw of the Core. Can be useful to manually redraw when
+ * option autoResize=false
+ */
+ Core.prototype.redraw = function() {
+ var resized = false;
+ var options = this.options;
+ var props = this.props;
+ var dom = this.dom;
+
+ if (!dom) return; // when destroyed
+
+ DateUtil.updateHiddenDates(this.body, this.options.hiddenDates);
+
+ // update class names
+ if (options.orientation == 'top') {
+ util.addClassName(dom.root, 'top');
+ util.removeClassName(dom.root, 'bottom');
+ }
+ else {
+ util.removeClassName(dom.root, 'top');
+ util.addClassName(dom.root, 'bottom');
+ }
+
+ // update root width and height options
+ dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
+ dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
+ dom.root.style.width = util.option.asSize(options.width, '');
+
+ // calculate border widths
+ props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
+ props.border.right = props.border.left;
+ props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
+ props.border.bottom = props.border.top;
+ var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
+ var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
+
+ // workaround for a bug in IE: the clientWidth of an element with
+ // a height:0px and overflow:hidden is not calculated and always has value 0
+ if (dom.centerContainer.clientHeight === 0) {
+ props.border.left = props.border.top;
+ props.border.right = props.border.left;
+ }
+ if (dom.root.clientHeight === 0) {
+ borderRootWidth = borderRootHeight;
+ }
+
+ // calculate the heights. If any of the side panels is empty, we set the height to
+ // minus the border width, such that the border will be invisible
+ props.center.height = dom.center.offsetHeight;
+ props.left.height = dom.left.offsetHeight;
+ props.right.height = dom.right.offsetHeight;
+ props.top.height = dom.top.clientHeight || -props.border.top;
+ props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
+
+ // TODO: compensate borders when any of the panels is empty.
+
+ // apply auto height
+ // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
+ var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
+ var autoHeight = props.top.height + contentHeight + props.bottom.height +
+ borderRootHeight + props.border.top + props.border.bottom;
+ dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
+
+ // calculate heights of the content panels
+ props.root.height = dom.root.offsetHeight;
+ props.background.height = props.root.height - borderRootHeight;
+ var containerHeight = props.root.height - props.top.height - props.bottom.height -
+ borderRootHeight;
+ props.centerContainer.height = containerHeight;
+ props.leftContainer.height = containerHeight;
+ props.rightContainer.height = props.leftContainer.height;
+
+ // calculate the widths of the panels
+ props.root.width = dom.root.offsetWidth;
+ props.background.width = props.root.width - borderRootWidth;
+ props.left.width = dom.leftContainer.clientWidth || -props.border.left;
+ props.leftContainer.width = props.left.width;
+ props.right.width = dom.rightContainer.clientWidth || -props.border.right;
+ props.rightContainer.width = props.right.width;
+ var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
+ props.center.width = centerWidth;
+ props.centerContainer.width = centerWidth;
+ props.top.width = centerWidth;
+ props.bottom.width = centerWidth;
+
+ // resize the panels
+ dom.background.style.height = props.background.height + 'px';
+ dom.backgroundVertical.style.height = props.background.height + 'px';
+ dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px';
+ dom.centerContainer.style.height = props.centerContainer.height + 'px';
+ dom.leftContainer.style.height = props.leftContainer.height + 'px';
+ dom.rightContainer.style.height = props.rightContainer.height + 'px';
+
+ dom.background.style.width = props.background.width + 'px';
+ dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
+ dom.backgroundHorizontal.style.width = props.background.width + 'px';
+ dom.centerContainer.style.width = props.center.width + 'px';
+ dom.top.style.width = props.top.width + 'px';
+ dom.bottom.style.width = props.bottom.width + 'px';
+
+ // reposition the panels
+ dom.background.style.left = '0';
+ dom.background.style.top = '0';
+ dom.backgroundVertical.style.left = (props.left.width + props.border.left) + 'px';
+ dom.backgroundVertical.style.top = '0';
+ dom.backgroundHorizontal.style.left = '0';
+ dom.backgroundHorizontal.style.top = props.top.height + 'px';
+ dom.centerContainer.style.left = props.left.width + 'px';
+ dom.centerContainer.style.top = props.top.height + 'px';
+ dom.leftContainer.style.left = '0';
+ dom.leftContainer.style.top = props.top.height + 'px';
+ dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
+ dom.rightContainer.style.top = props.top.height + 'px';
+ dom.top.style.left = props.left.width + 'px';
+ dom.top.style.top = '0';
+ dom.bottom.style.left = props.left.width + 'px';
+ dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
+
+ // update the scrollTop, feasible range for the offset can be changed
+ // when the height of the Core or of the contents of the center changed
+ this._updateScrollTop();
+
+ // reposition the scrollable contents
+ var offset = this.props.scrollTop;
+ if (options.orientation == 'bottom') {
+ offset += Math.max(this.props.centerContainer.height - this.props.center.height -
+ this.props.border.top - this.props.border.bottom, 0);
+ }
+ dom.center.style.left = '0';
+ dom.center.style.top = offset + 'px';
+ dom.left.style.left = '0';
+ dom.left.style.top = offset + 'px';
+ dom.right.style.left = '0';
+ dom.right.style.top = offset + 'px';
+
+ // show shadows when vertical scrolling is available
+ var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
+ var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
+ dom.shadowTop.style.visibility = visibilityTop;
+ dom.shadowBottom.style.visibility = visibilityBottom;
+ dom.shadowTopLeft.style.visibility = visibilityTop;
+ dom.shadowBottomLeft.style.visibility = visibilityBottom;
+ dom.shadowTopRight.style.visibility = visibilityTop;
+ dom.shadowBottomRight.style.visibility = visibilityBottom;
+
+ // redraw all components
+ this.components.forEach(function (component) {
+ resized = component.redraw() || resized;
+ });
+ if (resized) {
+ // keep repainting until all sizes are settled
+ var MAX_REDRAWS = 3; // maximum number of consecutive redraws
+ if (this.redrawCount < MAX_REDRAWS) {
+ this.redrawCount++;
+ this.redraw();
+ }
+ else {
+ console.log('WARNING: infinite loop in redraw?')
+ }
+ this.redrawCount = 0;
+ }
+
+ this.emit("finishedRedraw");
+ };
+
+ // TODO: deprecated since version 1.1.0, remove some day
+ Core.prototype.repaint = function () {
+ throw new Error('Function repaint is deprecated. Use redraw instead.');
+ };
+
+ /**
+ * Set a current time. This can be used for example to ensure that a client's
+ * time is synchronized with a shared server time.
+ * Only applicable when option `showCurrentTime` is true.
+ * @param {Date | String | Number} time A Date, unix timestamp, or
+ * ISO date string.
+ */
+ Core.prototype.setCurrentTime = function(time) {
+ if (!this.currentTime) {
+ throw new Error('Option showCurrentTime must be true');
+ }
+
+ this.currentTime.setCurrentTime(time);
+ };
+
+ /**
+ * Get the current time.
+ * Only applicable when option `showCurrentTime` is true.
+ * @return {Date} Returns the current time.
+ */
+ Core.prototype.getCurrentTime = function() {
+ if (!this.currentTime) {
+ throw new Error('Option showCurrentTime must be true');
+ }
+
+ return this.currentTime.getCurrentTime();
+ };
+
+ /**
+ * Convert a position on screen (pixels) to a datetime
+ * @param {int} x Position on the screen in pixels
+ * @return {Date} time The datetime the corresponds with given position x
+ * @private
+ */
+ // TODO: move this function to Range
+ Core.prototype._toTime = function(x) {
+ return DateUtil.toTime(this, x, this.props.center.width);
+ };
+
+ /**
+ * Convert a position on the global screen (pixels) to a datetime
+ * @param {int} x Position on the screen in pixels
+ * @return {Date} time The datetime the corresponds with given position x
+ * @private
+ */
+ // TODO: move this function to Range
+ Core.prototype._toGlobalTime = function(x) {
+ return DateUtil.toTime(this, x, this.props.root.width);
+ //var conversion = this.range.conversion(this.props.root.width);
+ //return new Date(x / conversion.scale + conversion.offset);
+ };
+
+ /**
+ * Convert a datetime (Date object) into a position on the screen
+ * @param {Date} time A date
+ * @return {int} x The position on the screen in pixels which corresponds
+ * with the given date.
+ * @private
+ */
+ // TODO: move this function to Range
+ Core.prototype._toScreen = function(time) {
+ return DateUtil.toScreen(this, time, this.props.center.width);
+ };
+
+
+
+ /**
+ * Convert a datetime (Date object) into a position on the root
+ * This is used to get the pixel density estimate for the screen, not the center panel
+ * @param {Date} time A date
+ * @return {int} x The position on root in pixels which corresponds
+ * with the given date.
+ * @private
+ */
+ // TODO: move this function to Range
+ Core.prototype._toGlobalScreen = function(time) {
+ return DateUtil.toScreen(this, time, this.props.root.width);
+ //var conversion = this.range.conversion(this.props.root.width);
+ //return (time.valueOf() - conversion.offset) * conversion.scale;
+ };
+
+
+ /**
+ * Initialize watching when option autoResize is true
+ * @private
+ */
+ Core.prototype._initAutoResize = function () {
+ if (this.options.autoResize == true) {
+ this._startAutoResize();
+ }
+ else {
+ this._stopAutoResize();
+ }
+ };
+
+ /**
+ * Watch for changes in the size of the container. On resize, the Panel will
+ * automatically redraw itself.
+ * @private
+ */
+ Core.prototype._startAutoResize = function () {
+ var me = this;
+
+ this._stopAutoResize();
+
+ this._onResize = function() {
+ if (me.options.autoResize != true) {
+ // stop watching when the option autoResize is changed to false
+ me._stopAutoResize();
+ return;
+ }
+
+ if (me.dom.root) {
+ // check whether the frame is resized
+ // Note: we compare offsetWidth here, not clientWidth. For some reason,
+ // IE does not restore the clientWidth from 0 to the actual width after
+ // changing the timeline's container display style from none to visible
+ if ((me.dom.root.offsetWidth != me.props.lastWidth) ||
+ (me.dom.root.offsetHeight != me.props.lastHeight)) {
+ me.props.lastWidth = me.dom.root.offsetWidth;
+ me.props.lastHeight = me.dom.root.offsetHeight;
+
+ me.emit('change');
+ }
+ }
+ };
+
+ // add event listener to window resize
+ util.addEventListener(window, 'resize', this._onResize);
+
+ this.watchTimer = setInterval(this._onResize, 1000);
+ };
+
+ /**
+ * Stop watching for a resize of the frame.
+ * @private
+ */
+ Core.prototype._stopAutoResize = function () {
+ if (this.watchTimer) {
+ clearInterval(this.watchTimer);
+ this.watchTimer = undefined;
+ }
+
+ // remove event listener on window.resize
+ util.removeEventListener(window, 'resize', this._onResize);
+ this._onResize = null;
+ };
+
+ /**
+ * Start moving the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+ Core.prototype._onTouch = function (event) {
+ this.touch.allowDragging = true;
+ };
+
+ /**
+ * Start moving the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+ Core.prototype._onPinch = function (event) {
+ this.touch.allowDragging = false;
+ };
+
+ /**
+ * Start moving the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+ Core.prototype._onDragStart = function (event) {
+ this.touch.initialScrollTop = this.props.scrollTop;
+ };
+
+ /**
+ * Move the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+ Core.prototype._onDrag = function (event) {
+ // refuse to drag when we where pinching to prevent the timeline make a jump
+ // when releasing the fingers in opposite order from the touch screen
+ if (!this.touch.allowDragging) return;
+
+ var delta = event.gesture.deltaY;
+
+ var oldScrollTop = this._getScrollTop();
+ var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
+
+
+ if (newScrollTop != oldScrollTop) {
+ this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
+ this.emit("verticalDrag");
+ }
+ };
+
+ /**
+ * Apply a scrollTop
+ * @param {Number} scrollTop
+ * @returns {Number} scrollTop Returns the applied scrollTop
+ * @private
+ */
+ Core.prototype._setScrollTop = function (scrollTop) {
+ this.props.scrollTop = scrollTop;
+ this._updateScrollTop();
+ return this.props.scrollTop;
+ };
+
+ /**
+ * Update the current scrollTop when the height of the containers has been changed
+ * @returns {Number} scrollTop Returns the applied scrollTop
+ * @private
+ */
+ Core.prototype._updateScrollTop = function () {
+ // recalculate the scrollTopMin
+ var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
+ if (scrollTopMin != this.props.scrollTopMin) {
+ // in case of bottom orientation, change the scrollTop such that the contents
+ // do not move relative to the time axis at the bottom
+ if (this.options.orientation == 'bottom') {
+ this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
+ }
+ this.props.scrollTopMin = scrollTopMin;
+ }
+
+ // limit the scrollTop to the feasible scroll range
+ if (this.props.scrollTop > 0) this.props.scrollTop = 0;
+ if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
+
+ return this.props.scrollTop;
+ };
+
+ /**
+ * Get the current scrollTop
+ * @returns {number} scrollTop
+ * @private
+ */
+ Core.prototype._getScrollTop = function () {
+ return this.props.scrollTop;
+ };
+
+ module.exports = Core;
+
+
+/***/ },
+/* 26 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Hammer = __webpack_require__(19);
+ var util = __webpack_require__(1);
+ var DataSet = __webpack_require__(7);
+ var DataView = __webpack_require__(9);
+ var Component = __webpack_require__(23);
+ var Group = __webpack_require__(27);
+ var BackgroundGroup = __webpack_require__(31);
+ var BoxItem = __webpack_require__(32);
+ var PointItem = __webpack_require__(33);
+ var RangeItem = __webpack_require__(29);
+ var BackgroundItem = __webpack_require__(34);
+
+
+ var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
+ var BACKGROUND = '__background__'; // reserved group id for background items without group
+
+ /**
+ * An ItemSet holds a set of items and ranges which can be displayed in a
+ * range. The width is determined by the parent of the ItemSet, and the height
+ * is determined by the size of the items.
+ * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
+ * @param {Object} [options] See ItemSet.setOptions for the available options.
+ * @constructor ItemSet
+ * @extends Component
+ */
+ function ItemSet(body, options) {
+ this.body = body;
+
+ this.defaultOptions = {
+ type: null, // 'box', 'point', 'range', 'background'
+ orientation: 'bottom', // 'top' or 'bottom'
+ align: 'auto', // alignment of box items
+ stack: true,
+ groupOrder: null,
+
+ selectable: true,
+ editable: {
+ updateTime: false,
+ updateGroup: false,
+ add: false,
+ remove: false
+ },
+
+ onAdd: function (item, callback) {
+ callback(item);
+ },
+ onUpdate: function (item, callback) {
+ callback(item);
+ },
+ onMove: function (item, callback) {
+ callback(item);
+ },
+ onRemove: function (item, callback) {
+ callback(item);
+ },
+ onMoving: function (item, callback) {
+ callback(item);
+ },
+
+ margin: {
+ item: {
+ horizontal: 10,
+ vertical: 10
+ },
+ axis: 20
+ },
+ padding: 5
+ };
+
+ // options is shared by this ItemSet and all its items
+ this.options = util.extend({}, this.defaultOptions);
+
+ // options for getting items from the DataSet with the correct type
+ this.itemOptions = {
+ type: {start: 'Date', end: 'Date'}
+ };
+
+ this.conversion = {
+ toScreen: body.util.toScreen,
+ toTime: body.util.toTime
+ };
+ this.dom = {};
+ this.props = {};
+ this.hammer = null;
+
+ var me = this;
+ this.itemsData = null; // DataSet
+ this.groupsData = null; // DataSet
+
+ // listeners for the DataSet of the items
+ this.itemListeners = {
+ 'add': function (event, params, senderId) {
+ me._onAdd(params.items);
+ },
+ 'update': function (event, params, senderId) {
+ me._onUpdate(params.items);
+ },
+ 'remove': function (event, params, senderId) {
+ me._onRemove(params.items);
+ }
+ };
+
+ // listeners for the DataSet of the groups
+ this.groupListeners = {
+ 'add': function (event, params, senderId) {
+ me._onAddGroups(params.items);
+ },
+ 'update': function (event, params, senderId) {
+ me._onUpdateGroups(params.items);
+ },
+ 'remove': function (event, params, senderId) {
+ me._onRemoveGroups(params.items);
+ }
+ };
+
+ this.items = {}; // object with an Item for every data item
+ this.groups = {}; // Group object for every group
+ this.groupIds = [];
+
+ this.selection = []; // list with the ids of all selected nodes
+ this.stackDirty = true; // if true, all items will be restacked on next redraw
+
+ this.touchParams = {}; // stores properties while dragging
+ // create the HTML DOM
+
+ this._create();
+
+ this.setOptions(options);
+ }
+
+ ItemSet.prototype = new Component();
+
+ // available item types will be registered here
+ ItemSet.types = {
+ background: BackgroundItem,
+ box: BoxItem,
+ range: RangeItem,
+ point: PointItem
+ };
+
+ /**
+ * Create the HTML DOM for the ItemSet
+ */
+ ItemSet.prototype._create = function(){
+ var frame = document.createElement('div');
+ frame.className = 'itemset';
+ frame['timeline-itemset'] = this;
+ this.dom.frame = frame;
+
+ // create background panel
+ var background = document.createElement('div');
+ background.className = 'background';
+ frame.appendChild(background);
+ this.dom.background = background;
+
+ // create foreground panel
+ var foreground = document.createElement('div');
+ foreground.className = 'foreground';
+ frame.appendChild(foreground);
+ this.dom.foreground = foreground;
+
+ // create axis panel
+ var axis = document.createElement('div');
+ axis.className = 'axis';
+ this.dom.axis = axis;
+
+ // create labelset
+ var labelSet = document.createElement('div');
+ labelSet.className = 'labelset';
+ this.dom.labelSet = labelSet;
+
+ // create ungrouped Group
+ this._updateUngrouped();
+
+ // create background Group
+ var backgroundGroup = new BackgroundGroup(BACKGROUND, null, this);
+ backgroundGroup.show();
+ this.groups[BACKGROUND] = backgroundGroup;
+
+ // attach event listeners
+ // Note: we bind to the centerContainer for the case where the height
+ // of the center container is larger than of the ItemSet, so we
+ // can click in the empty area to create a new item or deselect an item.
+ this.hammer = Hammer(this.body.dom.centerContainer, {
+ preventDefault: true
+ });
+
+ // drag items when selected
+ this.hammer.on('touch', this._onTouch.bind(this));
+ this.hammer.on('dragstart', this._onDragStart.bind(this));
+ this.hammer.on('drag', this._onDrag.bind(this));
+ this.hammer.on('dragend', this._onDragEnd.bind(this));
+
+ // single select (or unselect) when tapping an item
+ this.hammer.on('tap', this._onSelectItem.bind(this));
+
+ // multi select when holding mouse/touch, or on ctrl+click
+ this.hammer.on('hold', this._onMultiSelectItem.bind(this));
+
+ // add item on doubletap
+ this.hammer.on('doubletap', this._onAddItem.bind(this));
+
+ // attach to the DOM
+ this.show();
+ };
+
+ /**
+ * Set options for the ItemSet. Existing options will be extended/overwritten.
+ * @param {Object} [options] The following options are available:
+ * {String} type
+ * Default type for the items. Choose from 'box'
+ * (default), 'point', 'range', or 'background'.
+ * The default style can be overwritten by
+ * individual items.
+ * {String} align
+ * Alignment for the items, only applicable for
+ * BoxItem. Choose 'center' (default), 'left', or
+ * 'right'.
+ * {String} orientation
+ * Orientation of the item set. Choose 'top' or
+ * 'bottom' (default).
+ * {Function} groupOrder
+ * A sorting function for ordering groups
+ * {Boolean} stack
+ * If true (deafult), items will be stacked on
+ * top of each other.
+ * {Number} margin.axis
+ * Margin between the axis and the items in pixels.
+ * Default is 20.
+ * {Number} margin.item.horizontal
+ * Horizontal margin between items in pixels.
+ * Default is 10.
+ * {Number} margin.item.vertical
+ * Vertical Margin between items in pixels.
+ * Default is 10.
+ * {Number} margin.item
+ * Margin between items in pixels in both horizontal
+ * and vertical direction. Default is 10.
+ * {Number} margin
+ * Set margin for both axis and items in pixels.
+ * {Number} padding
+ * Padding of the contents of an item in pixels.
+ * Must correspond with the items css. Default is 5.
+ * {Boolean} selectable
+ * If true (default), items can be selected.
+ * {Boolean} editable
+ * Set all editable options to true or false
+ * {Boolean} editable.updateTime
+ * Allow dragging an item to an other moment in time
+ * {Boolean} editable.updateGroup
+ * Allow dragging an item to an other group
+ * {Boolean} editable.add
+ * Allow creating new items on double tap
+ * {Boolean} editable.remove
+ * Allow removing items by clicking the delete button
+ * top right of a selected item.
+ * {Function(item: Item, callback: Function)} onAdd
+ * Callback function triggered when an item is about to be added:
+ * when the user double taps an empty space in the Timeline.
+ * {Function(item: Item, callback: Function)} onUpdate
+ * Callback function fired when an item is about to be updated.
+ * This function typically has to show a dialog where the user
+ * change the item. If not implemented, nothing happens.
+ * {Function(item: Item, callback: Function)} onMove
+ * Fired when an item has been moved. If not implemented,
+ * the move action will be accepted.
+ * {Function(item: Item, callback: Function)} onRemove
+ * Fired when an item is about to be deleted.
+ * If not implemented, the item will be always removed.
+ */
+ ItemSet.prototype.setOptions = function(options) {
+ if (options) {
+ // copy all options that we know
+ var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder', 'dataAttributes', 'template','hide'];
+ util.selectiveExtend(fields, this.options, options);
+
+ if ('margin' in options) {
+ if (typeof options.margin === 'number') {
+ this.options.margin.axis = options.margin;
+ this.options.margin.item.horizontal = options.margin;
+ this.options.margin.item.vertical = options.margin;
+ }
+ else if (typeof options.margin === 'object') {
+ util.selectiveExtend(['axis'], this.options.margin, options.margin);
+ if ('item' in options.margin) {
+ if (typeof options.margin.item === 'number') {
+ this.options.margin.item.horizontal = options.margin.item;
+ this.options.margin.item.vertical = options.margin.item;
+ }
+ else if (typeof options.margin.item === 'object') {
+ util.selectiveExtend(['horizontal', 'vertical'], this.options.margin.item, options.margin.item);
+ }
+ }
+ }
+ }
+
+ if ('editable' in options) {
+ if (typeof options.editable === 'boolean') {
+ this.options.editable.updateTime = options.editable;
+ this.options.editable.updateGroup = options.editable;
+ this.options.editable.add = options.editable;
+ this.options.editable.remove = options.editable;
+ }
+ else if (typeof options.editable === 'object') {
+ util.selectiveExtend(['updateTime', 'updateGroup', 'add', 'remove'], this.options.editable, options.editable);
+ }
+ }
+
+ // callback functions
+ var addCallback = (function (name) {
+ var fn = options[name];
+ if (fn) {
+ if (!(fn instanceof Function)) {
+ throw new Error('option ' + name + ' must be a function ' + name + '(item, callback)');
+ }
+ this.options[name] = fn;
+ }
+ }).bind(this);
+ ['onAdd', 'onUpdate', 'onRemove', 'onMove', 'onMoving'].forEach(addCallback);
+
+ // force the itemSet to refresh: options like orientation and margins may be changed
+ this.markDirty();
+ }
+ };
+
+ /**
+ * Mark the ItemSet dirty so it will refresh everything with next redraw
+ */
+ ItemSet.prototype.markDirty = function() {
+ this.groupIds = [];
+ this.stackDirty = true;
+ };
+
+ /**
+ * Destroy the ItemSet
+ */
+ ItemSet.prototype.destroy = function() {
+ this.hide();
+ this.setItems(null);
+ this.setGroups(null);
+
+ this.hammer = null;
+
+ this.body = null;
+ this.conversion = null;
+ };
+
+ /**
+ * Hide the component from the DOM
+ */
+ ItemSet.prototype.hide = function() {
+ // remove the frame containing the items
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
+ }
+
+ // remove the axis with dots
+ if (this.dom.axis.parentNode) {
+ this.dom.axis.parentNode.removeChild(this.dom.axis);
+ }
+
+ // remove the labelset containing all group labels
+ if (this.dom.labelSet.parentNode) {
+ this.dom.labelSet.parentNode.removeChild(this.dom.labelSet);
+ }
+ };
+
+ /**
+ * Show the component in the DOM (when not already visible).
+ * @return {Boolean} changed
+ */
+ ItemSet.prototype.show = function() {
+ // show frame containing the items
+ if (!this.dom.frame.parentNode) {
+ this.body.dom.center.appendChild(this.dom.frame);
+ }
+
+ // show axis with dots
+ if (!this.dom.axis.parentNode) {
+ this.body.dom.backgroundVertical.appendChild(this.dom.axis);
+ }
+
+ // show labelset containing labels
+ if (!this.dom.labelSet.parentNode) {
+ this.body.dom.left.appendChild(this.dom.labelSet);
+ }
+ };
+
+ /**
+ * Set selected items by their id. Replaces the current selection
+ * Unknown id's are silently ignored.
+ * @param {string[] | string} [ids] An array with zero or more id's of the items to be
+ * selected, or a single item id. If ids is undefined
+ * or an empty array, all items will be unselected.
+ */
+ ItemSet.prototype.setSelection = function(ids) {
+ var i, ii, id, item;
+
+ if (ids == undefined) ids = [];
+ if (!Array.isArray(ids)) ids = [ids];
+
+ // unselect currently selected items
+ for (i = 0, ii = this.selection.length; i < ii; i++) {
+ id = this.selection[i];
+ item = this.items[id];
+ if (item) item.unselect();
+ }
+
+ // select items
+ this.selection = [];
+ for (i = 0, ii = ids.length; i < ii; i++) {
+ id = ids[i];
+ item = this.items[id];
+ if (item) {
+ this.selection.push(id);
+ item.select();
+ }
+ }
+ };
+
+ /**
+ * Get the selected items by their id
+ * @return {Array} ids The ids of the selected items
+ */
+ ItemSet.prototype.getSelection = function() {
+ return this.selection.concat([]);
+ };
+
+ /**
+ * Get the id's of the currently visible items.
+ * @returns {Array} The ids of the visible items
+ */
+ ItemSet.prototype.getVisibleItems = function() {
+ var range = this.body.range.getRange();
+ var left = this.body.util.toScreen(range.start);
+ var right = this.body.util.toScreen(range.end);
+
+ var ids = [];
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ var group = this.groups[groupId];
+ var rawVisibleItems = group.visibleItems;
+
+ // filter the "raw" set with visibleItems into a set which is really
+ // visible by pixels
+ for (var i = 0; i < rawVisibleItems.length; i++) {
+ var item = rawVisibleItems[i];
+ // TODO: also check whether visible vertically
+ if ((item.left < right) && (item.left + item.width > left)) {
+ ids.push(item.id);
+ }
+ }
+ }
+ }
+
+ return ids;
+ };
+
+ /**
+ * Deselect a selected item
+ * @param {String | Number} id
+ * @private
+ */
+ ItemSet.prototype._deselect = function(id) {
+ var selection = this.selection;
+ for (var i = 0, ii = selection.length; i < ii; i++) {
+ if (selection[i] == id) { // non-strict comparison!
+ selection.splice(i, 1);
+ break;
+ }
+ }
+ };
+
+ /**
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
+ */
+ ItemSet.prototype.redraw = function() {
+ var margin = this.options.margin,
+ range = this.body.range,
+ asSize = util.option.asSize,
+ options = this.options,
+ orientation = options.orientation,
+ resized = false,
+ frame = this.dom.frame,
+ editable = options.editable.updateTime || options.editable.updateGroup;
+
+ // recalculate absolute position (before redrawing groups)
+ this.props.top = this.body.domProps.top.height + this.body.domProps.border.top;
+ this.props.left = this.body.domProps.left.width + this.body.domProps.border.left;
+
+ // update class name
+ frame.className = 'itemset' + (editable ? ' editable' : '');
+
+ // reorder the groups (if needed)
+ resized = this._orderGroups() || resized;
+
+ // check whether zoomed (in that case we need to re-stack everything)
+ // TODO: would be nicer to get this as a trigger from Range
+ var visibleInterval = range.end - range.start;
+ var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.props.width != this.props.lastWidth);
+ if (zoomed) this.stackDirty = true;
+ this.lastVisibleInterval = visibleInterval;
+ this.props.lastWidth = this.props.width;
+
+ var restack = this.stackDirty;
+ var firstGroup = this._firstGroup();
+ var firstMargin = {
+ item: margin.item,
+ axis: margin.axis
+ };
+ var nonFirstMargin = {
+ item: margin.item,
+ axis: margin.item.vertical / 2
+ };
+ var height = 0;
+ var minHeight = margin.axis + margin.item.vertical;
+
+ // redraw the background group
+ this.groups[BACKGROUND].redraw(range, nonFirstMargin, restack);
+
+ // redraw all regular groups
+ util.forEach(this.groups, function (group) {
+ var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin;
+ var groupResized = group.redraw(range, groupMargin, restack);
+ resized = groupResized || resized;
+ height += group.height;
+ });
+ height = Math.max(height, minHeight);
+ this.stackDirty = false;
+
+ // update frame height
+ frame.style.height = asSize(height);
+
+ // calculate actual size
+ this.props.width = frame.offsetWidth;
+ this.props.height = height;
+
+ // reposition axis
+ this.dom.axis.style.top = asSize((orientation == 'top') ?
+ (this.body.domProps.top.height + this.body.domProps.border.top) :
+ (this.body.domProps.top.height + this.body.domProps.centerContainer.height));
+ this.dom.axis.style.left = '0';
+
+ // check if this component is resized
+ resized = this._isResized() || resized;
+
+ return resized;
+ };
+
+ /**
+ * Get the first group, aligned with the axis
+ * @return {Group | null} firstGroup
+ * @private
+ */
+ ItemSet.prototype._firstGroup = function() {
+ var firstGroupIndex = (this.options.orientation == 'top') ? 0 : (this.groupIds.length - 1);
+ var firstGroupId = this.groupIds[firstGroupIndex];
+ var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED];
+
+ return firstGroup || null;
+ };
+
+ /**
+ * Create or delete the group holding all ungrouped items. This group is used when
+ * there are no groups specified.
+ * @protected
+ */
+ ItemSet.prototype._updateUngrouped = function() {
+ var ungrouped = this.groups[UNGROUPED];
+ var background = this.groups[BACKGROUND];
+ var item, itemId;
+
+ if (this.groupsData) {
+ // remove the group holding all ungrouped items
+ if (ungrouped) {
+ ungrouped.hide();
+ delete this.groups[UNGROUPED];
+
+ for (itemId in this.items) {
+ if (this.items.hasOwnProperty(itemId)) {
+ item = this.items[itemId];
+ item.parent && item.parent.remove(item);
+ var groupId = this._getGroupId(item.data);
+ var group = this.groups[groupId];
+ group && group.add(item) || item.hide();
+ }
+ }
+ }
+ }
+ else {
+ // create a group holding all (unfiltered) items
+ if (!ungrouped) {
+ var id = null;
+ var data = null;
+ ungrouped = new Group(id, data, this);
+ this.groups[UNGROUPED] = ungrouped;
+
+ for (itemId in this.items) {
+ if (this.items.hasOwnProperty(itemId)) {
+ item = this.items[itemId];
+ ungrouped.add(item);
+ }
+ }
+
+ ungrouped.show();
+ }
+ }
+ };
+
+ /**
+ * Get the element for the labelset
+ * @return {HTMLElement} labelSet
+ */
+ ItemSet.prototype.getLabelSet = function() {
+ return this.dom.labelSet;
+ };
+
+ /**
+ * Set items
+ * @param {vis.DataSet | null} items
+ */
+ ItemSet.prototype.setItems = function(items) {
+ var me = this,
+ ids,
+ oldItemsData = this.itemsData;
+
+ // replace the dataset
+ if (!items) {
+ this.itemsData = null;
+ }
+ else if (items instanceof DataSet || items instanceof DataView) {
+ this.itemsData = items;
+ }
+ else {
+ throw new TypeError('Data must be an instance of DataSet or DataView');
+ }
+
+ if (oldItemsData) {
+ // unsubscribe from old dataset
+ util.forEach(this.itemListeners, function (callback, event) {
+ oldItemsData.off(event, callback);
+ });
+
+ // remove all drawn items
+ ids = oldItemsData.getIds();
+ this._onRemove(ids);
+ }
+
+ if (this.itemsData) {
+ // subscribe to new dataset
+ var id = this.id;
+ util.forEach(this.itemListeners, function (callback, event) {
+ me.itemsData.on(event, callback, id);
+ });
+
+ // add all new items
+ ids = this.itemsData.getIds();
+ this._onAdd(ids);
+
+ // update the group holding all ungrouped items
+ this._updateUngrouped();
+ }
+ };
+
+ /**
+ * Get the current items
+ * @returns {vis.DataSet | null}
+ */
+ ItemSet.prototype.getItems = function() {
+ return this.itemsData;
+ };
+
+ /**
+ * Set groups
+ * @param {vis.DataSet} groups
+ */
+ ItemSet.prototype.setGroups = function(groups) {
+ var me = this,
+ ids;
+
+ // unsubscribe from current dataset
+ if (this.groupsData) {
+ util.forEach(this.groupListeners, function (callback, event) {
+ me.groupsData.unsubscribe(event, callback);
+ });
+
+ // remove all drawn groups
+ ids = this.groupsData.getIds();
+ this.groupsData = null;
+ this._onRemoveGroups(ids); // note: this will cause a redraw
+ }
+
+ // replace the dataset
+ if (!groups) {
+ this.groupsData = null;
+ }
+ else if (groups instanceof DataSet || groups instanceof DataView) {
+ this.groupsData = groups;
+ }
+ else {
+ throw new TypeError('Data must be an instance of DataSet or DataView');
+ }
+
+ if (this.groupsData) {
+ // subscribe to new dataset
+ var id = this.id;
+ util.forEach(this.groupListeners, function (callback, event) {
+ me.groupsData.on(event, callback, id);
+ });
+
+ // draw all ms
+ ids = this.groupsData.getIds();
+ this._onAddGroups(ids);
+ }
+
+ // update the group holding all ungrouped items
+ this._updateUngrouped();
+
+ // update the order of all items in each group
+ this._order();
+
+ this.body.emitter.emit('change', {queue: true});
+ };
+
+ /**
+ * Get the current groups
+ * @returns {vis.DataSet | null} groups
+ */
+ ItemSet.prototype.getGroups = function() {
+ return this.groupsData;
+ };
+
+ /**
+ * Remove an item by its id
+ * @param {String | Number} id
+ */
+ ItemSet.prototype.removeItem = function(id) {
+ var item = this.itemsData.get(id),
+ dataset = this.itemsData.getDataSet();
+
+ if (item) {
+ // confirm deletion
+ this.options.onRemove(item, function (item) {
+ if (item) {
+ // remove by id here, it is possible that an item has no id defined
+ // itself, so better not delete by the item itself
+ dataset.remove(id);
+ }
+ });
+ }
+ };
+
+ /**
+ * Get the time of an item based on it's data and options.type
+ * @param {Object} itemData
+ * @returns {string} Returns the type
+ * @private
+ */
+ ItemSet.prototype._getType = function (itemData) {
+ return itemData.type || this.options.type || (itemData.end ? 'range' : 'box');
+ };
+
+
+ /**
+ * Get the group id for an item
+ * @param {Object} itemData
+ * @returns {string} Returns the groupId
+ * @private
+ */
+ ItemSet.prototype._getGroupId = function (itemData) {
+ var type = this._getType(itemData);
+ if (type == 'background' && itemData.group == undefined) {
+ return BACKGROUND;
+ }
+ else {
+ return this.groupsData ? itemData.group : UNGROUPED;
+ }
+ };
+
+ /**
+ * Handle updated items
+ * @param {Number[]} ids
+ * @protected
+ */
+ ItemSet.prototype._onUpdate = function(ids) {
+ var me = this;
+
+ ids.forEach(function (id) {
+ var itemData = me.itemsData.get(id, me.itemOptions);
+ var item = me.items[id];
+ var type = me._getType(itemData);
+
+ var constructor = ItemSet.types[type];
+
+ if (item) {
+ // update item
+ if (!constructor || !(item instanceof constructor)) {
+ // item type has changed, delete the item and recreate it
+ me._removeItem(item);
+ item = null;
+ }
+ else {
+ me._updateItem(item, itemData);
+ }
+ }
+
+ if (!item) {
+ // create item
+ if (constructor) {
+ item = new constructor(itemData, me.conversion, me.options);
+ item.id = id; // TODO: not so nice setting id afterwards
+ me._addItem(item);
+ }
+ else if (type == 'rangeoverflow') {
+ // TODO: deprecated since version 2.1.0 (or 3.0.0?). cleanup some day
+ throw new TypeError('Item type "rangeoverflow" is deprecated. Use css styling instead: ' +
+ '.vis.timeline .item.range .content {overflow: visible;}');
+ }
+ else {
+ throw new TypeError('Unknown item type "' + type + '"');
+ }
+ }
+ });
+
+ this._order();
+ this.stackDirty = true; // force re-stacking of all items next redraw
+ this.body.emitter.emit('change', {queue: true});
+ };
+
+ /**
+ * Handle added items
+ * @param {Number[]} ids
+ * @protected
+ */
+ ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
+
+ /**
+ * Handle removed items
+ * @param {Number[]} ids
+ * @protected
+ */
+ ItemSet.prototype._onRemove = function(ids) {
+ var count = 0;
+ var me = this;
+ ids.forEach(function (id) {
+ var item = me.items[id];
+ if (item) {
+ count++;
+ me._removeItem(item);
+ }
+ });
+
+ if (count) {
+ // update order
+ this._order();
+ this.stackDirty = true; // force re-stacking of all items next redraw
+ this.body.emitter.emit('change', {queue: true});
+ }
+ };
+
+ /**
+ * Update the order of item in all groups
+ * @private
+ */
+ ItemSet.prototype._order = function() {
+ // reorder the items in all groups
+ // TODO: optimization: only reorder groups affected by the changed items
+ util.forEach(this.groups, function (group) {
+ group.order();
+ });
+ };
+
+ /**
+ * Handle updated groups
+ * @param {Number[]} ids
+ * @private
+ */
+ ItemSet.prototype._onUpdateGroups = function(ids) {
+ this._onAddGroups(ids);
+ };
+
+ /**
+ * Handle changed groups (added or updated)
+ * @param {Number[]} ids
+ * @private
+ */
+ ItemSet.prototype._onAddGroups = function(ids) {
+ var me = this;
+
+ ids.forEach(function (id) {
+ var groupData = me.groupsData.get(id);
+ var group = me.groups[id];
+
+ if (!group) {
+ // check for reserved ids
+ if (id == UNGROUPED || id == BACKGROUND) {
+ throw new Error('Illegal group id. ' + id + ' is a reserved id.');
+ }
+
+ var groupOptions = Object.create(me.options);
+ util.extend(groupOptions, {
+ height: null
+ });
+
+ group = new Group(id, groupData, me);
+ me.groups[id] = group;
+
+ // add items with this groupId to the new group
+ for (var itemId in me.items) {
+ if (me.items.hasOwnProperty(itemId)) {
+ var item = me.items[itemId];
+ if (item.data.group == id) {
+ group.add(item);
+ }
+ }
+ }
+
+ group.order();
+ group.show();
+ }
+ else {
+ // update group
+ group.setData(groupData);
+ }
+ });
+
+ this.body.emitter.emit('change', {queue: true});
+ };
+
+ /**
+ * Handle removed groups
+ * @param {Number[]} ids
+ * @private
+ */
+ ItemSet.prototype._onRemoveGroups = function(ids) {
+ var groups = this.groups;
+ ids.forEach(function (id) {
+ var group = groups[id];
+
+ if (group) {
+ group.hide();
+ delete groups[id];
+ }
+ });
+
+ this.markDirty();
+
+ this.body.emitter.emit('change', {queue: true});
+ };
+
+ /**
+ * Reorder the groups if needed
+ * @return {boolean} changed
+ * @private
+ */
+ ItemSet.prototype._orderGroups = function () {
+ if (this.groupsData) {
+ // reorder the groups
+ var groupIds = this.groupsData.getIds({
+ order: this.options.groupOrder
+ });
+
+ var changed = !util.equalArray(groupIds, this.groupIds);
+ if (changed) {
+ // hide all groups, removes them from the DOM
+ var groups = this.groups;
+ groupIds.forEach(function (groupId) {
+ groups[groupId].hide();
+ });
+
+ // show the groups again, attach them to the DOM in correct order
+ groupIds.forEach(function (groupId) {
+ groups[groupId].show();
+ });
+
+ this.groupIds = groupIds;
+ }
+
+ return changed;
+ }
+ else {
+ return false;
+ }
+ };
+
+ /**
+ * Add a new item
+ * @param {Item} item
+ * @private
+ */
+ ItemSet.prototype._addItem = function(item) {
+ this.items[item.id] = item;
+
+ // add to group
+ var groupId = this._getGroupId(item.data);
+ var group = this.groups[groupId];
+ if (group) group.add(item);
+ };
+
+ /**
+ * Update an existing item
+ * @param {Item} item
+ * @param {Object} itemData
+ * @private
+ */
+ ItemSet.prototype._updateItem = function(item, itemData) {
+ var oldGroupId = item.data.group;
+
+ // update the items data (will redraw the item when displayed)
+ item.setData(itemData);
+
+ // update group
+ if (oldGroupId != item.data.group) {
+ var oldGroup = this.groups[oldGroupId];
+ if (oldGroup) oldGroup.remove(item);
+
+ var groupId = this._getGroupId(item.data);
+ var group = this.groups[groupId];
+ if (group) group.add(item);
+ }
+ };
+
+ /**
+ * Delete an item from the ItemSet: remove it from the DOM, from the map
+ * with items, and from the map with visible items, and from the selection
+ * @param {Item} item
+ * @private
+ */
+ ItemSet.prototype._removeItem = function(item) {
+ // remove from DOM
+ item.hide();
+
+ // remove from items
+ delete this.items[item.id];
+
+ // remove from selection
+ var index = this.selection.indexOf(item.id);
+ if (index != -1) this.selection.splice(index, 1);
+
+ // remove from group
+ item.parent && item.parent.remove(item);
+ };
+
+ /**
+ * Create an array containing all items being a range (having an end date)
+ * @param array
+ * @returns {Array}
+ * @private
+ */
+ ItemSet.prototype._constructByEndArray = function(array) {
+ var endArray = [];
+
+ for (var i = 0; i < array.length; i++) {
+ if (array[i] instanceof RangeItem) {
+ endArray.push(array[i]);
+ }
+ }
+ return endArray;
+ };
+
+ /**
+ * Register the clicked item on touch, before dragStart is initiated.
+ *
+ * dragStart is initiated from a mousemove event, which can have left the item
+ * already resulting in an item == null
+ *
+ * @param {Event} event
+ * @private
+ */
+ ItemSet.prototype._onTouch = function (event) {
+ // store the touched item, used in _onDragStart
+ this.touchParams.item = ItemSet.itemFromTarget(event);
+ };
+
+ /**
+ * Start dragging the selected events
+ * @param {Event} event
+ * @private
+ */
+ ItemSet.prototype._onDragStart = function (event) {
+ if (!this.options.editable.updateTime && !this.options.editable.updateGroup) {
+ return;
+ }
+
+ var item = this.touchParams.item || null;
+ var me = this;
+ var props;
+
+ if (item && item.selected) {
+ var dragLeftItem = event.target.dragLeftItem;
+ var dragRightItem = event.target.dragRightItem;
+
+ if (dragLeftItem) {
+ props = {
+ item: dragLeftItem,
+ initialX: event.gesture.center.clientX
+ };
+
+ if (me.options.editable.updateTime) {
+ props.start = item.data.start.valueOf();
+ }
+ if (me.options.editable.updateGroup) {
+ if ('group' in item.data) props.group = item.data.group;
+ }
+
+ this.touchParams.itemProps = [props];
+ }
+ else if (dragRightItem) {
+ props = {
+ item: dragRightItem,
+ initialX: event.gesture.center.clientX
+ };
+
+ if (me.options.editable.updateTime) {
+ props.end = item.data.end.valueOf();
+ }
+ if (me.options.editable.updateGroup) {
+ if ('group' in item.data) props.group = item.data.group;
+ }
+
+ this.touchParams.itemProps = [props];
+ }
+ else {
+ this.touchParams.itemProps = this.getSelection().map(function (id) {
+ var item = me.items[id];
+ var props = {
+ item: item,
+ initialX: event.gesture.center.clientX
+ };
+
+ if (me.options.editable.updateTime) {
+ if ('start' in item.data) props.start = item.data.start.valueOf();
+ if ('end' in item.data) props.end = item.data.end.valueOf();
+ }
+ if (me.options.editable.updateGroup) {
+ if ('group' in item.data) props.group = item.data.group;
+ }
+
+ return props;
+ });
+ }
+
+ event.stopPropagation();
+ }
+ };
+
+ /**
+ * Drag selected items
+ * @param {Event} event
+ * @private
+ */
+ ItemSet.prototype._onDrag = function (event) {
+ event.preventDefault()
+
+ if (this.touchParams.itemProps) {
+ var me = this;
+ var snap = this.body.util.snap || null;
+ var xOffset = this.body.dom.root.offsetLeft + this.body.domProps.left.width;
+
+ // move
+ this.touchParams.itemProps.forEach(function (props) {
+ var newProps = {};
+ var current = me.body.util.toTime(event.gesture.center.clientX - xOffset);
+ var initial = me.body.util.toTime(props.initialX - xOffset);
+ var offset = current - initial;
+
+ if ('start' in props) {
+ var start = new Date(props.start + offset);
+ newProps.start = snap ? snap(start) : start;
+ }
+
+ if ('end' in props) {
+ var end = new Date(props.end + offset);
+ newProps.end = snap ? snap(end) : end;
+ }
+
+ if ('group' in props) {
+ // drag from one group to another
+ var group = ItemSet.groupFromTarget(event);
+ newProps.group = group && group.groupId;
+ }
+
+ // confirm moving the item
+ var itemData = util.extend({}, props.item.data, newProps);
+ me.options.onMoving(itemData, function (itemData) {
+ if (itemData) {
+ me._updateItemProps(props.item, itemData);
+ }
+ });
+ });
+
+ this.stackDirty = true; // force re-stacking of all items next redraw
+ this.body.emitter.emit('change');
+
+ event.stopPropagation();
+ }
+ };
+
+ /**
+ * Update an items properties
+ * @param {Item} item
+ * @param {Object} props Can contain properties start, end, and group.
+ * @private
+ */
+ ItemSet.prototype._updateItemProps = function(item, props) {
+ // TODO: copy all properties from props to item? (also new ones)
+ if ('start' in props) item.data.start = props.start;
+ if ('end' in props) item.data.end = props.end;
+ if ('group' in props && item.data.group != props.group) {
+ this._moveToGroup(item, props.group)
+ }
+ };
+
+ /**
+ * Move an item to another group
+ * @param {Item} item
+ * @param {String | Number} groupId
+ * @private
+ */
+ ItemSet.prototype._moveToGroup = function(item, groupId) {
+ var group = this.groups[groupId];
+ if (group && group.groupId != item.data.group) {
+ var oldGroup = item.parent;
+ oldGroup.remove(item);
+ oldGroup.order();
+ group.add(item);
+ group.order();
+
+ item.data.group = group.groupId;
+ }
+ };
+
+ /**
+ * End of dragging selected items
+ * @param {Event} event
+ * @private
+ */
+ ItemSet.prototype._onDragEnd = function (event) {
+ event.preventDefault()
+
+ if (this.touchParams.itemProps) {
+ // prepare a change set for the changed items
+ var changes = [],
+ me = this,
+ dataset = this.itemsData.getDataSet();
+
+ var itemProps = this.touchParams.itemProps ;
+ this.touchParams.itemProps = null;
+ itemProps.forEach(function (props) {
+ var id = props.item.id,
+ itemData = me.itemsData.get(id, me.itemOptions);
+
+ var changed = false;
+ if ('start' in props.item.data) {
+ changed = (props.start != props.item.data.start.valueOf());
+ itemData.start = util.convert(props.item.data.start,
+ dataset._options.type && dataset._options.type.start || 'Date');
+ }
+ if ('end' in props.item.data) {
+ changed = changed || (props.end != props.item.data.end.valueOf());
+ itemData.end = util.convert(props.item.data.end,
+ dataset._options.type && dataset._options.type.end || 'Date');
+ }
+ if ('group' in props.item.data) {
+ changed = changed || (props.group != props.item.data.group);
+ itemData.group = props.item.data.group;
+ }
+
+ // only apply changes when start or end is actually changed
+ if (changed) {
+ me.options.onMove(itemData, function (itemData) {
+ if (itemData) {
+ // apply changes
+ itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined)
+ changes.push(itemData);
+ }
+ else {
+ // restore original values
+ me._updateItemProps(props.item, props);
+
+ me.stackDirty = true; // force re-stacking of all items next redraw
+ me.body.emitter.emit('change');
+ }
+ });
+ }
+ });
+
+ // apply the changes to the data (if there are changes)
+ if (changes.length) {
+ dataset.update(changes);
+ }
+
+ event.stopPropagation();
+ }
+ };
+
+ /**
+ * Handle selecting/deselecting an item when tapping it
+ * @param {Event} event
+ * @private
+ */
+ ItemSet.prototype._onSelectItem = function (event) {
+ if (!this.options.selectable) return;
+
+ var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
+ var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
+ if (ctrlKey || shiftKey) {
+ this._onMultiSelectItem(event);
+ return;
+ }
+
+ var oldSelection = this.getSelection();
+
+ var item = ItemSet.itemFromTarget(event);
+ var selection = item ? [item.id] : [];
+ this.setSelection(selection);
+
+ var newSelection = this.getSelection();
+
+ // emit a select event,
+ // except when old selection is empty and new selection is still empty
+ if (newSelection.length > 0 || oldSelection.length > 0) {
+ this.body.emitter.emit('select', {
+ items: newSelection
+ });
+ }
+ };
+
+ /**
+ * Handle creation and updates of an item on double tap
+ * @param event
+ * @private
+ */
+ ItemSet.prototype._onAddItem = function (event) {
+ if (!this.options.selectable) return;
+ if (!this.options.editable.add) return;
+
+ var me = this,
+ snap = this.body.util.snap || null,
+ item = ItemSet.itemFromTarget(event);
+
+ if (item) {
+ // update item
+
+ // execute async handler to update the item (or cancel it)
+ var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
+ this.options.onUpdate(itemData, function (itemData) {
+ if (itemData) {
+ me.itemsData.getDataSet().update(itemData);
+ }
+ });
+ }
+ else {
+ // add item
+ var xAbs = util.getAbsoluteLeft(this.dom.frame);
+ var x = event.gesture.center.pageX - xAbs;
+ var start = this.body.util.toTime(x);
+ var newItem = {
+ start: snap ? snap(start) : start,
+ content: 'new item'
+ };
+
+ // when default type is a range, add a default end date to the new item
+ if (this.options.type === 'range') {
+ var end = this.body.util.toTime(x + this.props.width / 5);
+ newItem.end = snap ? snap(end) : end;
+ }
+
+ newItem[this.itemsData._fieldId] = util.randomUUID();
+
+ var group = ItemSet.groupFromTarget(event);
+ if (group) {
+ newItem.group = group.groupId;
+ }
+
+ // execute async handler to customize (or cancel) adding an item
+ this.options.onAdd(newItem, function (item) {
+ if (item) {
+ me.itemsData.getDataSet().add(item);
+ // TODO: need to trigger a redraw?
+ }
+ });
+ }
+ };
+
+ /**
+ * Handle selecting/deselecting multiple items when holding an item
+ * @param {Event} event
+ * @private
+ */
+ ItemSet.prototype._onMultiSelectItem = function (event) {
+ if (!this.options.selectable) return;
+
+ var selection,
+ item = ItemSet.itemFromTarget(event);
+
+ if (item) {
+ // multi select items
+ selection = this.getSelection(); // current selection
+
+ var shiftKey = event.gesture.touches[0] && event.gesture.touches[0].shiftKey || false;
+ if (shiftKey) {
+ // select all items between the old selection and the tapped item
+
+ // determine the selection range
+ selection.push(item.id);
+ var range = ItemSet._getItemRange(this.itemsData.get(selection, this.itemOptions));
+
+ // select all items within the selection range
+ selection = [];
+ for (var id in this.items) {
+ if (this.items.hasOwnProperty(id)) {
+ var _item = this.items[id];
+ var start = _item.data.start;
+ var end = (_item.data.end !== undefined) ? _item.data.end : start;
+
+ if (start >= range.min && end <= range.max) {
+ selection.push(_item.id); // do not use id but item.id, id itself is stringified
+ }
+ }
+ }
+ }
+ else {
+ // add/remove this item from the current selection
+ var index = selection.indexOf(item.id);
+ if (index == -1) {
+ // item is not yet selected -> select it
+ selection.push(item.id);
+ }
+ else {
+ // item is already selected -> deselect it
+ selection.splice(index, 1);
+ }
+ }
+
+ this.setSelection(selection);
+
+ this.body.emitter.emit('select', {
+ items: this.getSelection()
+ });
+ }
+ };
+
+ /**
+ * Calculate the time range of a list of items
+ * @param {Array.
+