diff --git a/README.md b/README.md index 535df1a3..776da5d7 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ Options can be passed either as a data (data-slider-foo) attribute, or as part o | tooltip | string | 'show' | whether to show the tooltip on drag, hide the tooltip, or always show the tooltip. Accepts: 'show', 'hide', or 'always' | | tooltip_split | bool | false | if false show one tootip if true show two tooltips one for each handler | | tooltip_position | string | null | Position of tooltip, relative to slider. Accepts 'top'/'bottom' for horizontal sliders and 'left'/'right' for vertically orientated sliders. Default positions are 'top' for horizontal and 'right' for vertical slider. | +| preventOverflow | bool | false | whether to shift tooltips to keep them within the viewport, similar to Popper.js's [`preventOverflow` modifier](https://popper.js.org/docs/v2/modifiers/prevent-overflow/) | handle | string | 'round' | handle shape. Accepts: 'round', 'square', 'triangle' or 'custom' | | reversed | bool | false | whether or not the slider should be reversed | | rtl | string, bool| 'auto' | whether or not the slider should be shown in rtl mode. Accepts true, false, 'auto'. Default 'auto' : use actual direction of HTML (`dir='rtl'`) | diff --git a/src/js/bootstrap-slider.js b/src/js/bootstrap-slider.js index 0694544e..855895a1 100644 --- a/src/js/bootstrap-slider.js +++ b/src/js/bootstrap-slider.js @@ -791,6 +791,11 @@ const windowIsDefined = (typeof window === "object"); // Bind window handlers this.resize = this._resize.bind(this); window.addEventListener("resize", this.resize, false); + // Simulate Popper's behavior of listening to both resize and scroll: + // https://popper.js.org/docs/v2/modifiers/event-listeners/ + if (this.options.preventOverflow) { + window.addEventListener("scroll", this.resize, false); + } // Bind tooltip-related handlers @@ -906,6 +911,7 @@ const windowIsDefined = (typeof window === "object"); scale: 'linear', focus: false, tooltip_position: null, + preventOverflow: false, labelledby: null, rangeHighlights: [] }, @@ -1243,6 +1249,7 @@ const windowIsDefined = (typeof window === "object"); this._setText(this.tooltipInner, formattedTooltipVal); this.tooltip.style[this.stylePos] = `${positionPercentages[0]}%`; + this._preventOverflow(this.tooltip); function getPositionPercentages(state, reversed){ if (reversed) { @@ -1250,6 +1257,54 @@ const windowIsDefined = (typeof window === "object"); } return [state.percentage[0], state.percentage[1]]; } + }, + _preventOverflow: function _prevetOverflow(tooltip){ + if (!this.options.preventOverflow) { + return; + } + const rect = tooltip.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) { + return; // not rendered + } + const style = tooltip.style[this.stylePos]; + const arrow = tooltip.querySelector('.arrow'); + let offset; + if (this.options.orientation === 'vertical') { + const minY = 0, maxY = document.body.clientHeight; + if (rect.y < minY) { + offset = minY - rect.y; + } else if (rect.y + rect.height > maxY) { + offset = maxY - (rect.y + rect.height); + } + if (offset) { + tooltip.style.top = `calc(${style} + ${offset}px)`; + //arrow.style.top = `calc(50% - .4rem + ${offset}px)`; + arrow.style.transform = `translateY(${-offset}px)`; + } else { + arrow.style.transform = null; + } + } else { + const minX = 0, maxX = document.body.clientWidth; + // Because of .tooltip-inner's left: -50%, the actual rectangle span + // is [rect.x - rect.width / 2, rect.x + rect.width + 2] + if (rect.x - rect.width / 2 < minX) { + offset = minX - (rect.x - rect.width / 2); + } else if (rect.x + rect.width / 2 > maxX) { + offset = maxX - (rect.x + rect.width / 2); + } + if (offset) { + if (this.stylePos === 'left') { + tooltip.style.left = `calc(${style} + ${offset}px)`; + //arrow.style.left = `calc(50% - .4rem + ${offset}px)`; + } else { + tooltip.style.right = `calc(${style} - ${offset}px)`; + } + arrow.style.transform = `translateX(${-offset}px)`; + } else { + arrow.style.transform = null; + } + } + // right, top }, _copyState: function() { return { @@ -1465,6 +1520,7 @@ const windowIsDefined = (typeof window === "object"); formattedTooltipVal = this.options.formatter(this._state.value); this._setText(this.tooltipInner, formattedTooltipVal); this.tooltip.style[this.stylePos] = `${ (positionPercentages[1] + positionPercentages[0])/2 }%`; + this._preventOverflow(this.tooltip); var innerTooltipMinText = this.options.formatter(this._state.value[0]); this._setText(this.tooltipInner_min, innerTooltipMinText); @@ -1473,14 +1529,17 @@ const windowIsDefined = (typeof window === "object"); this._setText(this.tooltipInner_max, innerTooltipMaxText); this.tooltip_min.style[this.stylePos] = `${ positionPercentages[0] }%`; + this._preventOverflow(this.tooltip_min); this.tooltip_max.style[this.stylePos] = `${ positionPercentages[1] }%`; + this._preventOverflow(this.tooltip_max); } else { formattedTooltipVal = this.options.formatter(this._state.value[0]); this._setText(this.tooltipInner, formattedTooltipVal); this.tooltip.style[this.stylePos] = `${ positionPercentages[0] }%`; + this._preventOverflow(this.tooltip); } if (this.options.orientation === 'vertical') {