Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 36 additions & 21 deletions assets/src/js/frontend/services/site-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,29 @@ const CSS_VARIABLE_HEADER_OFFSET = '--tutor-site-header-offset';
const CSS_VARIABLE_HEADER_OVERLAY_OFFSET = '--tutor-site-header-overlay-offset';

const WP_ADMIN_BAR_ID = 'wpadminbar';
const MAX_HEADER_HEIGHT = 250;

const SITE_SHELL_ROOT_SELECTOR = '[data-tutor-learning-site-shell], [data-tutor-dashboard-site-shell]';

/**
* Returns the lowest visible bottom edge among the site header and admin bar.
*/
const getVisibleHeaderBoundary = (elements: HTMLElement[]): number => {
return elements.reduce((offset, element) => {
return Math.max(offset, element.getBoundingClientRect().bottom, 0);
const boundary = elements.reduce((offset, element) => {
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden') {
return offset;
}

const rect = element.getBoundingClientRect();
if (rect.bottom <= 0 || rect.top > MAX_HEADER_HEIGHT) {
return offset;
}

return Math.max(offset, rect.bottom);
}, 0);

return Math.min(Math.round(boundary), MAX_HEADER_HEIGHT);
};

class SiteShellController {
Expand All @@ -29,6 +42,7 @@ class SiteShellController {
this.updateOffset();

const isSticky = this.isStickyHeader(this.themeHeader);
const hasAdminBar = Boolean(document.getElementById(WP_ADMIN_BAR_ID));

if (isSticky && this.themeHeader && typeof ResizeObserver !== 'undefined') {
this.resizeObserver = new ResizeObserver(() => this.scheduleOffsetUpdate());
Expand All @@ -37,7 +51,7 @@ class SiteShellController {

window.addEventListener('resize', this.scheduleOffsetUpdate);

if (isSticky) {
if (isSticky || hasAdminBar) {
window.addEventListener('scroll', this.scheduleOffsetUpdate, { passive: true });
}
}
Expand Down Expand Up @@ -76,40 +90,41 @@ class SiteShellController {
}

const position = window.getComputedStyle(header).position;
if (position === 'fixed' || position === 'sticky') {
if (position === 'fixed' || position === 'sticky' || position === 'absolute') {
return true;
}

const className = (header.className || '').toString();
return /sticky/i.test(className) || header.hasAttribute('data-sticky');
return /sticky|transparent/i.test(className) || header.hasAttribute('data-sticky');
}

private getStickyHeader(header: HTMLElement | null): HTMLElement | null {
if (!header) {
return null;
}

const position = window.getComputedStyle(header).position;
if (position === 'fixed' || position === 'sticky') {
const isHeaderBar = (el: HTMLElement): boolean => {
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden') {
return false;
}
const rect = el.getBoundingClientRect();
// Must have realistic header bar dimensions (reject full-screen mobile drawers/modals > 250px)
return rect.height > 0 && rect.height <= MAX_HEADER_HEIGHT && rect.width >= window.innerWidth * 0.4;
};

if (this.isStickyHeader(header) && isHeaderBar(header)) {
return header;
}

// Check if an inner row is fixed or sticky (e.g. Sydney, Astra)
const children = header.querySelectorAll<HTMLElement>('*');
const children = header.querySelectorAll<HTMLElement>(
'div, nav, header, [class*="header"], [class*="sticky"], [class*="navbar"], [class*="shfb"]',
);
for (let i = 0; i < children.length; i++) {
const pos = window.getComputedStyle(children[i]).position;
if (pos === 'fixed' || pos === 'sticky') {
return children[i];
}
}

// If the header has sticky classes (e.g. TutorStarter .header-sticky, Sydney .has-sticky-header)
// and is active in the viewport
const className = (header.className || '').toString();
if (/sticky/i.test(className) || header.hasAttribute('data-sticky')) {
const rect = header.getBoundingClientRect();
if (rect.top <= 50 && rect.bottom > 0) {
return header;
const child = children[i];
if (this.isStickyHeader(child) && isHeaderBar(child)) {
return child;
}
}

Expand Down
7 changes: 3 additions & 4 deletions assets/src/scss/frontend/dashboard/layout/_account.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
--tutor-site-header-offset: 0px;
--tutor-site-header-overlay-offset: 0px;

width: 100%;

.tutor-account-header {
position: sticky;
top: 0;
Expand Down Expand Up @@ -36,13 +38,10 @@

&.tutor-has-site-shell {
padding-top: var(--tutor-site-header-overlay-offset);

.tutor-account-header {
top: var(--tutor-site-header-offset);
}
}
}

.tutor-account-page-wrapper.tutor-has-site-shell .tutor-account-header,
body:has(#wpadminbar) .tutor-account-page-wrapper.tutor-has-site-shell .tutor-account-header {
top: var(--tutor-site-header-offset);
}
Expand Down
5 changes: 1 addition & 4 deletions assets/src/scss/frontend/dashboard/layout/_header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,7 @@
}
}

.tutor-dashboard-layout.tutor-has-site-shell .tutor-dashboard-header {
top: var(--tutor-site-header-offset);
}

.tutor-dashboard-layout.tutor-has-site-shell .tutor-dashboard-header,
body:has(#wpadminbar) .tutor-dashboard-layout.tutor-has-site-shell .tutor-dashboard-header {
top: var(--tutor-site-header-offset);
}
3 changes: 3 additions & 0 deletions assets/src/scss/frontend/dashboard/layout/_layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
--tutor-site-header-offset: 0px;
--tutor-site-header-overlay-offset: 0px;

width: 100%;
display: grid;
grid-template-columns: 274px 1fr;

Expand Down Expand Up @@ -42,6 +43,8 @@
--tutor-site-header-offset: 0px;
--tutor-site-header-overlay-offset: 0px;

width: 100%;

&.tutor-has-site-shell {
padding-top: var(--tutor-site-header-overlay-offset);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,7 @@
}
}

.tutor-has-site-shell .tutor-profile-header {
top: var(--tutor-site-header-offset);
}

.tutor-has-site-shell .tutor-profile-header,
body:has(#wpadminbar) .tutor-has-site-shell .tutor-profile-header {
top: var(--tutor-site-header-offset);
}
6 changes: 1 addition & 5 deletions assets/src/scss/frontend/dashboard/layout/_sidebar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,7 @@
}
}

.tutor-dashboard-layout.tutor-has-site-shell .tutor-dashboard-sidebar {
top: var(--tutor-site-header-offset);
min-height: calc(100dvh - var(--tutor-site-header-offset));
}

.tutor-dashboard-layout.tutor-has-site-shell .tutor-dashboard-sidebar,
body:has(#wpadminbar) .tutor-dashboard-layout.tutor-has-site-shell .tutor-dashboard-sidebar {
top: var(--tutor-site-header-offset);
min-height: calc(100dvh - var(--tutor-site-header-offset));
Expand Down
55 changes: 8 additions & 47 deletions assets/src/scss/frontend/learning-area/_quiz.scss
Original file line number Diff line number Diff line change
Expand Up @@ -555,18 +555,8 @@ $tutor-quiz-draw-pin-chrome: '#{$tutor-spacing-5} + 2.5rem + #{$tutor-spacing-8}
}

&[data-has-scroll-left='true'][data-has-scroll-right='false'] {
-webkit-mask-image: linear-gradient(
to right,
transparent 0,
black #{$tutor-spacing-10},
black 100%
);
mask-image: linear-gradient(
to right,
transparent 0,
black #{$tutor-spacing-10},
black 100%
);
-webkit-mask-image: linear-gradient(to right, transparent 0, black #{$tutor-spacing-10}, black 100%);
mask-image: linear-gradient(to right, transparent 0, black #{$tutor-spacing-10}, black 100%);
}

&[data-has-scroll-left='false'][data-has-scroll-right='true'] {
Expand All @@ -576,12 +566,7 @@ $tutor-quiz-draw-pin-chrome: '#{$tutor-spacing-5} + 2.5rem + #{$tutor-spacing-8}
black calc(100% - #{$tutor-spacing-10}),
transparent 100%
);
mask-image: linear-gradient(
to right,
black 0,
black calc(100% - #{$tutor-spacing-10}),
transparent 100%
);
mask-image: linear-gradient(to right, black 0, black calc(100% - #{$tutor-spacing-10}), transparent 100%);
}

&::-webkit-scrollbar {
Expand Down Expand Up @@ -1299,7 +1284,7 @@ $tutor-quiz-draw-pin-chrome: '#{$tutor-spacing-5} + 2.5rem + #{$tutor-spacing-8}
display: block;
// Cap against the questions slot (size container), not a stretched card.
// Interpolate the whole value so Sass does not strip the outer calc().
max-height: #{"min(744px, calc(100cqh - (#{$tutor-quiz-draw-pin-chrome})))"};
max-height: #{'min(744px, calc(100cqh - (#{$tutor-quiz-draw-pin-chrome})))'};
max-width: 100%;
width: auto;
height: auto;
Expand Down Expand Up @@ -1471,13 +1456,16 @@ $tutor-quiz-draw-pin-chrome: '#{$tutor-spacing-5} + 2.5rem + #{$tutor-spacing-8}
// An active quiz is normally an isolated, viewport-fixed screen. When the
// Learning Area renders the theme shell, keep its controls inside the quiz
// instead so the theme footer remains in document flow after the attempt.
.tutor-learning-area.tutor-has-site-shell {
.tutor-learning-area.tutor-has-site-shell,
body:has(#wpadminbar) .tutor-learning-area.tutor-has-site-shell {
.tutor-quiz-header {
position: sticky;
top: var(--tutor-site-header-offset);
}

.tutor-quiz-submission {
padding-top: 0;

&[data-question-layout-view='single_question'],
&[data-question-layout-view='question_pagination'] {
height: calc(100dvh - var(--tutor-site-header-overlay-offset));
Expand Down Expand Up @@ -1505,30 +1493,3 @@ $tutor-quiz-draw-pin-chrome: '#{$tutor-spacing-5} + 2.5rem + #{$tutor-spacing-8}
position: absolute;
}
}

// Keep the site-shell geometry authoritative when the WordPress admin bar is
// present; the isolated-mode admin-bar selectors above intentionally win in
// every other context.
body:has(#wpadminbar) .tutor-learning-area.tutor-has-site-shell {
.tutor-quiz-header {
position: sticky;
top: var(--tutor-site-header-offset);
}

.tutor-quiz-submission {
&[data-question-layout-view='single_question'],
&[data-question-layout-view='question_pagination'] {
height: calc(100dvh - var(--tutor-site-header-overlay-offset));
}
}

.tutor-quiz-questions {
&[data-question-layout-view='single_question'],
&[data-question-layout-view='question_pagination'] {
top: calc(#{$tutor-quiz-header-offset} + var(--tutor-site-header-overlay-offset));
height: calc(
100% - #{$tutor-quiz-header-offset} - #{$tutor-quiz-footer-offset} - var(--tutor-site-header-overlay-offset)
);
}
}
}
19 changes: 19 additions & 0 deletions assets/src/scss/frontend/learning-area/layout/_layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
$tutor-fixed-footer-height: 80px;
$tutor-fixed-footer-offset: $tutor-spacing-6;

.tutor-learning-page {
width: 100%;
}

.tutor-learning-area {
--tutor-site-header-offset: 0px;
--tutor-site-header-overlay-offset: 0px;
Expand All @@ -27,6 +31,14 @@ $tutor-fixed-footer-offset: $tutor-spacing-6;
padding-inline-start: 0;
}

.tutor-learning-area-content {
padding-inline-end: $tutor-spacing-4;
}

&.is-fullscreen .tutor-learning-area-content {
padding-inline: $tutor-spacing-4;
}

&.tutor-has-site-shell {
// Sticky positioning offsets the header visually without reserving that
// offset in normal flow. Reserve only the theme-header portion that
Expand All @@ -35,6 +47,7 @@ $tutor-fixed-footer-offset: $tutor-spacing-6;

.tutor-learning-area-content {
padding-top: 0;
padding-inline-end: $tutor-spacing-4;
}

@include tutor-breakpoint-up(lg) {
Expand All @@ -57,6 +70,7 @@ $tutor-fixed-footer-offset: $tutor-spacing-6;
.tutor-learning-area-content {
grid-column: 2;
min-width: 0;
padding-inline-end: $tutor-spacing-4;
}
}

Expand All @@ -67,6 +81,10 @@ $tutor-fixed-footer-offset: $tutor-spacing-6;
top: var(--tutor-site-header-offset);
height: calc(100dvh - var(--tutor-site-header-offset));
}

.tutor-learning-area-content {
padding-inline-end: 0;
}
}
}

Expand All @@ -82,6 +100,7 @@ $tutor-fixed-footer-offset: $tutor-spacing-6;

.tutor-learning-area-content {
grid-column: 2;
padding-inline: $tutor-spacing-4;
}
}
}
Expand Down
17 changes: 13 additions & 4 deletions classes/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -1334,7 +1334,7 @@
$user_id = $this->get_user_id( $user_id );

// Delete Quiz submissions.
$attempts = \Tutor\Models\QuizModel::get_quiz_attempts_by_course_ids( $start = 0, $limit = 99999999, $course_ids = array( $course_id ), $search_filter = '', $course_filter = '', $date_filter = '', $order_filter = '', $user_id = $user_id, false, true );

Check failure on line 1337 in classes/Utils.php

View workflow job for this annotation

GitHub Actions / WPCS

Assignments must be the first block of code on a line

if ( is_array( $attempts ) ) {
$attempt_ids = array_map(
Expand Down Expand Up @@ -1512,7 +1512,7 @@
ON topic.ID = items.post_parent
WHERE topic.post_parent = %d
AND items.post_status = %s
" . ( $post_type ? " AND items.post_type='{$post_type}' " : '' ) . '

Check failure on line 1515 in classes/Utils.php

View workflow job for this annotation

GitHub Actions / WPCS

Use placeholders and $wpdb-&gt;prepare(); found :

Check failure on line 1515 in classes/Utils.php

View workflow job for this annotation

GitHub Actions / WPCS

Use placeholders and $wpdb-&gt;prepare(); found interpolated variable {$post_type} at &quot; AND items.post_type='{$post_type}' &quot;

Check failure on line 1515 in classes/Utils.php

View workflow job for this annotation

GitHub Actions / WPCS

Use placeholders and $wpdb-&gt;prepare(); found ?

Check failure on line 1515 in classes/Utils.php

View workflow job for this annotation

GitHub Actions / WPCS

Use placeholders and $wpdb-&gt;prepare(); found $post_type
ORDER BY topic.menu_order ASC,
items.menu_order ASC;
',
Expand Down Expand Up @@ -2145,11 +2145,11 @@
ON user.ID = posts.post_author
WHERE posts.post_type = %s
AND posts.post_status = %s
{$course_query}

Check failure on line 2148 in classes/Utils.php

View workflow job for this annotation

GitHub Actions / WPCS

Use placeholders and $wpdb-&gt;prepare(); found interpolated variable {$course_query} at {$course_query}&#10;
{$date_query}

Check failure on line 2149 in classes/Utils.php

View workflow job for this annotation

GitHub Actions / WPCS

Use placeholders and $wpdb-&gt;prepare(); found interpolated variable {$date_query} at {$date_query}&#10;
AND (user.display_name LIKE %s OR user.user_email = %s OR user.user_login LIKE %s)
GROUP BY post_author
{$order_query}

Check failure on line 2152 in classes/Utils.php

View workflow job for this annotation

GitHub Actions / WPCS

Use placeholders and $wpdb-&gt;prepare(); found interpolated variable {$order_query} at {$order_query}&#10;
LIMIT %d, %d
",
'tutor_enrolled',
Expand Down Expand Up @@ -2204,8 +2204,8 @@
ON user.ID = posts.post_author
WHERE posts.post_type = %s
AND posts.post_status = %s
{$course_query}

Check failure on line 2207 in classes/Utils.php

View workflow job for this annotation

GitHub Actions / WPCS

Use placeholders and $wpdb-&gt;prepare(); found interpolated variable {$course_query} at {$course_query}&#10;
{$date_query}

Check failure on line 2208 in classes/Utils.php

View workflow job for this annotation

GitHub Actions / WPCS

Use placeholders and $wpdb-&gt;prepare(); found interpolated variable {$date_query} at {$date_query}&#10;
AND (user.display_name LIKE %s OR user.user_email = %s OR user.user_login LIKE %s)
GROUP BY user.ID
",
Expand Down Expand Up @@ -9067,6 +9067,9 @@

if ( $show ) {
if ( $is_block_theme ) {
$theme = wp_get_theme();
$theme_slug = $theme->get( 'TextDomain' );
$header_content = do_blocks( '<!-- wp:template-part {"slug":"header","theme":"' . $theme_slug . '","tagName":"header","className":"site-header","layout":{"inherit":true}} /-->' );
?>
<!doctype html>
<html <?php language_attributes(); ?>>
Expand All @@ -9078,9 +9081,7 @@
<?php wp_body_open(); ?>
<div class="wp-site-blocks">
<?php
$theme = wp_get_theme();
$theme_slug = $theme->get( 'TextDomain' );
echo do_blocks( '<!-- wp:template-part {"slug":"header","theme":"' . $theme_slug . '","tagName":"header","className":"site-header","layout":{"inherit":true}} /-->' );
echo $header_content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
get_header();
}
Expand Down Expand Up @@ -9127,8 +9128,16 @@
if ( $is_block_theme ) {
$theme = wp_get_theme();
$theme_slug = $theme->get( 'TextDomain' );
echo do_blocks( '<!-- wp:template-part {"slug":"footer","theme":"' . $theme_slug . '","tagName":"footer","className":"site-footer","layout":{"inherit":true}} /-->' );
echo do_blocks( '<!-- wp:template-part {"slug":"footer","theme":"' . $theme_slug . '","tagName":"footer","className":"site-footer","layout":{"inherit":true}} /-->' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div>';

if ( function_exists( 'wp_style_engine_get_stylesheet_from_context' ) ) {
$late_styles = wp_style_engine_get_stylesheet_from_context( 'block-supports' );
if ( ! empty( $late_styles ) ) {
echo '<style id="wp-block-supports-late-inline-css">' . $late_styles . '</style>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}

wp_footer();
echo '</body>';
echo '</html>';
Expand Down
Loading