From a820935b647a29b5b40274f14a415543ce43f9e7 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Wed, 29 Jul 2026 15:46:07 +0600 Subject: [PATCH 01/51] feat(icons): add dark mode icon --- assets/icons/dark.svg | 1 + assets/src/js/v3/shared/icons/types.ts | 1 + classes/Icon.php | 1 + 3 files changed, 3 insertions(+) create mode 100644 assets/icons/dark.svg diff --git a/assets/icons/dark.svg b/assets/icons/dark.svg new file mode 100644 index 0000000000..4b5a89f7c3 --- /dev/null +++ b/assets/icons/dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/src/js/v3/shared/icons/types.ts b/assets/src/js/v3/shared/icons/types.ts index 2ff6d7597a..bf77d6c9b0 100644 --- a/assets/src/js/v3/shared/icons/types.ts +++ b/assets/src/js/v3/shared/icons/types.ts @@ -123,6 +123,7 @@ export const icons = [ 'css', 'csv', 'currency', + 'dark', 'dbf', 'delete', 'delete2', diff --git a/classes/Icon.php b/classes/Icon.php index db5cbadcc1..eb85ec4822 100644 --- a/classes/Icon.php +++ b/classes/Icon.php @@ -139,6 +139,7 @@ final class Icon { const CSS = 'css'; const CSV = 'csv'; const CURRENCY = 'currency'; + const DARK = 'dark'; const DBF = 'dbf'; const DELETE = 'delete'; const DELETE_2 = 'delete-2'; From 7a3fc992f2f7b53bde2f6cde32808e414cd53e26 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Wed, 29 Jul 2026 15:46:41 +0600 Subject: [PATCH 02/51] feat(brand-logo): add brand logo template and frontend styles --- .../scss/frontend/components/_brand-logo.scss | 15 ++++++ .../src/scss/frontend/components/_index.scss | 1 + .../frontend/dashboard/layout/_sidebar.scss | 7 ++- templates/dashboard/components/sidebar.php | 11 +---- templates/shared/brand-logo.php | 49 +++++++++++++++++++ 5 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 assets/src/scss/frontend/components/_brand-logo.scss create mode 100644 templates/shared/brand-logo.php diff --git a/assets/src/scss/frontend/components/_brand-logo.scss b/assets/src/scss/frontend/components/_brand-logo.scss new file mode 100644 index 0000000000..8bc1463b36 --- /dev/null +++ b/assets/src/scss/frontend/components/_brand-logo.scss @@ -0,0 +1,15 @@ +// Theme-aware Tutor branding shared by the dashboard and learning area. + +.tutor-brand-logo__dark { + display: none; +} + +[data-tutor-theme='dark'] { + .tutor-brand-logo__light { + display: none; + } + + .tutor-brand-logo__dark { + display: inline; + } +} diff --git a/assets/src/scss/frontend/components/_index.scss b/assets/src/scss/frontend/components/_index.scss index eefc76093a..20504733d0 100644 --- a/assets/src/scss/frontend/components/_index.scss +++ b/assets/src/scss/frontend/components/_index.scss @@ -11,3 +11,4 @@ @forward 'quiz-summary'; @forward 'player'; @forward 'tour'; +@forward 'brand-logo'; diff --git a/assets/src/scss/frontend/dashboard/layout/_sidebar.scss b/assets/src/scss/frontend/dashboard/layout/_sidebar.scss index d791179aa6..81a5c2ca54 100644 --- a/assets/src/scss/frontend/dashboard/layout/_sidebar.scss +++ b/assets/src/scss/frontend/dashboard/layout/_sidebar.scss @@ -29,6 +29,12 @@ border-bottom: 1px solid $tutor-border-idle; overflow: hidden; + + .tutor-brand-logo { + display: block; + max-width: 120px; + } + img { display: block; max-width: 120px; @@ -43,7 +49,6 @@ } } } - .tutor-dashboard-sidebar-nav { padding: $tutor-spacing-8 $tutor-spacing-6; diff --git a/templates/dashboard/components/sidebar.php b/templates/dashboard/components/sidebar.php index 914a6b5ecb..b90b3a0745 100644 --- a/templates/dashboard/components/sidebar.php +++ b/templates/dashboard/components/sidebar.php @@ -41,16 +41,7 @@ ?>
diff --git a/templates/account.php b/templates/account.php index fbfb1e3865..87b890e897 100644 --- a/templates/account.php +++ b/templates/account.php @@ -26,19 +26,14 @@ $close_url = $dashboard_url; Dashboard::set_document_title( $meta_title ); + +$show_dashboard_site_header = (bool) tutor_utils()->get_option( 'show_dashboard_site_header', true ); +$show_dashboard_site_footer = (bool) tutor_utils()->get_option( 'show_dashboard_site_footer', true ); + +tutor_page_elements_header( $show_dashboard_site_header ); ?> - -> - - - - - -> - - - - + - -> - - - - - -> - -get_option( 'show_dashboard_site_header', true ); +$show_dashboard_site_footer = (bool) tutor_utils()->get_option( 'show_dashboard_site_footer', true ); + +tutor_page_elements_header( $show_dashboard_site_header ); $page_template = $page_data['template'] ?? ''; $back_url = tutor_utils()->tutor_dashboard_url(); @@ -44,6 +37,5 @@ } ?>
- - - + - - > - - - - - - > - - get_option( 'show_dashboard_site_header', true ); + tutor_page_elements_header( $show_dashboard_site_header ); endif; $user_id = get_current_user_id(); @@ -156,7 +147,5 @@ - + get_option( 'show_dashboard_site_footer', true ) ); ?> - - diff --git a/templates/learning-area/index.php b/templates/learning-area/index.php index 421b040a6c..b1f35f53a7 100644 --- a/templates/learning-area/index.php +++ b/templates/learning-area/index.php @@ -72,16 +72,12 @@ $page_meta_title = sprintf( __( '%1$s - %2$s', 'tutor' ), $learning_meta_title, $site_name ); Dashboard::set_document_title( $page_meta_title ); + +$show_learning_site_header = (bool) tutor_utils()->get_option( 'show_learning_site_header', false ); +$show_learning_site_footer = (bool) tutor_utils()->get_option( 'show_learning_site_footer', false ); +tutor_page_elements_header( $show_learning_site_header ); ?> - - > - - - - - - x-data="tutorCourseCompleteHandler()"> - +
'; + tutor_page_elements_footer( $show_learning_site_footer ); return; } @@ -119,7 +117,8 @@ if ( $tutor_is_started_quiz ) { tutor_load_template( 'learning-area.quiz.attempt' ); - wp_footer(); + echo '
'; + tutor_page_elements_footer( $show_learning_site_footer ); exit; } } @@ -129,13 +128,14 @@ if ( Quiz::ACTION_VIEW_DETAILS === $user_action && $attempt_id ) { tutor_load_template( 'learning-area.quiz.attempt-details' ); - wp_footer(); + echo ''; + tutor_page_elements_footer( $show_learning_site_footer ); exit; } ?>
@@ -178,8 +178,6 @@ class="tutor-btn tutor-btn-outline tutor-btn-small tutor-btn-icon tutor-expand-b tutor_load_template( 'shared.tour' ); } ?> - - render(); } ?> - - \ No newline at end of file +
+ diff --git a/templates/single-content-loader.php b/templates/single-content-loader.php index 87b03b7f0e..e606a52b5b 100644 --- a/templates/single-content-loader.php +++ b/templates/single-content-loader.php @@ -24,8 +24,7 @@ $next_id = $contents->next_id; $user_id = get_current_user_id(); -$is_course_completed = tutor_utils()->is_completed_course( $course_id, $user_id ); -$enable_spotlight_mode = tutor_utils()->get_option( 'enable_spotlight_mode' ); +$is_course_completed = tutor_utils()->is_completed_course( $course_id, $user_id ); //phpcs:ignore WordPress.PHP.DontExtract.extract_extract extract( $data ); // $data variable consist $context, $html_content. @@ -63,7 +62,7 @@ function tutor_course_single_sidebar( $echo = true, $context = 'desktop' ) { ?> -
+
diff --git a/templates/single-quiz.php b/templates/single-quiz.php index 01a7eba93a..d0cf83d805 100644 --- a/templates/single-quiz.php +++ b/templates/single-quiz.php @@ -19,7 +19,6 @@ $contents = tutor_utils()->get_course_prev_next_contents_by_id( $content_id ); $previous_id = $contents->previous_id; $course = CourseModel::get_course_by_quiz( get_the_ID() ); -$enable_spotlight_mode = tutor_utils()->get_option( 'enable_spotlight_mode' ); ob_start(); ?> diff --git a/views/options/field-types/page_elements.php b/views/options/field-types/page_elements.php new file mode 100644 index 0000000000..af53bebc92 --- /dev/null +++ b/views/options/field-types/page_elements.php @@ -0,0 +1,92 @@ + array( + 'label' => __( 'Dashboard', 'tutor' ), + 'header' => array( + 'key' => 'show_dashboard_site_header', + 'default' => 'on', + ), + 'footer' => array( + 'key' => 'show_dashboard_site_footer', + 'default' => 'on', + ), + ), + 'learning' => array( + 'label' => __( 'Learning Experience', 'tutor' ), + 'header' => array( + 'key' => 'show_learning_site_header', + 'default' => 'off', + ), + 'footer' => array( + 'key' => 'show_learning_site_footer', + 'default' => 'off', + ), + ), +); +?> +
+
+
+
+
+ +
+
+ + + + + + + + + +
+ + +
+
+ + get( $setting['key'], $setting['default'] ); + $is_enabled = 'on' === $option_value || true === $option_value || 1 === (int) $option_value; + ?> + + +
+ + + + +
+
From 39d57cf76ba2e5a80129aec7fde8c2ea127c6ee2 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Wed, 29 Jul 2026 16:42:09 +0600 Subject: [PATCH 06/51] feat(icons): replace dashicons with custom footer svg icon in page elements settings --- assets/icons/footer.svg | 1 + assets/src/js/v3/shared/icons/types.ts | 1 + assets/src/scss/admin-dashboard/_tutor-admin.scss | 13 +++++++++++++ classes/Icon.php | 1 + views/options/field-types/page_elements.php | 11 +++++++++-- 5 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 assets/icons/footer.svg diff --git a/assets/icons/footer.svg b/assets/icons/footer.svg new file mode 100644 index 0000000000..cf8df45ae0 --- /dev/null +++ b/assets/icons/footer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/src/js/v3/shared/icons/types.ts b/assets/src/js/v3/shared/icons/types.ts index bf77d6c9b0..cda7648e6f 100644 --- a/assets/src/js/v3/shared/icons/types.ts +++ b/assets/src/js/v3/shared/icons/types.ts @@ -181,6 +181,7 @@ export const icons = [ 'fireDisabled', 'fla', 'font', + 'footer', 'freeShippingType', 'gift', 'giftCard', diff --git a/assets/src/scss/admin-dashboard/_tutor-admin.scss b/assets/src/scss/admin-dashboard/_tutor-admin.scss index ab1bc5bb0f..728278d55c 100644 --- a/assets/src/scss/admin-dashboard/_tutor-admin.scss +++ b/assets/src/scss/admin-dashboard/_tutor-admin.scss @@ -492,6 +492,19 @@ body.tutor-backend-tutor_settings #wpbody-content { border-bottom: 1px solid var(--tutor-border-color); font-size: 16px; font-weight: 500; + + .tutor-page-elements-head-icon { + padding: 4px; + background-color: rgba(244, 246, 249, 1); + border-radius: 50%; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + svg { + color: var(--tutor-color-primary); + } + } } .tutor-page-elements-matrix-column { diff --git a/classes/Icon.php b/classes/Icon.php index eb85ec4822..4d52575344 100644 --- a/classes/Icon.php +++ b/classes/Icon.php @@ -197,6 +197,7 @@ final class Icon { const FIRE_DISABLED = 'fire-disabled'; const FLA = 'fla'; const FONT = 'font'; + const FOOTER = 'footer'; const FREE_SHIPPING_TYPE = 'free-shipping-type'; const GIFT = 'gift'; const GIFT_CARD = 'gift-card'; diff --git a/views/options/field-types/page_elements.php b/views/options/field-types/page_elements.php index af53bebc92..9f43da440d 100644 --- a/views/options/field-types/page_elements.php +++ b/views/options/field-types/page_elements.php @@ -8,6 +8,9 @@ defined( 'ABSPATH' ) || exit; +use TUTOR\Icon; +use Tutor\Components\SvgIcon; + $elements = array( 'dashboard' => array( 'label' => __( 'Dashboard', 'tutor' ), @@ -43,11 +46,15 @@
- + - +
From 6727240cb1f4679d01b821e7189032352a7ac03f Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Wed, 29 Jul 2026 16:46:17 +0600 Subject: [PATCH 07/51] refactor: remove unused CSS selectors from brand logo and page elements fields --- .../scss/admin-dashboard/_tutor-admin.scss | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/assets/src/scss/admin-dashboard/_tutor-admin.scss b/assets/src/scss/admin-dashboard/_tutor-admin.scss index 728278d55c..b6fa243279 100644 --- a/assets/src/scss/admin-dashboard/_tutor-admin.scss +++ b/assets/src/scss/admin-dashboard/_tutor-admin.scss @@ -324,20 +324,6 @@ body.tutor-backend-tutor_settings #wpbody-content { } } - .tutor-brand-logo-upload-label { - display: flex; - align-items: center; - gap: 10px; - font-size: 20px; - font-weight: 500; - color: #41454f; - - [class^='tutor-icon-'] { - font-size: 22px; - color: #606777; - } - } - .tutor-brand-logo-upload-control { position: relative; max-width: 141px; @@ -367,10 +353,6 @@ body.tutor-backend-tutor_settings #wpbody-content { } } - .tutor-brand-logo-hint { - color: #6b7280; - } - .tutor-brand-logo-upload-actions { display: none; position: absolute; @@ -448,11 +430,6 @@ body.tutor-backend-tutor_settings #wpbody-content { } } - .tutor-brand-logo-hint { - margin: 16px 0 0; - font-size: 16px; - } - @include breakpoint-max(782) { .tutor-brand-logo-list { padding: 0 16px; @@ -514,10 +491,6 @@ body.tutor-backend-tutor_settings #wpbody-content { gap: 8px; } - .tutor-page-elements-matrix-column .dashicons { - color: var(--tutor-color-primary); - } - .tutor-page-elements-toggle { justify-self: center; } From e7b29122bf8b933907ff9bbe06c9abcd4ae40730 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Wed, 29 Jul 2026 16:50:42 +0600 Subject: [PATCH 08/51] refactor: rename brand logo CSS classes from BEM to hyphenated --- assets/src/scss/frontend/components/_brand-logo.scss | 6 +++--- templates/shared/brand-logo.php | 2 +- views/options/field-types/page_elements.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/assets/src/scss/frontend/components/_brand-logo.scss b/assets/src/scss/frontend/components/_brand-logo.scss index 8bc1463b36..2f968bbb29 100644 --- a/assets/src/scss/frontend/components/_brand-logo.scss +++ b/assets/src/scss/frontend/components/_brand-logo.scss @@ -1,15 +1,15 @@ // Theme-aware Tutor branding shared by the dashboard and learning area. -.tutor-brand-logo__dark { +.tutor-brand-logo-dark { display: none; } [data-tutor-theme='dark'] { - .tutor-brand-logo__light { + .tutor-brand-logo-light { display: none; } - .tutor-brand-logo__dark { + .tutor-brand-logo-dark { display: inline; } } diff --git a/templates/shared/brand-logo.php b/templates/shared/brand-logo.php index 22c83360d8..0f4a4e85a9 100644 --- a/templates/shared/brand-logo.php +++ b/templates/shared/brand-logo.php @@ -38,7 +38,7 @@
@@ -687,8 +697,11 @@ function tutor_alert( $msg = null, $type = 'warning', $echo = true, $use_compone break; } - $html = Alert::make()->variant( $type )->text( $msg )->icon( $icon ); - return $echo ? $html->render() : $html; + $html = Alert::make()->variant( $type )->text( $msg )->icon( $icon )->get(); + if ( $echo ) { + echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + return $html; } $html = '
@@ -861,10 +874,8 @@ function tutor_maintenance_mode( $enable = false ) { if ( ! file_exists( $file ) ) { file_put_contents( $file, $maintenance_string ); } - } else { - if ( file_exists( $file ) ) { + } elseif ( file_exists( $file ) ) { unlink( $file ); - } } } } @@ -1021,7 +1032,6 @@ function _tutor_search_by_title_only( $search, $wp_query ) { */ function get_request( $var ) { return isset( $_REQUEST[ $var ] ) ? sanitize_text_field( $_REQUEST[ $var ] ) : false;//phpcs:ignore - } } @@ -1151,7 +1161,7 @@ function tutor_log() { */ function tutor_wc_price_currency_format( $amount ) { - $symbol = get_woocommerce_currency_symbol(); + $symbol = function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol() : ''; $position = get_option( 'woocommerce_currency_pos', 'left' ); switch ( $position ) { @@ -1800,11 +1810,11 @@ function tutor_get_formatted_price( $price ) { $price = number_format( $price, $no_of_decimal, $decimal_separator, $thousand_separator ); $price = 'left' === $currency_position ? $currency_symbol . $price : $price . $currency_symbol; - } elseif ( 'wc' === $monetize_by ) { + } elseif ( 'wc' === $monetize_by && function_exists( 'wc_price' ) ) { $price = wc_price( $price ); - } elseif ( 'edd' === $monetize_by ) { - $price = edd_currency_filter( edd_format_amount( $price ) ); - } elseif ( 'pmpro' === $monetize_by ) { + } elseif ( 'edd' === $monetize_by && function_exists( 'edd_currency_filter' ) ) { + $price = edd_currency_filter( function_exists( 'edd_format_amount' ) ? edd_format_amount( $price ) : $price ); + } elseif ( 'pmpro' === $monetize_by && function_exists( 'pmpro_formatPrice' ) ) { $price = pmpro_formatPrice( $price ); } From 0a1768117543c90bfe02c5f32e540694accad280 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Wed, 29 Jul 2026 18:44:49 +0600 Subject: [PATCH 11/51] refactor: Revert unintentional changes --- includes/tutor-general-functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/tutor-general-functions.php b/includes/tutor-general-functions.php index 999cdda214..b81f4ac1c2 100644 --- a/includes/tutor-general-functions.php +++ b/includes/tutor-general-functions.php @@ -282,7 +282,7 @@ function _generate_categories_dropdown_option( $post_ID = 0, $categories = array $output .= ''; if ( tutor_utils()->count( $childrens ) ) { - ++$depth; + $depth++; $output .= _generate_categories_dropdown_option( $post_ID, $childrens, $args, $depth ); } } From 33326256eb6b2e02952dffdd190b147583607e3d Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Thu, 30 Jul 2026 11:07:28 +0600 Subject: [PATCH 12/51] refactor: Update code readability --- .../scss/admin-dashboard/_tutor-admin.scss | 4 ++ views/options/field-types/brand_logo.php | 8 ++- views/options/field-types/page_elements.php | 59 +++++++++++++------ 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/assets/src/scss/admin-dashboard/_tutor-admin.scss b/assets/src/scss/admin-dashboard/_tutor-admin.scss index b6fa243279..6b4f714e19 100644 --- a/assets/src/scss/admin-dashboard/_tutor-admin.scss +++ b/assets/src/scss/admin-dashboard/_tutor-admin.scss @@ -481,6 +481,10 @@ body.tutor-backend-tutor_settings #wpbody-content { svg { color: var(--tutor-color-primary); } + + [data-header-icon='true'] { + transform: rotate(180deg); + } } } diff --git a/views/options/field-types/brand_logo.php b/views/options/field-types/brand_logo.php index f7df389cfd..5213ada4de 100644 --- a/views/options/field-types/brand_logo.php +++ b/views/options/field-types/brand_logo.php @@ -1,6 +1,7 @@ array( 'label' => __( 'Light Mode', 'tutor' ), @@ -36,7 +42,7 @@ $index = 0; foreach ( $logos as $theme => $logo ) : ++$index; - $attachment_id = absint( $this->get( $logo['key'], 0 ) ); + $attachment_id = absint( get_tutor_option( $logo['key'], 0 ) ); $image_url = $attachment_id && wp_attachment_is_image( $attachment_id ) ? wp_get_attachment_image_url( $attachment_id, 'medium' ) : ''; ?>
diff --git a/views/options/field-types/page_elements.php b/views/options/field-types/page_elements.php index 06a4ce3bb1..987fd2227c 100644 --- a/views/options/field-types/page_elements.php +++ b/views/options/field-types/page_elements.php @@ -1,6 +1,7 @@ array( 'label' => __( 'Dashboard', 'tutor' ), @@ -47,13 +53,22 @@ @@ -69,24 +84,32 @@
get( $setting['key'], $setting['default'] ); - $is_enabled = 'on' === $option_value || true === $option_value || 1 === (int) $option_value; + $setting = $element[ $position ]; + $option_value = get_tutor_option( $setting['key'], $setting['default'] ); + $is_enabled = 'on' === $option_value || true === $option_value || 1 === (int) $option_value; ?> From a29d553289fa1ed80a603d1d2b0b4dfed223a5fa Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Thu, 30 Jul 2026 11:15:02 +0600 Subject: [PATCH 13/51] style: Update header positioning --- .../scss/frontend/learning-area/layout/_header.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/assets/src/scss/frontend/learning-area/layout/_header.scss b/assets/src/scss/frontend/learning-area/layout/_header.scss index 5f854af93c..c2fae7618e 100644 --- a/assets/src/scss/frontend/learning-area/layout/_header.scss +++ b/assets/src/scss/frontend/learning-area/layout/_header.scss @@ -32,6 +32,16 @@ top: 0; } + .tutor-has-site-header.tutor-has-admin-bar & { + @include tutor-breakpoint-up(md) { + top: 32px; + } + + @include tutor-breakpoint-between(md, sm) { + top: 46px; + } + } + &-inner { @include tutor-flex(row, center); From d64abd62114583359c748cff6d4833bd1f3241ad Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Tue, 4 Aug 2026 11:31:09 +0600 Subject: [PATCH 14/51] fix(settings): resolve PHPStan type errors in Options_V2 --- classes/Options_V2.php | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/classes/Options_V2.php b/classes/Options_V2.php index d81c46bab9..7825baa234 100644 --- a/classes/Options_V2.php +++ b/classes/Options_V2.php @@ -540,7 +540,7 @@ public function tutor_option_save() { do_action( 'tutor_option_save_before', $option ); if ( ! isset( $option['tutor_login_page'] ) ) { - $option['tutor_login_page'] = $login_page_id ?? 0; + $option['tutor_login_page'] = $login_page_id; } $option = Input::sanitize_array( @@ -641,6 +641,7 @@ public function tutor_option_default_save() { $attr = $this->get_setting_fields(); $tutor_default_option = get_option( 'tutor_default_option' ); $tutor_saved_option = get_option( 'tutor_option' ); + $attr_default = array(); foreach ( $attr as $sections ) { if ( is_array( $sections ) && count( $sections ) ) { @@ -675,6 +676,7 @@ public function tutor_option_default_save() { * @return void */ public function load_settings_page() { + $template_path = ''; extract( $this->get_setting_fields() ); //phpcs:ignore if ( ! $template_path ) { @@ -1993,22 +1995,16 @@ public function update_settings_log( $new_settings_data, $action_type ) { } if ( 'imported' === $action ) { - if ( ! empty( $save_import_data ) ) { - update_option( 'tutor_option', $save_import_data['dataset'] ); - } + update_option( 'tutor_option', $save_import_data['dataset'] ); } $get_final_data = get_option( 'tutor_settings_log' ); } else { - if ( ! empty( $import_data ) ) { - update_option( 'tutor_settings_log', $import_data ); - } + update_option( 'tutor_settings_log', $import_data ); if ( 'imported' === $action ) { - if ( ! empty( $save_import_data ) ) { - update_option( 'tutor_option', $save_import_data['dataset'] ); - } + update_option( 'tutor_option', $save_import_data['dataset'] ); } $get_final_data = get_option( 'tutor_settings_log' ); From 11d63b5f21eb11503958da958a0d4bd3ea4a0d0e Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Tue, 4 Aug 2026 11:31:35 +0600 Subject: [PATCH 15/51] refactor(settings): generic image upload list field type - Replace brand-logo specific field with reusable image_upload_list - Update JS and SCSS selectors to the new generic classes --- .../js/admin-dashboard/segments/options.js | 20 ++--- .../scss/admin-dashboard/_tutor-admin.scss | 36 ++++---- classes/Options_V2.php | 15 +++- views/options/field-types/brand_logo.php | 87 ------------------- .../options/field-types/image_upload_list.php | 78 +++++++++++++++++ 5 files changed, 120 insertions(+), 116 deletions(-) delete mode 100644 views/options/field-types/brand_logo.php create mode 100644 views/options/field-types/image_upload_list.php diff --git a/assets/src/js/admin-dashboard/segments/options.js b/assets/src/js/admin-dashboard/segments/options.js index bba2be745f..012579ccef 100644 --- a/assets/src/js/admin-dashboard/segments/options.js +++ b/assets/src/js/admin-dashboard/segments/options.js @@ -84,14 +84,14 @@ document.addEventListener('DOMContentLoaded', function () { const allowedBrandLogoMimeTypes = ['image/jpeg', 'image/png']; - $(document).on('click', '.tutor-brand-logo-upload-select', function (event) { + $(document).on('click', '.tutor-image-upload-select', function (event) { event.preventDefault(); - const $upload = $(this).closest('.tutor-brand-logo-upload'); - const $input = $upload.find('.tutor-brand-logo-upload-input'); - const $preview = $upload.find('.tutor-brand-logo-upload-preview'); + const $upload = $(this).closest('.tutor-image-upload-item'); + const $input = $upload.find('.tutor-image-upload-input'); + const $preview = $upload.find('.tutor-image-upload-preview'); const $image = $preview.find('img'); - const $remove = $upload.find('.tutor-brand-logo-upload-remove'); + const $remove = $upload.find('.tutor-image-upload-remove'); const frame = wp.media({ title: __('Select a brand logo', 'tutor'), button: { text: __('Use this logo', 'tutor') }, @@ -117,13 +117,13 @@ document.addEventListener('DOMContentLoaded', function () { frame.open(); }); - $(document).on('click', '.tutor-brand-logo-upload-remove', function (event) { + $(document).on('click', '.tutor-image-upload-remove', function (event) { event.preventDefault(); - const $upload = $(this).closest('.tutor-brand-logo-upload'); - $upload.find('.tutor-brand-logo-upload-input').val('').trigger('change'); - $upload.find('.tutor-brand-logo-upload-preview img').attr('src', '').prop('hidden', true); - $upload.find('.tutor-brand-logo-upload-preview').removeClass('has-image'); + const $upload = $(this).closest('.tutor-image-upload-item'); + $upload.find('.tutor-image-upload-input').val('').trigger('change'); + $upload.find('.tutor-image-upload-preview img').attr('src', '').prop('hidden', true); + $upload.find('.tutor-image-upload-preview').removeClass('has-image'); $upload.removeClass('has-image'); $(this).prop('hidden', true); $('#save_tutor_option').prop('disabled', false); diff --git a/assets/src/scss/admin-dashboard/_tutor-admin.scss b/assets/src/scss/admin-dashboard/_tutor-admin.scss index 6b4f714e19..782a80cc86 100644 --- a/assets/src/scss/admin-dashboard/_tutor-admin.scss +++ b/assets/src/scss/admin-dashboard/_tutor-admin.scss @@ -291,8 +291,8 @@ body.tutor-backend-tutor_settings #wpbody-content { border-bottom: none; } -.tutor-brand-logo-field { - .tutor-brand-logo-list { +.tutor-image-upload-list-field { + .tutor-image-upload-list { display: flex; flex-direction: column; gap: 6px; @@ -304,7 +304,7 @@ body.tutor-backend-tutor_settings #wpbody-content { min-width: 0; } - .tutor-brand-logo-upload { + .tutor-image-upload-item { display: grid; grid-template-columns: minmax(160px, 1fr) minmax(240px, 320px); gap: 16px; @@ -324,13 +324,13 @@ body.tutor-backend-tutor_settings #wpbody-content { } } - .tutor-brand-logo-upload-control { + .tutor-image-upload-control { position: relative; max-width: 141px; width: 100%; } - .tutor-brand-logo-upload-preview { + .tutor-image-upload-preview { display: flex; align-items: center; justify-content: center; @@ -353,7 +353,7 @@ body.tutor-backend-tutor_settings #wpbody-content { } } - .tutor-brand-logo-upload-actions { + .tutor-image-upload-actions { display: none; position: absolute; inset: 0; @@ -377,22 +377,22 @@ body.tutor-backend-tutor_settings #wpbody-content { font-size: 20px; } - .tutor-brand-logo-upload-remove { + .tutor-image-upload-remove { background: #e53935; color: #fff; } } - .tutor-brand-logo-upload-preview.has-image:hover .tutor-brand-logo-upload-actions, - .tutor-brand-logo-upload-preview.has-image:focus-within .tutor-brand-logo-upload-actions { + .tutor-image-upload-preview.has-image:hover .tutor-image-upload-actions, + .tutor-image-upload-preview.has-image:focus-within .tutor-image-upload-actions { display: flex; } - .tutor-brand-logo-upload.has-image .tutor-brand-logo-upload-empty { + .tutor-image-upload-item.has-image .tutor-image-upload-empty { display: none; } - .tutor-brand-logo-upload-empty { + .tutor-image-upload-empty { display: flex; align-items: center; justify-content: center; @@ -412,30 +412,30 @@ body.tutor-backend-tutor_settings #wpbody-content { } } - .tutor-brand-logo-upload[data-brand-logo-theme='dark'] { - .tutor-brand-logo-upload-preview, - .tutor-brand-logo-upload-empty { + .tutor-image-upload-item[data-image-upload-theme='dark'] { + .tutor-image-upload-preview, + .tutor-image-upload-empty { background: #25272d; } - .tutor-brand-logo-upload-empty { + .tutor-image-upload-empty { border-color: #69738a; color: #b8bdc8; } } @media (hover: none) { - .tutor-brand-logo-upload-preview.has-image .tutor-brand-logo-upload-actions { + .tutor-image-upload-preview.has-image .tutor-image-upload-actions { display: flex; } } @include breakpoint-max(782) { - .tutor-brand-logo-list { + .tutor-image-upload-list { padding: 0 16px; } - .tutor-brand-logo-upload { + .tutor-image-upload-item { grid-template-columns: 1fr; gap: 10px; padding: 16px 0; diff --git a/classes/Options_V2.php b/classes/Options_V2.php index 7825baa234..6e23740b2d 100644 --- a/classes/Options_V2.php +++ b/classes/Options_V2.php @@ -12,6 +12,7 @@ defined( 'ABSPATH' ) || exit; +use TUTOR\Icon; use TUTOR\Input; use Tutor\Traits\JsonResponse; use Tutor\Ecommerce\OptionKeys; @@ -1295,10 +1296,22 @@ public function get_setting_fields() { ), array( 'key' => 'brand_logo', - 'type' => 'brand_logo', + 'type' => 'image_upload_list', 'label' => __( 'Brand Logo', 'tutor' ), 'desc' => __( 'Upload separate logos for light and dark mode.', 'tutor' ), 'default' => 0, + 'items' => array( + 'light' => array( + 'label' => __( 'Light Mode', 'tutor' ), + 'key' => 'brand_logo_light', + 'icon' => Icon::LIGHT, + ), + 'dark' => array( + 'label' => __( 'Dark Mode', 'tutor' ), + 'key' => 'brand_logo_dark', + 'icon' => Icon::DARK, + ), + ), ), ), ), diff --git a/views/options/field-types/brand_logo.php b/views/options/field-types/brand_logo.php deleted file mode 100644 index 5213ada4de..0000000000 --- a/views/options/field-types/brand_logo.php +++ /dev/null @@ -1,87 +0,0 @@ - array( - 'label' => __( 'Light Mode', 'tutor' ), - 'key' => 'brand_logo_light', - 'icon' => Icon::LIGHT, - ), - 'dark' => array( - 'label' => __( 'Dark Mode', 'tutor' ), - 'key' => 'brand_logo_dark', - 'icon' => Icon::DARK, - ), -); -?> - diff --git a/views/options/field-types/image_upload_list.php b/views/options/field-types/image_upload_list.php new file mode 100644 index 0000000000..876d228c7b --- /dev/null +++ b/views/options/field-types/image_upload_list.php @@ -0,0 +1,78 @@ + +
+
+
+
+
+ $item ) : + ++$index; + $attachment_id = absint( get_tutor_option( $item['key'], 0 ) ); + $image_url = $attachment_id && wp_attachment_is_image( $attachment_id ) ? wp_get_attachment_image_url( $attachment_id, 'medium' ) : ''; + ?> +
+
+ name( $item['icon'] )->size( 20 )->render(); ?> + +
+ +
+
+ /> +
+ + +
+
+ +
+
+ + + + +
+

+ name( Icon::INFO_OCTAGON )->render(); ?> + + +

+
From 25965d5477123d26bff737d961d79a39c7de977b Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Tue, 4 Aug 2026 11:32:38 +0600 Subject: [PATCH 16/51] refactor(settings): generic toggle matrix field type - Replace page_elements specific field with reusable toggle_matrix - Update SCSS selectors to the new generic classes --- .../scss/admin-dashboard/_tutor-admin.scss | 24 ++-- classes/Options_V2.php | 37 +++++- views/options/field-types/page_elements.php | 122 ------------------ views/options/field-types/toggle_matrix.php | 97 ++++++++++++++ 4 files changed, 145 insertions(+), 135 deletions(-) delete mode 100644 views/options/field-types/page_elements.php create mode 100644 views/options/field-types/toggle_matrix.php diff --git a/assets/src/scss/admin-dashboard/_tutor-admin.scss b/assets/src/scss/admin-dashboard/_tutor-admin.scss index 782a80cc86..7c15e23981 100644 --- a/assets/src/scss/admin-dashboard/_tutor-admin.scss +++ b/assets/src/scss/admin-dashboard/_tutor-admin.scss @@ -443,16 +443,16 @@ body.tutor-backend-tutor_settings #wpbody-content { } } -.tutor-page-elements-field { - .tutor-page-elements-matrix { +.tutor-toggle-matrix-field { + .tutor-toggle-matrix { margin-top: 24px; border: 1px solid var(--tutor-border-color); border-radius: 6px; overflow: hidden; } - .tutor-page-elements-matrix-head, - .tutor-page-elements-matrix-row { + .tutor-toggle-matrix-head, + .tutor-toggle-matrix-row { display: grid; grid-template-columns: minmax(180px, 1fr) 180px 180px; align-items: center; @@ -460,17 +460,17 @@ body.tutor-backend-tutor_settings #wpbody-content { padding: 8px 12px; } - .tutor-page-elements-matrix-row { + .tutor-toggle-matrix-row { padding: 12px; } - .tutor-page-elements-matrix-head { + .tutor-toggle-matrix-head { background: #fff; border-bottom: 1px solid var(--tutor-border-color); font-size: 16px; font-weight: 500; - .tutor-page-elements-head-icon { + .tutor-toggle-matrix-head-icon { padding: 4px; background-color: rgba(244, 246, 249, 1); border-radius: 50%; @@ -488,26 +488,26 @@ body.tutor-backend-tutor_settings #wpbody-content { } } - .tutor-page-elements-matrix-column { + .tutor-toggle-matrix-column { display: flex; align-items: center; justify-content: center; gap: 8px; } - .tutor-page-elements-toggle { + .tutor-toggle-matrix-toggle { justify-self: center; } @include breakpoint-max(782) { - .tutor-page-elements-matrix-head, - .tutor-page-elements-matrix-row { + .tutor-toggle-matrix-head, + .tutor-toggle-matrix-row { grid-template-columns: minmax(140px, 1fr) 1fr 1fr; gap: 8px; padding: 16px; } - .tutor-page-elements-matrix-row { + .tutor-toggle-matrix-row { font-size: 16px; } } diff --git a/classes/Options_V2.php b/classes/Options_V2.php index 6e23740b2d..b33f8d23fd 100644 --- a/classes/Options_V2.php +++ b/classes/Options_V2.php @@ -1471,10 +1471,45 @@ public function get_setting_fields() { 'fields' => array( array( 'key' => 'page_elements', - 'type' => 'page_elements', + 'type' => 'toggle_matrix', 'label' => __( 'Header & Footer', 'tutor' ), 'desc' => __( 'Control the visibility of Header and Footer for Dashboard and Learning Experience', 'tutor' ), 'default' => $default_page_elements, + 'columns' => array( + 'header' => array( + 'label' => __( 'Header', 'tutor' ), + 'icon' => Icon::FOOTER, + 'icon_attrs' => array( 'data-header-icon' => 'true' ), + ), + 'footer' => array( + 'label' => __( 'Footer', 'tutor' ), + 'icon' => Icon::FOOTER, + ), + ), + 'items' => array( + 'dashboard' => array( + 'label' => __( 'Dashboard', 'tutor' ), + 'header' => array( + 'key' => 'show_dashboard_site_header', + 'default' => 'off', + ), + 'footer' => array( + 'key' => 'show_dashboard_site_footer', + 'default' => 'off', + ), + ), + 'learning' => array( + 'label' => __( 'Learning Experience', 'tutor' ), + 'header' => array( + 'key' => 'show_learning_site_header', + 'default' => 'off', + ), + 'footer' => array( + 'key' => 'show_learning_site_footer', + 'default' => 'off', + ), + ), + ), ), ), ), diff --git a/views/options/field-types/page_elements.php b/views/options/field-types/page_elements.php deleted file mode 100644 index 987fd2227c..0000000000 --- a/views/options/field-types/page_elements.php +++ /dev/null @@ -1,122 +0,0 @@ - array( - 'label' => __( 'Dashboard', 'tutor' ), - 'header' => array( - 'key' => 'show_dashboard_site_header', - 'default' => 'off', - ), - 'footer' => array( - 'key' => 'show_dashboard_site_footer', - 'default' => 'off', - ), - ), - 'learning' => array( - 'label' => __( 'Learning Experience', 'tutor' ), - 'header' => array( - 'key' => 'show_learning_site_header', - 'default' => 'off', - ), - 'footer' => array( - 'key' => 'show_learning_site_footer', - 'default' => 'off', - ), - ), -); -?> -
-
-
-
-
- -
-
- - - - - - - - - -
- - -
-
- - - - -
- - - - -
-
diff --git a/views/options/field-types/toggle_matrix.php b/views/options/field-types/toggle_matrix.php new file mode 100644 index 0000000000..59eb40e9aa --- /dev/null +++ b/views/options/field-types/toggle_matrix.php @@ -0,0 +1,97 @@ + +
+
+
+
+
+ +
+
+ + + + + + + + + +
+ + +
+
+ + + + +
+ + + + +
+
From 6e747d116dc945552d0774069990c5a2d548f7ec Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Tue, 4 Aug 2026 11:48:06 +0600 Subject: [PATCH 17/51] refactor: Resolve pho stan warning --- classes/Options_V2.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/classes/Options_V2.php b/classes/Options_V2.php index b33f8d23fd..d422b0994c 100644 --- a/classes/Options_V2.php +++ b/classes/Options_V2.php @@ -541,7 +541,7 @@ public function tutor_option_save() { do_action( 'tutor_option_save_before', $option ); if ( ! isset( $option['tutor_login_page'] ) ) { - $option['tutor_login_page'] = $login_page_id; + $option['tutor_login_page'] = $login_page_id ? $login_page_id : 0; } $option = Input::sanitize_array( @@ -2043,7 +2043,9 @@ public function update_settings_log( $new_settings_data, $action_type ) { } if ( 'imported' === $action ) { - update_option( 'tutor_option', $save_import_data['dataset'] ); + if ( ! empty( $save_import_data['dataset'] ) ) { + update_option( 'tutor_option', $save_import_data['dataset'] ); + } } $get_final_data = get_option( 'tutor_settings_log' ); @@ -2052,7 +2054,9 @@ public function update_settings_log( $new_settings_data, $action_type ) { update_option( 'tutor_settings_log', $import_data ); if ( 'imported' === $action ) { - update_option( 'tutor_option', $save_import_data['dataset'] ); + if ( ! empty( $save_import_data['dataset'] ) ) { + update_option( 'tutor_option', $save_import_data['dataset'] ); + } } $get_final_data = get_option( 'tutor_settings_log' ); From 164455a9d94c83864edd4066cfae734aff4cf066 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Wed, 5 Aug 2026 12:31:02 +0600 Subject: [PATCH 18/51] docs: Update since tag --- includes/tutor-general-functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/tutor-general-functions.php b/includes/tutor-general-functions.php index b81f4ac1c2..7391662158 100644 --- a/includes/tutor-general-functions.php +++ b/includes/tutor-general-functions.php @@ -464,7 +464,7 @@ function tutor_generate_tags_checkbox( $post_ID = 0, $tags = array(), $args = ar /** * Open a standalone Tutor page with optional WordPress theme header. * - * @since 4.0.4 + * @since 4.0.5 * * @param bool $show_site_header Whether to render the active theme header. * @@ -496,7 +496,7 @@ function tutor_page_elements_header( $show_site_header ) { * Classic themes may couple markup between header.php and footer.php. When * only one is enabled, this deliberately provides a valid bare closure. * - * @since 4.0.4 + * @since 4.0.5 * * @param bool $show_site_footer Whether to render the active theme footer. * From a2b2a5fc4f2591001332a82ed8d42020a1cf25b0 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Wed, 5 Aug 2026 18:13:32 +0600 Subject: [PATCH 19/51] refactor: Update since tag and default values --- templates/account.php | 4 ++-- templates/dashboard-isolated.php | 4 ++-- templates/dashboard.php | 2 +- views/options/field-types/image_upload_list.php | 2 +- views/options/field-types/toggle_matrix.php | 2 ++ 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/templates/account.php b/templates/account.php index 87b890e897..54878a0106 100644 --- a/templates/account.php +++ b/templates/account.php @@ -27,8 +27,8 @@ Dashboard::set_document_title( $meta_title ); -$show_dashboard_site_header = (bool) tutor_utils()->get_option( 'show_dashboard_site_header', true ); -$show_dashboard_site_footer = (bool) tutor_utils()->get_option( 'show_dashboard_site_footer', true ); +$show_dashboard_site_header = (bool) tutor_utils()->get_option( 'show_dashboard_site_header', false ); +$show_dashboard_site_footer = (bool) tutor_utils()->get_option( 'show_dashboard_site_footer', false ); tutor_page_elements_header( $show_dashboard_site_header ); ?> diff --git a/templates/dashboard-isolated.php b/templates/dashboard-isolated.php index 3a1377edcc..e1a7fad9a5 100644 --- a/templates/dashboard-isolated.php +++ b/templates/dashboard-isolated.php @@ -21,8 +21,8 @@ $meta_title = $page_meta['meta_title']; Dashboard::set_document_title( $meta_title ); -$show_dashboard_site_header = (bool) tutor_utils()->get_option( 'show_dashboard_site_header', true ); -$show_dashboard_site_footer = (bool) tutor_utils()->get_option( 'show_dashboard_site_footer', true ); +$show_dashboard_site_header = (bool) tutor_utils()->get_option( 'show_dashboard_site_header', false ); +$show_dashboard_site_footer = (bool) tutor_utils()->get_option( 'show_dashboard_site_footer', false ); tutor_page_elements_header( $show_dashboard_site_header ); diff --git a/templates/dashboard.php b/templates/dashboard.php index f437c9b9c4..cc93ac4b6f 100644 --- a/templates/dashboard.php +++ b/templates/dashboard.php @@ -56,7 +56,7 @@ $is_by_short_code = isset( $is_shortcode ) && true === $is_shortcode; if ( ! $is_by_short_code && ! defined( 'OTLMS_VERSION' ) ) : - $show_dashboard_site_header = (bool) tutor_utils()->get_option( 'show_dashboard_site_header', true ); + $show_dashboard_site_header = (bool) tutor_utils()->get_option( 'show_dashboard_site_header', false ); tutor_page_elements_header( $show_dashboard_site_header ); endif; diff --git a/views/options/field-types/image_upload_list.php b/views/options/field-types/image_upload_list.php index 876d228c7b..4c34afad84 100644 --- a/views/options/field-types/image_upload_list.php +++ b/views/options/field-types/image_upload_list.php @@ -7,7 +7,7 @@ * @package Tutor\Views * @subpackage Tutor\Settings * - * @since 4.0.4 + * @since 4.0.5 */ defined( 'ABSPATH' ) || exit; diff --git a/views/options/field-types/toggle_matrix.php b/views/options/field-types/toggle_matrix.php index 59eb40e9aa..095edf23f6 100644 --- a/views/options/field-types/toggle_matrix.php +++ b/views/options/field-types/toggle_matrix.php @@ -7,6 +7,8 @@ * * @package Tutor\Views * @subpackage Tutor\Settings + * + * @since 4.0.5 */ defined( 'ABSPATH' ) || exit; From 32d3157397efffdcac262e0ade6f6926d6942578 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Thu, 6 Aug 2026 11:52:05 +0600 Subject: [PATCH 20/51] refactor(settings): make image upload list reusable --- .../js/admin-dashboard/segments/options.js | 98 +++++++++++-------- 1 file changed, 59 insertions(+), 39 deletions(-) diff --git a/assets/src/js/admin-dashboard/segments/options.js b/assets/src/js/admin-dashboard/segments/options.js index 012579ccef..690f806fbc 100644 --- a/assets/src/js/admin-dashboard/segments/options.js +++ b/assets/src/js/admin-dashboard/segments/options.js @@ -82,51 +82,71 @@ document.addEventListener('DOMContentLoaded', function () { }; } - const allowedBrandLogoMimeTypes = ['image/jpeg', 'image/png']; - - $(document).on('click', '.tutor-image-upload-select', function (event) { - event.preventDefault(); - - const $upload = $(this).closest('.tutor-image-upload-item'); - const $input = $upload.find('.tutor-image-upload-input'); - const $preview = $upload.find('.tutor-image-upload-preview'); - const $image = $preview.find('img'); - const $remove = $upload.find('.tutor-image-upload-remove'); - const frame = wp.media({ - title: __('Select a brand logo', 'tutor'), - button: { text: __('Use this logo', 'tutor') }, - library: { type: 'image' }, - multiple: false, - }); + /** + * Initialize a reusable image upload list field. + * + * Wires up the select/remove buttons rendered by the `image_upload_list` + * field type, so any similar field can reuse it with its own settings. + * + * @since 4.0.5 + * + * @param {Object} config + * @param {string} config.mediaTitle Media frame title. + * @param {string} config.mediaButtonText Media frame select button text. + * @param {string[]} [config.allowedMimeTypes] Restrict selectable attachment mime types. Empty means any image. + * @return {void} + */ + function initImageUploadList({ mediaTitle, mediaButtonText, allowedMimeTypes = [] }) { + $(document).on('click', '.tutor-image-upload-select', function (event) { + event.preventDefault(); + + const $upload = $(this).closest('.tutor-image-upload-item'); + const $input = $upload.find('.tutor-image-upload-input'); + const $preview = $upload.find('.tutor-image-upload-preview'); + const $image = $preview.find('img'); + const $remove = $upload.find('.tutor-image-upload-remove'); + const frame = wp.media({ + title: mediaTitle, + button: { text: mediaButtonText }, + library: { type: 'image' }, + multiple: false, + }); - frame.on('select', function () { - const attachment = frame.state().get('selection').first().toJSON(); - if (!allowedBrandLogoMimeTypes.includes(attachment.mime)) { - tutor_toast(__('Error!', 'tutor'), __('Please select a JPG, JPEG, or PNG image.', 'tutor'), 'error'); - return; - } + frame.on('select', function () { + const attachment = frame.state().get('selection').first().toJSON(); + if (allowedMimeTypes.length && !allowedMimeTypes.includes(attachment.mime)) { + tutor_toast(__('Error!', 'tutor'), __('Please select a supported image.', 'tutor'), 'error'); + return; + } - $input.val(attachment.id).trigger('change'); - $image.attr('src', attachment.url).prop('hidden', false); - $preview.addClass('has-image'); - $upload.addClass('has-image'); - $remove.prop('hidden', false); - $('#save_tutor_option').prop('disabled', false); + $input.val(attachment.id).trigger('change'); + $image.attr('src', attachment.url).prop('hidden', false); + $preview.addClass('has-image'); + $upload.addClass('has-image'); + $remove.prop('hidden', false); + $('#save_tutor_option').prop('disabled', false); + }); + + frame.open(); }); - frame.open(); - }); + $(document).on('click', '.tutor-image-upload-remove', function (event) { + event.preventDefault(); - $(document).on('click', '.tutor-image-upload-remove', function (event) { - event.preventDefault(); + const $upload = $(this).closest('.tutor-image-upload-item'); + $upload.find('.tutor-image-upload-input').val('').trigger('change'); + $upload.find('.tutor-image-upload-preview img').attr('src', '').prop('hidden', true); + $upload.find('.tutor-image-upload-preview').removeClass('has-image'); + $upload.removeClass('has-image'); + $(this).prop('hidden', true); + $('#save_tutor_option').prop('disabled', false); + }); + } - const $upload = $(this).closest('.tutor-image-upload-item'); - $upload.find('.tutor-image-upload-input').val('').trigger('change'); - $upload.find('.tutor-image-upload-preview img').attr('src', '').prop('hidden', true); - $upload.find('.tutor-image-upload-preview').removeClass('has-image'); - $upload.removeClass('has-image'); - $(this).prop('hidden', true); - $('#save_tutor_option').prop('disabled', false); + initImageUploadList({ + mediaTitle: __('Select a brand logo', 'tutor'), + mediaButtonText: __('Use this logo', 'tutor'), + allowedMimeTypes: ['image/jpeg', 'image/png'], }); const validateEmail = (email) => { From d960a313fc743e6c3f5383a481650d3915311c52 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Thu, 6 Aug 2026 12:23:37 +0600 Subject: [PATCH 21/51] refactor: Remove redundant default value --- classes/Options_V2.php | 4 ++-- includes/tutor-general-functions.php | 2 +- templates/account.php | 4 ++-- templates/dashboard-isolated.php | 4 ++-- templates/dashboard.php | 2 +- templates/learning-area/index.php | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/classes/Options_V2.php b/classes/Options_V2.php index d422b0994c..8c5a95ddfc 100644 --- a/classes/Options_V2.php +++ b/classes/Options_V2.php @@ -730,8 +730,8 @@ public function get_setting_fields() { $default_visibility = (bool) tutor_utils()->get_option( 'enable_spotlight_mode', true ) ? 'off' : 'on'; $default_page_elements = array( - 'show_dashboard_site_header' => $default_visibility, - 'show_dashboard_site_footer' => $default_visibility, + 'show_dashboard_site_header' => false, + 'show_dashboard_site_footer' => false, 'show_learning_site_header' => $default_visibility, 'show_learning_site_footer' => $default_visibility, ); diff --git a/includes/tutor-general-functions.php b/includes/tutor-general-functions.php index 7391662158..a3b56a8f63 100644 --- a/includes/tutor-general-functions.php +++ b/includes/tutor-general-functions.php @@ -526,7 +526,7 @@ function tutor_page_elements_footer( $show_site_footer ) { * @return void */ function get_tutor_header( $full_screen = false ) { - $show_learning_area_header = (bool) tutor_utils()->get_option( 'show_learning_site_header', false ); + $show_learning_area_header = (bool) tutor_utils()->get_option( 'show_learning_site_header' ); if ( ! $show_learning_area_header || $full_screen ) { ?> diff --git a/templates/account.php b/templates/account.php index 54878a0106..3651345828 100644 --- a/templates/account.php +++ b/templates/account.php @@ -27,8 +27,8 @@ Dashboard::set_document_title( $meta_title ); -$show_dashboard_site_header = (bool) tutor_utils()->get_option( 'show_dashboard_site_header', false ); -$show_dashboard_site_footer = (bool) tutor_utils()->get_option( 'show_dashboard_site_footer', false ); +$show_dashboard_site_header = (bool) tutor_utils()->get_option( 'show_dashboard_site_header' ); +$show_dashboard_site_footer = (bool) tutor_utils()->get_option( 'show_dashboard_site_footer' ); tutor_page_elements_header( $show_dashboard_site_header ); ?> diff --git a/templates/dashboard-isolated.php b/templates/dashboard-isolated.php index e1a7fad9a5..d4c2aaf410 100644 --- a/templates/dashboard-isolated.php +++ b/templates/dashboard-isolated.php @@ -21,8 +21,8 @@ $meta_title = $page_meta['meta_title']; Dashboard::set_document_title( $meta_title ); -$show_dashboard_site_header = (bool) tutor_utils()->get_option( 'show_dashboard_site_header', false ); -$show_dashboard_site_footer = (bool) tutor_utils()->get_option( 'show_dashboard_site_footer', false ); +$show_dashboard_site_header = (bool) tutor_utils()->get_option( 'show_dashboard_site_header' ); +$show_dashboard_site_footer = (bool) tutor_utils()->get_option( 'show_dashboard_site_footer' ); tutor_page_elements_header( $show_dashboard_site_header ); diff --git a/templates/dashboard.php b/templates/dashboard.php index cc93ac4b6f..7f8540b00d 100644 --- a/templates/dashboard.php +++ b/templates/dashboard.php @@ -56,7 +56,7 @@ $is_by_short_code = isset( $is_shortcode ) && true === $is_shortcode; if ( ! $is_by_short_code && ! defined( 'OTLMS_VERSION' ) ) : - $show_dashboard_site_header = (bool) tutor_utils()->get_option( 'show_dashboard_site_header', false ); + $show_dashboard_site_header = (bool) tutor_utils()->get_option( 'show_dashboard_site_header' ); tutor_page_elements_header( $show_dashboard_site_header ); endif; diff --git a/templates/learning-area/index.php b/templates/learning-area/index.php index b1f35f53a7..ddd59a9b89 100644 --- a/templates/learning-area/index.php +++ b/templates/learning-area/index.php @@ -73,8 +73,8 @@ Dashboard::set_document_title( $page_meta_title ); -$show_learning_site_header = (bool) tutor_utils()->get_option( 'show_learning_site_header', false ); -$show_learning_site_footer = (bool) tutor_utils()->get_option( 'show_learning_site_footer', false ); +$show_learning_site_header = (bool) tutor_utils()->get_option( 'show_learning_site_header' ); +$show_learning_site_footer = (bool) tutor_utils()->get_option( 'show_learning_site_footer' ); tutor_page_elements_header( $show_learning_site_header ); ?>
From 8842a35376313afcaf5ecb969b90da13e438fc66 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Thu, 6 Aug 2026 16:26:26 +0600 Subject: [PATCH 22/51] feat: Add sidebar logo visbility controll --- classes/Options_V2.php | 7 +++++++ templates/shared/brand-logo.php | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/classes/Options_V2.php b/classes/Options_V2.php index 8c5a95ddfc..86f6238aba 100644 --- a/classes/Options_V2.php +++ b/classes/Options_V2.php @@ -1313,6 +1313,13 @@ public function get_setting_fields() { ), ), ), + array( + 'key' => 'sidebar_logo_visibility', + 'type' => 'toggle_switch', + 'default' => 'on', + 'label' => __( 'Sidebar Logo Visibility', 'tutor' ), + 'desc' => __( 'Enable or disable the logo display in the sidebar', 'tutor' ), + ), ), ), 'block_course' => array( diff --git a/templates/shared/brand-logo.php b/templates/shared/brand-logo.php index 0f4a4e85a9..782e931e79 100644 --- a/templates/shared/brand-logo.php +++ b/templates/shared/brand-logo.php @@ -12,6 +12,12 @@ defined( 'ABSPATH' ) || exit; +$sidebar_logo_visibility = get_tutor_option( 'sidebar_logo_visibility', true ); + +if ( ! $sidebar_logo_visibility ) { + return; +} + $site_name = get_bloginfo( 'name' ); $custom_logo_id = absint( get_theme_mod( 'custom_logo' ) ); $logo_ids = array( From 172c23569f79feacc462777d7e78b8bddf8f6544 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Fri, 7 Aug 2026 15:23:34 +0600 Subject: [PATCH 23/51] feat(learning-area): track theme site-shell header offset --- assets/src/js/frontend/learning-area/index.ts | 2 + .../js/frontend/learning-area/site-shell.ts | 110 ++++++++++++++++++ .../learning-area/layout/_header.scss | 14 +-- .../learning-area/layout/_layout.scss | 56 +++++++++ templates/learning-area/index.php | 29 ++++- 5 files changed, 197 insertions(+), 14 deletions(-) create mode 100644 assets/src/js/frontend/learning-area/site-shell.ts diff --git a/assets/src/js/frontend/learning-area/index.ts b/assets/src/js/frontend/learning-area/index.ts index f24c83fbb1..559e659d78 100644 --- a/assets/src/js/frontend/learning-area/index.ts +++ b/assets/src/js/frontend/learning-area/index.ts @@ -11,6 +11,7 @@ import { initializeCourseCourseInfo } from './pages/course-info'; import { initializeQna } from './pages/qna'; import { initializeQuizInterface } from './quiz'; import { initializeSidebar } from './sidebar'; +import { initializeSiteShell } from './site-shell'; const decodePathSegment = (segment: string): string => { try { @@ -21,6 +22,7 @@ const decodePathSegment = (segment: string): string => { }; const initializeLearningArea = () => { + initializeSiteShell(); initializeLearningAreaCommon(); initializeCommon(); initializeSidebar(); diff --git a/assets/src/js/frontend/learning-area/site-shell.ts b/assets/src/js/frontend/learning-area/site-shell.ts new file mode 100644 index 0000000000..2dd7fba54a --- /dev/null +++ b/assets/src/js/frontend/learning-area/site-shell.ts @@ -0,0 +1,110 @@ +const CSS_VARIABLE = '--tutor-site-header-offset'; + +/** + * Returns the lowest visible bottom edge among the site header and admin bar. + * + * A normal-flow header contributes while it is visible, then naturally drops + * out of the boundary as the document scrolls. Fixed and sticky headers keep + * contributing because their bottom edge remains in the viewport. + */ +const getVisibleHeaderBoundary = (elements: HTMLElement[]): number => { + return elements.reduce((offset, element) => { + return Math.max(offset, element.getBoundingClientRect().bottom, 0); + }, 0); +}; + +class LearningAreaSiteShellController { + private animationFrame: number | null = null; + private resizeObserver: ResizeObserver | null = null; + private themeHeader: HTMLElement | null = null; + + constructor( + private readonly root: HTMLElement, + private readonly themeHeaderSelector: string, + ) {} + + start(): void { + this.themeHeader = this.findThemeHeader(); + this.updateOffset(); + + if (this.themeHeader && typeof ResizeObserver !== 'undefined') { + this.resizeObserver = new ResizeObserver(() => this.scheduleOffsetUpdate()); + this.resizeObserver.observe(this.themeHeader); + } + + window.addEventListener('resize', this.scheduleOffsetUpdate); + window.addEventListener('scroll', this.scheduleOffsetUpdate, { passive: true }); + } + + destroy(): void { + this.resizeObserver?.disconnect(); + this.resizeObserver = null; + + window.removeEventListener('resize', this.scheduleOffsetUpdate); + window.removeEventListener('scroll', this.scheduleOffsetUpdate); + + if (this.animationFrame !== null) { + window.cancelAnimationFrame(this.animationFrame); + this.animationFrame = null; + } + + this.themeHeader = null; + } + + private findThemeHeader(): HTMLElement | null { + if (!this.themeHeaderSelector) { + return null; + } + + try { + const header = document.querySelector(this.themeHeaderSelector); + return header instanceof HTMLElement ? header : null; + } catch { + return null; + } + } + + private scheduleOffsetUpdate = (): void => { + if (this.animationFrame !== null) { + return; + } + + this.animationFrame = window.requestAnimationFrame(() => { + this.animationFrame = null; + this.updateOffset(); + }); + }; + + private updateOffset(): void { + const adminBar = document.getElementById('wpadminbar'); + const elements = [this.themeHeader, adminBar].filter( + (element): element is HTMLElement => element instanceof HTMLElement, + ); + const offset = getVisibleHeaderBoundary(elements); + + this.root.style.setProperty(CSS_VARIABLE, `${offset}px`); + } +} + +/** + * Initializes all learning-area site-shell roots found in the current DOM. + * + * Returns a disposer that tears down every controller it created (removes + * listeners, disconnects observers, cancels pending rAFs). Callers are + * responsible for invoking it when the roots are removed or re-initialized, + * to avoid duplicate listeners/observers stacking up across repeated calls. + */ +export const initializeSiteShell = (): (() => void) => { + const controllers = Array.from(document.querySelectorAll('[data-tutor-learning-site-shell]')).map( + (root) => { + const selector = root.dataset.tutorThemeHeaderSelector || ''; + const controller = new LearningAreaSiteShellController(root, selector); + controller.start(); + return controller; + }, + ); + + return () => { + controllers.forEach((controller) => controller.destroy()); + }; +}; diff --git a/assets/src/scss/frontend/learning-area/layout/_header.scss b/assets/src/scss/frontend/learning-area/layout/_header.scss index c2fae7618e..a9afddb208 100644 --- a/assets/src/scss/frontend/learning-area/layout/_header.scss +++ b/assets/src/scss/frontend/learning-area/layout/_header.scss @@ -27,19 +27,9 @@ position: sticky; } - .tutor-has-site-header & { + .tutor-has-site-shell & { position: sticky; - top: 0; - } - - .tutor-has-site-header.tutor-has-admin-bar & { - @include tutor-breakpoint-up(md) { - top: 32px; - } - - @include tutor-breakpoint-between(md, sm) { - top: 46px; - } + top: var(--tutor-site-header-offset); } &-inner { diff --git a/assets/src/scss/frontend/learning-area/layout/_layout.scss b/assets/src/scss/frontend/learning-area/layout/_layout.scss index 156e8a3ddd..cbdac610ee 100644 --- a/assets/src/scss/frontend/learning-area/layout/_layout.scss +++ b/assets/src/scss/frontend/learning-area/layout/_layout.scss @@ -7,6 +7,9 @@ $tutor-fixed-footer-height: 80px; $tutor-fixed-footer-offset: $tutor-spacing-6; .tutor-learning-area { + --tutor-site-header-offset: 0px; + --tutor-learning-header-height: 64px; + background-color: $tutor-surface-base; .tutor-learning-area-body { @@ -23,6 +26,59 @@ $tutor-fixed-footer-offset: $tutor-spacing-6; padding-inline-start: 0; } + &.tutor-has-site-shell { + .tutor-learning-area-content { + padding-top: 0; + } + + @include tutor-breakpoint-up(lg) { + .tutor-learning-area-body { + display: grid; + grid-template-columns: 320px minmax(0, 1fr); + align-items: start; + padding-inline-start: 0; + } + + .tutor-learning-sidebar { + position: sticky; + top: calc(var(--tutor-site-header-offset) + var(--tutor-learning-header-height)); + grid-column: 1; + width: 320px; + height: calc(100dvh - var(--tutor-site-header-offset) - var(--tutor-learning-header-height)); + } + + .tutor-learning-area-content { + grid-column: 2; + min-width: 0; + } + } + + @include tutor-breakpoint-down(lg) { + --tutor-learning-header-height: 56px; + + .tutor-learning-sidebar { + top: var(--tutor-site-header-offset); + height: calc(100dvh - var(--tutor-site-header-offset)); + } + } + } + + &.tutor-has-site-shell.is-fullscreen { + @include tutor-breakpoint-up(lg) { + .tutor-learning-area-body { + grid-template-columns: minmax(0, 1fr); + } + + .tutor-learning-sidebar { + display: none; + } + + .tutor-learning-area-content { + grid-column: 1; + } + } + } + @include tutor-breakpoint-up(md) { .tutor-learning-area-content { padding-top: 64px; diff --git a/templates/learning-area/index.php b/templates/learning-area/index.php index ddd59a9b89..153c734169 100644 --- a/templates/learning-area/index.php +++ b/templates/learning-area/index.php @@ -75,6 +75,27 @@ $show_learning_site_header = (bool) tutor_utils()->get_option( 'show_learning_site_header' ); $show_learning_site_footer = (bool) tutor_utils()->get_option( 'show_learning_site_footer' ); +$has_learning_site_shell = $show_learning_site_header || $show_learning_site_footer; +$theme_header_selector = ''; + +if ( $show_learning_site_header ) { + /** + * Filters the selector used to find a theme header that overlays the Learning Area. + * + * Themes with non-standard header markup can return their header selector here. + * The selector is evaluated in the browser and invalid selectors are ignored. + * + * @since 4.0.5 + * + * @param string $selector CSS selector for the active theme header. + */ + $theme_header_selector = apply_filters( + 'tutor_learning_area_theme_header_selector', + 'header[role="banner"], header.site-header, header#masthead, #masthead, .site-header, header.wp-block-template-part' + ); + $theme_header_selector = is_string( $theme_header_selector ) ? $theme_header_selector : ''; +} + tutor_page_elements_header( $show_learning_site_header ); ?>
@@ -135,7 +156,11 @@ ?>
+ data-tutor-learning-site-shell + data-tutor-theme-header-selector="" + x-data="{ sidebarOpen: false, isFullScreen: false }" :class="{ 'is-fullscreen': isFullScreen }" > @@ -158,7 +183,7 @@ class="tutor-learning-area
-
diff --git a/templates/learning-area/index.php b/templates/learning-area/index.php index 153c734169..4b14023800 100644 --- a/templates/learning-area/index.php +++ b/templates/learning-area/index.php @@ -85,7 +85,7 @@ * Themes with non-standard header markup can return their header selector here. * The selector is evaluated in the browser and invalid selectors are ignored. * - * @since 4.0.5 + * @since 4.0.6 * * @param string $selector CSS selector for the active theme header. */ diff --git a/templates/shared/brand-logo.php b/templates/shared/brand-logo.php index 782e931e79..5436529495 100644 --- a/templates/shared/brand-logo.php +++ b/templates/shared/brand-logo.php @@ -7,7 +7,7 @@ * @author Themeum * @link https://themeum.com * - * @since 4.0.4 + * @since 4.0.6 */ defined( 'ABSPATH' ) || exit; diff --git a/views/options/field-types/image_upload_list.php b/views/options/field-types/image_upload_list.php index 4c34afad84..44e6f9979c 100644 --- a/views/options/field-types/image_upload_list.php +++ b/views/options/field-types/image_upload_list.php @@ -7,7 +7,7 @@ * @package Tutor\Views * @subpackage Tutor\Settings * - * @since 4.0.5 + * @since 4.0.6 */ defined( 'ABSPATH' ) || exit; diff --git a/views/options/field-types/toggle_matrix.php b/views/options/field-types/toggle_matrix.php index 095edf23f6..c6d5dea440 100644 --- a/views/options/field-types/toggle_matrix.php +++ b/views/options/field-types/toggle_matrix.php @@ -8,7 +8,7 @@ * @package Tutor\Views * @subpackage Tutor\Settings * - * @since 4.0.5 + * @since 4.0.6 */ defined( 'ABSPATH' ) || exit; From abb1871ee2a4a82bc412c4a971400d70f3859a2c Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Thu, 13 Aug 2026 15:58:36 +0600 Subject: [PATCH 26/51] refactor: Use ignore-kids to resolve unintentional icon changes --- .../options/field-types/image_upload_list.php | 32 ++++++++++++++++--- views/options/field-types/toggle_matrix.php | 4 ++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/views/options/field-types/image_upload_list.php b/views/options/field-types/image_upload_list.php index 44e6f9979c..4b7ee4000c 100644 --- a/views/options/field-types/image_upload_list.php +++ b/views/options/field-types/image_upload_list.php @@ -38,7 +38,13 @@ ?>
- name( $item['icon'] )->size( 20 )->render(); ?> + name( $item['icon'] ) + ->size( 20 ) + ->ignore_kids() + ->render(); + ?>
@@ -47,10 +53,22 @@ />
@@ -58,6 +76,7 @@ name( Icon::IMAGE_PLUS ) + ->ignore_kids() ->size( 20 ) ->render(); ?> @@ -71,7 +90,12 @@

- name( Icon::INFO_OCTAGON )->render(); ?> + name( Icon::INFO_OCTAGON ) + ->ignore_kids() + ->render(); + ?>

diff --git a/views/options/field-types/toggle_matrix.php b/views/options/field-types/toggle_matrix.php index c6d5dea440..5cc948c6e6 100644 --- a/views/options/field-types/toggle_matrix.php +++ b/views/options/field-types/toggle_matrix.php @@ -38,7 +38,9 @@ - \ No newline at end of file + diff --git a/views/options/field-types/image_upload_list.php b/views/options/field-types/image_upload_list.php index 4b7ee4000c..f375fe31c3 100644 --- a/views/options/field-types/image_upload_list.php +++ b/views/options/field-types/image_upload_list.php @@ -33,10 +33,12 @@ $index = 0; foreach ( $items as $theme => $item ) : ++$index; - $attachment_id = absint( get_tutor_option( $item['key'], 0 ) ); - $image_url = $attachment_id && wp_attachment_is_image( $attachment_id ) ? wp_get_attachment_image_url( $attachment_id, 'medium' ) : ''; + $default_attachment_id = absint( $item['default'] ?? 0 ); + $attachment_id = absint( get_tutor_option( $item['key'], $default_attachment_id ) ); + $image_url = $attachment_id && wp_attachment_is_image( $attachment_id ) ? wp_get_attachment_image_url( $attachment_id, 'medium' ) : ''; + $default_image_url = $default_attachment_id && wp_attachment_is_image( $default_attachment_id ) ? wp_get_attachment_image_url( $default_attachment_id, 'medium' ) : ''; ?> -
+
Date: Fri, 14 Aug 2026 12:01:55 +0600 Subject: [PATCH 28/51] feat(settings): reset image_upload_list and toggle_matrix fields --- .../src/js/admin-dashboard/segments/reset.js | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/assets/src/js/admin-dashboard/segments/reset.js b/assets/src/js/admin-dashboard/segments/reset.js index 0ee942f629..49cec03a6b 100644 --- a/assets/src/js/admin-dashboard/segments/reset.js +++ b/assets/src/js/admin-dashboard/segments/reset.js @@ -47,7 +47,7 @@ const resetConfirmation = () => { if (xhttp.readyState === 4) { let pageData = JSON.parse(xhttp.response).data; pageData.forEach((item) => { - const field_types_associate = ['color_field', 'upload_full', 'checkbox_notification', 'checkgroup', 'group_radio_full_3', 'group_radio', 'radio_vertical', 'checkbox_horizontal', 'radio_horizontal', 'radio_horizontal_full', 'radio_horizontal_image', 'checkbox_vertical', 'toggle_switch', 'toggle_switch_button', 'text', 'textarea', 'email', 'hidden', 'select', 'number']; + const field_types_associate = ['color_field', 'upload_full', 'image_upload_list', 'toggle_matrix', 'checkbox_notification', 'checkgroup', 'group_radio_full_3', 'group_radio', 'radio_vertical', 'checkbox_horizontal', 'radio_horizontal', 'radio_horizontal_full', 'radio_horizontal_image', 'checkbox_vertical', 'toggle_switch', 'toggle_switch_button', 'text', 'textarea', 'email', 'hidden', 'select', 'number']; if (field_types_associate.includes(item.type)) { let itemName = 'tutor_option[' + item.key + ']'; @@ -84,6 +84,36 @@ const resetConfirmation = () => { }); } + } else if (item.type == 'image_upload_list') { + Object.values(item.items || {}).forEach((imageItem) => { + const imageInput = elementByName(`tutor_option[${imageItem.key}]`)[0]; + if (!imageInput) return; + + const uploadItem = imageInput.closest('.tutor-image-upload-item'); + const defaultValue = imageItem.default || ''; + const defaultImageUrl = uploadItem?.dataset.defaultImageUrl || ''; + imageInput.value = defaultValue; + uploadItem?.classList.toggle('has-image', !!defaultImageUrl); + uploadItem?.querySelector('.tutor-image-upload-preview')?.classList.toggle('has-image', !!defaultImageUrl); + const image = uploadItem?.querySelector('.tutor-image-upload-preview img'); + if (image) { + image.src = defaultImageUrl; + image.hidden = !defaultImageUrl; + } + }); + } else if (item.type == 'toggle_matrix') { + Object.values(item.items || {}).forEach((matrixItem) => { + Object.values(matrixItem).forEach((setting) => { + if (!setting || !setting.key) return; + const matrixInput = elementByName(`tutor_option[${setting.key}]`)[0]; + if (!matrixInput) return; + + const isEnabled = setting.default === 'on'; + matrixInput.value = isEnabled ? 'on' : 'off'; + const checkbox = matrixInput.nextElementSibling; + if (checkbox) checkbox.checked = isEnabled; + }); + }); } else if (item.type == 'upload_full') { elementItem.value = ''; elementItem.nextElementSibling.src = ''; @@ -193,4 +223,4 @@ if (null !== optionForm) { document.getElementById('save_tutor_option').disabled = false; } }); -} \ No newline at end of file +} From aa5ad9a7261523e403f322dc15c5c0e7e82078c1 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Fri, 14 Aug 2026 12:01:58 +0600 Subject: [PATCH 29/51] style(admin): hide isolated course details wrappers when options disabled --- assets/src/scss/admin-dashboard/_tutor-admin.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/assets/src/scss/admin-dashboard/_tutor-admin.scss b/assets/src/scss/admin-dashboard/_tutor-admin.scss index 7c15e23981..5e08f2098d 100644 --- a/assets/src/scss/admin-dashboard/_tutor-admin.scss +++ b/assets/src/scss/admin-dashboard/_tutor-admin.scss @@ -267,6 +267,12 @@ body.tutor-backend-tutor_settings #wpbody-content { display: none !important; } +// Isolated option blocks wrap each field in their own item wrapper. Hide that +// wrapper when a dependent Course Details field is toggled off as well. +.course-details > .item-wrapper:has(.tutor-hide-option) { + display: none; +} + .tutor-option-no-bottom-border { border-bottom: 0 !important; padding-bottom: 0 !important; From c43ef02e6a6e2e27e3b105e9d23681be1c2fd74e Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Fri, 14 Aug 2026 12:03:03 +0600 Subject: [PATCH 30/51] refactor(page-elements): migrate spotlight mode to per-shell visibility --- templates/dashboard.php | 2 +- templates/dashboard/components/sidebar.php | 9 +++++++-- templates/learning-area/index.php | 5 +++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/templates/dashboard.php b/templates/dashboard.php index 7f8540b00d..932b6ac702 100644 --- a/templates/dashboard.php +++ b/templates/dashboard.php @@ -147,5 +147,5 @@ - get_option( 'show_dashboard_site_footer', true ) ); ?> + get_option( 'show_dashboard_site_footer', false ) ); ?> diff --git a/templates/dashboard/components/sidebar.php b/templates/dashboard/components/sidebar.php index b90b3a0745..e2787e787e 100644 --- a/templates/dashboard/components/sidebar.php +++ b/templates/dashboard/components/sidebar.php @@ -37,18 +37,23 @@ } $dashboard_page_name = apply_filters( 'tutor_dashboard_sub_page_template', $dashboard_page_name ); $dashboard_pages = tutor_utils()->tutor_dashboard_nav_ui_items(); +$show_site_header = (bool) get_tutor_option( 'show_dashboard_site_header' ); ?>
+ + @@ -147,5 +159,5 @@ - get_option( 'show_dashboard_site_footer', false ) ); ?> + diff --git a/templates/dashboard/components/sidebar.php b/templates/dashboard/components/sidebar.php index e2787e787e..e2c9d5acb0 100644 --- a/templates/dashboard/components/sidebar.php +++ b/templates/dashboard/components/sidebar.php @@ -106,12 +106,3 @@
- $dashboard_pages, - 'dashboard_page_slug' => $dashboard_page_slug, - ) - ); - ?> From 7244b23543dc0c8a73ec86606ccdca7ab6f2fc5e Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Mon, 17 Aug 2026 16:39:03 +0600 Subject: [PATCH 36/51] refactor(template): centralize site-shell data resolution Add Template::get_site_shell_data() as the single source of truth for theme header/footer visibility and header selector, and consume it from the dashboard, account, and learning-area templates. --- classes/Template.php | 81 +++++++++++++++++++++++++++++++ templates/account.php | 16 ++++-- templates/dashboard-isolated.php | 16 ++++-- templates/dashboard.php | 18 +++++-- templates/learning-area/index.php | 28 ++--------- 5 files changed, 126 insertions(+), 33 deletions(-) diff --git a/classes/Template.php b/classes/Template.php index f7be8b7b0c..c65d86976f 100644 --- a/classes/Template.php +++ b/classes/Template.php @@ -23,6 +23,27 @@ */ class Template extends Tutor_Base { + /** + * Page context for the Learning Area theme shell. + * + * @var string + */ + public const SITE_SHELL_CONTEXT_LEARNING = 'learning'; + + /** + * Page context for the Dashboard theme shell. + * + * @var string + */ + public const SITE_SHELL_CONTEXT_DASHBOARD = 'dashboard'; + + /** + * Default selector used to find common theme header markup. + * + * @var string + */ + private const THEME_HEADER_SELECTOR = 'header[role="banner"], header.site-header, header#masthead, #masthead, .site-header, header.wp-block-template-part, header.header-sticky'; + /** * Store Shortcode Object * @@ -30,6 +51,66 @@ class Template extends Tutor_Base { */ public $shortcode_obj; + /** + * Get the site-shell state for a Tutor page context. + * + * This is the single source of truth for theme header/footer visibility and + * the browser selector used to measure a rendered theme header. + * + * @since 4.0.6 + * + * @param string $context One of the SITE_SHELL_CONTEXT_* constants. + * + * @return array{ + * show_site_header: bool, + * show_site_footer: bool, + * has_site_shell: bool, + * theme_header_selector: string + * } + */ + public static function get_site_shell_data( string $context ): array { + if ( self::SITE_SHELL_CONTEXT_LEARNING === $context ) { + $legacy_spotlight_mode = (bool) tutor_utils()->get_option( 'enable_spotlight_mode' ); + $show_site_header = (bool) tutor_utils()->get_option( 'show_learning_site_header', ! $legacy_spotlight_mode ); + $show_site_footer = (bool) tutor_utils()->get_option( 'show_learning_site_footer', ! $legacy_spotlight_mode ); + } elseif ( self::SITE_SHELL_CONTEXT_DASHBOARD === $context ) { + $show_site_header = (bool) tutor_utils()->get_option( 'show_dashboard_site_header' ); + $show_site_footer = (bool) tutor_utils()->get_option( 'show_dashboard_site_footer' ); + } else { + return array( + 'show_site_header' => false, + 'show_site_footer' => false, + 'has_site_shell' => false, + 'theme_header_selector' => '', + ); + } + + $has_site_shell = $show_site_header || $show_site_footer; + $selector = ''; + + if ( $show_site_header ) { + /** + * Filters the selector used to find a theme header that overlays a Tutor page. + * + * Themes with non-standard header markup can return their exact selector. + * The selector is evaluated in the browser and invalid selectors are ignored. + * + * @since 4.0.6 + * + * @param string $selector CSS selector for the active theme header. + */ + $selector = apply_filters( 'tutor_theme_header_selector', self::THEME_HEADER_SELECTOR ); + $selector = is_string( $selector ) ? $selector : ''; + } + + return array( + 'show_site_header' => $show_site_header, + 'show_site_footer' => $show_site_footer, + 'has_site_shell' => $has_site_shell, + 'theme_header_selector' => $selector, + ); + } + /** * Register Hooks * diff --git a/templates/account.php b/templates/account.php index 3651345828..2e3a66a6b7 100644 --- a/templates/account.php +++ b/templates/account.php @@ -11,6 +11,7 @@ defined( 'ABSPATH' ) || exit; use TUTOR\Dashboard; +use TUTOR\Template; global $wp_query; @@ -27,12 +28,21 @@ Dashboard::set_document_title( $meta_title ); -$show_dashboard_site_header = (bool) tutor_utils()->get_option( 'show_dashboard_site_header' ); -$show_dashboard_site_footer = (bool) tutor_utils()->get_option( 'show_dashboard_site_footer' ); +$site_shell = Template::get_site_shell_data( Template::SITE_SHELL_CONTEXT_DASHBOARD ); +$show_dashboard_site_header = $site_shell['show_site_header']; +$show_dashboard_site_footer = $site_shell['show_site_footer']; +$has_dashboard_site_shell = $site_shell['has_site_shell']; +$theme_header_selector = $site_shell['theme_header_selector']; tutor_page_elements_header( $show_dashboard_site_header ); ?> -