From 21d1a86466e235a548aa7445dd3bc181efd2f7d4 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Sun, 15 Feb 2026 10:22:27 +0100 Subject: [PATCH 01/27] Include permalink and sanitized name in the response when updating a lesson. Still need to click away from lesson and go back to get the permalink to show. --- assets/js/builder/Controllers/Sync.js | 18 ++++++++++++++++++ includes/admin/class.llms.admin.builder.php | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/assets/js/builder/Controllers/Sync.js b/assets/js/builder/Controllers/Sync.js index 45b9a9accd..fec5c249d6 100644 --- a/assets/js/builder/Controllers/Sync.js +++ b/assets/js/builder/Controllers/Sync.js @@ -430,6 +430,15 @@ define( [], function() { model.set( 'id', info.id ); delete model._unsavedChanges.id; } + + // Update permalink and name if provided by the server. + if ( info.permalink ) { + model.set( 'permalink', info.permalink ); + } + if ( info.name ) { + model.set( 'name', info.name ); + } + maybe_restart_tracking( model, info ); // check children @@ -460,6 +469,15 @@ define( [], function() { model.set( 'id', info.id ); delete model._unsavedChanges.id; } + + // Update permalink and name if provided by the server. + if ( info.permalink ) { + model.set( 'permalink', info.permalink ); + } + if ( info.name ) { + model.set( 'name', info.name ); + } + maybe_restart_tracking( model, info ); // check children diff --git a/includes/admin/class.llms.admin.builder.php b/includes/admin/class.llms.admin.builder.php index 58ff37a458..599faab2c4 100644 --- a/includes/admin/class.llms.admin.builder.php +++ b/includes/admin/class.llms.admin.builder.php @@ -1097,6 +1097,10 @@ private static function update_lessons( $lessons, $section ) { $lesson->set( 'name', sanitize_title( $lesson_data['title'] ) ); } + // Include permalink and slug in the response so the builder can update the model. + $res['permalink'] = get_permalink( $lesson->get( 'id' ) ); + $res['name'] = $lesson->get( 'name' ); + // Remove revision prevention. remove_filter( 'wp_revisions_to_keep', '__return_zero', 999 ); From 765fc81d1edfb676cda174988d23f9fb9480046f Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Sun, 15 Feb 2026 10:27:37 +0100 Subject: [PATCH 02/27] Make sure settings panel updates. --- assets/js/builder/Views/LessonEditor.js | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/js/builder/Views/LessonEditor.js b/assets/js/builder/Views/LessonEditor.js index 869b17a34d..336590ecd6 100644 --- a/assets/js/builder/Views/LessonEditor.js +++ b/assets/js/builder/Views/LessonEditor.js @@ -76,6 +76,7 @@ define( [ var change_events = window.llms.hooks.applyFilters( 'llms_lesson_rerender_change_events', [ 'change:date_available', 'change:drip_method', + 'change:permalink', 'change:time_available', ] ); _.each( change_events, function( event ) { From 3589a5b12976db4f80d3290ae9e4c92dee03d354 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Sun, 15 Feb 2026 17:40:15 +0100 Subject: [PATCH 03/27] Default to published when creating a new quiz. --- assets/js/builder/Models/Quiz.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/builder/Models/Quiz.js b/assets/js/builder/Models/Quiz.js index 3ef1c9c25c..a2eb3cdd4e 100644 --- a/assets/js/builder/Models/Quiz.js +++ b/assets/js/builder/Models/Quiz.js @@ -64,7 +64,7 @@ define( [ type: 'llms_quiz', lesson_id: '', - status: 'draft', + status: 'publish', // editable fields. content: '', From 2a0d49ba38acdf2caa3a228d3aa58e53b7e77543 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Sun, 15 Feb 2026 17:47:52 +0100 Subject: [PATCH 04/27] Show permalink when creating a new quiz and saving, or editing (if it changes). --- assets/js/builder/Views/Quiz.js | 26 +++++++++++++++++++++ includes/admin/class.llms.admin.builder.php | 4 ++++ 2 files changed, 30 insertions(+) diff --git a/assets/js/builder/Views/Quiz.js b/assets/js/builder/Views/Quiz.js index 0d3385000b..ae14f1d9b6 100644 --- a/assets/js/builder/Views/Quiz.js +++ b/assets/js/builder/Views/Quiz.js @@ -125,6 +125,7 @@ define( [ this.model.set_parent( this.lesson ); this.listenTo( this.model, 'change:_points', this.render_points ); + this.listenTo( this.model, 'change:permalink', this.render_settings ); } @@ -226,6 +227,27 @@ define( [ }, + /** + * Re-render the settings subview. + * + * Used when the permalink is updated after saving so the settings + * panel reflects the new permalink without a full re-render. + * + * @since [version] + * + * @return {Void} + */ + render_settings: function() { + + var view = this.get_subview( 'settings' ); + if ( view && view.instance ) { + view.instance.render(); + this.init_datepickers(); + this.init_selects(); + } + + }, + /** * Bulk expand / collapse question buttons. * @@ -261,6 +283,8 @@ define( [ } this.model = quiz; + this.listenTo( this.model, 'change:_points', this.render_points ); + this.listenTo( this.model, 'change:permalink', this.render_settings ); this.render(); }, @@ -298,6 +322,8 @@ define( [ this.lesson.add_quiz( quiz ); this.model = this.lesson.get( 'quiz' ); + this.listenTo( this.model, 'change:_points', this.render_points ); + this.listenTo( this.model, 'change:permalink', this.render_settings ); this.render(); }, diff --git a/includes/admin/class.llms.admin.builder.php b/includes/admin/class.llms.admin.builder.php index 599faab2c4..31e5be6aa0 100644 --- a/includes/admin/class.llms.admin.builder.php +++ b/includes/admin/class.llms.admin.builder.php @@ -1305,6 +1305,10 @@ private static function update_quiz( $quiz_data, $lesson ) { } } + // Include permalink and slug in the response so the builder can update the model. + $res['permalink'] = get_permalink( $quiz->get( 'id' ) ); + $res['name'] = $quiz->get( 'name' ); + if ( isset( $quiz_data['questions'] ) && is_array( $quiz_data['questions'] ) ) { $res['questions'] = self::update_questions( $quiz_data['questions'], $quiz ); } From c3f236150e3bd92e6249277bee9da1f44aae7587 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Mon, 16 Feb 2026 14:42:50 +0100 Subject: [PATCH 05/27] Listen to the name change to update the text input correctly. --- assets/js/builder/Views/LessonEditor.js | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/js/builder/Views/LessonEditor.js b/assets/js/builder/Views/LessonEditor.js index 336590ecd6..3ee856882e 100644 --- a/assets/js/builder/Views/LessonEditor.js +++ b/assets/js/builder/Views/LessonEditor.js @@ -77,6 +77,7 @@ define( [ 'change:date_available', 'change:drip_method', 'change:permalink', + 'change:name', 'change:time_available', ] ); _.each( change_events, function( event ) { From a2bc5b4e1a3232b05d84db38f5062e5b306377b9 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Mon, 16 Feb 2026 16:23:13 +0100 Subject: [PATCH 06/27] Listen to the name change to update the text input correctly when adding a quiz. --- assets/js/builder/Views/Quiz.js | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/js/builder/Views/Quiz.js b/assets/js/builder/Views/Quiz.js index ae14f1d9b6..23ff871297 100644 --- a/assets/js/builder/Views/Quiz.js +++ b/assets/js/builder/Views/Quiz.js @@ -285,6 +285,7 @@ define( [ this.model = quiz; this.listenTo( this.model, 'change:_points', this.render_points ); this.listenTo( this.model, 'change:permalink', this.render_settings ); + this.listenTo( this.model, 'change:name', this.render_settings ); this.render(); }, From e942a325114faf5386ce9e82b2536bba15a06e7a Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Mon, 16 Feb 2026 16:23:34 +0100 Subject: [PATCH 07/27] Add post title from title data to generate a permalink, if not overridden by manual entry. --- includes/admin/class.llms.admin.builder.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/includes/admin/class.llms.admin.builder.php b/includes/admin/class.llms.admin.builder.php index 31e5be6aa0..be880d40f8 100644 --- a/includes/admin/class.llms.admin.builder.php +++ b/includes/admin/class.llms.admin.builder.php @@ -1257,7 +1257,12 @@ private static function update_quiz( $quiz_data, $lesson ) { // Create a quiz. if ( self::is_temp_id( $quiz_data['id'] ) ) { - $quiz = new LLMS_Quiz( 'new' ); + $quiz = new LLMS_Quiz( + 'new', + array( + 'post_title' => isset( $quiz_data['title'] ) ? $quiz_data['title'] : __( 'New Quiz', 'lifterlms' ), + ) + ); // Update existing quiz. } else { From 1d3f5ee9458b1b3fdb98413df1e3f1ec8c595802 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Mon, 16 Feb 2026 17:46:29 +0100 Subject: [PATCH 08/27] WIP: Adding experimental description field to lessons. --- assets/js/builder/Schemas/Lesson.js | 57 +++++++++++++++--- assets/js/builder/Views/SettingsFields.js | 5 ++ includes/admin/class.llms.admin.builder.php | 67 +++++++++++++++++++++ 3 files changed, 120 insertions(+), 9 deletions(-) diff --git a/assets/js/builder/Schemas/Lesson.js b/assets/js/builder/Schemas/Lesson.js index a652a075c1..991e7247db 100644 --- a/assets/js/builder/Schemas/Lesson.js +++ b/assets/js/builder/Schemas/Lesson.js @@ -12,15 +12,54 @@ define( [], function() { title: LLMS.l10n.translate( 'General Settings' ), toggleable: true, fields: [ - [ - { - attribute: 'permalink', - id: 'permalink', - type: 'permalink', - }, - ], [ - { - attribute: 'video_embed', + [ + { + attribute: 'permalink', + id: 'permalink', + type: 'permalink', + }, + ], [ + { + attribute: 'content', + id: 'description', + label: LLMS.l10n.translate( 'Description' ), + type: 'editor', + condition: function() { + var editorType = this.get( '_content_editor_type' ); + return ! editorType || 'classic' === editorType; + }, + }, + ], [ + { + id: 'description-page-builder-notice', + label: LLMS.l10n.translate( 'Description' ), + type: 'heading', + condition: function() { + var editorType = this.get( '_content_editor_type' ); + return editorType && 'classic' !== editorType; + }, + detail: function() { + var type = this.get( '_content_editor_type' ), + name; + + if ( 'block' === type ) { + name = LLMS.l10n.translate( 'the WordPress block editor' ); + } else if ( 'elementor' === type ) { + name = 'Elementor'; + } else if ( 'beaver_builder' === type ) { + name = 'Beaver Builder'; + } else { + name = LLMS.l10n.translate( 'a page builder' ); + } + + return LLMS.l10n.replace( "This lesson's content was created with %1$s.", { '%1$s': name } ) + + ' ' + + LLMS.l10n.translate( 'Edit in WordPress' ) + ''; + }, + }, + ], [ + { + attribute: 'video_embed', id: 'video-embed', label: LLMS.l10n.translate( 'Video Embed URL' ), type: 'video_embed', diff --git a/assets/js/builder/Views/SettingsFields.js b/assets/js/builder/Views/SettingsFields.js index cf7691dfb8..930fec86f4 100644 --- a/assets/js/builder/Views/SettingsFields.js +++ b/assets/js/builder/Views/SettingsFields.js @@ -366,6 +366,11 @@ define( [], function() { field.options = _.bind( field.options, this.model )(); } + // if detail is a function run it (allows dynamic content based on model data) + if ( _.isFunction( orig_field.detail ) ) { + field.detail = _.bind( orig_field.detail, this.model )(); + } + // if it's a radio field options values can be submitted as images // this will transform those images into html if ( -1 !== [ 'radio', 'switch-radio' ].indexOf( orig_field.type ) ) { diff --git a/includes/admin/class.llms.admin.builder.php b/includes/admin/class.llms.admin.builder.php index be880d40f8..214dbafa29 100644 --- a/includes/admin/class.llms.admin.builder.php +++ b/includes/admin/class.llms.admin.builder.php @@ -507,6 +507,66 @@ public static function is_temp_id( $id ) { return ( ! is_numeric( $id ) && 0 === strpos( $id, 'temp_' ) ); } + /** + * Determine which content editor was used for a given post. + * + * Checks for Elementor, Beaver Builder, and the WordPress block editor. + * Third parties can hook into `llms_builder_lesson_content_editor_type` to + * indicate their own page builder. + * + * @since [version] + * + * @param int $post_id WP Post ID. + * @return string Editor type: 'classic', 'block', 'elementor', 'beaver_builder', or a custom value. + */ + public static function get_content_editor_type( $post_id ) { + + if ( ! $post_id || ! is_numeric( $post_id ) ) { + return 'classic'; + } + + if ( function_exists( 'llms_is_elementor_post' ) && llms_is_elementor_post( $post_id ) ) { + return 'elementor'; + } + + if ( function_exists( 'llms_is_beaver_builder_post' ) && llms_is_beaver_builder_post( $post_id ) ) { + return 'beaver_builder'; + } + + if ( function_exists( 'has_blocks' ) && has_blocks( $post_id ) ) { + return 'block'; + } + + /** + * Filter the detected content editor type for a lesson in the course builder. + * + * Allows third-party page builders (e.g. WPBakery, Divi) to indicate that + * a lesson's content was created with their editor, preventing the builder's + * TinyMCE editor from overwriting it. + * + * @since 7.8.1 + * + * @param string $type Editor type. Default 'classic'. + * @param int $post_id WP Post ID of the lesson. + */ + return apply_filters( 'llms_builder_lesson_content_editor_type', 'classic', $post_id ); + } + + /** + * Add content editor type to lesson toArray output for the builder. + * + * @since 7.8.1 + * + * @param array $arr Lesson data array. + * @param LLMS_Post_Model $lesson Lesson instance. + * @return array + */ + public static function add_lesson_content_editor_type( $arr, $lesson ) { + + $arr['_content_editor_type'] = self::get_content_editor_type( $lesson->get( 'id' ) ); + return $arr; + } + /** * Modify the "Take Over" link on the post locked modal to send users to the builder when taking over a course * @@ -575,6 +635,8 @@ public static function output() { global $llms_builder_lazy_load; $llms_builder_lazy_load = true; + + add_filter( 'llms_lesson_to_array', array( __CLASS__, 'add_lesson_content_editor_type' ), 10, 2 ); ?>
@@ -1073,6 +1135,11 @@ private static function update_lessons( $lessons, $section ) { $skip_props = apply_filters( 'llms_builder_update_lesson_skip_props', array( 'quiz' ) ); + // Don't overwrite content if a page builder or block editor was used. + if ( ! $created && 'classic' !== self::get_content_editor_type( $lesson->get( 'id' ) ) ) { + $skip_props[] = 'content'; + } + // Update all updatable properties. foreach ( $properties as $prop ) { if ( isset( $lesson_data[ $prop ] ) && ! in_array( $prop, $skip_props, true ) ) { From 3ce4ef6ee1007ddb34834e7a95c2090b1f065442 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Mon, 16 Feb 2026 18:34:44 +0100 Subject: [PATCH 09/27] Moving to lesson model instead of hidden detail in schema. --- assets/js/builder/Controllers/Sync.js | 6 ++ assets/js/builder/Schemas/Lesson.js | 20 +----- assets/js/builder/Views/SettingsFields.js | 5 -- includes/admin/class.llms.admin.builder.php | 70 ++----------------- .../admin/views/builder/settings-fields.php | 26 +++++++ includes/models/model.llms.lesson.php | 48 +++++++++++++ 6 files changed, 86 insertions(+), 89 deletions(-) diff --git a/assets/js/builder/Controllers/Sync.js b/assets/js/builder/Controllers/Sync.js index fec5c249d6..9e74891c88 100644 --- a/assets/js/builder/Controllers/Sync.js +++ b/assets/js/builder/Controllers/Sync.js @@ -438,6 +438,9 @@ define( [], function() { if ( info.name ) { model.set( 'name', info.name ); } + if ( info._content_editor_type ) { + model.set( '_content_editor_type', info._content_editor_type ); + } maybe_restart_tracking( model, info ); @@ -477,6 +480,9 @@ define( [], function() { if ( info.name ) { model.set( 'name', info.name ); } + if ( info._content_editor_type ) { + model.set( '_content_editor_type', info._content_editor_type ); + } maybe_restart_tracking( model, info ); diff --git a/assets/js/builder/Schemas/Lesson.js b/assets/js/builder/Schemas/Lesson.js index 991e7247db..b3b4f1b135 100644 --- a/assets/js/builder/Schemas/Lesson.js +++ b/assets/js/builder/Schemas/Lesson.js @@ -33,29 +33,11 @@ define( [], function() { { id: 'description-page-builder-notice', label: LLMS.l10n.translate( 'Description' ), - type: 'heading', + type: 'page_builder_notice', condition: function() { var editorType = this.get( '_content_editor_type' ); return editorType && 'classic' !== editorType; }, - detail: function() { - var type = this.get( '_content_editor_type' ), - name; - - if ( 'block' === type ) { - name = LLMS.l10n.translate( 'the WordPress block editor' ); - } else if ( 'elementor' === type ) { - name = 'Elementor'; - } else if ( 'beaver_builder' === type ) { - name = 'Beaver Builder'; - } else { - name = LLMS.l10n.translate( 'a page builder' ); - } - - return LLMS.l10n.replace( "This lesson's content was created with %1$s.", { '%1$s': name } ) - + ' ' - + LLMS.l10n.translate( 'Edit in WordPress' ) + ''; - }, }, ], [ { diff --git a/assets/js/builder/Views/SettingsFields.js b/assets/js/builder/Views/SettingsFields.js index 930fec86f4..cf7691dfb8 100644 --- a/assets/js/builder/Views/SettingsFields.js +++ b/assets/js/builder/Views/SettingsFields.js @@ -366,11 +366,6 @@ define( [], function() { field.options = _.bind( field.options, this.model )(); } - // if detail is a function run it (allows dynamic content based on model data) - if ( _.isFunction( orig_field.detail ) ) { - field.detail = _.bind( orig_field.detail, this.model )(); - } - // if it's a radio field options values can be submitted as images // this will transform those images into html if ( -1 !== [ 'radio', 'switch-radio' ].indexOf( orig_field.type ) ) { diff --git a/includes/admin/class.llms.admin.builder.php b/includes/admin/class.llms.admin.builder.php index 214dbafa29..2bf80d8ccd 100644 --- a/includes/admin/class.llms.admin.builder.php +++ b/includes/admin/class.llms.admin.builder.php @@ -507,66 +507,6 @@ public static function is_temp_id( $id ) { return ( ! is_numeric( $id ) && 0 === strpos( $id, 'temp_' ) ); } - /** - * Determine which content editor was used for a given post. - * - * Checks for Elementor, Beaver Builder, and the WordPress block editor. - * Third parties can hook into `llms_builder_lesson_content_editor_type` to - * indicate their own page builder. - * - * @since [version] - * - * @param int $post_id WP Post ID. - * @return string Editor type: 'classic', 'block', 'elementor', 'beaver_builder', or a custom value. - */ - public static function get_content_editor_type( $post_id ) { - - if ( ! $post_id || ! is_numeric( $post_id ) ) { - return 'classic'; - } - - if ( function_exists( 'llms_is_elementor_post' ) && llms_is_elementor_post( $post_id ) ) { - return 'elementor'; - } - - if ( function_exists( 'llms_is_beaver_builder_post' ) && llms_is_beaver_builder_post( $post_id ) ) { - return 'beaver_builder'; - } - - if ( function_exists( 'has_blocks' ) && has_blocks( $post_id ) ) { - return 'block'; - } - - /** - * Filter the detected content editor type for a lesson in the course builder. - * - * Allows third-party page builders (e.g. WPBakery, Divi) to indicate that - * a lesson's content was created with their editor, preventing the builder's - * TinyMCE editor from overwriting it. - * - * @since 7.8.1 - * - * @param string $type Editor type. Default 'classic'. - * @param int $post_id WP Post ID of the lesson. - */ - return apply_filters( 'llms_builder_lesson_content_editor_type', 'classic', $post_id ); - } - - /** - * Add content editor type to lesson toArray output for the builder. - * - * @since 7.8.1 - * - * @param array $arr Lesson data array. - * @param LLMS_Post_Model $lesson Lesson instance. - * @return array - */ - public static function add_lesson_content_editor_type( $arr, $lesson ) { - - $arr['_content_editor_type'] = self::get_content_editor_type( $lesson->get( 'id' ) ); - return $arr; - } - /** * Modify the "Take Over" link on the post locked modal to send users to the builder when taking over a course * @@ -636,7 +576,6 @@ public static function output() { global $llms_builder_lazy_load; $llms_builder_lazy_load = true; - add_filter( 'llms_lesson_to_array', array( __CLASS__, 'add_lesson_content_editor_type' ), 10, 2 ); ?>
@@ -1136,7 +1075,7 @@ private static function update_lessons( $lessons, $section ) { $skip_props = apply_filters( 'llms_builder_update_lesson_skip_props', array( 'quiz' ) ); // Don't overwrite content if a page builder or block editor was used. - if ( ! $created && 'classic' !== self::get_content_editor_type( $lesson->get( 'id' ) ) ) { + if ( ! $created && 'classic' !== $lesson->get_content_editor_type() ) { $skip_props[] = 'content'; } @@ -1164,9 +1103,10 @@ private static function update_lessons( $lessons, $section ) { $lesson->set( 'name', sanitize_title( $lesson_data['title'] ) ); } - // Include permalink and slug in the response so the builder can update the model. - $res['permalink'] = get_permalink( $lesson->get( 'id' ) ); - $res['name'] = $lesson->get( 'name' ); + // Include permalink, slug, and editor type in the response so the builder can update the model. + $res['permalink'] = get_permalink( $lesson->get( 'id' ) ); + $res['name'] = $lesson->get( 'name' ); + $res['_content_editor_type'] = $lesson->get_content_editor_type(); // Remove revision prevention. remove_filter( 'wp_revisions_to_keep', '__return_zero', 999 ); diff --git a/includes/admin/views/builder/settings-fields.php b/includes/admin/views/builder/settings-fields.php index 11f36382ce..49b749a544 100644 --- a/includes/admin/views/builder/settings-fields.php +++ b/includes/admin/views/builder/settings-fields.php @@ -67,6 +67,32 @@ + <# } else if ( 'page_builder_notice' === field.type ) { #> + + <# + var editorType = data.model.get( '_content_editor_type' ), + editorName; + if ( 'block' === editorType ) { + editorName = ''; + } else if ( 'elementor' === editorType ) { + editorName = 'Elementor'; + } else if ( 'beaver_builder' === editorType ) { + editorName = 'Beaver Builder'; + } else { + editorName = ''; + } + #> +

+ + +

+ <# } else if ( 'upsell' === field.type ) { #> diff --git a/includes/models/model.llms.lesson.php b/includes/models/model.llms.lesson.php index d4f55b5715..6090b151e1 100644 --- a/includes/models/model.llms.lesson.php +++ b/includes/models/model.llms.lesson.php @@ -590,9 +590,57 @@ public function toArrayAfter( $arr ) { } } + $arr['_content_editor_type'] = $this->get_content_editor_type(); + return $arr; } + /** + * Determine which content editor was used for this lesson. + * + * Checks for Elementor, Beaver Builder, and the WordPress block editor. + * Third parties can hook into `llms_lesson_content_editor_type` to + * indicate their own page builder. + * + * @since 7.8.1 + * + * @return string Editor type: 'classic', 'block', 'elementor', 'beaver_builder', or a custom value. + */ + public function get_content_editor_type() { + + $post_id = $this->get( 'id' ); + + if ( ! $post_id || ! is_numeric( $post_id ) ) { + return 'classic'; + } + + if ( function_exists( 'llms_is_elementor_post' ) && llms_is_elementor_post( $post_id ) ) { + return 'elementor'; + } + + if ( function_exists( 'llms_is_beaver_builder_post' ) && llms_is_beaver_builder_post( $post_id ) ) { + return 'beaver_builder'; + } + + if ( function_exists( 'has_blocks' ) && has_blocks( $post_id ) ) { + return 'block'; + } + + /** + * Filter the detected content editor type for a lesson. + * + * Allows third-party page builders (e.g. WPBakery, Divi) to indicate that + * a lesson's content was created with their editor, preventing the course + * builder's TinyMCE editor from overwriting it. + * + * @since 7.8.1 + * + * @param string $type Editor type. Default 'classic'. + * @param int $post_id WP Post ID of the lesson. + */ + return apply_filters( 'llms_lesson_content_editor_type', 'classic', $post_id ); + } + /** * Update object data * From 3b61b910936634e47e43b08ecf6da9b1770935e2 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Tue, 17 Feb 2026 09:58:44 +0100 Subject: [PATCH 10/27] Switch to allowing edit only if the content is blank, or if the content was added inside of the course builder (as indicated by the "content_added_in_builder" flag). --- assets/js/builder/Controllers/Sync.js | 13 +++-- assets/js/builder/Models/Lesson.js | 2 + assets/js/builder/Schemas/Lesson.js | 14 +++-- assets/js/builder/Views/LessonEditor.js | 1 + includes/admin/class.llms.admin.builder.php | 15 ++++-- .../admin/views/builder/settings-fields.php | 19 +------ includes/models/model.llms.lesson.php | 51 ++----------------- 7 files changed, 31 insertions(+), 84 deletions(-) diff --git a/assets/js/builder/Controllers/Sync.js b/assets/js/builder/Controllers/Sync.js index 9e74891c88..89364a3a0d 100644 --- a/assets/js/builder/Controllers/Sync.js +++ b/assets/js/builder/Controllers/Sync.js @@ -431,15 +431,15 @@ define( [], function() { delete model._unsavedChanges.id; } - // Update permalink and name if provided by the server. if ( info.permalink ) { model.set( 'permalink', info.permalink ); } if ( info.name ) { model.set( 'name', info.name ); } - if ( info._content_editor_type ) { - model.set( '_content_editor_type', info._content_editor_type ); + + if ( info.content_added_in_builder ) { + model.set( 'content_added_in_builder', info.content_added_in_builder ); } maybe_restart_tracking( model, info ); @@ -480,10 +480,13 @@ define( [], function() { if ( info.name ) { model.set( 'name', info.name ); } - if ( info._content_editor_type ) { - model.set( '_content_editor_type', info._content_editor_type ); + console.log( 'setting content added in builder (on update)?' ); + if ( info.content_added_in_builder ) { + console.log('setting from update!!'); + model.set( 'content_added_in_builder', info.content_added_in_builder ); } + maybe_restart_tracking( model, info ); // check children diff --git a/assets/js/builder/Models/Lesson.js b/assets/js/builder/Models/Lesson.js index d3d8b22b12..722fba9099 100644 --- a/assets/js/builder/Models/Lesson.js +++ b/assets/js/builder/Models/Lesson.js @@ -76,6 +76,8 @@ define( [ 'Models/Quiz', 'Models/_Relationships', 'Models/_Utilities', 'Schemas/ quiz: {}, // Quiz model/data. quiz_enabled: 'no', + content_added_in_builder: '', + _forceSync: false, }; diff --git a/assets/js/builder/Schemas/Lesson.js b/assets/js/builder/Schemas/Lesson.js index b3b4f1b135..0ef456896f 100644 --- a/assets/js/builder/Schemas/Lesson.js +++ b/assets/js/builder/Schemas/Lesson.js @@ -21,22 +21,20 @@ define( [], function() { ], [ { attribute: 'content', - id: 'description', - label: LLMS.l10n.translate( 'Description' ), + id: 'content', + label: LLMS.l10n.translate( 'Content' ), type: 'editor', condition: function() { - var editorType = this.get( '_content_editor_type' ); - return ! editorType || 'classic' === editorType; + return '' === this.get( 'content' ) || 'yes' === this.get( 'content_added_in_builder' ); }, }, ], [ { - id: 'description-page-builder-notice', - label: LLMS.l10n.translate( 'Description' ), + id: 'content-page-builder-notice', + label: LLMS.l10n.translate( 'Content' ), type: 'page_builder_notice', condition: function() { - var editorType = this.get( '_content_editor_type' ); - return editorType && 'classic' !== editorType; + return '' !== this.get( 'content' ) && 'yes' !== this.get( 'content_added_in_builder' ); }, }, ], [ diff --git a/assets/js/builder/Views/LessonEditor.js b/assets/js/builder/Views/LessonEditor.js index 3ee856882e..a08db42970 100644 --- a/assets/js/builder/Views/LessonEditor.js +++ b/assets/js/builder/Views/LessonEditor.js @@ -77,6 +77,7 @@ define( [ 'change:date_available', 'change:drip_method', 'change:permalink', + 'change:content_added_in_builder', 'change:name', 'change:time_available', ] ); diff --git a/includes/admin/class.llms.admin.builder.php b/includes/admin/class.llms.admin.builder.php index 2bf80d8ccd..db88427be6 100644 --- a/includes/admin/class.llms.admin.builder.php +++ b/includes/admin/class.llms.admin.builder.php @@ -1074,11 +1074,16 @@ private static function update_lessons( $lessons, $section ) { $skip_props = apply_filters( 'llms_builder_update_lesson_skip_props', array( 'quiz' ) ); - // Don't overwrite content if a page builder or block editor was used. - if ( ! $created && 'classic' !== $lesson->get_content_editor_type() ) { + // Don't overwrite content if the content editor doesn't display. + if ( ! $created && '' !== $lesson->get( 'content' ) && ! llms_parse_bool( $lesson->get( 'content_added_in_builder' ) ) ) { $skip_props[] = 'content'; } + if ( '' === $lesson->get( 'content' ) && '' !== $lesson_data['content'] ) { + // We're adding content via the builder for the first time; add a flag saying so. + $lesson->set( 'content_added_in_builder', 'yes' ); + } + // Update all updatable properties. foreach ( $properties as $prop ) { if ( isset( $lesson_data[ $prop ] ) && ! in_array( $prop, $skip_props, true ) ) { @@ -1104,9 +1109,9 @@ private static function update_lessons( $lessons, $section ) { } // Include permalink, slug, and editor type in the response so the builder can update the model. - $res['permalink'] = get_permalink( $lesson->get( 'id' ) ); - $res['name'] = $lesson->get( 'name' ); - $res['_content_editor_type'] = $lesson->get_content_editor_type(); + $res['permalink'] = get_permalink( $lesson->get( 'id' ) ); + $res['name'] = $lesson->get( 'name' ); + $res['content_added_in_builder'] = $lesson->get( 'content_added_in_builder' ); // Remove revision prevention. remove_filter( 'wp_revisions_to_keep', '__return_zero', 999 ); diff --git a/includes/admin/views/builder/settings-fields.php b/includes/admin/views/builder/settings-fields.php index 49b749a544..f00a2ea88d 100644 --- a/includes/admin/views/builder/settings-fields.php +++ b/includes/admin/views/builder/settings-fields.php @@ -69,26 +69,9 @@ <# } else if ( 'page_builder_notice' === field.type ) { #> - <# - var editorType = data.model.get( '_content_editor_type' ), - editorName; - if ( 'block' === editorType ) { - editorName = ''; - } else if ( 'elementor' === editorType ) { - editorName = 'Elementor'; - } else if ( 'beaver_builder' === editorType ) { - editorName = 'Beaver Builder'; - } else { - editorName = ''; - } - #>

diff --git a/includes/models/model.llms.lesson.php b/includes/models/model.llms.lesson.php index 6090b151e1..99f3cbeaf0 100644 --- a/includes/models/model.llms.lesson.php +++ b/includes/models/model.llms.lesson.php @@ -42,6 +42,7 @@ * @property string $require_assignment_passing_grade Whether of not students have to pass the assignment to advance to the next lesson [yes|no]. * @property string $time_available Optional time to make lesson available on $date_available when $drip_method is "date". * @property string $video_embed URL to an oEmbed enable video URL. + * @property string $content_added_in_builder Whether content was (at least initially) added within the page builder. */ class LLMS_Lesson extends LLMS_Post_Model { @@ -68,6 +69,8 @@ class LLMS_Lesson extends LLMS_Post_Model { 'require_assignment_passing_grade' => 'yesno', 'points' => 'absint', + 'content_added_in_builder' => 'yesno', + // Quizzes. 'quiz' => 'absint', 'quiz_enabled' => 'yesno', @@ -590,57 +593,9 @@ public function toArrayAfter( $arr ) { } } - $arr['_content_editor_type'] = $this->get_content_editor_type(); - return $arr; } - /** - * Determine which content editor was used for this lesson. - * - * Checks for Elementor, Beaver Builder, and the WordPress block editor. - * Third parties can hook into `llms_lesson_content_editor_type` to - * indicate their own page builder. - * - * @since 7.8.1 - * - * @return string Editor type: 'classic', 'block', 'elementor', 'beaver_builder', or a custom value. - */ - public function get_content_editor_type() { - - $post_id = $this->get( 'id' ); - - if ( ! $post_id || ! is_numeric( $post_id ) ) { - return 'classic'; - } - - if ( function_exists( 'llms_is_elementor_post' ) && llms_is_elementor_post( $post_id ) ) { - return 'elementor'; - } - - if ( function_exists( 'llms_is_beaver_builder_post' ) && llms_is_beaver_builder_post( $post_id ) ) { - return 'beaver_builder'; - } - - if ( function_exists( 'has_blocks' ) && has_blocks( $post_id ) ) { - return 'block'; - } - - /** - * Filter the detected content editor type for a lesson. - * - * Allows third-party page builders (e.g. WPBakery, Divi) to indicate that - * a lesson's content was created with their editor, preventing the course - * builder's TinyMCE editor from overwriting it. - * - * @since 7.8.1 - * - * @param string $type Editor type. Default 'classic'. - * @param int $post_id WP Post ID of the lesson. - */ - return apply_filters( 'llms_lesson_content_editor_type', 'classic', $post_id ); - } - /** * Update object data * From a53ff2bc666d57ef111a7f8e006af593c0395c0d Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Tue, 17 Feb 2026 12:26:01 +0100 Subject: [PATCH 11/27] Change the lesson data rather than saving the flag. Otherwise content_added_in_builder is passed into the lesson data, and it overrides to empty string (no). --- includes/admin/class.llms.admin.builder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/admin/class.llms.admin.builder.php b/includes/admin/class.llms.admin.builder.php index db88427be6..72f2cf2bdc 100644 --- a/includes/admin/class.llms.admin.builder.php +++ b/includes/admin/class.llms.admin.builder.php @@ -1081,7 +1081,7 @@ private static function update_lessons( $lessons, $section ) { if ( '' === $lesson->get( 'content' ) && '' !== $lesson_data['content'] ) { // We're adding content via the builder for the first time; add a flag saying so. - $lesson->set( 'content_added_in_builder', 'yes' ); + $lesson_data['content_added_in_builder'] = 'yes'; } // Update all updatable properties. From 02f7470b98c3082b466ea0f5ef5113d3898bfe85 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Tue, 17 Feb 2026 12:35:18 +0100 Subject: [PATCH 12/27] Close the editor sidebar if the lesson we're currently editing is being deleted. --- assets/js/builder/Views/_Trashable.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/assets/js/builder/Views/_Trashable.js b/assets/js/builder/Views/_Trashable.js index 948a9176ac..6f94c72f66 100644 --- a/assets/js/builder/Views/_Trashable.js +++ b/assets/js/builder/Views/_Trashable.js @@ -49,6 +49,11 @@ define( [], function() { // publish event Backbone.pubSub.trigger( 'model-trashed', this.model ); + // close the editor sidebar if the trashed model is the one currently being edited + if ( this.model.get( '_selected' ) ) { + Backbone.pubSub.trigger( 'sidebar-editor-close' ); + } + // trigger local event so extending views can run other actions where necessary this.trigger( 'model-trashed', this.model ); From 54eebf1bef51da6a018e0ed8f9bd0687096da5a9 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Tue, 17 Feb 2026 15:43:37 +0100 Subject: [PATCH 13/27] Attempt to fix tests. --- includes/admin/class.llms.admin.builder.php | 2 +- tests/phpunit/unit-tests/class-llms-test-generator-courses.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/admin/class.llms.admin.builder.php b/includes/admin/class.llms.admin.builder.php index 72f2cf2bdc..61cc4e0165 100644 --- a/includes/admin/class.llms.admin.builder.php +++ b/includes/admin/class.llms.admin.builder.php @@ -1079,7 +1079,7 @@ private static function update_lessons( $lessons, $section ) { $skip_props[] = 'content'; } - if ( '' === $lesson->get( 'content' ) && '' !== $lesson_data['content'] ) { + if ( '' === $lesson->get( 'content' ) && isset( $lesson_data['content'] ) && '' !== $lesson_data['content'] ) { // We're adding content via the builder for the first time; add a flag saying so. $lesson_data['content_added_in_builder'] = 'yes'; } diff --git a/tests/phpunit/unit-tests/class-llms-test-generator-courses.php b/tests/phpunit/unit-tests/class-llms-test-generator-courses.php index 7bcb35b782..38f4c55dab 100644 --- a/tests/phpunit/unit-tests/class-llms-test-generator-courses.php +++ b/tests/phpunit/unit-tests/class-llms-test-generator-courses.php @@ -395,7 +395,7 @@ public function test_create_lesson() { // Test meta props are set. foreach ( array_keys( LLMS_Unit_Test_Util::get_private_property_value( $lesson, 'properties' ) ) as $prop ) { // This data is not based off raw. - if ( in_array( $prop, array( 'order', 'parent_course', 'parent_section', 'quiz' ), true ) ) { + if ( in_array( $prop, array( 'order', 'parent_course', 'parent_section', 'quiz', 'content_added_in_builder' ), true ) ) { continue; } $this->assertEquals( $raw[ $prop ], $lesson->get( $prop ), $prop ); From 00e5986ed24fcac61f99109f04c4379c4b05d32e Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Tue, 17 Feb 2026 15:46:16 +0100 Subject: [PATCH 14/27] WIP: Trying to hide add existing lesson modal after picking a lesson. --- assets/js/builder/Views/Elements.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/assets/js/builder/Views/Elements.js b/assets/js/builder/Views/Elements.js index 1d4deb2c9d..fdf728f26b 100644 --- a/assets/js/builder/Views/Elements.js +++ b/assets/js/builder/Views/Elements.js @@ -130,7 +130,13 @@ define( [ 'Models/Section', 'Views/Section', 'Models/Lesson', 'Views/Lesson', 'V event.preventDefault(); - var pop = new Popover( { + var pop, hidePopover; + + hidePopover = function() { + pop.hide(); + }; + + pop = new Popover( { el: '#llms-existing-lesson', args: { backdrop: true, @@ -144,13 +150,14 @@ define( [ 'Models/Section', 'Views/Section', 'Models/Lesson', 'Views/Lesson', 'V post_type: 'lesson', searching_message: LLMS.l10n.translate( 'Search for existing lessons...' ), } ).render().$el, + onHide: function() { + Backbone.pubSub.off( 'lesson-search-select', hidePopover ); + }, } } ); pop.show(); - Backbone.pubSub.on( 'lesson-search-select', function() { - pop.hide() - } ); + Backbone.pubSub.once( 'lesson-search-select', hidePopover ); }, From c947ef272407d42893e6e042d45f0aab5c18a1ab Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 19 Feb 2026 15:36:36 +0100 Subject: [PATCH 15/27] Destroy the webui-popover elements. Fixes #3097 --- assets/js/builder/Views/Elements.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/assets/js/builder/Views/Elements.js b/assets/js/builder/Views/Elements.js index fdf728f26b..8f42084610 100644 --- a/assets/js/builder/Views/Elements.js +++ b/assets/js/builder/Views/Elements.js @@ -157,7 +157,13 @@ define( [ 'Models/Section', 'Views/Section', 'Models/Lesson', 'Views/Lesson', 'V } ); pop.show(); - Backbone.pubSub.once( 'lesson-search-select', hidePopover ); + Backbone.pubSub.once( 'lesson-search-select', function() { + Backbone.pubSub.once( 'lesson-search-select', hidePopover ); + + // @todo For some reason the above doesn't close via pop.hide(). Seems internal to webui-popup. Ref #3097 + $( '.webui-popover' ).remove(); + $( '.webui-popover-backdrop' ).remove(); + } ); }, From df8649e255edf89bfea1dd82850c57cbdbcb6d91 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 19 Feb 2026 15:59:46 +0100 Subject: [PATCH 16/27] Selecting the first section on load. Improved highlighting of the selected section. --- assets/js/builder/Views/Course.js | 9 +++++++-- assets/js/builder/Views/Section.js | 1 - assets/js/builder/Views/SectionList.js | 2 +- assets/scss/admin/_course-builder.scss | 3 +++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/assets/js/builder/Views/Course.js b/assets/js/builder/Views/Course.js index 4b0995a4b4..a28cfb5415 100644 --- a/assets/js/builder/Views/Course.js +++ b/assets/js/builder/Views/Course.js @@ -89,6 +89,12 @@ define( [ Backbone.pubSub.on( 'lesson-selected', this.active_lesson_change, this ); + // Select the first section by default on load. + var firstSection = this.model.get( 'sections' ).first(); + if ( firstSection ) { + this.sectionListView.setSelectedModel( firstSection ); + } + }, /** @@ -167,8 +173,7 @@ define( [ */ on_section_toggle: function( model ) { - var selected = model.get( '_expanded' ) ? [ model ] : []; - this.sectionListView.setSelectedModels( selected ); + this.sectionListView.setSelectedModel( model ); }, diff --git a/assets/js/builder/Views/Section.js b/assets/js/builder/Views/Section.js index 3691f7d47c..a13ea8cf26 100644 --- a/assets/js/builder/Views/Section.js +++ b/assets/js/builder/Views/Section.js @@ -43,7 +43,6 @@ define( [ */ events: _.defaults( { - 'click': 'select', 'click .expand': 'expand', 'click .collapse': 'collapse', 'click .shift-up--section': 'shift_up', diff --git a/assets/js/builder/Views/SectionList.js b/assets/js/builder/Views/SectionList.js index 58b0485e74..6af1f18017 100644 --- a/assets/js/builder/Views/SectionList.js +++ b/assets/js/builder/Views/SectionList.js @@ -16,7 +16,7 @@ define( [ 'Views/Section', 'Views/_Receivable' ], function( SectionView, Receiva el: '#llms-sections', events : { - 'mousedown > li.llms-section > .llms-builder-header .llms-headline' : '_listItem_onMousedown', + 'mousedown > li.llms-section' : '_listItem_onMousedown', // 'dblclick > li, tbody > tr > td' : '_listItem_onDoubleClick', 'click' : '_listBackground_onClick', 'click ul.collection-view' : '_listBackground_onClick', diff --git a/assets/scss/admin/_course-builder.scss b/assets/scss/admin/_course-builder.scss index 29923cd2a8..f93b4c48e9 100644 --- a/assets/scss/admin/_course-builder.scss +++ b/assets/scss/admin/_course-builder.scss @@ -186,6 +186,9 @@ body.admin_page_llms-course-builder { .llms-lessons { overflow: visible; } } &.selected { + border-left: 3px solid $color-brand-blue; + box-shadow: 2px 2px 8px rgba( 0, 0, 0, 0.12 ); + margin-left: -2px; .llms-drag-utility.drag-section { border-color: $color-brand-blue; } From c00b606e1e217789efa4cd54197d6b772e46f86f Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 19 Feb 2026 16:16:44 +0100 Subject: [PATCH 17/27] Fix for cloned lessons showing content editor incorrectly. --- includes/admin/class.llms.admin.builder.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/admin/class.llms.admin.builder.php b/includes/admin/class.llms.admin.builder.php index 61cc4e0165..f45738c9c0 100644 --- a/includes/admin/class.llms.admin.builder.php +++ b/includes/admin/class.llms.admin.builder.php @@ -1079,7 +1079,8 @@ private static function update_lessons( $lessons, $section ) { $skip_props[] = 'content'; } - if ( '' === $lesson->get( 'content' ) && isset( $lesson_data['content'] ) && '' !== $lesson_data['content'] ) { + if ( '' === $lesson->get( 'content' ) && isset( $lesson_data['content'] ) && '' !== $lesson_data['content'] + && ! isset( $lesson_data['content_added_in_builder'] ) ) { // We're adding content via the builder for the first time; add a flag saying so. $lesson_data['content_added_in_builder'] = 'yes'; } From 7cc1e6a96cc5b0bc4b9d5f53cc4472b97842f786 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 19 Feb 2026 17:38:35 +0100 Subject: [PATCH 18/27] Remove console logs. --- assets/js/builder/Controllers/Sync.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/assets/js/builder/Controllers/Sync.js b/assets/js/builder/Controllers/Sync.js index 89364a3a0d..fc08219bf7 100644 --- a/assets/js/builder/Controllers/Sync.js +++ b/assets/js/builder/Controllers/Sync.js @@ -480,9 +480,8 @@ define( [], function() { if ( info.name ) { model.set( 'name', info.name ); } - console.log( 'setting content added in builder (on update)?' ); + if ( info.content_added_in_builder ) { - console.log('setting from update!!'); model.set( 'content_added_in_builder', info.content_added_in_builder ); } From 6729084bcf01fff35a58449ea9d316275f593b9c Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 19 Feb 2026 17:44:01 +0100 Subject: [PATCH 19/27] Avoiding propagation up to the top-level of quiz when the select is changed in a question. --- assets/js/builder/Views/_Editable.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/js/builder/Views/_Editable.js b/assets/js/builder/Views/_Editable.js index 6875fe0436..d23ea034e1 100644 --- a/assets/js/builder/Views/_Editable.js +++ b/assets/js/builder/Views/_Editable.js @@ -313,6 +313,8 @@ define( [], function() { */ on_select: function( event ) { + event.stopPropagation(); + var $el = $( event.target ), multi = ( $el.attr( 'multiple' ) ), attr = $el.attr( 'name' ), From 83b8219fa6b5fae96481345d320e63c2ed2be9ff Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Fri, 20 Feb 2026 14:13:53 +0100 Subject: [PATCH 20/27] Listen for changes to assignment model. --- assets/js/builder/Views/Assignment.js | 83 +++++++++++++++++---------- 1 file changed, 52 insertions(+), 31 deletions(-) diff --git a/assets/js/builder/Views/Assignment.js b/assets/js/builder/Views/Assignment.js index 14c7c4b727..0b5615a966 100644 --- a/assets/js/builder/Views/Assignment.js +++ b/assets/js/builder/Views/Assignment.js @@ -93,28 +93,30 @@ define( [ // initialize the model if the assignment is enabled or it's disabled but we still have data for a assignment if ( 'yes' === this.lesson.get( 'assignment_enabled' ) || ! _.isEmpty( this.lesson.get( 'assignment' ) ) ) { - this.model = this.lesson.get( 'assignment' ); - - /** - * Todo Item. - * - * @todo this is a terrible terrible patch - * I've spent nearly 3 days trying to figure out how to not use this line of code - * ISSUE REPRODUCTION: - * Open course builder - * Open a lesson (A) and add a assignment - * Switch to a new lesson (B) - * Add a new assignment - * Return to lesson A and the assignment's parent will be set to LESSON B - * This will happen for *every* assignment in the builder... - * Adding this set_parent on init guarantees that the assignment's correct parent is set - * after adding new assignment's to other lessons - * it's awful and it's gross... - * I'm confused and tired and going to miss release dates again because of it - */ - this.model.set_parent( this.lesson ); - - } + this.model = this.lesson.get( 'assignment' ); + + /** + * Todo Item. + * + * @todo this is a terrible terrible patch + * I've spent nearly 3 days trying to figure out how to not use this line of code + * ISSUE REPRODUCTION: + * Open course builder + * Open a lesson (A) and add a assignment + * Switch to a new lesson (B) + * Add a new assignment + * Return to lesson A and the assignment's parent will be set to LESSON B + * This will happen for *every* assignment in the builder... + * Adding this set_parent on init guarantees that the assignment's correct parent is set + * after adding new assignment's to other lessons + * it's awful and it's gross... + * I'm confused and tired and going to miss release dates again because of it + */ + this.model.set_parent( this.lesson ); + + this.listenTo( this.model, 'change:permalink', this.render_settings ); + + } this.on( 'model-trashed', this.on_trashed ); @@ -154,6 +156,23 @@ define( [ }, + /** + * Re-render the settings subview when permalink updates after saving. + * + * @since [version] + * + * @return {Void} + */ + render_settings: function() { + + var view = this.get_subview( 'settings' ); + if ( view && view.instance ) { + view.instance.render(); + this.init_selects(); + } + + }, + /** * Adds a new assignment to a lesson which currently has no assignment associated with it. * @@ -173,14 +192,15 @@ define( [ lesson_id: this.lesson.get( 'id' ), } ); - this.lesson.set( 'assignment_enabled', 'yes' ); - this.lesson.set( 'assignment', this.model ); + this.lesson.set( 'assignment_enabled', 'yes' ); + this.lesson.set( 'assignment', this.model ); - this.render(); + this.listenTo( this.model, 'change:permalink', this.render_settings ); + this.render(); - } else { + } else { - this.show_ad_popover( '#llms-new-assignment' ); + this.show_ad_popover( '#llms-new-assignment' ); } @@ -217,11 +237,12 @@ define( [ assignment = window.llms_builder.construct.get_model( 'Assignment', assignment ); - this.lesson.set( 'assignment_enabled', 'yes' ); - this.lesson.set( 'assignment', assignment ); - this.model = assignment; + this.lesson.set( 'assignment_enabled', 'yes' ); + this.lesson.set( 'assignment', assignment ); + this.model = assignment; - this.render(); + this.listenTo( this.model, 'change:permalink', this.render_settings ); + this.render(); }, From 66026e898ce56f0c6f1cc21a528389ac12a7baf2 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Fri, 20 Feb 2026 15:33:22 +0100 Subject: [PATCH 21/27] Changelogs --- .changelogs/course-builder-improvements-1.yml | 4 ++++ .changelogs/course-builder-improvements.yml | 9 +++++++++ 2 files changed, 13 insertions(+) create mode 100644 .changelogs/course-builder-improvements-1.yml create mode 100644 .changelogs/course-builder-improvements.yml diff --git a/.changelogs/course-builder-improvements-1.yml b/.changelogs/course-builder-improvements-1.yml new file mode 100644 index 0000000000..d5d40f0afe --- /dev/null +++ b/.changelogs/course-builder-improvements-1.yml @@ -0,0 +1,4 @@ +significance: minor +type: added +entry: Lesson content can be edited within the Course Builder for new lessons, + or existing lessons with no existing content. diff --git a/.changelogs/course-builder-improvements.yml b/.changelogs/course-builder-improvements.yml new file mode 100644 index 0000000000..47f488c947 --- /dev/null +++ b/.changelogs/course-builder-improvements.yml @@ -0,0 +1,9 @@ +significance: minor +type: changed +links: + - "#3033" + - "#3056" + - "#3097" + - "#2938" + - "#3030" +entry: Various course builder fixes, with quizzes set to published by default. From 6576b2d5241d20cf3c7adff96e1fc4a8bd16483a Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Fri, 20 Feb 2026 15:34:44 +0100 Subject: [PATCH 22/27] Changelogs --- .changelogs/course-builder-improvements-2.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelogs/course-builder-improvements-2.yml diff --git a/.changelogs/course-builder-improvements-2.yml b/.changelogs/course-builder-improvements-2.yml new file mode 100644 index 0000000000..37da4c8a94 --- /dev/null +++ b/.changelogs/course-builder-improvements-2.yml @@ -0,0 +1,3 @@ +significance: patch +type: fixed +entry: Close lesson settings panel when lesson has been trashed. From 46b9a57a3fe3f3da92f85db31a9d60eacb95ab03 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Fri, 20 Feb 2026 15:37:37 +0100 Subject: [PATCH 23/27] Spacing --- assets/js/builder/Views/Assignment.js | 68 +++++++++++++-------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/assets/js/builder/Views/Assignment.js b/assets/js/builder/Views/Assignment.js index 0b5615a966..56b6777c63 100644 --- a/assets/js/builder/Views/Assignment.js +++ b/assets/js/builder/Views/Assignment.js @@ -93,30 +93,30 @@ define( [ // initialize the model if the assignment is enabled or it's disabled but we still have data for a assignment if ( 'yes' === this.lesson.get( 'assignment_enabled' ) || ! _.isEmpty( this.lesson.get( 'assignment' ) ) ) { - this.model = this.lesson.get( 'assignment' ); - - /** - * Todo Item. - * - * @todo this is a terrible terrible patch - * I've spent nearly 3 days trying to figure out how to not use this line of code - * ISSUE REPRODUCTION: - * Open course builder - * Open a lesson (A) and add a assignment - * Switch to a new lesson (B) - * Add a new assignment - * Return to lesson A and the assignment's parent will be set to LESSON B - * This will happen for *every* assignment in the builder... - * Adding this set_parent on init guarantees that the assignment's correct parent is set - * after adding new assignment's to other lessons - * it's awful and it's gross... - * I'm confused and tired and going to miss release dates again because of it - */ - this.model.set_parent( this.lesson ); + this.model = this.lesson.get( 'assignment' ); + + /** + * Todo Item. + * + * @todo this is a terrible terrible patch + * I've spent nearly 3 days trying to figure out how to not use this line of code + * ISSUE REPRODUCTION: + * Open course builder + * Open a lesson (A) and add a assignment + * Switch to a new lesson (B) + * Add a new assignment + * Return to lesson A and the assignment's parent will be set to LESSON B + * This will happen for *every* assignment in the builder... + * Adding this set_parent on init guarantees that the assignment's correct parent is set + * after adding new assignment's to other lessons + * it's awful and it's gross... + * I'm confused and tired and going to miss release dates again because of it + */ + this.model.set_parent( this.lesson ); + + this.listenTo( this.model, 'change:permalink', this.render_settings ); - this.listenTo( this.model, 'change:permalink', this.render_settings ); - - } + } this.on( 'model-trashed', this.on_trashed ); @@ -192,15 +192,15 @@ define( [ lesson_id: this.lesson.get( 'id' ), } ); - this.lesson.set( 'assignment_enabled', 'yes' ); - this.lesson.set( 'assignment', this.model ); + this.lesson.set( 'assignment_enabled', 'yes' ); + this.lesson.set( 'assignment', this.model ); - this.listenTo( this.model, 'change:permalink', this.render_settings ); - this.render(); + this.listenTo( this.model, 'change:permalink', this.render_settings ); + this.render(); - } else { + } else { - this.show_ad_popover( '#llms-new-assignment' ); + this.show_ad_popover( '#llms-new-assignment' ); } @@ -237,12 +237,12 @@ define( [ assignment = window.llms_builder.construct.get_model( 'Assignment', assignment ); - this.lesson.set( 'assignment_enabled', 'yes' ); - this.lesson.set( 'assignment', assignment ); - this.model = assignment; + this.lesson.set( 'assignment_enabled', 'yes' ); + this.lesson.set( 'assignment', assignment ); + this.model = assignment; - this.listenTo( this.model, 'change:permalink', this.render_settings ); - this.render(); + this.listenTo( this.model, 'change:permalink', this.render_settings ); + this.render(); }, From 26678da654e20bf9bc2c5cdd740e287ffe27432b Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Fri, 20 Feb 2026 15:40:50 +0100 Subject: [PATCH 24/27] Fixing double pubsub call. --- assets/js/builder/Views/Elements.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/builder/Views/Elements.js b/assets/js/builder/Views/Elements.js index 8f42084610..ef28c21182 100644 --- a/assets/js/builder/Views/Elements.js +++ b/assets/js/builder/Views/Elements.js @@ -158,7 +158,7 @@ define( [ 'Models/Section', 'Views/Section', 'Models/Lesson', 'Views/Lesson', 'V pop.show(); Backbone.pubSub.once( 'lesson-search-select', function() { - Backbone.pubSub.once( 'lesson-search-select', hidePopover ); + hidePopover(); // @todo For some reason the above doesn't close via pop.hide(). Seems internal to webui-popup. Ref #3097 $( '.webui-popover' ).remove(); From 3e7d46ee924d2f42d42b9f3391562aa3aa017b8e Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Fri, 20 Feb 2026 15:43:21 +0100 Subject: [PATCH 25/27] Reverting spacing. --- includes/admin/class.llms.admin.builder.php | 1 - 1 file changed, 1 deletion(-) diff --git a/includes/admin/class.llms.admin.builder.php b/includes/admin/class.llms.admin.builder.php index f45738c9c0..b123e30892 100644 --- a/includes/admin/class.llms.admin.builder.php +++ b/includes/admin/class.llms.admin.builder.php @@ -575,7 +575,6 @@ public static function output() { global $llms_builder_lazy_load; $llms_builder_lazy_load = true; - ?>
From d490100b9a1ed3acfaaab06ef3df2044e3f19abd Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 9 Apr 2026 05:29:48 -0400 Subject: [PATCH 26/27] Fix DOM accumulation with add existing lesson popup. --- assets/js/builder/Views/Elements.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/assets/js/builder/Views/Elements.js b/assets/js/builder/Views/Elements.js index ef28c21182..71d5bae058 100644 --- a/assets/js/builder/Views/Elements.js +++ b/assets/js/builder/Views/Elements.js @@ -130,11 +130,7 @@ define( [ 'Models/Section', 'Views/Section', 'Models/Lesson', 'Views/Lesson', 'V event.preventDefault(); - var pop, hidePopover; - - hidePopover = function() { - pop.hide(); - }; + var pop, onLessonSelect; pop = new Popover( { el: '#llms-existing-lesson', @@ -151,19 +147,21 @@ define( [ 'Models/Section', 'Views/Section', 'Models/Lesson', 'Views/Lesson', 'V searching_message: LLMS.l10n.translate( 'Search for existing lessons...' ), } ).render().$el, onHide: function() { - Backbone.pubSub.off( 'lesson-search-select', hidePopover ); + Backbone.pubSub.off( 'lesson-search-select', onLessonSelect ); }, } } ); - pop.show(); - Backbone.pubSub.once( 'lesson-search-select', function() { - hidePopover(); + onLessonSelect = function() { + pop.hide(); - // @todo For some reason the above doesn't close via pop.hide(). Seems internal to webui-popup. Ref #3097 + // Ref #3097 — pop.hide() doesn't always remove the DOM elements. $( '.webui-popover' ).remove(); $( '.webui-popover-backdrop' ).remove(); - } ); + }; + + pop.show(); + Backbone.pubSub.once( 'lesson-search-select', onLessonSelect ); }, From c947d1e41b09758bfda36b2ef1b8b9e6643bc7ef Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 9 Apr 2026 05:33:42 -0400 Subject: [PATCH 27/27] Listen to name change for quizzes too. --- assets/js/builder/Views/Quiz.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/js/builder/Views/Quiz.js b/assets/js/builder/Views/Quiz.js index 23ff871297..65255d77dd 100644 --- a/assets/js/builder/Views/Quiz.js +++ b/assets/js/builder/Views/Quiz.js @@ -126,6 +126,7 @@ define( [ this.listenTo( this.model, 'change:_points', this.render_points ); this.listenTo( this.model, 'change:permalink', this.render_settings ); + this.listenTo( this.model, 'change:name', this.render_settings ); } @@ -325,6 +326,7 @@ define( [ this.model = this.lesson.get( 'quiz' ); this.listenTo( this.model, 'change:_points', this.render_points ); this.listenTo( this.model, 'change:permalink', this.render_settings ); + this.listenTo( this.model, 'change:name', this.render_settings ); this.render(); },