From 7b26a08d17a17d00947adbd71b880022cb7c038b Mon Sep 17 00:00:00 2001 From: Jarred Stelfox Date: Mon, 27 Mar 2017 15:18:27 -0700 Subject: [PATCH 1/5] Implement pickerPosition:'auto' option --- js/bootstrap-datetimepicker.js | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/js/bootstrap-datetimepicker.js b/js/bootstrap-datetimepicker.js index f66d69c1..2bed4e76 100644 --- a/js/bootstrap-datetimepicker.js +++ b/js/bootstrap-datetimepicker.js @@ -104,7 +104,7 @@ this.linkField = options.linkField || this.element.data('link-field') || false; this.linkFormat = DPGlobal.parseFormat(options.linkFormat || this.element.data('link-format') || DPGlobal.getDefaultFormat(this.formatType, 'link'), this.formatType); this.minuteStep = options.minuteStep || this.element.data('minute-step') || 5; - this.pickerPosition = options.pickerPosition || this.element.data('picker-position') || 'bottom-right'; + this.pickerPosition = options.pickerPosition || this.element.data('picker-position') || 'auto'; this.showMeridian = options.showMeridian || this.element.data('show-meridian') || false; this.initialDate = options.initialDate || new Date(); this.zIndex = options.zIndex || this.element.data('z-index') || undefined; @@ -222,7 +222,7 @@ if (this.isInline) { this.picker.addClass('datetimepicker-inline'); } else { - this.picker.addClass('datetimepicker-dropdown-' + this.pickerPosition + ' dropdown-menu'); + this.picker.addClass('datetimepicker-dropdown-' + this.getAutoPosition() + ' dropdown-menu'); } if (this.isRTL) { this.picker.addClass('datetimepicker-rtl'); @@ -592,6 +592,27 @@ this.updateNavArrows(); }, + getAutoPosition: function () { + if (this.pickerPosition != 'auto') { + return this.pickerPosition; + } + + var offset = this.component ? this.component.offset() : this.element.offset(); + var pickerHeight = this.picker.outerHeight(); + var pickerHeightWithMargin = this.picker.outerHeight(true); + var bottomOverflow = $(window).height() - (offset.top + pickerHeightWithMargin + pickerHeight); + + var prefix = bottomOverflow < 0 ? 'top' : 'bottom'; + + var pickerWidth = this.picker.outerWidth(); + var pickerWidthWithMargin = this.picker.outerWidth(true); + var leftOverflow = $(window).width() - (offset.left + pickerWidthWithMargin + pickerWidth); + + var suffix = leftOverflow < 0 ? 'left' : 'right'; + + return prefix + '-' + suffix; + }, + place: function () { if (this.isInline) return; @@ -613,6 +634,10 @@ containerOffset = $(this.container).offset(); } + if (this.pickerPosition == 'auto') { + this.pickerPosition = this.getAutoPosition(); + } + if (this.component) { offset = this.component.offset(); left = offset.left; From db4918ca3b4af5e912bce162102d5b9726cb1c5b Mon Sep 17 00:00:00 2001 From: Jarred Stelfox Date: Mon, 27 Mar 2017 16:43:53 -0700 Subject: [PATCH 2/5] Fix bug where arrow is on opposite side Even using the old defaults, 'bottom-right' css is being applied to a datepicker which is created on the left hand side and the arrow is on the left. Swapping the CSS moves the arrow to the right side even though the name would now be 'bottom-left'. Moveover, the picker would be on the left, but the arrow on the right of the picker. This should keep this working as is instead of swapping css. I worry changing the CSS would be a breaking change for many people using this library. Even if it seems correct. --- js/bootstrap-datetimepicker.js | 43 +++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/js/bootstrap-datetimepicker.js b/js/bootstrap-datetimepicker.js index 2bed4e76..2f0cd434 100644 --- a/js/bootstrap-datetimepicker.js +++ b/js/bootstrap-datetimepicker.js @@ -222,7 +222,7 @@ if (this.isInline) { this.picker.addClass('datetimepicker-inline'); } else { - this.picker.addClass('datetimepicker-dropdown-' + this.getAutoPosition() + ' dropdown-menu'); + this.picker.addClass('datetimepicker-dropdown-' + this.getAutoPosition(true) + ' dropdown-menu'); } if (this.isRTL) { this.picker.addClass('datetimepicker-rtl'); @@ -592,25 +592,46 @@ this.updateNavArrows(); }, - getAutoPosition: function () { + getAutoHorizonalPosition: function(inverse) { if (this.pickerPosition != 'auto') { return this.pickerPosition; - } + } + + var offset = this.component ? this.component.offset() : this.element.offset(); + var pickerWidth = this.picker.outerWidth(); + var pickerWidthWithMargin = this.picker.outerWidth(true); + var leftOverflow = $(window).width() - (offset.left + pickerWidthWithMargin + pickerWidth); + + var side = leftOverflow < 0 ? 'left' : 'right'; + + var position = side; + + if (!!inverse) { + position = side == 'left' ? 'right' : 'left' + } + + return position; + }, + + getAutoVerticalPosition: function() { + if (this.pickerPosition != 'auto') { + return this.pickerPosition; + } var offset = this.component ? this.component.offset() : this.element.offset(); var pickerHeight = this.picker.outerHeight(); var pickerHeightWithMargin = this.picker.outerHeight(true); var bottomOverflow = $(window).height() - (offset.top + pickerHeightWithMargin + pickerHeight); - var prefix = bottomOverflow < 0 ? 'top' : 'bottom'; - - var pickerWidth = this.picker.outerWidth(); - var pickerWidthWithMargin = this.picker.outerWidth(true); - var leftOverflow = $(window).width() - (offset.left + pickerWidthWithMargin + pickerWidth); - - var suffix = leftOverflow < 0 ? 'left' : 'right'; + return bottomOverflow < 0 ? 'top' : 'bottom'; + }, + + getAutoPosition: function (inverse) { + if (this.pickerPosition != 'auto') { + return this.pickerPosition; + } - return prefix + '-' + suffix; + return this.getAutoVerticalPosition() + '-' + this.getAutoHorizonalPosition(inverse); }, place: function () { From 32fc867d83b773bd4a3df2d7609474ca1186570d Mon Sep 17 00:00:00 2001 From: Jarred Stelfox Date: Wed, 9 Aug 2017 09:00:48 -0700 Subject: [PATCH 3/5] Ensure any scrollbars are accounted for when doing overflow math Offset is not enough when a parent dive has overflow:scroll/auto. We need to calculated the bottom of the document regardless of if we are in a scrollable area. --- js/bootstrap-datetimepicker.js | 35 ++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/js/bootstrap-datetimepicker.js b/js/bootstrap-datetimepicker.js index 2f0cd434..dfe8b48e 100644 --- a/js/bootstrap-datetimepicker.js +++ b/js/bootstrap-datetimepicker.js @@ -596,11 +596,13 @@ if (this.pickerPosition != 'auto') { return this.pickerPosition; } - - var offset = this.component ? this.component.offset() : this.element.offset(); + var el = this.component ? this.component : this.element; + var scrollElement = this.getScrollParent(el[0]); + + var leftOffset = scrollElement ? el.offset().left + scrollElement.scrollLeft : el.offset().left; var pickerWidth = this.picker.outerWidth(); var pickerWidthWithMargin = this.picker.outerWidth(true); - var leftOverflow = $(window).width() - (offset.left + pickerWidthWithMargin + pickerWidth); + var leftOverflow = $(window).width() - (leftOffset + pickerWidthWithMargin + pickerWidth); var side = leftOverflow < 0 ? 'left' : 'right'; @@ -617,11 +619,15 @@ if (this.pickerPosition != 'auto') { return this.pickerPosition; } - - var offset = this.component ? this.component.offset() : this.element.offset(); + + var el = this.component ? this.component : this.element; + var scrollElement = this.getScrollParent(el[0]); + + var topOffset = scrollElement ? el.offset().top + scrollElement.scrollTop : el.offset().top; + var pickerHeight = this.picker.outerHeight(); var pickerHeightWithMargin = this.picker.outerHeight(true); - var bottomOverflow = $(window).height() - (offset.top + pickerHeightWithMargin + pickerHeight); + var bottomOverflow = $(window).height() - (topOffset + pickerHeightWithMargin + pickerHeight); return bottomOverflow < 0 ? 'top' : 'bottom'; }, @@ -634,6 +640,23 @@ return this.getAutoVerticalPosition() + '-' + this.getAutoHorizonalPosition(inverse); }, + getScrollParent: function(element, includeHidden) { + var style = getComputedStyle(element); + var excludeStaticParent = style.position === "absolute"; + var overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/; + + if (style.position === "fixed") return document.body; + for (var parent = element; (parent = parent.parentElement);) { + style = getComputedStyle(parent); + if (excludeStaticParent && style.position === "static") { + continue; + } + if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent; + } + + return document.body; + }, + place: function () { if (this.isInline) return; From 306a85e8550bd3a5c71d96394dd164d7e65c9943 Mon Sep 17 00:00:00 2001 From: Jarred Stelfox Date: Tue, 22 Aug 2017 13:41:43 -0700 Subject: [PATCH 4/5] Ensure picker position is updated dynamically This will recalculate the auto positioning of a picker when the window is resized. This includes auto positioning, meaning a picker may flip upwards if the window is resized vertically and there is no room on the bottom of the page for the picker --- js/bootstrap-datetimepicker.js | 38 +++++++++++++--------------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/js/bootstrap-datetimepicker.js b/js/bootstrap-datetimepicker.js index dfe8b48e..21b7d68a 100644 --- a/js/bootstrap-datetimepicker.js +++ b/js/bootstrap-datetimepicker.js @@ -222,7 +222,7 @@ if (this.isInline) { this.picker.addClass('datetimepicker-inline'); } else { - this.picker.addClass('datetimepicker-dropdown-' + this.getAutoPosition(true) + ' dropdown-menu'); + this.picker.addClass('datetimepicker-dropdown-' + this.getAutoPosition() + ' dropdown-menu'); } if (this.isRTL) { this.picker.addClass('datetimepicker-rtl'); @@ -600,19 +600,10 @@ var scrollElement = this.getScrollParent(el[0]); var leftOffset = scrollElement ? el.offset().left + scrollElement.scrollLeft : el.offset().left; - var pickerWidth = this.picker.outerWidth(); var pickerWidthWithMargin = this.picker.outerWidth(true); - var leftOverflow = $(window).width() - (leftOffset + pickerWidthWithMargin + pickerWidth); - - var side = leftOverflow < 0 ? 'left' : 'right'; + var leftOverflow = $(window).width() - (leftOffset + pickerWidthWithMargin); - var position = side; - - if (!!inverse) { - position = side == 'left' ? 'right' : 'left' - } - - return position; + return leftOverflow < 0 ? 'left' : 'right'; }, getAutoVerticalPosition: function() { @@ -624,20 +615,18 @@ var scrollElement = this.getScrollParent(el[0]); var topOffset = scrollElement ? el.offset().top + scrollElement.scrollTop : el.offset().top; - - var pickerHeight = this.picker.outerHeight(); var pickerHeightWithMargin = this.picker.outerHeight(true); - var bottomOverflow = $(window).height() - (topOffset + pickerHeightWithMargin + pickerHeight); + var bottomOverflow = $(window).height() - (topOffset + pickerHeightWithMargin); return bottomOverflow < 0 ? 'top' : 'bottom'; }, - getAutoPosition: function (inverse) { + getAutoPosition: function () { if (this.pickerPosition != 'auto') { return this.pickerPosition; } - return this.getAutoVerticalPosition() + '-' + this.getAutoHorizonalPosition(inverse); + return this.getAutoVerticalPosition() + '-' + this.getAutoHorizonalPosition(); }, getScrollParent: function(element, includeHidden) { @@ -671,27 +660,28 @@ this.zIndex = index_highest + 10; } - var offset, top, left, containerOffset; + var offset, top, left, containerOffset, position; if (this.container instanceof $) { containerOffset = this.container.offset(); } else { containerOffset = $(this.container).offset(); } - if (this.pickerPosition == 'auto') { - this.pickerPosition = this.getAutoPosition(); - } + var isAutoPosition = this.pickerPosition == 'auto'; + position = isAutoPosition ? this.getAutoPosition() : this.pickerPosition; + this.picker.removeClass('datetimepicker-dropdown-top-right datetimepicker-dropdown-top-left datetimepicker-dropdown-bottom-right datetimepicker-dropdown-bottom-left'); + this.picker.addClass('datetimepicker-dropdown-' + this.getAutoPosition(true) + ' dropdown-menu'); if (this.component) { offset = this.component.offset(); left = offset.left; - if (this.pickerPosition === 'bottom-left' || this.pickerPosition === 'top-left') { + if (position == 'bottom-left' || position == 'top-left') { left += this.component.outerWidth() - this.picker.outerWidth(); } } else { offset = this.element.offset(); left = offset.left; - if (this.pickerPosition === 'bottom-left' || this.pickerPosition === 'top-left') { + if (position == 'bottom-left' || position == 'top-left') { left += this.element.outerWidth() - this.picker.outerWidth(); } } @@ -701,7 +691,7 @@ left = bodyWidth - 220; } - if (this.pickerPosition === 'top-left' || this.pickerPosition === 'top-right') { + if (position == 'top-left' || position == 'top-right') { top = offset.top - this.picker.outerHeight(); } else { top = offset.top + this.height; From edadb5b47db534e14b498de80978011d4fc5d7df Mon Sep 17 00:00:00 2001 From: Jarred Stelfox Date: Wed, 23 Aug 2017 16:01:52 -0700 Subject: [PATCH 5/5] Picker will follow input if page scrolls --- js/bootstrap-datetimepicker.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/js/bootstrap-datetimepicker.js b/js/bootstrap-datetimepicker.js index 21b7d68a..0fa7ba0e 100644 --- a/js/bootstrap-datetimepicker.js +++ b/js/bootstrap-datetimepicker.js @@ -401,6 +401,10 @@ } this.place(); $(window).on('resize', $.proxy(this.place, this)); + + var scrollElement = this.getScrollParent(this.element[0]); + $(scrollElement).on('scroll', $.proxy(this.place, this)); + if (e) { e.stopPropagation(); e.preventDefault(); @@ -417,6 +421,10 @@ if (this.isInline) return; this.picker.hide(); $(window).off('resize', this.place); + + var scrollElement = this.getScrollParent(this.element[0]); + $(scrollElement).off('scroll', this.place); + this.viewMode = this.startViewMode; this.showMode(); if (!this.isInput) {