From 2d0d67a7f55bf5e280e84a64d262d32d2a37a1a6 Mon Sep 17 00:00:00 2001 From: Nurguly Ashyrov Date: Thu, 15 Dec 2022 14:37:42 +0400 Subject: [PATCH 1/7] Learning Mode - Implement sticky sidebar for LM templates: modern, video-full --- assets/course-theme/learning-mode.js | 1 + assets/course-theme/sidebar.js | 160 ++++++++++++++++++ .../class-sensei-course-theme-templates.php | 13 +- 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 assets/course-theme/sidebar.js diff --git a/assets/course-theme/learning-mode.js b/assets/course-theme/learning-mode.js index 52db0205a6..446ec83cd2 100644 --- a/assets/course-theme/learning-mode.js +++ b/assets/course-theme/learning-mode.js @@ -4,6 +4,7 @@ import './scroll-direction'; import './adminbar-layout'; import './featured-video-size'; +import './sidebar'; import { toggleFocusMode } from './focus-mode'; import { submitContactTeacher } from './contact-teacher'; import { initCompleteLessonTransition } from './complete-lesson-button'; diff --git a/assets/course-theme/sidebar.js b/assets/course-theme/sidebar.js new file mode 100644 index 0000000000..d85cd3e6e1 --- /dev/null +++ b/assets/course-theme/sidebar.js @@ -0,0 +1,160 @@ +/** + * External dependencies + */ +import debounce from 'lodash/debounce'; + +/** + * The last scroll top value. + * + * @member {number} + */ +let lastScrollTop = 0; + +/** + * Detect if a scroll movement is upward or downward. + */ +const getScrollDelta = () => { + const { scrollTop } = document.documentElement; + const delta = scrollTop - lastScrollTop; + lastScrollTop = Math.max( 0, scrollTop ); + return delta; +}; + +/** + * Tells if the sidebar is supposed to be sticky. + * + * @return {boolean} True if it is sticky. False otherwise. + */ +const isStickySidebar = () => + [ 'modern', 'video-full' ].some( ( templateName ) => + document.body.classList.contains( `learning-mode--${ templateName }` ) + ); + +/** + * Sidebar margin top. + * + * @member {number} + */ +let sidebarMarginTop = 0; + +/** + * The sidebar DOM element. + * + * @member {HTMLElement} + */ +let sidebar = null; + +/** + * The clone of the sidebar DOM element. + * + * @member {HTMLElement} + */ +let sidebarClone = null; + +function prepareSidebarClone() { + sidebar = document.querySelector( '.sensei-course-theme__sidebar' ); + const sidebarRect = sidebar.getBoundingClientRect(); + sidebarMarginTop = sidebar.style.marginTop + ? parseInt( sidebar.style.marginTop, 10 ) + : 0; + if ( sidebarClone?.remove ) { + sidebarClone.remove(); + } + sidebarClone = sidebar.cloneNode( true ); + sidebarClone.style.position = 'fixed'; + sidebarClone.style.opacity = 1; + sidebarClone.style.zIndex = 2; + sidebarClone.style.top = `${ sidebarRect.top }px`; + sidebarClone.style.left = `${ sidebarRect.left }px`; + sidebarClone.style.width = `${ sidebarRect.right - sidebarRect.left }px`; + sidebarClone.style.transition = 'none'; + sidebar.parentElement.append( sidebarClone ); + sidebar.style.opacity = 0; +} + +/** + * Sidebar bottom margin. + * + * @member {number} + */ +const SIDEBAR_BOTTOM_MARGIN = 32; + +/** + * Monitors the sidebar position and sticks/unsticks it when needed. + * + * @param {boolean} initialPosition True if the sidebar should be positioned + * for it's initial position given the current + * state of the scrollbar. Used when user opens + * the page and it is scrolled into the middle. + */ +function updateSidebarPosition( initialPosition = false ) { + if ( ! sidebar || ! sidebarClone ) { + return; + } + const header = document.querySelector( '.sensei-course-theme__header' ); + const headerRect = header.getBoundingClientRect(); + const sidebarRect = sidebar.getBoundingClientRect(); + const sidebarCloneRect = sidebarClone.getBoundingClientRect(); + const delta = getScrollDelta(); + let sidebarCloneNewTop = sidebarRect.top; + const sidebarCloneHeight = sidebarCloneRect.bottom - sidebarCloneRect.top; + const sidebarCloneIsTallerThanViewport = + sidebarCloneHeight > + window.innerHeight - + ( headerRect.bottom + sidebarMarginTop + SIDEBAR_BOTTOM_MARGIN ); + + if ( sidebarCloneIsTallerThanViewport && ! initialPosition ) { + sidebarCloneNewTop = sidebarCloneRect.top - delta; + const sidebarCloneNewBottom = sidebarCloneRect.bottom - delta; + const sidebarCloneMinTop = sidebarRect.top; + const sidebarCloneMinBottom = + window.innerHeight - SIDEBAR_BOTTOM_MARGIN; + // The sidebar is moving upwards. + if ( delta >= 0 ) { + if ( sidebarCloneNewBottom < sidebarCloneMinBottom ) { + sidebarCloneNewTop = sidebarCloneMinBottom - sidebarCloneHeight; + } + } else { + if ( sidebarCloneNewTop > headerRect.bottom ) { + sidebarCloneNewTop = headerRect.bottom; + } + if ( sidebarCloneNewTop < sidebarCloneMinTop ) { + sidebarCloneNewTop = sidebarCloneMinTop; + } + } + } else if ( sidebarRect.top <= headerRect.bottom ) { + sidebarCloneNewTop = headerRect.bottom; + } else { + sidebarCloneNewTop = sidebarRect.top; + } + + sidebarClone.style.top = `${ sidebarCloneNewTop - sidebarMarginTop }px`; +} + +/** + * Makes the sidebar sticky for relevant LM templates. + */ +function stickySidebar() { + if ( ! isStickySidebar() ) { + return; + } + + prepareSidebarClone(); + updateSidebarPosition( true ); + + document.defaultView.addEventListener( 'scroll', () => + updateSidebarPosition() + ); + + // eslint-disable-next-line @wordpress/no-global-event-listener + window.addEventListener( + 'resize', + debounce( () => { + prepareSidebarClone(); + updateSidebarPosition( true ); + }, 500 ) + ); +} + +// eslint-disable-next-line @wordpress/no-global-event-listener +window.addEventListener( 'DOMContentLoaded', stickySidebar ); diff --git a/includes/course-theme/class-sensei-course-theme-templates.php b/includes/course-theme/class-sensei-course-theme-templates.php index 9fbb34e1a7..01a8872dff 100644 --- a/includes/course-theme/class-sensei-course-theme-templates.php +++ b/includes/course-theme/class-sensei-course-theme-templates.php @@ -70,7 +70,7 @@ public function init() { add_filter( 'pre_get_block_file_template', [ $this, 'get_single_block_template' ], 10, 3 ); add_filter( 'theme_lesson_templates', [ $this, 'add_learning_mode_template' ], 10, 4 ); add_filter( 'theme_quiz_templates', [ $this, 'add_learning_mode_template' ], 10, 4 ); - + add_filter( 'body_class', [ $this, 'add_body_class' ], 10, 2 ); } @@ -499,5 +499,16 @@ private function should_hide_lesson_template( $post_type ) { return false; } + /** + * Adds the active template class to body tag. + * + * @param array $classes The list of body class names. + * @param array $class The list of additional class names added to the body. + */ + public function add_body_class( array $classes, array $class ): array { + $active_template_name = Sensei_Course_Theme_Template_Selection::get_active_template_name(); + $classes[] = "learning-mode--{$active_template_name}"; + return $classes; + } } From 19a1512f23938f7ad82277a1208994d3957f8287 Mon Sep 17 00:00:00 2001 From: Nurguly Ashyrov Date: Thu, 15 Dec 2022 17:18:24 +0400 Subject: [PATCH 2/7] Learning Mode - Make sidebar's height match video. --- assets/course-theme/sidebar.js | 69 ++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/assets/course-theme/sidebar.js b/assets/course-theme/sidebar.js index d85cd3e6e1..2e7fb46055 100644 --- a/assets/course-theme/sidebar.js +++ b/assets/course-theme/sidebar.js @@ -11,7 +11,7 @@ import debounce from 'lodash/debounce'; let lastScrollTop = 0; /** - * Detect if a scroll movement is upward or downward. + * Calculates the scroll delta. */ const getScrollDelta = () => { const { scrollTop } = document.documentElement; @@ -44,6 +44,13 @@ let sidebarMarginTop = 0; */ let sidebar = null; +/** + * The header DOM element. + * + * @member {HTMLElement} + */ +let header = null; + /** * The clone of the sidebar DOM element. * @@ -51,8 +58,22 @@ let sidebar = null; */ let sidebarClone = null; -function prepareSidebarClone() { +/** + * The featured video DOM element. + * + * @member {HTMLElement} + */ +let featuredVideo = null; + +const queryDomElements = () => { sidebar = document.querySelector( '.sensei-course-theme__sidebar' ); + header = document.querySelector( '.sensei-course-theme__header' ); + featuredVideo = document.querySelector( + '.sensei-course-theme-lesson-video' + ); +}; + +function prepareSidebarClone() { const sidebarRect = sidebar.getBoundingClientRect(); sidebarMarginTop = sidebar.style.marginTop ? parseInt( sidebar.style.marginTop, 10 ) @@ -91,7 +112,7 @@ function updateSidebarPosition( initialPosition = false ) { if ( ! sidebar || ! sidebarClone ) { return; } - const header = document.querySelector( '.sensei-course-theme__header' ); + const headerRect = header.getBoundingClientRect(); const sidebarRect = sidebar.getBoundingClientRect(); const sidebarCloneRect = sidebarClone.getBoundingClientRect(); @@ -131,6 +152,29 @@ function updateSidebarPosition( initialPosition = false ) { sidebarClone.style.top = `${ sidebarCloneNewTop - sidebarMarginTop }px`; } +const reinitializeSidebar = debounce( () => { + prepareSidebarClone(); + updateSidebarPosition( true ); +}, 500 ); + +function syncSidebarSizeWithVideo() { + if ( featuredVideo && sidebar ) { + new window.ResizeObserver( () => { + const videoHeight = featuredVideo.offsetHeight; + const sidebarHeight = sidebar.offsetHeight; + if ( + ! videoHeight || + ! sidebarHeight || + sidebarHeight >= videoHeight + ) { + return; + } + sidebar.style.height = `${ videoHeight }px`; + reinitializeSidebar(); + } ).observe( featuredVideo ); + } +} + /** * Makes the sidebar sticky for relevant LM templates. */ @@ -139,21 +183,22 @@ function stickySidebar() { return; } - prepareSidebarClone(); - updateSidebarPosition( true ); + queryDomElements(); document.defaultView.addEventListener( 'scroll', () => updateSidebarPosition() ); // eslint-disable-next-line @wordpress/no-global-event-listener - window.addEventListener( - 'resize', - debounce( () => { - prepareSidebarClone(); - updateSidebarPosition( true ); - }, 500 ) - ); + window.addEventListener( 'resize', reinitializeSidebar ); + + // Make sure sidebar height is not shorter than the video height + // for `moderm` lm template. + if ( document.body.classList.contains( 'learning-mode--modern' ) ) { + syncSidebarSizeWithVideo(); + } + + reinitializeSidebar(); } // eslint-disable-next-line @wordpress/no-global-event-listener From 6f2686fd4c39cc9859b0cd3813aec0d38eeef054 Mon Sep 17 00:00:00 2001 From: Nurguly Ashyrov Date: Thu, 15 Dec 2022 18:43:20 +0400 Subject: [PATCH 3/7] Add some docs/comments for Learning Mode Sticky Sidebar. --- assets/course-theme/sidebar.js | 124 ++++++++++++++++++++++----------- 1 file changed, 84 insertions(+), 40 deletions(-) diff --git a/assets/course-theme/sidebar.js b/assets/course-theme/sidebar.js index 2e7fb46055..c630fe4d08 100644 --- a/assets/course-theme/sidebar.js +++ b/assets/course-theme/sidebar.js @@ -52,11 +52,12 @@ let sidebar = null; let header = null; /** - * The clone of the sidebar DOM element. + * The clone of the sidebar DOM element. This is the sidebar element + * that user sees and interacts with. * * @member {HTMLElement} */ -let sidebarClone = null; +let stickySidebar = null; /** * The featured video DOM element. @@ -65,6 +66,9 @@ let sidebarClone = null; */ let featuredVideo = null; +/** + * Populates the DOM elements that we need. + */ const queryDomElements = () => { sidebar = document.querySelector( '.sensei-course-theme__sidebar' ); header = document.querySelector( '.sensei-course-theme__header' ); @@ -73,23 +77,34 @@ const queryDomElements = () => { ); }; -function prepareSidebarClone() { +/** + * Creates an exact copy of the sidebar DOM element + * and sets it's position to fixed. The original sidebar + * element is hidden by seting it's opacity to 0. The clone, "stickySidebar" + * is used to keep the sidebar sticky. We still need the original sidebar + * element because we use it's original position to calculate and decide + * where the stickySideber should be position at any given time. + * + * This can be called multiple times and if it detects an existing stickySidebar + * present in the DOM it will remove it and insert the new one. + */ +function preparestickySidebar() { const sidebarRect = sidebar.getBoundingClientRect(); sidebarMarginTop = sidebar.style.marginTop ? parseInt( sidebar.style.marginTop, 10 ) : 0; - if ( sidebarClone?.remove ) { - sidebarClone.remove(); + if ( stickySidebar?.remove ) { + stickySidebar.remove(); } - sidebarClone = sidebar.cloneNode( true ); - sidebarClone.style.position = 'fixed'; - sidebarClone.style.opacity = 1; - sidebarClone.style.zIndex = 2; - sidebarClone.style.top = `${ sidebarRect.top }px`; - sidebarClone.style.left = `${ sidebarRect.left }px`; - sidebarClone.style.width = `${ sidebarRect.right - sidebarRect.left }px`; - sidebarClone.style.transition = 'none'; - sidebar.parentElement.append( sidebarClone ); + stickySidebar = sidebar.cloneNode( true ); + stickySidebar.style.position = 'fixed'; + stickySidebar.style.opacity = 1; + stickySidebar.style.zIndex = 2; + stickySidebar.style.top = `${ sidebarRect.top }px`; + stickySidebar.style.left = `${ sidebarRect.left }px`; + stickySidebar.style.width = `${ sidebarRect.right - sidebarRect.left }px`; + stickySidebar.style.transition = 'none'; + sidebar.parentElement.append( stickySidebar ); sidebar.style.opacity = 0; } @@ -101,62 +116,91 @@ function prepareSidebarClone() { const SIDEBAR_BOTTOM_MARGIN = 32; /** - * Monitors the sidebar position and sticks/unsticks it when needed. + * Updates the stickySidebar position. The position of the stickySidebar + * is relative to the Learning Mode header block. It assumes the header is + * fixed. * * @param {boolean} initialPosition True if the sidebar should be positioned * for it's initial position given the current * state of the scrollbar. Used when user opens - * the page and it is scrolled into the middle. + * the page and the page is scrolled into the middle. */ function updateSidebarPosition( initialPosition = false ) { - if ( ! sidebar || ! sidebarClone ) { + if ( ! sidebar || ! stickySidebar ) { return; } + // Get the current dimensions of the elements. const headerRect = header.getBoundingClientRect(); const sidebarRect = sidebar.getBoundingClientRect(); - const sidebarCloneRect = sidebarClone.getBoundingClientRect(); + const stickySidebarRect = stickySidebar.getBoundingClientRect(); + + // Calculate required values. const delta = getScrollDelta(); - let sidebarCloneNewTop = sidebarRect.top; - const sidebarCloneHeight = sidebarCloneRect.bottom - sidebarCloneRect.top; - const sidebarCloneIsTallerThanViewport = - sidebarCloneHeight > + const stickySidebarHeight = + stickySidebarRect.bottom - stickySidebarRect.top; + const stickySidebarIsTallerThanViewport = + stickySidebarHeight > window.innerHeight - ( headerRect.bottom + sidebarMarginTop + SIDEBAR_BOTTOM_MARGIN ); - - if ( sidebarCloneIsTallerThanViewport && ! initialPosition ) { - sidebarCloneNewTop = sidebarCloneRect.top - delta; - const sidebarCloneNewBottom = sidebarCloneRect.bottom - delta; - const sidebarCloneMinTop = sidebarRect.top; - const sidebarCloneMinBottom = + let stickySidebarNewTop = sidebarRect.top; + + // If the sidebar is very tall and does not fit into the viewport vertically + // we scroll the sticky sidebar up until the bottom is reached. Or we scroll + // the sticky sidebar down until the top of the sidebar is reached. + if ( stickySidebarIsTallerThanViewport && ! initialPosition ) { + stickySidebarNewTop = stickySidebarRect.top - delta; + const stickySidebarNewBottom = stickySidebarRect.bottom - delta; + const stickySidebarMinTop = sidebarRect.top; + const stickySidebarMinBottom = window.innerHeight - SIDEBAR_BOTTOM_MARGIN; + // The sidebar is moving upwards. if ( delta >= 0 ) { - if ( sidebarCloneNewBottom < sidebarCloneMinBottom ) { - sidebarCloneNewTop = sidebarCloneMinBottom - sidebarCloneHeight; + if ( stickySidebarNewBottom < stickySidebarMinBottom ) { + stickySidebarNewTop = + stickySidebarMinBottom - stickySidebarHeight; } + + // The sidebar is moving downwards. } else { - if ( sidebarCloneNewTop > headerRect.bottom ) { - sidebarCloneNewTop = headerRect.bottom; + if ( stickySidebarNewTop > headerRect.bottom ) { + stickySidebarNewTop = headerRect.bottom; } - if ( sidebarCloneNewTop < sidebarCloneMinTop ) { - sidebarCloneNewTop = sidebarCloneMinTop; + if ( stickySidebarNewTop < stickySidebarMinTop ) { + stickySidebarNewTop = stickySidebarMinTop; } } + + // If the sidebar fits into the viewport vertically + // then we simply stick it below the header when user + // scrolls it up above the header. } else if ( sidebarRect.top <= headerRect.bottom ) { - sidebarCloneNewTop = headerRect.bottom; + stickySidebarNewTop = headerRect.bottom; + + // By default we position the sticky sidebar on top + // of the original sidebar. } else { - sidebarCloneNewTop = sidebarRect.top; + stickySidebarNewTop = sidebarRect.top; } - sidebarClone.style.top = `${ sidebarCloneNewTop - sidebarMarginTop }px`; + // Need to subtract the sidebar top margin because fixed positioned elements + // are pushed down by css top margin. + stickySidebar.style.top = `${ stickySidebarNewTop - sidebarMarginTop }px`; } +/** + * Reinitializes the sticky sideber + */ const reinitializeSidebar = debounce( () => { - prepareSidebarClone(); + preparestickySidebar(); updateSidebarPosition( true ); }, 500 ); +/** + * Makes sure the height of the sidebar is at least the height + * of the featured video in 'modern' LM template. + */ function syncSidebarSizeWithVideo() { if ( featuredVideo && sidebar ) { new window.ResizeObserver( () => { @@ -178,7 +222,7 @@ function syncSidebarSizeWithVideo() { /** * Makes the sidebar sticky for relevant LM templates. */ -function stickySidebar() { +function setupStickySidebar() { if ( ! isStickySidebar() ) { return; } @@ -202,4 +246,4 @@ function stickySidebar() { } // eslint-disable-next-line @wordpress/no-global-event-listener -window.addEventListener( 'DOMContentLoaded', stickySidebar ); +window.addEventListener( 'DOMContentLoaded', setupStickySidebar ); From 4cbd7b66e396f73ca8bf118affcf7d5bf23208cc Mon Sep 17 00:00:00 2001 From: Nurguly Ashyrov Date: Fri, 16 Dec 2022 12:20:22 +0400 Subject: [PATCH 4/7] Learning Mode - Use empty placeholder for sticky sidebar instead of using a full tree clone. --- assets/course-theme/sidebar.js | 88 +++++++++++++++------------------- 1 file changed, 38 insertions(+), 50 deletions(-) diff --git a/assets/course-theme/sidebar.js b/assets/course-theme/sidebar.js index c630fe4d08..33ca66f678 100644 --- a/assets/course-theme/sidebar.js +++ b/assets/course-theme/sidebar.js @@ -52,12 +52,11 @@ let sidebar = null; let header = null; /** - * The clone of the sidebar DOM element. This is the sidebar element - * that user sees and interacts with. + * A placeholder for the sidebar. * * @member {HTMLElement} */ -let stickySidebar = null; +let sidebarPlaceholder = null; /** * The featured video DOM element. @@ -78,34 +77,26 @@ const queryDomElements = () => { }; /** - * Creates an exact copy of the sidebar DOM element - * and sets it's position to fixed. The original sidebar - * element is hidden by seting it's opacity to 0. The clone, "stickySidebar" - * is used to keep the sidebar sticky. We still need the original sidebar - * element because we use it's original position to calculate and decide - * where the stickySideber should be position at any given time. - * - * This can be called multiple times and if it detects an existing stickySidebar - * present in the DOM it will remove it and insert the new one. + * Sets 'position: fixed' for the sidebar and puts a placeholder in it's original + * place so the original layout is preserved. We also use the placeholder for sticky + * sidebar position calculation to determine where to put it in any given time. */ function preparestickySidebar() { - const sidebarRect = sidebar.getBoundingClientRect(); + if ( ! sidebarPlaceholder ) { + sidebarPlaceholder = sidebar.cloneNode(); + sidebarPlaceholder.style.visibility = 'hidden'; + sidebarPlaceholder.setAttribute( 'aria-hidden', 'true' ); + sidebar.style.transition = 'none'; + sidebar.style.position = 'fixed'; + sidebar.parentElement.append( sidebarPlaceholder ); + } sidebarMarginTop = sidebar.style.marginTop ? parseInt( sidebar.style.marginTop, 10 ) : 0; - if ( stickySidebar?.remove ) { - stickySidebar.remove(); - } - stickySidebar = sidebar.cloneNode( true ); - stickySidebar.style.position = 'fixed'; - stickySidebar.style.opacity = 1; - stickySidebar.style.zIndex = 2; - stickySidebar.style.top = `${ sidebarRect.top }px`; - stickySidebar.style.left = `${ sidebarRect.left }px`; - stickySidebar.style.width = `${ sidebarRect.right - sidebarRect.left }px`; - stickySidebar.style.transition = 'none'; - sidebar.parentElement.append( stickySidebar ); - sidebar.style.opacity = 0; + const sidebarRect = sidebarPlaceholder.getBoundingClientRect(); + sidebar.style.top = `${ sidebarRect.top }px`; + sidebar.style.left = `${ sidebarRect.left }px`; + sidebar.style.width = `${ sidebarRect.right - sidebarRect.left }px`; } /** @@ -126,67 +117,64 @@ const SIDEBAR_BOTTOM_MARGIN = 32; * the page and the page is scrolled into the middle. */ function updateSidebarPosition( initialPosition = false ) { - if ( ! sidebar || ! stickySidebar ) { + if ( ! sidebar ) { return; } // Get the current dimensions of the elements. const headerRect = header.getBoundingClientRect(); + const sidebarPlaceholderRect = sidebarPlaceholder.getBoundingClientRect(); const sidebarRect = sidebar.getBoundingClientRect(); - const stickySidebarRect = stickySidebar.getBoundingClientRect(); // Calculate required values. const delta = getScrollDelta(); - const stickySidebarHeight = - stickySidebarRect.bottom - stickySidebarRect.top; - const stickySidebarIsTallerThanViewport = - stickySidebarHeight > + const sidebarHeight = sidebarRect.bottom - sidebarRect.top; + const sidebarIsTallerThanViewport = + sidebarHeight > window.innerHeight - ( headerRect.bottom + sidebarMarginTop + SIDEBAR_BOTTOM_MARGIN ); - let stickySidebarNewTop = sidebarRect.top; + let sidebarNewTop = sidebarPlaceholderRect.top; // If the sidebar is very tall and does not fit into the viewport vertically // we scroll the sticky sidebar up until the bottom is reached. Or we scroll // the sticky sidebar down until the top of the sidebar is reached. - if ( stickySidebarIsTallerThanViewport && ! initialPosition ) { - stickySidebarNewTop = stickySidebarRect.top - delta; - const stickySidebarNewBottom = stickySidebarRect.bottom - delta; - const stickySidebarMinTop = sidebarRect.top; - const stickySidebarMinBottom = - window.innerHeight - SIDEBAR_BOTTOM_MARGIN; + if ( sidebarIsTallerThanViewport && ! initialPosition ) { + sidebarNewTop = sidebarRect.top - delta; + const sidebarNewBottom = sidebarRect.bottom - delta; + const sidebarMinTop = sidebarPlaceholderRect.top; + const sidebarMinBottom = window.innerHeight - SIDEBAR_BOTTOM_MARGIN; // The sidebar is moving upwards. if ( delta >= 0 ) { - if ( stickySidebarNewBottom < stickySidebarMinBottom ) { - stickySidebarNewTop = - stickySidebarMinBottom - stickySidebarHeight; + if ( sidebarNewBottom < sidebarMinBottom ) { + sidebarNewTop = sidebarMinBottom - sidebarHeight; } // The sidebar is moving downwards. } else { - if ( stickySidebarNewTop > headerRect.bottom ) { - stickySidebarNewTop = headerRect.bottom; + if ( sidebarNewTop > headerRect.bottom ) { + sidebarNewTop = headerRect.bottom; } - if ( stickySidebarNewTop < stickySidebarMinTop ) { - stickySidebarNewTop = stickySidebarMinTop; + if ( sidebarNewTop < sidebarMinTop ) { + sidebarNewTop = sidebarMinTop; } } // If the sidebar fits into the viewport vertically // then we simply stick it below the header when user // scrolls it up above the header. - } else if ( sidebarRect.top <= headerRect.bottom ) { - stickySidebarNewTop = headerRect.bottom; + } else if ( sidebarPlaceholderRect.top <= headerRect.bottom ) { + sidebarNewTop = headerRect.bottom; // By default we position the sticky sidebar on top // of the original sidebar. } else { - stickySidebarNewTop = sidebarRect.top; + sidebarNewTop = sidebarPlaceholderRect.top; } // Need to subtract the sidebar top margin because fixed positioned elements // are pushed down by css top margin. - stickySidebar.style.top = `${ stickySidebarNewTop - sidebarMarginTop }px`; + sidebar.style.top = `${ sidebarNewTop - sidebarMarginTop }px`; } /** From 66fe8f400aaa89d733781471b69036175697617c Mon Sep 17 00:00:00 2001 From: Nurguly Ashyrov Date: Fri, 16 Dec 2022 15:03:17 +0400 Subject: [PATCH 5/7] Learning Mode - Get rid of the top margin for sticky sidebar. --- assets/course-theme/sidebar.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/assets/course-theme/sidebar.js b/assets/course-theme/sidebar.js index 33ca66f678..eaa6719581 100644 --- a/assets/course-theme/sidebar.js +++ b/assets/course-theme/sidebar.js @@ -30,13 +30,6 @@ const isStickySidebar = () => document.body.classList.contains( `learning-mode--${ templateName }` ) ); -/** - * Sidebar margin top. - * - * @member {number} - */ -let sidebarMarginTop = 0; - /** * The sidebar DOM element. * @@ -89,10 +82,8 @@ function preparestickySidebar() { sidebar.style.transition = 'none'; sidebar.style.position = 'fixed'; sidebar.parentElement.append( sidebarPlaceholder ); + sidebar.style.marginTop = '0'; } - sidebarMarginTop = sidebar.style.marginTop - ? parseInt( sidebar.style.marginTop, 10 ) - : 0; const sidebarRect = sidebarPlaceholder.getBoundingClientRect(); sidebar.style.top = `${ sidebarRect.top }px`; sidebar.style.left = `${ sidebarRect.left }px`; @@ -131,8 +122,7 @@ function updateSidebarPosition( initialPosition = false ) { const sidebarHeight = sidebarRect.bottom - sidebarRect.top; const sidebarIsTallerThanViewport = sidebarHeight > - window.innerHeight - - ( headerRect.bottom + sidebarMarginTop + SIDEBAR_BOTTOM_MARGIN ); + window.innerHeight - ( headerRect.bottom + SIDEBAR_BOTTOM_MARGIN ); let sidebarNewTop = sidebarPlaceholderRect.top; // If the sidebar is very tall and does not fit into the viewport vertically @@ -174,7 +164,7 @@ function updateSidebarPosition( initialPosition = false ) { // Need to subtract the sidebar top margin because fixed positioned elements // are pushed down by css top margin. - sidebar.style.top = `${ sidebarNewTop - sidebarMarginTop }px`; + sidebar.style.top = `${ sidebarNewTop }px`; } /** From b5c8462a623219dc9414845a4674b90a481e417f Mon Sep 17 00:00:00 2001 From: Nurguly Ashyrov Date: Fri, 16 Dec 2022 17:11:30 +0400 Subject: [PATCH 6/7] Learning Mode - Detect the sticky sidebar from the sidebar block class. --- assets/course-theme/sidebar.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/assets/course-theme/sidebar.js b/assets/course-theme/sidebar.js index eaa6719581..5ca249b79b 100644 --- a/assets/course-theme/sidebar.js +++ b/assets/course-theme/sidebar.js @@ -26,9 +26,8 @@ const getScrollDelta = () => { * @return {boolean} True if it is sticky. False otherwise. */ const isStickySidebar = () => - [ 'modern', 'video-full' ].some( ( templateName ) => - document.body.classList.contains( `learning-mode--${ templateName }` ) - ); + !! document.querySelectorAll( '.sensei-course-theme__sidebar--is-sticky' ) + .length; /** * The sidebar DOM element. From 76f664b1a26593aa63405e857003594dd9f694ca Mon Sep 17 00:00:00 2001 From: Nurguly Ashyrov Date: Thu, 22 Dec 2022 16:55:28 +0400 Subject: [PATCH 7/7] Fine tune the sticky sidebar. Use `transform: translateY()` for scrolling. --- assets/course-theme/sidebar.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/assets/course-theme/sidebar.js b/assets/course-theme/sidebar.js index 5ca249b79b..e6c549f130 100644 --- a/assets/course-theme/sidebar.js +++ b/assets/course-theme/sidebar.js @@ -80,13 +80,14 @@ function preparestickySidebar() { sidebarPlaceholder.setAttribute( 'aria-hidden', 'true' ); sidebar.style.transition = 'none'; sidebar.style.position = 'fixed'; - sidebar.parentElement.append( sidebarPlaceholder ); sidebar.style.marginTop = '0'; + sidebar.parentElement.prepend( sidebarPlaceholder ); } const sidebarRect = sidebarPlaceholder.getBoundingClientRect(); - sidebar.style.top = `${ sidebarRect.top }px`; + sidebar.style.top = `0`; sidebar.style.left = `${ sidebarRect.left }px`; sidebar.style.width = `${ sidebarRect.right - sidebarRect.left }px`; + sidebar.style.transform = `translateY(${ sidebarRect.top }px)`; } /** @@ -163,7 +164,8 @@ function updateSidebarPosition( initialPosition = false ) { // Need to subtract the sidebar top margin because fixed positioned elements // are pushed down by css top margin. - sidebar.style.top = `${ sidebarNewTop }px`; + + sidebar.style.transform = `translateY(${ sidebarNewTop }px)`; } /**