PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.8
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.8
4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 All 139 releases
learnpress / inc / TemplateHooks / CourseBuilder / Course / BuilderEditCourseTemplate.php

BuilderEditCourseTemplate.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.4.8, at inc/TemplateHooks/CourseBuilder/Course/BuilderEditCourseTemplate.php

1,429 lines 49.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Template hooks Course Builder.
4 *
5 * @since 4.3.0
6 * @version 1.0.0
7 */
8
9 namespace LearnPress\TemplateHooks\CourseBuilder\Course;
10
11 use Exception;
12 use LearnPress\CourseBuilder\CourseBuilder;
13 use LearnPress\CourseBuilder\CourseBuilderAccessPolicy;
14 use LearnPress\Helpers\Singleton;
15 use LearnPress\Helpers\Template;
16 use LearnPress\Models\CourseModel;
17 use LearnPress\Models\CoursePostModel;
18 use LearnPress\Models\PostModel;
19 use LearnPress\Models\UserModel;
20 use LearnPress\TemplateHooks\Admin\AdminTemplate;
21 use LearnPress\TemplateHooks\Admin\AI\AdminEditCourseCurriculumWithAITemplate;
22 use LearnPress\TemplateHooks\Admin\AI\AdminEditWithAITemplate;
23 use LearnPress\TemplateHooks\Course\AdminEditCurriculumTemplate;
24 use LearnPress\TemplateHooks\CourseBuilder\BuilderPopupTemplate;
25 use LearnPress\TemplateHooks\TemplateAJAX;
26 use LP_Settings;
27 use LP_WP_Filesystem;
28 use Throwable;
29 use WP_User;
30
31 class BuilderEditCourseTemplate {
32 use Singleton;
33
34 public function init() {
35 add_filter( 'lp/rest/ajax/allow_callback', [ $this, 'allow_callback' ] );
36
37 // Register filter for adding edit popup button in Course Builder curriculum
38 add_filter( 'learn-press/admin/curriculum/section-item/actions', [ $this, 'add_edit_popup_button' ], 10, 5 );
39 }
40
41 /**
42 * HTML edit/create course on Course Builder screen
43 *
44 * @param array $data
45 *
46 * @since 4.3.6
47 * @version 1.0.1
48 * @return string
49 */
50 public function layout( array $data = [] ): string {
51 $html = '';
52
53 try {
54 // Check permission
55 $userModel = $data['userModel'] ?? false;
56 if ( ! $userModel || ! $userModel->is_instructor() ) {
57 throw new Exception( __( 'You do not have permission to create or edit courses', 'learnpress' ) );
58 }
59
60 $userCoursePostModel = new CoursePostModel();
61 if ( ! $userCoursePostModel->check_capabilities_create() ) {
62 throw new Exception( __( 'You do not have permission to create or edit courses', 'learnpress' ) );
63 }
64
65 $item_id = $data['item_id'] ?? '';
66 if ( empty( $item_id ) ) {
67 throw new Exception( __( 'Invalid course ID', 'learnpress' ) );
68 }
69
70 /*if ( ! CourseBuilderAccessPolicy::can_access_tab_post( 'courses', $item_id ) ) {
71 throw new Exception( __( "Sorry, you don't have permission to access this content", 'learnpress' ) );
72 }*/
73
74 $is_create_new = $item_id === CourseBuilder::POST_NEW;
75 $courseModel = false;
76
77 if ( ! $is_create_new ) {
78 $courseModel = CourseModel::find( (int) $item_id, true );
79 if ( ! $courseModel ) {
80 throw new Exception( __( 'Course not found', 'learnpress' ) );
81 }
82
83 // Check permission
84 $coursePostModel = $courseModel->get_post_model();
85 if ( ! $coursePostModel->check_capabilities_update() ) {
86 throw new Exception( __( 'You do not have permission to edit this course', 'learnpress' ) );
87 }
88
89 if ( $courseModel->get_status() === PostModel::STATUS_TRASH ) {
90 throw new Exception(
91 __(
92 'You cannot edit this item because it is in the Trash. Please restore it and try again.',
93 'learnpress'
94 )
95 );
96 }
97 }
98
99 $data['courseModel'] = $courseModel;
100
101 $section = [
102 'wrap' => sprintf(
103 '<div class="lp-cb-content" data-post-id="%1$s">',
104 esc_attr( $item_id ),
105 ),
106 'header' => $this->html_header( $data ),
107 'tabs' => $this->html_tabs( $data ),
108 'wrap_end' => '</div>',
109 ];
110
111 $html = Template::combine_components( $section );
112 } catch ( Throwable $e ) {
113 $html = Template::print_message( $e->getMessage(), 'error', false );
114 }
115
116 return $html;
117 }
118
119 /**
120 * HTML header
121 *
122 * @param array $data
123 *
124 * @return string
125 */
126 public function html_header( array $data = [] ): string {
127 /** @var UserModel $userModel */
128 $userModel = $data['userModel'] ?? false;
129 if ( ! $userModel instanceof UserModel ) {
130 return '';
131 }
132
133 /** @var CourseModel|false $courseModel */
134 $courseModel = $data['courseModel'] ?? false;
135 $hide_instructor_access_admin_screen = LP_Settings::is_hide_instructor_access_admin_screen();
136 $more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );
137 $title = $courseModel ? $courseModel->get_title() : __( 'Add New Course', 'learnpress' );
138 $status_badge = $courseModel ? $courseModel->get_status() : '';
139 $status = '';
140 if ( $courseModel ) {
141 $status = $courseModel->get_status();
142 }
143 $main_action_status = in_array(
144 $status,
145 array(
146 'publish',
147 'draft',
148 'pending',
149 'future',
150 'private',
151 ),
152 true
153 ) ? $status : 'publish';
154 $wp_user = new WP_User( $userModel );
155 $hide_wp_edit_link = $hide_instructor_access_admin_screen && user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );
156
157 $section = [
158 'header_wrap' => '<div class="lp-cb-header">',
159 'header_left' => '<div class="lp-cb-header__left">',
160 'title' => sprintf(
161 '<h1 class="lp-cb-header__title">%s</h1>',
162 esc_html( $title )
163 ),
164 'status_badge' => $courseModel ? sprintf(
165 '<span class="course-status %s">%s</span>',
166 $status_badge,
167 esc_html( $courseModel->get_post_model()->get_status_i18n() )
168 ) : '',
169 'link_edit_on_wp' => $courseModel && ! $hide_wp_edit_link ? sprintf(
170 '<a href="%1$s" class="lp-cb-admin-link" target="_blank" title="%2$s"%3$s>
171 <span class="dashicons dashicons-wordpress"></span>
172 <span>%2$s</span>
173 </a>',
174 esc_url( admin_url( "post.php?post={$courseModel->get_id()}&action=edit" ) ),
175 esc_attr__( 'Edit with WordPress', 'learnpress' ),
176 PostModel::STATUS_TRASH === $status ? ' style="display:none"' : ''
177 ) : '',
178 'header_left_end' => '</div>',
179 'header_actions' => '<div class="lp-cb-header__actions">',
180 'preview_btn' => $courseModel && $courseModel->get_status() !== PostModel::STATUS_TRASH ? sprintf(
181 '<a href="%1$s" class="cb-button cb-btn-preview cb-btn-secondary lp-button" target="_blank">%2$s</a>',
182 esc_url( get_permalink( $courseModel->get_id() ) ),
183 esc_html__( 'Preview', 'learnpress' )
184 ) : '',
185 'dropdown_wrap' => '<div class="cb-header-actions-dropdown cb-header-actions-dropdown--single">',
186 'update_btn' => sprintf(
187 '<div class="cb-btn-update cb-btn-primary cb-btn-main-action lp-button"
188 data-status="%1$s"
189 data-title-update="%2$s"
190 data-title-publish="%3$s"
191 data-title-draft="%4$s"
192 data-title-submit-review="%5$s">%6$s</div>',
193 esc_attr( $main_action_status ),
194 esc_attr__( 'Update', 'learnpress' ),
195 esc_attr__( 'Publish', 'learnpress' ),
196 esc_attr__( 'Save Draft', 'learnpress' ),
197 esc_attr__( 'Submit for Review', 'learnpress' ),
198 esc_html__( 'Update', 'learnpress' )
199 ),
200 'dropdown_wrap_end' => '</div>',
201 'expanded_actions' => $courseModel ? sprintf(
202 '<div class="cb-header-action-expanded">
203 <button type="button" class="course-action-expanded lp-button" aria-haspopup="true" aria-expanded="false" aria-label="%1$s">
204 %2$s
205 </button>
206 <div class="cb-header-action-expanded__items">
207 <div class="cb-header-action-expanded__duplicate cb-btn-duplicate-course lp-button"
208 data-title="%3$s"
209 data-content="%4$s">
210 <span class="dashicons dashicons-admin-page"></span>
211 %5$s
212 </div>
213 <div class="cb-header-action-expanded__trash cb-btn-trash lp-button">
214 <span class="dashicons dashicons-trash"></span>
215 %6$s
216 </div>
217 </div>
218 </div>',
219 esc_attr__( 'More actions', 'learnpress' ),
220 $more_actions_icon,
221 esc_attr__( 'Are you sure?', 'learnpress' ),
222 esc_attr__( 'Are you sure you want to duplicate this course?', 'learnpress' ),
223 esc_html__( 'Duplicate', 'learnpress' ),
224 esc_html__( 'Move to Trash', 'learnpress' )
225 ) : '',
226 'header_actions_end' => '</div>',
227 'header_end' => '</div>',
228 ];
229
230 return Template::combine_components( $section );
231 }
232
233 /**
234 * Render tabs
235 *
236 * @param array $data
237 *
238 * @return string
239 */
240 public function html_tabs( array $data = [] ): string {
241 $section_tab = [
242 'tabs_wrap' => '<div class="lp-cb-tabs">',
243 'tabs' => '',
244 'tabs_end' => '</div>',
245 ];
246
247 $section_content = [
248 'wrap' => '<div class="lp-cb-tab-content">',
249 'content' => '',
250 'wrap-end' => '</div>',
251 ];
252
253 $tabs = apply_filters(
254 'learn-press/course-builder/course/edit/tabs',
255 [
256 'overview' => [
257 'title' => esc_html__( 'Overview', 'learnpress' ),
258 'html' => $this->html_tab_overview( $data ),
259 ],
260 'curriculum' => [
261 'title' => esc_html__( 'Curriculum', 'learnpress' ),
262 'html' => $this->html_tab_curriculum( $data ),
263 ],
264 'settings' => [
265 'title' => esc_html__( 'Settings', 'learnpress' ),
266 'html' => $this->html_tab_settings( $data ),
267 ],
268 ],
269 $data
270 );
271
272 $tab_active = array_key_first( $tabs );
273 if ( isset( $_GET['tab'] ) ) {
274 $tab_active = $_GET['tab'];
275 }
276
277 foreach ( $tabs as $key => $tab ) {
278 $is_active = $key === $tab_active;
279
280 $section_tab['tabs'] .= sprintf(
281 '<a href="#" class="lp-cb-tabs__item %s" data-tab-section="%s">%s</a>',
282 $is_active ? 'is-active' : '',
283 esc_attr( $tab['slug'] ?? $key ),
284 esc_html( $tab['title'] ?? '' )
285 );
286
287 /**
288 * @uses html_tab_overview
289 * @uses html_tab_curriculum
290 * @uses html_tab_settings
291 */
292 $section_content['content'] .= sprintf(
293 '<div class="lp-cb-tab-panel %s" data-section="%s">%s</div>',
294 $is_active ? '' : 'lp-hidden',
295 esc_attr( $key ),
296 $tab['html']
297 );
298 }
299
300 $section = [
301 'tabs' => Template::combine_components( $section_tab ),
302 'contents' => Template::combine_components( $section_content ),
303 ];
304
305 return Template::combine_components( $section );
306 }
307
308 public function html_tab_overview( array $data = [] ) {
309 wp_enqueue_script( 'lp-course-builder' );
310
311 $courseModel = $data['courseModel'] ?? null;
312 $course_id = 0;
313 if ( $courseModel instanceof CourseModel ) {
314 $course_id = $courseModel->get_id();
315 }
316
317 $html_edit_title = $this->edit_title( $courseModel );
318 $html_edit_permalink = $this->edit_permalink( $courseModel );
319 $html_edit_features = $this->edit_featured_image( $courseModel );
320 $html_edit_publish = $this->edit_publish( $courseModel );
321 $html_edit_desc = $this->edit_desc( $courseModel );
322 $html_edit_cat = $this->edit_categories( $courseModel );
323 $html_edit_tags = $this->edit_tags( $courseModel );
324
325 $section = [
326 'wrapper' => sprintf( '<div class="cb-section__course-edit" data-course-id="%s">', $course_id ),
327 'content_wrapper' => '<div class="cb-course-edit-content">',
328 // Left column
329 'left_column' => '<div class="cb-course-edit-column cb-course-edit-column--left">',
330 'edit_title' => $html_edit_title,
331 'edit_permalink' => $html_edit_permalink,
332 'edit_publish' => $html_edit_publish,
333 'edit_features' => $html_edit_features,
334 'left_column_end' => '</div>',
335 // Right column
336 'right_column' => '<div class="cb-course-edit-column cb-course-edit-column--right">',
337 'edit_desc' => $html_edit_desc,
338 'edit_term_category' => '<div class="cb-course-edit-terms-categories-wrapper">',
339 'edit_cat' => $html_edit_cat,
340 'edit_term' => $html_edit_tags,
341 'edit_term_category_end' => '</div>',
342 'right_column_end' => '</div>',
343 'content_wrapper_end' => '</div>',
344 'ai_templates' => $this->html_overview_ai_templates(),
345 'wrapper_end' => '</div>',
346 ];
347
348 return Template::combine_components( $section );
349 }
350
351 /**
352 * HTML Curriculum
353 *
354 * @param array $data
355 *
356 * @return string
357 */
358 public function html_tab_curriculum( array $data = [] ): string {
359 wp_enqueue_script( 'lp-cb-edit-curriculum' );
360 wp_enqueue_style( 'lp-cb-edit-curriculum' );
361 wp_enqueue_script( 'lp-cb-admin-learnpress' );
362
363 $course_id = CourseBuilder::get_item_id();
364
365 if ( $course_id === CourseBuilder::POST_NEW ) {
366 return Template::print_message( __( 'Please save Course before add Section', 'learnpress' ), 'info', false );
367 }
368
369 $courseModel = $data['courseModel'] ?? null;
370 if ( ! $courseModel instanceof CourseModel ) {
371 return Template::print_message( __( 'Course is invalid!', 'learnpress' ), 'error', false );
372 }
373
374 return $this->html_curriculum( $courseModel );
375 }
376
377 public function html_tab_settings( array $data = [] ) {
378 wp_enqueue_script( 'lp-cb-edit-curriculum' );
379 wp_enqueue_script( 'lp-tom-select' );
380 wp_enqueue_style( 'lp-cb-edit-curriculum' );
381 wp_enqueue_script( 'lp-cb-learnpress' );
382
383 $course_id = CourseBuilder::get_item_id();
384
385 if ( $course_id === CourseBuilder::POST_NEW ) {
386 return Template::print_message( __( 'Please save Course before setting course', 'learnpress' ), 'info', false );
387 }
388
389 $courseModel = $data['courseModel'] ?? null;
390 if ( ! $courseModel instanceof CourseModel ) {
391 return Template::print_message( __( 'Course is invalid!', 'learnpress' ), 'error', false );
392 }
393
394 if ( ! class_exists( 'LP_Meta_Box_Course' ) ) {
395 require_once LP_PLUGIN_PATH . 'inc/admin/views/meta-boxes/course/settings.php';
396 }
397
398 add_filter( 'learnpress/course/metabox/tabs', [ $this, 'filter_course_builder_settings_tabs' ], 999 );
399 add_filter(
400 'learn-press/course/meta-box/assessment/final-quiz/edit-link',
401 [
402 $this,
403 'filter_course_builder_assessment_final_quiz_edit_link',
404 ],
405 10,
406 2
407 );
408
409 $metabox = \LP_Meta_Box_Course::instance();
410 ob_start();
411 $metabox->output( $courseModel );
412 $settings = ob_get_clean();
413
414 remove_filter( 'learnpress/course/metabox/tabs', [ $this, 'filter_course_builder_settings_tabs' ], 999 );
415 remove_filter(
416 'learn-press/course/meta-box/assessment/final-quiz/edit-link',
417 [
418 $this,
419 'filter_course_builder_assessment_final_quiz_edit_link',
420 ],
421 10
422 );
423
424 $output = [
425 'wrapper' => sprintf( '<div class="cb-section__course-edit" data-course-id="%s">', $course_id ),
426 'form_setting' => '<form name="lp-form-setting-course" class="lp-form-setting-course" method="post" enctype="multipart/form-data">',
427 'settings' => $settings,
428 'form_setting_end' => '</form>',
429 'wrapper_end' => '</div>',
430 ];
431
432 return Template::combine_components( $output );
433 }
434
435 /**
436 * Allow callback for AJAX.
437 * @use self::render_edit_course_curriculum
438 * @use self::render_html
439 *
440 * @param array $callbacks
441 *
442 * @return array
443 */
444 public function allow_callback( array $callbacks ): array {
445 $callbacks[] = AdminEditCurriculumTemplate::class . ':render_edit_course_curriculum';
446
447 return $callbacks;
448 }
449
450 /**
451 * Render Course Builder course title field.
452 *
453 * Uses the raw post title for existing courses so the edit field does not receive
454 * display filters. New courses start with an empty value and still render the
455 * optional AI button when AI settings are enabled.
456 *
457 * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
458 *
459 * @return string HTML markup for the title input, character count, and optional AI button.
460 * @since 4.3.0
461 * @version 1.0.0
462 */
463 public function edit_title( $courseModel ) {
464 $title = '';
465 if ( ! empty( $courseModel ) ) {
466 $post_id = absint( $courseModel->get_id() );
467 $title = $post_id ? (string) get_post_field( 'post_title', $post_id, 'raw' ) : '';
468 }
469 $char_count = mb_strlen( wp_strip_all_tags( $title ) );
470 $ai_button = $this->html_overview_ai_button( '#lp-tmpl-edit-title-ai' );
471 $edit = [
472 'wrapper' => '<div class="cb-course-edit-title">',
473 'label_wrap' => '<div class="cb-course-edit-title__label-wrap">',
474 'label' => sprintf( '<label for="title" class="cb-course-edit-title__label">%s <span class="required">*</span></label>', __( 'Course Title', 'learnpress' ) ),
475 'char_count' => sprintf( '<span class="cb-course-edit-title__char-count">%s</span>', sprintf( __( '%d characters', 'learnpress' ), $char_count ) ),
476 'ai_button' => $ai_button,
477 'label_wrap_end' => '</div>',
478 'input' => sprintf( '<input type="text" name="course_title" size="30" value="%s" id="title" class="cb-course-edit-title__input" placeholder="%s">', esc_attr( $title ), esc_attr__( 'example', 'learnpress' ) ),
479 'wrapper_end' => '</div>',
480 ];
481
482 return Template::combine_components( $edit );
483 }
484
485 /**
486 * Render Course Builder permalink editor.
487 *
488 * Permalink editing is hidden for unsaved courses. Existing courses use
489 * get_sample_permalink() when available to display a publish-style URL for slug
490 * editing, while the preview link keeps the current permalink as its href.
491 *
492 * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
493 *
494 * @return string HTML markup for the permalink display/editor, or an empty string for new courses.
495 * @since 4.3.0
496 * @version 1.0.0
497 */
498 public function edit_permalink( $courseModel ) {
499 $post_id = ! empty( $courseModel ) ? $courseModel->get_id() : '';
500 $post_name = '';
501
502 // Hide permalink for new courses
503 if ( empty( $post_id ) || $post_id === CourseBuilder::POST_NEW ) {
504 return '';
505 }
506
507 if ( $post_id ) {
508 $post = get_post( $post_id );
509 $post_name = $post ? $post->post_name : '';
510 }
511
512 // Get base URL for courses
513 $courses_page_id = learn_press_get_page_id( 'courses' );
514 $base_url = '';
515 if ( $courses_page_id ) {
516 $base_url = trailingslashit( get_permalink( $courses_page_id ) );
517 } else {
518 $base_url = trailingslashit( home_url() ) . 'courses/';
519 }
520
521 $full_url = urldecode( (string) get_permalink( $post_id ) );
522 $display_data = $this->get_course_display_permalink_data( (int) $post_id, (string) $post_name );
523 $display_url = (string) ( $display_data['url'] ?? '' );
524 $editor_slug = (string) ( $display_data['slug'] ?? '' );
525
526 if ( empty( $display_url ) ) {
527 $display_url = $full_url;
528 }
529
530 if ( empty( $full_url ) ) {
531 $full_url = $display_url;
532 }
533
534 if ( empty( $editor_slug ) ) {
535 $editor_slug = (string) $post_name;
536 }
537
538 if ( empty( $editor_slug ) && ! empty( $display_url ) ) {
539 $display_path = parse_url( $display_url, PHP_URL_PATH );
540 if ( is_string( $display_path ) && '' !== $display_path ) {
541 $editor_slug = basename( untrailingslashit( $display_path ) );
542 }
543 }
544
545 // Use publish-style permalink as editable base, but keep href as current permalink.
546 if (
547 ! empty( $editor_slug ) &&
548 false === strpos( $display_url, '?p=' ) &&
549 false === strpos( $display_url, '&p=' ) &&
550 false === strpos( $display_url, '?lp_course=' ) &&
551 false === strpos( $display_url, '&lp_course=' )
552 ) {
553 $base_url = trailingslashit( preg_replace( '/' . preg_quote( $editor_slug, '/' ) . '\/?$/', '', $display_url ) );
554 }
555
556 $state_a = sprintf(
557 '<span class="cb-permalink-label">%s</span>
558 <div class="cb-permalink-display">
559 <a href="%s" target="_blank" class="cb-permalink-url">%s</a>
560 <button type="button" class="cb-permalink-edit-btn" title="%s">
561 <span class="dashicons dashicons-edit"></span>
562 </button>
563 </div>',
564 __( 'Permalink', 'learnpress' ),
565 esc_url( $full_url ),
566 esc_html( $display_url ),
567 __( 'Edit', 'learnpress' )
568 );
569
570 $state_b = sprintf(
571 '<div class="cb-permalink-editor lp-hidden">
572 <span class="cb-permalink-prefix">%s</span>
573 <div class="cb-permalink-input-row">
574 <input type="text" name="course_permalink" id="course_permalink" value="%s" class="cb-permalink-slug-input" placeholder="%s">
575 <div class="cb-permalink-actions">
576 <button type="button" class="cb-permalink-ok-btn">%s</button>
577 <button type="button" class="cb-permalink-cancel-btn">%s</button>
578 </div>
579 </div>
580 </div>',
581 esc_html( $base_url ),
582 esc_attr( $editor_slug ),
583 esc_attr__( 'your-slug', 'learnpress' ),
584 __( 'OK', 'learnpress' ),
585 __( 'Cancel', 'learnpress' )
586 );
587
588 $hidden_base = sprintf(
589 '<input type="hidden" id="cb-permalink-base-url" value="%s">',
590 esc_attr( $base_url )
591 );
592
593 $edit = [
594 'wrapper' => '<div class="cb-course-edit-permalink">',
595 'state_a' => $state_a,
596 'state_b' => $state_b,
597 'hidden_base' => $hidden_base,
598 'wrapper_end' => '</div>',
599 ];
600
601 return Template::combine_components( $edit );
602 }
603
604 /**
605 * Get permalink display URL in publish-style format for editing slug.
606 *
607 * @param int $post_id
608 * @param string $post_name
609 *
610 * @return array<string, string>
611 */
612 private function get_course_display_permalink_data( int $post_id, string $post_name = '' ): array {
613 $display_url = urldecode( (string) get_permalink( $post_id ) );
614 $sample_slug = $post_name;
615
616 if ( ! function_exists( 'get_sample_permalink' ) ) {
617 require_once ABSPATH . 'wp-admin/includes/post.php';
618 }
619
620 if ( function_exists( 'get_sample_permalink' ) ) {
621 $sample_permalink = get_sample_permalink( $post_id );
622 if ( is_array( $sample_permalink ) && ! empty( $sample_permalink[0] ) ) {
623 $sample_slug = ! empty( $sample_permalink[1] ) ? (string) $sample_permalink[1] : $sample_slug;
624 $display_url = str_replace(
625 [ '%postname%', '%pagename%' ],
626 $sample_slug,
627 (string) $sample_permalink[0]
628 );
629 }
630 }
631
632 $post = get_post( $post_id );
633 if ( $post instanceof \WP_Post ) {
634 $display_url = \LP_Helper::handle_lp_permalink_structure( $display_url, $post );
635 }
636
637 return [
638 'url' => urldecode( $display_url ),
639 'slug' => urldecode( (string) $sample_slug ),
640 ];
641 }
642
643 /**
644 * Render Course Builder description editor.
645 *
646 * Uses the current course post content when editing an existing CourseModel.
647 * For new courses, the editor starts with an empty value. AI button rendering
648 * is delegated to html_overview_ai_button().
649 *
650 * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
651 *
652 * @return string HTML markup for the course description editor.
653 */
654 public function edit_desc( $courseModel ) {
655 $desc = $courseModel ? $courseModel->post_content : '';
656
657 $editor_id = 'course_description_editor';
658 $ai_button = $this->html_overview_ai_button( '#lp-tmpl-edit-description-ai' );
659 $editor_settings = array(
660 'textarea_name' => 'course_description',
661 'textarea_rows' => 10,
662 'teeny' => false,
663 'media_buttons' => true,
664 'tinymce' => array(
665 'content_style' => "body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; font-size: 14px; line-height: 1.6; color: #1e1e1e; }",
666 'toolbar1' => 'formatselect,bold,italic,underline,bullist,numlist,blockquote,alignleft,aligncenter,alignright,link,unlink,spellchecker,wp_adv',
667 'toolbar2' => 'strikethrough,hr,forecolor,pastetext,removeformat,charmap,outdent,indent,undo,redo,wp_help',
668 ),
669 'quicktags' => true,
670 );
671
672 $edit = [
673 'wrapper' => '<div class="cb-course-edit-desc">',
674 'label_wrap' => '<div class="cb-course-edit-desc__label-wrap">',
675 'label' => sprintf(
676 '<label for="course_description" class="cb-course-edit-desc__label">%s</label>',
677 __( 'Description', 'learnpress' )
678 ),
679 'ai_button' => $ai_button,
680 'label_wrap_end' => '</div>',
681 'edit' => AdminTemplate::editor_tinymce(
682 $desc,
683 $editor_id,
684 $editor_settings
685 ),
686 'wrapper_end' => '</div>',
687 ];
688
689 return Template::combine_components( $edit );
690 }
691
692 /**
693 * Check if Overview AI button and templates should be shown.
694 *
695 * @return bool
696 */
697 protected function can_show_overview_ai_button(): bool {
698 return \LP_Settings::get_option( 'enable_open_ai', 'no' ) === 'yes'
699 && ! empty( \LP_Settings::get_option( 'open_ai_secret_key', '' ) );
700 }
701
702 /**
703 * Render icon-only AI button in Course Builder overview.
704 *
705 * @param string $template_id
706 *
707 * @return string
708 */
709 protected function html_overview_ai_button( string $template_id ): string {
710 if ( ! $this->can_show_overview_ai_button() ) {
711 return '';
712 }
713
714 $button_label = esc_html__( 'Generate with AI', 'learnpress' );
715
716 return sprintf(
717 '<button type="button" class="cb-course-edit-ai-btn lp-btn-generate-with-ai" data-template="%1$s" title="%2$s" aria-label="%2$s"><i class="lp-ico-ai" aria-hidden="true"></i></button>',
718 esc_attr( $template_id ),
719 esc_attr( $button_label )
720 );
721 }
722
723 /**
724 * Render AI popup templates for Course Builder overview edit page.
725 *
726 * @return string
727 */
728 protected function html_overview_ai_templates(): string {
729 if ( ! $this->can_show_overview_ai_button() ) {
730 return '';
731 }
732
733 try {
734 return AdminEditWithAITemplate::instance()->render_for_frontend( [ 'title', 'description', 'image' ] );
735 } catch ( Throwable $e ) {
736 error_log( __METHOD__ . ': ' . $e->getMessage() );
737 }
738
739 return '';
740 }
741
742 /**
743 * Render AI popup template for curriculum edit in Course Builder.
744 *
745 * @return string
746 */
747 protected function html_curriculum_ai_templates(): string {
748 if ( ! $this->can_show_overview_ai_button() ) {
749 return '';
750 }
751
752 try {
753 return AdminEditCourseCurriculumWithAITemplate::instance()->render_for_frontend();
754 } catch ( Throwable $e ) {
755 error_log( __METHOD__ . ': ' . $e->getMessage() );
756 }
757
758 return '';
759 }
760
761 /**
762 * Render Course Builder course category selector.
763 *
764 * Reuses the WordPress post_categories_meta_box() output for course_category,
765 * then adds Course Builder search and "add new category" controls. During render,
766 * checked_ontop is forced off so the checklist keeps taxonomy order.
767 *
768 * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
769 *
770 * @return string HTML markup for the course category checklist, search toolbar, and add-new form.
771 * @since 4.3.0
772 * @version 1.0.0
773 */
774 public function edit_categories( $courseModel ) {
775 if ( ! function_exists( 'post_categories_meta_box' ) ) {
776 require_once ABSPATH . 'wp-admin/includes/meta-boxes.php';
777 }
778 if ( ! function_exists( 'wp_popular_terms_checklist' ) ) {
779 require_once ABSPATH . 'wp-admin/includes/template.php';
780 }
781
782 $post_id = ! empty( $courseModel ) ? $courseModel->get_id() : get_the_ID();
783 $post = get_post( $post_id );
784
785 $force_checked_ontop_false = function ( $args ) {
786 if ( isset( $args['taxonomy'] ) && 'course_category' === $args['taxonomy'] ) {
787 $args['checked_ontop'] = false;
788 }
789
790 return $args;
791 };
792
793 ob_start();
794
795 add_filter( 'wp_terms_checklist_args', $force_checked_ontop_false );
796
797 if ( function_exists( 'post_categories_meta_box' ) ) {
798 \post_categories_meta_box(
799 $post,
800 array(
801 'id' => 'course_categorydiv',
802 'title' => __( 'Categories', 'learnpress' ),
803 'callback' => 'post_categories_meta_box',
804 'args' => array(
805 'taxonomy' => 'course_category',
806 'checked_ontop' => false,
807 ),
808 )
809 );
810 }
811
812 remove_filter( 'wp_terms_checklist_args', $force_checked_ontop_false );
813 $html_meta_box = ob_get_clean();
814
815 // Build add new category form (between header and content)
816 $parent_terms = get_terms(
817 [
818 'taxonomy' => 'course_category',
819 'hide_empty' => false,
820 ]
821 );
822 $parent_options = sprintf( '<option value="0">— %s —</option>', __( 'Parent Category', 'learnpress' ) );
823 if ( ! empty( $parent_terms ) && ! is_wp_error( $parent_terms ) ) {
824 foreach ( $parent_terms as $term ) {
825 $parent_options .= sprintf(
826 '<option value="%d">%s</option>',
827 $term->term_id,
828 esc_html( $term->name )
829 );
830 }
831 }
832
833 $form_add_category = sprintf(
834 '<div class="cb-course-edit-terms__form-add-category" style="display:none;">
835 <input type="text" class="cb-course-edit-category__input" placeholder="%s" id="cb-newcourse_category" />
836 <select class="cb-course-edit-category__select-parent" id="cb-newcourse_category_parent">%s</select>
837 <button type="button" class="cb-course-edit-category__btn-cancel">%s</button>
838 <button type="button" class="cb-course-edit-category__btn-save" id="cb-course_category-add-submit">%s</button>
839 </div>',
840 esc_attr__( 'Enter Category Name', 'learnpress' ),
841 $parent_options,
842 esc_html__( 'Cancel', 'learnpress' ),
843 esc_html__( 'Add', 'learnpress' ),
844 );
845
846 $edit = [
847 'wrapper' => '<div class="cb-course-edit-categories__wrapper">',
848 'header' => '<div class="cb-terms-header">',
849 'label_wrap' => '<div class="cb-terms-header__label-wrap">',
850 'label' => sprintf( '<label class="cb-terms-header__label">%s</label>', __( 'Categories', 'learnpress' ) ),
851 'btn_search' => sprintf(
852 '<button type="button" class="cb-terms-header__btn-search" data-toggle-target="#cb-course-edit-categories-search-toolbar" aria-expanded="false" aria-label="%s">
853 <i class="lp-icon-search"></i>
854 </button>',
855 esc_attr__( 'Search categories', 'learnpress' )
856 ),
857 'label_wrap_end' => '</div>',
858 'btn_add_new' => sprintf( '<button class="cb-course-edit-category__btn-add-new cb-terms-header__btn-add-new">%s</button>', __( 'Add New', 'learnpress' ) ),
859 'header_end' => '</div>',
860 'form_add_category' => $form_add_category,
861 'search' => sprintf(
862 '<div class="cb-course-edit-categories__toolbar cb-terms-search-toolbar" id="cb-course-edit-categories-search-toolbar">
863 <label class="cb-course-edit-categories__search-wrap">
864 <span class="screen-reader-text">%1$s</span>
865 <input type="search" class="cb-course-edit-category__search-input" placeholder="%2$s" />
866 </label>
867 </div>',
868 esc_html__( 'Search categories', 'learnpress' ),
869 esc_attr__( 'Search categories', 'learnpress' )
870 ),
871 'content' => $html_meta_box,
872 'wrapper_end' => '</div>',
873 ];
874
875 return Template::combine_components( $edit );
876 }
877
878 /**
879 * Render Course Builder course tag selector.
880 *
881 * Builds tag chips from all course tags, placing selected tags before available
882 * tags so the current course state is visible first. The returned markup includes
883 * search, empty-state text, and add-new controls consumed by the builder runtime.
884 *
885 * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
886 *
887 * @return string HTML markup for the selected/available tag chips, search toolbar, and add-new form.
888 * @since 4.3.0
889 * @version 1.0.0
890 */
891 public function edit_tags( $courseModel ) {
892 $course_terms = ! empty( $courseModel ) ? $courseModel->get_tags() : [];
893 $tags = get_terms(
894 [
895 'taxonomy' => LP_COURSE_TAXONOMY_TAG,
896 'hide_empty' => false,
897 ]
898 );
899
900 $selected_tag_ids = array_map(
901 function ( $term ) {
902 return (int) $term->term_id;
903 },
904 $course_terms
905 );
906
907 $html_selected_chips = '';
908 $html_available_chips = '';
909
910 if ( ! empty( $tags ) && ! is_wp_error( $tags ) ) {
911 foreach ( $tags as $tag ) {
912 $tag_id = $tag->term_id;
913 $tag_name = $tag->name;
914 $tag_count = $tag->count;
915 $is_checked = in_array( (int) $tag_id, $selected_tag_ids, true );
916 $html_chip = $this->input_checkbox_tag_item( $tag_id, $tag_name, $is_checked, $tag_count );
917
918 if ( $is_checked ) {
919 $html_selected_chips .= $html_chip;
920 } else {
921 $html_available_chips .= $html_chip;
922 }
923 }
924 }
925
926 $html_chips = $html_selected_chips . $html_available_chips;
927 $count_all = substr_count( $html_chips, 'class="cb-tag-chip"' );
928 $empty_default = esc_html__( 'No tags found.', 'learnpress' );
929 $empty_search = esc_html__( 'No matching tags.', 'learnpress' );
930
931 $toolbar = sprintf(
932 '<div class="cb-course-edit-tags__toolbar cb-terms-search-toolbar" id="cb-course-edit-tags-search-toolbar">
933 <label class="cb-course-edit-tags__search-wrap">
934 <span class="screen-reader-text">%1$s</span>
935 <input type="search" class="cb-course-edit-tags__search-input" placeholder="%2$s" />
936 </label>
937 </div>',
938 esc_html__( 'Search tags', 'learnpress' ),
939 esc_attr__( 'Search tags', 'learnpress' )
940 );
941
942 $edit = [
943 'wrapper' => '<div class="cb-course-edit-tags__wrapper">',
944 'header' => '<div class="cb-terms-header">',
945 'label_wrap' => '<div class="cb-terms-header__label-wrap">',
946 'label' => sprintf( '<label class="cb-terms-header__label">%s</label>', __( 'Tags', 'learnpress' ) ),
947 'btn_search' => sprintf(
948 '<button type="button" class="cb-terms-header__btn-search" data-toggle-target="#cb-course-edit-tags-search-toolbar" aria-expanded="false" aria-label="%s">
949 <i class="lp-icon-search"></i>
950 </button>',
951 esc_attr__( 'Search tags', 'learnpress' )
952 ),
953 'label_wrap_end' => '</div>',
954 'btn_add_new' => sprintf( '<button class="cb-course-edit-tag__btn-add-new cb-terms-header__btn-add-new">%s</button>', __( 'Add New', 'learnpress' ) ),
955 'header_end' => '</div>',
956 'form_add_tag_wrapper' => '<div class="cb-course-edit-terms__form-add-tag" style="display:none;">',
957 'input' => '<input type="text" class="cb-course-edit-tags__input" placeholder="' . esc_attr__( 'Enter Tag Name', 'learnpress' ) . '"/>',
958 'btn_cancel' => sprintf( '<button type="button" class="cb-course-edit-tag__btn-cancel">%s</button>', __( 'Cancel', 'learnpress' ) ),
959 'button' => '<button type="button" class="cb-course-edit-tags__btn-save">' . esc_html__( 'Add', 'learnpress' ) . '</button>',
960 'form_add_tag_wrapper_end' => '</div>',
961 'toolbar' => $toolbar,
962 'wrapper_checkbox' => '<div class="cb-course-edit-tags__checkbox-wrapper">',
963 'checkbox' => $html_chips,
964 'wrapper_checkbox_end' => '</div>',
965 'empty' => sprintf(
966 '<p class="cb-course-edit-tags__empty%1$s" data-empty-default="%2$s" data-empty-search="%3$s">%2$s</p>',
967 $count_all > 0 ? ' lp-hidden' : '',
968 esc_attr( $empty_default ),
969 esc_attr( $empty_search )
970 ),
971 'wrapper_end' => '</div>',
972 ];
973
974 return Template::combine_components( $edit );
975 }
976
977 public function input_checkbox_tag_item( $term_id, $term_name, $is_checked, $count = 0 ) {
978 if ( 0 === $count ) {
979 $tag_obj = get_term( $term_id, LP_COURSE_TAXONOMY_TAG );
980 if ( $tag_obj && ! is_wp_error( $tag_obj ) ) {
981 $count = $tag_obj->count;
982 }
983 }
984
985 $tag_name_search = wp_strip_all_tags( $term_name );
986 if ( function_exists( 'mb_strtolower' ) ) {
987 $tag_name_search = mb_strtolower( $tag_name_search );
988 } else {
989 $tag_name_search = strtolower( $tag_name_search );
990 }
991
992 $html = sprintf(
993 '<div class="cb-tag-chip" data-tag-name="%s" data-term-id="%d">',
994 esc_attr( $tag_name_search ),
995 (int) $term_id
996 );
997 $html .= sprintf(
998 '<input type="checkbox" name="course_tags[]" value="%s" id="course_tag_%s" %s>',
999 $term_id,
1000 $term_id,
1001 checked( $is_checked, true, false )
1002 );
1003 $html .= sprintf(
1004 '<label for="course_tag_%s"><span class="cb-tag-chip__name">%s</span><span class="cb-tag-chip__count">(%d)</span><span class="cb-tag-chip__remove">&times;</span></label>',
1005 $term_id,
1006 esc_html( $term_name ),
1007 $count
1008 );
1009 $html .= '</div>';
1010
1011 return $html;
1012 }
1013
1014 /**
1015 * Render Course Builder featured image uploader.
1016 *
1017 * Existing courses render the current thumbnail preview with replace/remove
1018 * controls. Courses without a thumbnail render the upload dropzone and hidden
1019 * course_thumbnail_id field used by the builder save flow.
1020 *
1021 * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
1022 *
1023 * @return string HTML markup for the featured image dropzone, thumbnail hidden field, and optional AI button.
1024 * @since 4.3.0
1025 * @version 1.0.0
1026 */
1027 public function edit_featured_image( $courseModel ) {
1028 $post_id = ! empty( $courseModel ) ? $courseModel->get_id() : '';
1029 $ai_button = $this->html_overview_ai_button( '#lp-tmpl-edit-image-ai' );
1030
1031 $thumbnail_id = ! empty( $post_id ) ? get_post_thumbnail_id( $post_id ) : '';
1032 $thumbnail_url = '';
1033 $thumbnail_alt = '';
1034
1035 if ( $thumbnail_id ) {
1036 $thumbnail_url = wp_get_attachment_image_url( $thumbnail_id, 'medium' );
1037 $thumbnail_alt = get_post_meta( $thumbnail_id, '_wp_attachment_image_alt', true );
1038 }
1039
1040 $has_image = ! empty( $thumbnail_url );
1041
1042 $featured_image_html = '<div class="cb-featured-image-container">';
1043
1044 // Upload area
1045 $featured_image_html .= sprintf(
1046 '<div class="cb-featured-image-dropzone %s" data-post-id="%s">',
1047 $has_image ? 'has-image' : '',
1048 esc_attr( $post_id )
1049 );
1050
1051 if ( $has_image ) {
1052 $featured_image_html .= sprintf(
1053 '<img src="%s" alt="%s" class="cb-featured-image-preview__img">',
1054 esc_url( $thumbnail_url ),
1055 esc_attr( $thumbnail_alt )
1056 );
1057 } else {
1058 $featured_image_html .= '<div class="cb-featured-image-upload-content">';
1059 $featured_image_html .= '<span class="cb-featured-image-icon"><span class="cb-featured-image-icon__image" aria-hidden="true"></span></span>';
1060 $featured_image_html .= sprintf(
1061 '<p class="cb-featured-image-text"><a href="#" class="cb-featured-image-link">%s</a></p>',
1062 __( 'Click to upload', 'learnpress' )
1063 );
1064 $featured_image_html .= sprintf(
1065 '<p class="cb-featured-image-hint">%s</p>',
1066 __( 'JPG, JPEG, PNG less than 1MB', 'learnpress' )
1067 );
1068 $featured_image_html .= '</div>';
1069 }
1070
1071 $featured_image_html .= '</div>';
1072
1073 $featured_image_html .= sprintf(
1074 '<input type="hidden" name="course_thumbnail_id" id="course_thumbnail_id" value="%s">',
1075 esc_attr( $thumbnail_id )
1076 );
1077
1078 // Action buttons wrapper
1079 $featured_image_html .= '<div class="cb-featured-image-actions">';
1080
1081 // Remove button (only show when has image)
1082 if ( $has_image ) {
1083 $featured_image_html .= sprintf(
1084 '<button type="button" class="cb-remove-featured-image">%s</button>',
1085 '<span class="cb-remove-featured-image__icon" aria-hidden="true"></span>'
1086 );
1087
1088 $featured_image_html .= sprintf(
1089 '<button type="button" class="cb-change-featured-image">%s</button>',
1090 __( 'Replace', 'learnpress' )
1091 );
1092 }
1093
1094 $featured_image_html .= '</div>'; // End actions wrapper
1095 $featured_image_html .= '</div>'; // End container
1096
1097 $edit = [
1098 'wrapper' => '<div class="cb-course-edit-featured-image">',
1099 'label_wrap' => '<div class="cb-course-edit-featured-image__label-wrap">',
1100 'label' => sprintf(
1101 '<label class="cb-course-edit-featured-image__title">%s</label>',
1102 __( 'Featured Image', 'learnpress' )
1103 ),
1104 'ai_button' => $ai_button,
1105 'label_wrap_end' => '</div>',
1106 'edit' => $featured_image_html,
1107 'wrapper_end' => '</div>',
1108 ];
1109
1110 return Template::combine_components( $edit );
1111 }
1112
1113 /**
1114 * Render publish panel (status, visibility, publish date, danger zone) for Course Builder overview.
1115 *
1116 * @param CourseModel|false|string|null $courseModel
1117 *
1118 * @return string
1119 */
1120 public function edit_publish( $courseModel ): string {
1121 $post_id = ! empty( $courseModel ) ? absint( $courseModel->get_id() ) : 0;
1122 $post = $post_id ? get_post( $post_id ) : null;
1123
1124 $current_status = 'draft';
1125 if ( $post && ! empty( $post->post_status ) ) {
1126 $current_status = sanitize_key( $post->post_status );
1127 }
1128
1129 $status_for_select = in_array( $current_status, [ 'publish', 'draft', 'pending', 'future' ], true )
1130 ? $current_status
1131 : 'publish';
1132 $current_password = $post ? (string) ( $post->post_password ?? '' ) : '';
1133 $visibility = 'private' === $current_status
1134 ? 'private'
1135 : ( ! empty( $current_password ) ? 'password' : 'public' );
1136 $published_on = '';
1137
1138 if ( $post && ! empty( $post->post_date ) && '0000-00-00 00:00:00' !== $post->post_date ) {
1139 $published_on = wp_date( 'Y-m-d\TH:i', strtotime( $post->post_date ), wp_timezone() );
1140 }
1141
1142 $has_future_publish_date = false;
1143 if ( $post && ! empty( $post->post_date ) && '0000-00-00 00:00:00' !== $post->post_date ) {
1144 $has_future_publish_date = strtotime( $post->post_date ) > current_time( 'timestamp' );
1145 }
1146
1147 $is_scheduled_status = 'future' === $status_for_select
1148 || ( in_array( $status_for_select, [ 'draft', 'pending' ], true ) && $has_future_publish_date );
1149 $primary_status_value = $is_scheduled_status ? 'future' : 'publish';
1150 $primary_status_label = $is_scheduled_status
1151 ? __( 'Scheduled', 'learnpress' )
1152 : __( 'Published', 'learnpress' );
1153 $selected_status_for_ui = in_array( $status_for_select, [ 'draft', 'pending' ], true )
1154 ? $status_for_select
1155 : $primary_status_value;
1156 $status_options = [
1157 $primary_status_value => $primary_status_label,
1158 'draft' => __( 'Draft', 'learnpress' ),
1159 'pending' => __( 'Pending Review', 'learnpress' ),
1160 ];
1161
1162 $publish_date_label = $has_future_publish_date
1163 ? __( 'Scheduled for', 'learnpress' )
1164 : __( 'Published on', 'learnpress' );
1165
1166 $status_options_html = '';
1167 foreach ( $status_options as $value => $label ) {
1168 $status_options_html .= sprintf(
1169 '<option value="%1$s" %2$s>%3$s</option>',
1170 esc_attr( $value ),
1171 selected( $selected_status_for_ui, $value, false ),
1172 esc_html( $label )
1173 );
1174 }
1175
1176 $visibility_options_html = sprintf(
1177 '<option value="public" %1$s>%2$s</option><option value="private" %3$s>%4$s</option><option value="password" %5$s>%6$s</option>',
1178 selected( $visibility, 'public', false ),
1179 esc_html__( 'Public', 'learnpress' ),
1180 selected( $visibility, 'private', false ),
1181 esc_html__( 'Private', 'learnpress' ),
1182 selected( $visibility, 'password', false ),
1183 esc_html__( 'Password protected', 'learnpress' )
1184 );
1185
1186 $edit = [
1187 'wrapper' => '<div class="cb-course-edit-publish">',
1188 'title' => sprintf( '<h3 class="cb-course-edit-publish__title">%s</h3>', esc_html__( 'Publish', 'learnpress' ) ),
1189 'status_row' => sprintf(
1190 '<div class="cb-course-edit-publish__row">
1191 <label for="cb-course-publish-status" class="cb-course-edit-publish__label">%1$s</label>
1192 <select id="cb-course-publish-status" name="cb_course_publish_status" class="cb-course-edit-publish__control" data-publish-label="%3$s" data-future-label="%4$s" data-primary-status="%5$s">%2$s</select>
1193 </div>',
1194 esc_html__( 'Status', 'learnpress' ),
1195 $status_options_html,
1196 esc_attr__( 'Published', 'learnpress' ),
1197 esc_attr__( 'Scheduled', 'learnpress' ),
1198 esc_attr( $primary_status_value )
1199 ),
1200 'visibility_row' => sprintf(
1201 '<div class="cb-course-edit-publish__row">
1202 <label for="cb-course-publish-visibility" class="cb-course-edit-publish__label">%1$s</label>
1203 <select id="cb-course-publish-visibility" name="cb_course_publish_visibility" class="cb-course-edit-publish__control">%2$s</select>
1204 </div>',
1205 esc_html__( 'Visibility', 'learnpress' ),
1206 $visibility_options_html
1207 ),
1208 'password_row' => sprintf(
1209 '<div class="cb-course-edit-publish__row cb-course-edit-publish__password-row %1$s">
1210 <label for="cb-course-publish-password" class="cb-course-edit-publish__label">%2$s</label>
1211 <input type="text" id="cb-course-publish-password" name="cb_course_publish_password" class="cb-course-edit-publish__control" value="%3$s" autocomplete="new-password">
1212 </div>',
1213 'password' === $visibility ? '' : 'lp-hidden',
1214 esc_html__( 'Password', 'learnpress' ),
1215 esc_attr( $current_password )
1216 ),
1217 'published_on_row' => sprintf(
1218 '<div class="cb-course-edit-publish__row">
1219 <label for="cb-course-publish-date" id="cb-course-publish-date-label" class="cb-course-edit-publish__label">%1$s</label>
1220 <input type="datetime-local" id="cb-course-publish-date" name="cb_course_publish_date" class="cb-course-edit-publish__control" value="%2$s">
1221 </div>',
1222 esc_html( $publish_date_label ),
1223 esc_attr( $published_on )
1224 ),
1225 'wrapper_end' => '</div>',
1226 ];
1227
1228 return Template::combine_components( $edit );
1229 }
1230
1231 /**
1232 * Render Course Builder curriculum editor.
1233 *
1234 * Delegates the main curriculum layout to AdminEditCurriculumTemplate with the
1235 * is_course_builder context flag, then appends popup templates and optional AI
1236 * curriculum templates needed by the builder UI.
1237 *
1238 * @param CourseModel $courseModel Course model used to render curriculum sections and popup template IDs.
1239 *
1240 * @return string HTML markup for the curriculum editor plus Course Builder popup and AI templates.
1241 * @since 4.3.0
1242 * @version 1.0.0
1243 */
1244 protected function html_curriculum( CourseModel $courseModel ): string {
1245 ob_start();
1246 AdminEditCurriculumTemplate::instance()->edit_course_curriculum_layout(
1247 $courseModel,
1248 [ 'is_course_builder' => true ]
1249 );
1250 $html_curriculum = ob_get_clean();
1251
1252 return $html_curriculum
1253 . $this->html_curriculum_popup_templates( $courseModel )
1254 . $this->html_curriculum_ai_templates();
1255 }
1256
1257 protected function html_curriculum_popup_templates( CourseModel $courseModel ): string {
1258 $popup_templates = apply_filters(
1259 'learn-press/course-builder/courses/curriculum/popup-templates',
1260 [
1261 'lesson' => sprintf(
1262 '<script type="text/template" id="%1$s"><div class="lp-builder-popup-overlay"></div><div class="lp-builder-popup lp-builder-popup--loading">%2$s</div></script>',
1263 esc_attr(
1264 sprintf(
1265 'lp-tmpl-builder-popup-curriculum-lesson-course-%d',
1266 $courseModel->get_id()
1267 )
1268 ),
1269 TemplateAJAX::load_content_via_ajax(
1270 [
1271 'id_url' => 'builder-popup-lesson-curriculum',
1272 'lesson_id' => 0,
1273 'html_no_load_ajax_first' => sprintf(
1274 '<div class="lp-builder-popup__loader"><div class="lp-loading-circle"></div><span>%s</span></div>',
1275 esc_html__( 'Loading...', 'learnpress' )
1276 ),
1277 ],
1278 [
1279 'class' => BuilderPopupTemplate::class,
1280 'method' => 'render_lesson_popup',
1281 ]
1282 )
1283 ),
1284 'quiz' => sprintf(
1285 '<script type="text/template" id="%1$s"><div class="lp-builder-popup-overlay"></div><div class="lp-builder-popup lp-builder-popup--loading">%2$s</div></script>',
1286 esc_attr(
1287 sprintf(
1288 'lp-tmpl-builder-popup-curriculum-quiz-course-%d',
1289 $courseModel->get_id()
1290 )
1291 ),
1292 TemplateAJAX::load_content_via_ajax(
1293 [
1294 'id_url' => 'builder-popup-quiz-curriculum',
1295 'quiz_id' => 0,
1296 'html_no_load_ajax_first' => sprintf(
1297 '<div class="lp-builder-popup__loader"><div class="lp-loading-circle"></div><span>%s</span></div>',
1298 esc_html__( 'Loading...', 'learnpress' )
1299 ),
1300 ],
1301 [
1302 'class' => BuilderPopupTemplate::class,
1303 'method' => 'render_quiz_popup',
1304 ]
1305 )
1306 ),
1307 ],
1308 $courseModel
1309 );
1310
1311 return Template::combine_components( $popup_templates );
1312 }
1313
1314
1315 /**
1316 * Add edit popup button for lesson and quiz items in Course Builder curriculum.
1317 * Replace the default edit button with popup button for lesson and quiz items.
1318 *
1319 * @param array $section_action Array of action buttons.
1320 * @param object|null $item Item data.
1321 * @param PostModel|null $itemModel Item model.
1322 * @param CourseModel $courseModel .
1323 * @param array $context_data Context data passed from AJAX.
1324 *
1325 * @return array
1326 * @since 4.3.0
1327 * @version 1.0.2
1328 *
1329 */
1330 public function add_edit_popup_button( array $section_action, $item, $itemModel, $courseModel, $context_data = [] ): array {
1331 // Check if we are in Course Builder context via the flag passed in AJAX args
1332 $is_course_builder = ! empty( $context_data['is_course_builder'] );
1333
1334 if ( ! $is_course_builder ) {
1335 return $section_action;
1336 }
1337
1338 $item_id = $item->item_id ?? 0;
1339 $item_type = $item->item_type ?? '';
1340
1341 $popup_data_attr = '';
1342 $popup_type = '';
1343 if ( $item_type === LP_LESSON_CPT ) {
1344 $popup_data_attr = sprintf( 'data-popup-lesson="%s"', $item_id );
1345 $popup_type = 'lesson';
1346 } elseif ( $item_type === LP_QUIZ_CPT ) {
1347 $popup_data_attr = sprintf( 'data-popup-quiz="%s"', $item_id );
1348 $popup_type = 'quiz';
1349 }
1350
1351 $popup_type = sanitize_key(
1352 (string) apply_filters(
1353 'learn-press/course-builder/courses/curriculum/popup-item-type',
1354 $popup_type,
1355 $item_type,
1356 $item,
1357 $itemModel,
1358 $courseModel,
1359 $context_data
1360 )
1361 );
1362 if ( empty( $popup_type ) ) {
1363 return $section_action;
1364 }
1365
1366 $popup_template_id = sprintf(
1367 'lp-tmpl-builder-popup-curriculum-%1$s-course-%2$d',
1368 $popup_type,
1369 $courseModel->get_id()
1370 );
1371
1372 // Replace edit button with popup button - use lp-icon-edit-square instead of lp-icon-expand
1373 $section_action['edit'] = sprintf(
1374 '<li title="%s" class="lp-btn-edit-item-popup"
1375 data-course-id="%s"
1376 data-template="#%s"
1377 data-popup-type="%s"
1378 data-popup-id="%d"
1379 %s>
1380 <a class="lp-icon-edit-square edit-link edit-popup-link"></a>
1381 </li>',
1382 __( 'Edit in popup', 'learnpress' ),
1383 $courseModel->get_id(),
1384 esc_attr( $popup_template_id ),
1385 esc_attr( $popup_type ),
1386 $item_id,
1387 $popup_data_attr
1388 );
1389
1390 return $section_action;
1391 }
1392
1393 /**
1394 * Keep supported course settings tabs in Course Builder.
1395 *
1396 * @param array $tabs
1397 *
1398 * @return array
1399 */
1400 public function filter_course_builder_settings_tabs( array $tabs ): array {
1401 $allowed_tabs = [ 'general', 'offline', 'price', 'extra', 'assessment', 'author', 'material' ];
1402
1403 foreach ( array_keys( $tabs ) as $tab_key ) {
1404 if ( ! in_array( $tab_key, $allowed_tabs, true ) ) {
1405 unset( $tabs[ $tab_key ] );
1406 }
1407 }
1408
1409 return apply_filters( 'learn-press/course-builder/edit-course/settings/tabs', $tabs );
1410 }
1411
1412 /**
1413 * Convert final quiz edit link to Course Builder quiz settings URL.
1414 *
1415 * @param string $url
1416 * @param int $final_quiz_id
1417 *
1418 * @return string
1419 */
1420 public function filter_course_builder_assessment_final_quiz_edit_link( string $url, int $final_quiz_id ): string {
1421 $final_quiz_id = absint( $final_quiz_id );
1422 if ( ! $final_quiz_id ) {
1423 return $url;
1424 }
1425
1426 return CourseBuilder::get_tab_link( 'quizzes', $final_quiz_id, 'settings' ) . '#_lp_passing_grade';
1427 }
1428 }
1429