From 686562909032fe95f618fd047f1297e7e087c108 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Tue, 4 Jun 2019 14:41:50 +1000 Subject: [PATCH 1/5] use position:sticky and IntersectionObserver instead of element cloning and position:fixed - much simpler. --- .../components/umbstickybar.directive.js | 197 +++++++----------- 1 file changed, 75 insertions(+), 122 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js index 2f2df7c12b..912a122c91 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js @@ -4,7 +4,7 @@ @restrict A @description -Use this directive make an element sticky and follow the page when scrolling. +Use this directive make an element sticky and follow the page when scrolling. `umb-sticky-bar--active` class is applied when the element is stuck

Markup example

@@ -12,140 +12,93 @@ Use this directive make an element sticky and follow the page when scrolling.
 
         
+ umb-sticky-bar>
-

CSS example

-
-    .my-sticky-bar {
-        padding: 15px 0;
-        background: #000000;
-        position: relative;
-        top: 0;
-    }
-
-    .my-sticky-bar.-umb-sticky-bar {
-        top: 100px;
-    }
-
- -@param {string} scrollableContainer Set the class (".element") or the id ("#element") of the scrollable container element. **/ (function () { 'use strict'; - function StickyBarDirective($rootScope) { + function StickyBarDirective() { + + /** + Toggle `umb-sticky-bar--active` class on the sticky-bar element + **/ + function setClass(addClass, current) { + current.classList.toggle('umb-sticky-bar--active', addClass); + } + + /** + Inserts two elements in the umbStickyBar parent element + These are used by the IntersectionObserve to calculate scroll position + **/ + function addSentinels(current) { + ['-top', '-bottom'].forEach(s => { + const sentinel = document.createElement('div'); + sentinel.classList.add('umb-sticky-sentinel', s); + current.parentElement.appendChild(sentinel); + }); + } + + /** + Calls into setClass when the footer sentinel enters/exits the bottom of the container + Container is the parent element of the umbStickyBar element + **/ + function observeFooter(current, container) { + const observer = new IntersectionObserver((records, observer) => { + let [target, rootBounds, intersected] + = [records[0].boundingClientRect, records[0].rootBounds, records[0].intersectionRatio === 1]; + + if (target.bottom > rootBounds.top && intersected) { + setClass(true, current); + } + if (target.top < rootBounds.top && target.bottom < rootBounds.bottom) { + setClass(false, current); + } + }, { + threshold: [1], + root: container + }); + + observer.observe(current.parentElement.querySelector('.umb-sticky-sentinel.-bottom')); + } + + /** + Calls into setClass when the header sentinel enters/exits the top of the container + Container is the parent element of the umbStickyBar element + **/ + function observeHeader(current, container) { + const observer = new IntersectionObserver((records, observer) => { + let [target, rootBounds] = [records[0].boundingClientRect, records[0].rootBounds]; + + if (target.bottom < rootBounds.top) { + setClass(true, current); + } + + if (target.bottom >= rootBounds.top && target.bottom < rootBounds.bottom) { + setClass(false, current); + } + }, { + threshold: [0], + root: container + }); + + observer.observe(current.parentElement.querySelector('.umb-sticky-sentinel.-top')); + } function link(scope, el, attr, ctrl) { + + let _el = el[0]; + let container = _el.closest('[data-element="editor-container"]'); + + addSentinels(_el); - var bar = $(el); - var scrollableContainer = null; - var clonedBar = null; - var cloneIsMade = false; - - function activate() { - - if (bar.parents(".umb-property").length > 1) { - bar.addClass("nested"); - return; - } - - if (attr.scrollableContainer) { - scrollableContainer = bar.closest(attr.scrollableContainer); - } else { - scrollableContainer = $(window); - } - - scrollableContainer.on('scroll.umbStickyBar', determineVisibility).trigger("scroll"); - $(window).on('resize.umbStickyBar', determineVisibility); - - scope.$on('$destroy', function () { - scrollableContainer.off('.umbStickyBar'); - $(window).off('.umbStickyBar'); - }); - - } - - function determineVisibility() { - - var barTop = bar[0].offsetTop; - var scrollTop = scrollableContainer.scrollTop(); - - if (scrollTop > barTop) { - - if (!cloneIsMade) { - - createClone(); - - clonedBar.css({ - 'visibility': 'visible' - }); - - } else { - - calculateSize(); - - } - - } else { - - if (cloneIsMade) { - - //remove cloned element (switched places with original on creation) - bar.remove(); - bar = clonedBar; - clonedBar = null; - - bar.removeClass('-umb-sticky-bar'); - bar.css({ - position: 'relative', - 'width': 'auto', - 'height': 'auto', - 'z-index': 'auto', - 'visibility': 'visible' - }); - - cloneIsMade = false; - - } - - } - - } - - function calculateSize() { - var width = bar.innerWidth(); - clonedBar.css({ - width: width + 10 // + 10 (5*2) because we need to add border to avoid seeing the shadow beneath. Look at the CSS. - }); - } - - function createClone() { - //switch place with cloned element, to keep binding intact - clonedBar = bar; - bar = clonedBar.clone(); - clonedBar.after(bar); - clonedBar.addClass('-umb-sticky-bar'); - clonedBar.css({ - 'position': 'fixed', - // if you change this z-index value, make sure the sticky editor sub headers do not - // clash with umb-dropdown (e.g. the content actions dropdown in content list view) - 'z-index': 99, - 'visibility': 'hidden' - }); - - cloneIsMade = true; - calculateSize(); - - } - - activate(); - + observeHeader(_el, container); + observeFooter(_el, container); } var directive = { From d17022d87dcdb6a6867be6caf2da5f7f36cec47b Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Tue, 4 Jun 2019 14:42:04 +1000 Subject: [PATCH 2/5] make it look pretty --- .../subheader/umb-editor-sub-header.less | 29 +++++++++++++++---- .../src/less/property-editors.less | 3 +- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less index 78cccac57a..6cf3598638 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less @@ -32,15 +32,32 @@ border-radius: 3px; } -.umb-editor-sub-header.-umb-sticky-bar { - box-shadow: 0 6px 3px -3px rgba(0,0,0,.16); +[umb-sticky-bar] { transition: box-shadow 240ms; - margin-top: 0; + margin-top: 0; margin-bottom: 0; - top: calc(@appHeaderHeight + @editorHeaderHeight); + position:sticky; + z-index: 99; - .umb-editor--infinityMode & { - top: calc(@editorHeaderHeight); + &.umb-sticky-bar--active { + box-shadow: 0 6px 3px -3px rgba(0,0,0,.16); + } +} + +.umb-sticky-sentinel { + position: absolute; + left: 0; + width: 100%; + pointer-events: none; + + &.-top { + top:0px; + height:1px; + } + + &.-bottom { + bottom:50px; + height:10px; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 9e8dd37ab9..92351d09ca 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -5,6 +5,7 @@ // -------------------------------------------------- .umb-property-editor { width: 100%; + position:relative; } .umb-property-editor-tiny { @@ -165,8 +166,6 @@ .sp-replacer { display: inline-flex; margin-right: 18px; - border: solid 1px @gray-8; - border-radius: 3px; } label { From 469054e9b34293f08cf6ef97ea131f916d324d88 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Tue, 4 Jun 2019 14:42:20 +1000 Subject: [PATCH 3/5] No longer need scrollable-container attribute --- .../views/components/editor/subheader/umb-editor-sub-header.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header.html index c385223baf..140931ec4b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header.html @@ -1,6 +1,5 @@
From e92f68d04054ead39f10f12f270003d985d7a276 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Tue, 4 Jun 2019 14:55:12 +1000 Subject: [PATCH 4/5] improve variable naming --- .../directives/components/umbstickybar.directive.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js index 912a122c91..9538930e0a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js @@ -92,13 +92,13 @@ Use this directive make an element sticky and follow the page when scrolling. `u function link(scope, el, attr, ctrl) { - let _el = el[0]; + let current = el[0]; let container = _el.closest('[data-element="editor-container"]'); - addSentinels(_el); + addSentinels(current); - observeHeader(_el, container); - observeFooter(_el, container); + observeHeader(current, container); + observeFooter(current, container); } var directive = { From 3144b51c91cc8843811a235e3b2161fc3f9b9f2a Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Thu, 27 Jun 2019 18:58:39 +1000 Subject: [PATCH 5/5] fix behaviour on page load - don't add active class if sticky-bar is in viewport --- .../components/umbstickybar.directive.js | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js index 9538930e0a..07e45ff0f7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js @@ -24,14 +24,24 @@ Use this directive make an element sticky and follow the page when scrolling. `u 'use strict'; function StickyBarDirective() { - + + /** + On initial load, the intersector fires if the grid editor is in the viewport + This flag is used to suppress the setClass behaviour on the initial load + **/ + var initial = true; + /** Toggle `umb-sticky-bar--active` class on the sticky-bar element - **/ + **/ function setClass(addClass, current) { - current.classList.toggle('umb-sticky-bar--active', addClass); + if (!initial) { + current.classList.toggle('umb-sticky-bar--active', addClass); + } else { + initial = false; + } } - + /** Inserts two elements in the umbStickyBar parent element These are used by the IntersectionObserve to calculate scroll position @@ -40,9 +50,9 @@ Use this directive make an element sticky and follow the page when scrolling. `u ['-top', '-bottom'].forEach(s => { const sentinel = document.createElement('div'); sentinel.classList.add('umb-sticky-sentinel', s); - current.parentElement.appendChild(sentinel); + current.parentElement.appendChild(sentinel); }); - } + } /** Calls into setClass when the footer sentinel enters/exits the bottom of the container @@ -50,15 +60,14 @@ Use this directive make an element sticky and follow the page when scrolling. `u **/ function observeFooter(current, container) { const observer = new IntersectionObserver((records, observer) => { - let [target, rootBounds, intersected] - = [records[0].boundingClientRect, records[0].rootBounds, records[0].intersectionRatio === 1]; - + let [target, rootBounds, intersected] = [records[0].boundingClientRect, records[0].rootBounds, records[0].intersectionRatio === 1]; + if (target.bottom > rootBounds.top && intersected) { setClass(true, current); } if (target.top < rootBounds.top && target.bottom < rootBounds.bottom) { setClass(false, current); - } + } }, { threshold: [1], root: container @@ -66,14 +75,14 @@ Use this directive make an element sticky and follow the page when scrolling. `u observer.observe(current.parentElement.querySelector('.umb-sticky-sentinel.-bottom')); } - + /** Calls into setClass when the header sentinel enters/exits the top of the container Container is the parent element of the umbStickyBar element - **/ + **/ function observeHeader(current, container) { const observer = new IntersectionObserver((records, observer) => { - let [target, rootBounds] = [records[0].boundingClientRect, records[0].rootBounds]; + let [target, rootBounds] = [records[0].boundingClientRect, records[0].rootBounds]; if (target.bottom < rootBounds.top) { setClass(true, current); @@ -86,19 +95,19 @@ Use this directive make an element sticky and follow the page when scrolling. `u threshold: [0], root: container }); - + observer.observe(current.parentElement.querySelector('.umb-sticky-sentinel.-top')); } function link(scope, el, attr, ctrl) { - - let current = el[0]; - let container = _el.closest('[data-element="editor-container"]'); - + + let current = el[0]; + let container = current.closest('[data-element="editor-container"]'); + addSentinels(current); observeHeader(current, container); - observeFooter(current, container); + observeFooter(current, container); } var directive = {