PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.1
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.1
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 4.2.1 All 138 releases
learnpress / inc / TemplateHooks / CourseBuilder / Course / BuilderEditCourseTemplate.php

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

1,426 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( '<label for="course_description" class="cb-course-edit-desc__label">%s</label>', __( 'Description', 'learnpress' ) ),
676 'ai_button' => $ai_button,
677 'label_wrap_end' => '</div>',
678 'edit' => AdminTemplate::editor_tinymce(
679 $desc,
680 $editor_id,
681 $editor_settings
682 ),
683 'wrapper_end' => '</div>',
684 ];
685
686 return Template::combine_components( $edit );
687 }
688
689 /**
690 * Check if Overview AI button and templates should be shown.
691 *
692 * @return bool
693 */
694 protected function can_show_overview_ai_button(): bool {
695 return \LP_Settings::get_option( 'enable_open_ai', 'no' ) === 'yes'
696 && ! empty( \LP_Settings::get_option( 'open_ai_secret_key', '' ) );
697 }
698
699 /**
700 * Render icon-only AI button in Course Builder overview.
701 *
702 * @param string $template_id
703 *
704 * @return string
705 */
706 protected function html_overview_ai_button( string $template_id ): string {
707 if ( ! $this->can_show_overview_ai_button() ) {
708 return '';
709 }
710
711 $button_label = esc_html__( 'Generate with AI', 'learnpress' );
712
713 return sprintf(
714 '<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>',
715 esc_attr( $template_id ),
716 esc_attr( $button_label )
717 );
718 }
719
720 /**
721 * Render AI popup templates for Course Builder overview edit page.
722 *
723 * @return string
724 */
725 protected function html_overview_ai_templates(): string {
726 if ( ! $this->can_show_overview_ai_button() ) {
727 return '';
728 }
729
730 try {
731 return AdminEditWithAITemplate::instance()->render_for_frontend( [ 'title', 'description', 'image' ] );
732 } catch ( Throwable $e ) {
733 error_log( __METHOD__ . ': ' . $e->getMessage() );
734 }
735
736 return '';
737 }
738
739 /**
740 * Render AI popup template for curriculum edit in Course Builder.
741 *
742 * @return string
743 */
744 protected function html_curriculum_ai_templates(): string {
745 if ( ! $this->can_show_overview_ai_button() ) {
746 return '';
747 }
748
749 try {
750 return AdminEditCourseCurriculumWithAITemplate::instance()->render_for_frontend();
751 } catch ( Throwable $e ) {
752 error_log( __METHOD__ . ': ' . $e->getMessage() );
753 }
754
755 return '';
756 }
757
758 /**
759 * Render Course Builder course category selector.
760 *
761 * Reuses the WordPress post_categories_meta_box() output for course_category,
762 * then adds Course Builder search and "add new category" controls. During render,
763 * checked_ontop is forced off so the checklist keeps taxonomy order.
764 *
765 * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
766 *
767 * @return string HTML markup for the course category checklist, search toolbar, and add-new form.
768 * @since 4.3.0
769 * @version 1.0.0
770 */
771 public function edit_categories( $courseModel ) {
772 if ( ! function_exists( 'post_categories_meta_box' ) ) {
773 require_once ABSPATH . 'wp-admin/includes/meta-boxes.php';
774 }
775 if ( ! function_exists( 'wp_popular_terms_checklist' ) ) {
776 require_once ABSPATH . 'wp-admin/includes/template.php';
777 }
778
779 $post_id = ! empty( $courseModel ) ? $courseModel->get_id() : get_the_ID();
780 $post = get_post( $post_id );
781
782 $force_checked_ontop_false = function ( $args ) {
783 if ( isset( $args['taxonomy'] ) && 'course_category' === $args['taxonomy'] ) {
784 $args['checked_ontop'] = false;
785 }
786
787 return $args;
788 };
789
790 ob_start();
791
792 add_filter( 'wp_terms_checklist_args', $force_checked_ontop_false );
793
794 if ( function_exists( 'post_categories_meta_box' ) ) {
795 \post_categories_meta_box(
796 $post,
797 array(
798 'id' => 'course_categorydiv',
799 'title' => __( 'Categories', 'learnpress' ),
800 'callback' => 'post_categories_meta_box',
801 'args' => array(
802 'taxonomy' => 'course_category',
803 'checked_ontop' => false,
804 ),
805 )
806 );
807 }
808
809 remove_filter( 'wp_terms_checklist_args', $force_checked_ontop_false );
810 $html_meta_box = ob_get_clean();
811
812 // Build add new category form (between header and content)
813 $parent_terms = get_terms(
814 [
815 'taxonomy' => 'course_category',
816 'hide_empty' => false,
817 ]
818 );
819 $parent_options = sprintf( '<option value="0">— %s —</option>', __( 'Parent Category', 'learnpress' ) );
820 if ( ! empty( $parent_terms ) && ! is_wp_error( $parent_terms ) ) {
821 foreach ( $parent_terms as $term ) {
822 $parent_options .= sprintf(
823 '<option value="%d">%s</option>',
824 $term->term_id,
825 esc_html( $term->name )
826 );
827 }
828 }
829
830 $form_add_category = sprintf(
831 '<div class="cb-course-edit-terms__form-add-category" style="display:none;">
832 <input type="text" class="cb-course-edit-category__input" placeholder="%s" id="cb-newcourse_category" />
833 <select class="cb-course-edit-category__select-parent" id="cb-newcourse_category_parent">%s</select>
834 <button type="button" class="cb-course-edit-category__btn-cancel">%s</button>
835 <button type="button" class="cb-course-edit-category__btn-save" id="cb-course_category-add-submit">%s</button>
836 </div>',
837 esc_attr__( 'Enter Category Name', 'learnpress' ),
838 $parent_options,
839 esc_html__( 'Cancel', 'learnpress' ),
840 esc_html__( 'Add', 'learnpress' ),
841 );
842
843 $edit = [
844 'wrapper' => '<div class="cb-course-edit-categories__wrapper">',
845 'header' => '<div class="cb-terms-header">',
846 'label_wrap' => '<div class="cb-terms-header__label-wrap">',
847 'label' => sprintf( '<label class="cb-terms-header__label">%s</label>', __( 'Categories', 'learnpress' ) ),
848 'btn_search' => sprintf(
849 '<button type="button" class="cb-terms-header__btn-search" data-toggle-target="#cb-course-edit-categories-search-toolbar" aria-expanded="false" aria-label="%s">
850 <i class="lp-icon-search"></i>
851 </button>',
852 esc_attr__( 'Search categories', 'learnpress' )
853 ),
854 'label_wrap_end' => '</div>',
855 'btn_add_new' => sprintf( '<button class="cb-course-edit-category__btn-add-new cb-terms-header__btn-add-new">%s</button>', __( 'Add New', 'learnpress' ) ),
856 'header_end' => '</div>',
857 'form_add_category' => $form_add_category,
858 'search' => sprintf(
859 '<div class="cb-course-edit-categories__toolbar cb-terms-search-toolbar" id="cb-course-edit-categories-search-toolbar">
860 <label class="cb-course-edit-categories__search-wrap">
861 <span class="screen-reader-text">%1$s</span>
862 <input type="search" class="cb-course-edit-category__search-input" placeholder="%2$s" />
863 </label>
864 </div>',
865 esc_html__( 'Search categories', 'learnpress' ),
866 esc_attr__( 'Search categories', 'learnpress' )
867 ),
868 'content' => $html_meta_box,
869 'wrapper_end' => '</div>',
870 ];
871
872 return Template::combine_components( $edit );
873 }
874
875 /**
876 * Render Course Builder course tag selector.
877 *
878 * Builds tag chips from all course tags, placing selected tags before available
879 * tags so the current course state is visible first. The returned markup includes
880 * search, empty-state text, and add-new controls consumed by the builder runtime.
881 *
882 * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
883 *
884 * @return string HTML markup for the selected/available tag chips, search toolbar, and add-new form.
885 * @since 4.3.0
886 * @version 1.0.0
887 */
888 public function edit_tags( $courseModel ) {
889 $course_terms = ! empty( $courseModel ) ? $courseModel->get_tags() : [];
890 $tags = get_terms(
891 [
892 'taxonomy' => LP_COURSE_TAXONOMY_TAG,
893 'hide_empty' => false,
894 ]
895 );
896
897 $selected_tag_ids = array_map(
898 function ( $term ) {
899 return (int) $term->term_id;
900 },
901 $course_terms
902 );
903
904 $html_selected_chips = '';
905 $html_available_chips = '';
906
907 if ( ! empty( $tags ) && ! is_wp_error( $tags ) ) {
908 foreach ( $tags as $tag ) {
909 $tag_id = $tag->term_id;
910 $tag_name = $tag->name;
911 $tag_count = $tag->count;
912 $is_checked = in_array( (int) $tag_id, $selected_tag_ids, true );
913 $html_chip = $this->input_checkbox_tag_item( $tag_id, $tag_name, $is_checked, $tag_count );
914
915 if ( $is_checked ) {
916 $html_selected_chips .= $html_chip;
917 } else {
918 $html_available_chips .= $html_chip;
919 }
920 }
921 }
922
923 $html_chips = $html_selected_chips . $html_available_chips;
924 $count_all = substr_count( $html_chips, 'class="cb-tag-chip"' );
925 $empty_default = esc_html__( 'No tags found.', 'learnpress' );
926 $empty_search = esc_html__( 'No matching tags.', 'learnpress' );
927
928 $toolbar = sprintf(
929 '<div class="cb-course-edit-tags__toolbar cb-terms-search-toolbar" id="cb-course-edit-tags-search-toolbar">
930 <label class="cb-course-edit-tags__search-wrap">
931 <span class="screen-reader-text">%1$s</span>
932 <input type="search" class="cb-course-edit-tags__search-input" placeholder="%2$s" />
933 </label>
934 </div>',
935 esc_html__( 'Search tags', 'learnpress' ),
936 esc_attr__( 'Search tags', 'learnpress' )
937 );
938
939 $edit = [
940 'wrapper' => '<div class="cb-course-edit-tags__wrapper">',
941 'header' => '<div class="cb-terms-header">',
942 'label_wrap' => '<div class="cb-terms-header__label-wrap">',
943 'label' => sprintf( '<label class="cb-terms-header__label">%s</label>', __( 'Tags', 'learnpress' ) ),
944 'btn_search' => sprintf(
945 '<button type="button" class="cb-terms-header__btn-search" data-toggle-target="#cb-course-edit-tags-search-toolbar" aria-expanded="false" aria-label="%s">
946 <i class="lp-icon-search"></i>
947 </button>',
948 esc_attr__( 'Search tags', 'learnpress' )
949 ),
950 'label_wrap_end' => '</div>',
951 'btn_add_new' => sprintf( '<button class="cb-course-edit-tag__btn-add-new cb-terms-header__btn-add-new">%s</button>', __( 'Add New', 'learnpress' ) ),
952 'header_end' => '</div>',
953 'form_add_tag_wrapper' => '<div class="cb-course-edit-terms__form-add-tag" style="display:none;">',
954 'input' => '<input type="text" class="cb-course-edit-tags__input" placeholder="' . esc_attr__( 'Enter Tag Name', 'learnpress' ) . '"/>',
955 'btn_cancel' => sprintf( '<button type="button" class="cb-course-edit-tag__btn-cancel">%s</button>', __( 'Cancel', 'learnpress' ) ),
956 'button' => '<button type="button" class="cb-course-edit-tags__btn-save">' . esc_html__( 'Add', 'learnpress' ) . '</button>',
957 'form_add_tag_wrapper_end' => '</div>',
958 'toolbar' => $toolbar,
959 'wrapper_checkbox' => '<div class="cb-course-edit-tags__checkbox-wrapper">',
960 'checkbox' => $html_chips,
961 'wrapper_checkbox_end' => '</div>',
962 'empty' => sprintf(
963 '<p class="cb-course-edit-tags__empty%1$s" data-empty-default="%2$s" data-empty-search="%3$s">%2$s</p>',
964 $count_all > 0 ? ' lp-hidden' : '',
965 esc_attr( $empty_default ),
966 esc_attr( $empty_search )
967 ),
968 'wrapper_end' => '</div>',
969 ];
970
971 return Template::combine_components( $edit );
972 }
973
974 public function input_checkbox_tag_item( $term_id, $term_name, $is_checked, $count = 0 ) {
975 if ( 0 === $count ) {
976 $tag_obj = get_term( $term_id, LP_COURSE_TAXONOMY_TAG );
977 if ( $tag_obj && ! is_wp_error( $tag_obj ) ) {
978 $count = $tag_obj->count;
979 }
980 }
981
982 $tag_name_search = wp_strip_all_tags( $term_name );
983 if ( function_exists( 'mb_strtolower' ) ) {
984 $tag_name_search = mb_strtolower( $tag_name_search );
985 } else {
986 $tag_name_search = strtolower( $tag_name_search );
987 }
988
989 $html = sprintf(
990 '<div class="cb-tag-chip" data-tag-name="%s" data-term-id="%d">',
991 esc_attr( $tag_name_search ),
992 (int) $term_id
993 );
994 $html .= sprintf(
995 '<input type="checkbox" name="course_tags[]" value="%s" id="course_tag_%s" %s>',
996 $term_id,
997 $term_id,
998 checked( $is_checked, true, false )
999 );
1000 $html .= sprintf(
1001 '<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>',
1002 $term_id,
1003 esc_html( $term_name ),
1004 $count
1005 );
1006 $html .= '</div>';
1007
1008 return $html;
1009 }
1010
1011 /**
1012 * Render Course Builder featured image uploader.
1013 *
1014 * Existing courses render the current thumbnail preview with replace/remove
1015 * controls. Courses without a thumbnail render the upload dropzone and hidden
1016 * course_thumbnail_id field used by the builder save flow.
1017 *
1018 * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
1019 *
1020 * @return string HTML markup for the featured image dropzone, thumbnail hidden field, and optional AI button.
1021 * @since 4.3.0
1022 * @version 1.0.0
1023 */
1024 public function edit_featured_image( $courseModel ) {
1025 $post_id = ! empty( $courseModel ) ? $courseModel->get_id() : '';
1026 $ai_button = $this->html_overview_ai_button( '#lp-tmpl-edit-image-ai' );
1027
1028 $thumbnail_id = ! empty( $post_id ) ? get_post_thumbnail_id( $post_id ) : '';
1029 $thumbnail_url = '';
1030 $thumbnail_alt = '';
1031
1032 if ( $thumbnail_id ) {
1033 $thumbnail_url = wp_get_attachment_image_url( $thumbnail_id, 'medium' );
1034 $thumbnail_alt = get_post_meta( $thumbnail_id, '_wp_attachment_image_alt', true );
1035 }
1036
1037 $has_image = ! empty( $thumbnail_url );
1038
1039 $featured_image_html = '<div class="cb-featured-image-container">';
1040
1041 // Upload area
1042 $featured_image_html .= sprintf(
1043 '<div class="cb-featured-image-dropzone %s" data-post-id="%s">',
1044 $has_image ? 'has-image' : '',
1045 esc_attr( $post_id )
1046 );
1047
1048 if ( $has_image ) {
1049 $featured_image_html .= sprintf(
1050 '<img src="%s" alt="%s" class="cb-featured-image-preview__img">',
1051 esc_url( $thumbnail_url ),
1052 esc_attr( $thumbnail_alt )
1053 );
1054 } else {
1055 $featured_image_html .= '<div class="cb-featured-image-upload-content">';
1056 $featured_image_html .= '<span class="cb-featured-image-icon"><span class="cb-featured-image-icon__image" aria-hidden="true"></span></span>';
1057 $featured_image_html .= sprintf(
1058 '<p class="cb-featured-image-text"><a href="#" class="cb-featured-image-link">%s</a></p>',
1059 __( 'Click to upload', 'learnpress' )
1060 );
1061 $featured_image_html .= sprintf(
1062 '<p class="cb-featured-image-hint">%s</p>',
1063 __( 'JPG, JPEG, PNG less than 1MB', 'learnpress' )
1064 );
1065 $featured_image_html .= '</div>';
1066 }
1067
1068 $featured_image_html .= '</div>';
1069
1070 $featured_image_html .= sprintf(
1071 '<input type="hidden" name="course_thumbnail_id" id="course_thumbnail_id" value="%s">',
1072 esc_attr( $thumbnail_id )
1073 );
1074
1075 // Action buttons wrapper
1076 $featured_image_html .= '<div class="cb-featured-image-actions">';
1077
1078 // Remove button (only show when has image)
1079 if ( $has_image ) {
1080 $featured_image_html .= sprintf(
1081 '<button type="button" class="cb-remove-featured-image">%s</button>',
1082 '<span class="cb-remove-featured-image__icon" aria-hidden="true"></span>'
1083 );
1084
1085 $featured_image_html .= sprintf(
1086 '<button type="button" class="cb-change-featured-image">%s</button>',
1087 __( 'Replace', 'learnpress' )
1088 );
1089 }
1090
1091 $featured_image_html .= '</div>'; // End actions wrapper
1092 $featured_image_html .= '</div>'; // End container
1093
1094 $edit = [
1095 'wrapper' => '<div class="cb-course-edit-featured-image">',
1096 'label_wrap' => '<div class="cb-course-edit-featured-image__label-wrap">',
1097 'label' => sprintf(
1098 '<label class="cb-course-edit-featured-image__title">%s</label>',
1099 __( 'Featured Image', 'learnpress' )
1100 ),
1101 'ai_button' => $ai_button,
1102 'label_wrap_end' => '</div>',
1103 'edit' => $featured_image_html,
1104 'wrapper_end' => '</div>',
1105 ];
1106
1107 return Template::combine_components( $edit );
1108 }
1109
1110 /**
1111 * Render publish panel (status, visibility, publish date, danger zone) for Course Builder overview.
1112 *
1113 * @param CourseModel|false|string|null $courseModel
1114 *
1115 * @return string
1116 */
1117 public function edit_publish( $courseModel ): string {
1118 $post_id = ! empty( $courseModel ) ? absint( $courseModel->get_id() ) : 0;
1119 $post = $post_id ? get_post( $post_id ) : null;
1120
1121 $current_status = 'draft';
1122 if ( $post && ! empty( $post->post_status ) ) {
1123 $current_status = sanitize_key( $post->post_status );
1124 }
1125
1126 $status_for_select = in_array( $current_status, [ 'publish', 'draft', 'pending', 'future' ], true )
1127 ? $current_status
1128 : 'publish';
1129 $current_password = $post ? (string) ( $post->post_password ?? '' ) : '';
1130 $visibility = 'private' === $current_status
1131 ? 'private'
1132 : ( ! empty( $current_password ) ? 'password' : 'public' );
1133 $published_on = '';
1134
1135 if ( $post && ! empty( $post->post_date ) && '0000-00-00 00:00:00' !== $post->post_date ) {
1136 $published_on = wp_date( 'Y-m-d\TH:i', strtotime( $post->post_date ), wp_timezone() );
1137 }
1138
1139 $has_future_publish_date = false;
1140 if ( $post && ! empty( $post->post_date ) && '0000-00-00 00:00:00' !== $post->post_date ) {
1141 $has_future_publish_date = strtotime( $post->post_date ) > current_time( 'timestamp' );
1142 }
1143
1144 $is_scheduled_status = 'future' === $status_for_select
1145 || ( in_array( $status_for_select, [ 'draft', 'pending' ], true ) && $has_future_publish_date );
1146 $primary_status_value = $is_scheduled_status ? 'future' : 'publish';
1147 $primary_status_label = $is_scheduled_status
1148 ? __( 'Scheduled', 'learnpress' )
1149 : __( 'Published', 'learnpress' );
1150 $selected_status_for_ui = in_array( $status_for_select, [ 'draft', 'pending' ], true )
1151 ? $status_for_select
1152 : $primary_status_value;
1153 $status_options = [
1154 $primary_status_value => $primary_status_label,
1155 'draft' => __( 'Draft', 'learnpress' ),
1156 'pending' => __( 'Pending Review', 'learnpress' ),
1157 ];
1158
1159 $publish_date_label = $has_future_publish_date
1160 ? __( 'Scheduled for', 'learnpress' )
1161 : __( 'Published on', 'learnpress' );
1162
1163 $status_options_html = '';
1164 foreach ( $status_options as $value => $label ) {
1165 $status_options_html .= sprintf(
1166 '<option value="%1$s" %2$s>%3$s</option>',
1167 esc_attr( $value ),
1168 selected( $selected_status_for_ui, $value, false ),
1169 esc_html( $label )
1170 );
1171 }
1172
1173 $visibility_options_html = sprintf(
1174 '<option value="public" %1$s>%2$s</option><option value="private" %3$s>%4$s</option><option value="password" %5$s>%6$s</option>',
1175 selected( $visibility, 'public', false ),
1176 esc_html__( 'Public', 'learnpress' ),
1177 selected( $visibility, 'private', false ),
1178 esc_html__( 'Private', 'learnpress' ),
1179 selected( $visibility, 'password', false ),
1180 esc_html__( 'Password protected', 'learnpress' )
1181 );
1182
1183 $edit = [
1184 'wrapper' => '<div class="cb-course-edit-publish">',
1185 'title' => sprintf( '<h3 class="cb-course-edit-publish__title">%s</h3>', esc_html__( 'Publish', 'learnpress' ) ),
1186 'status_row' => sprintf(
1187 '<div class="cb-course-edit-publish__row">
1188 <label for="cb-course-publish-status" class="cb-course-edit-publish__label">%1$s</label>
1189 <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>
1190 </div>',
1191 esc_html__( 'Status', 'learnpress' ),
1192 $status_options_html,
1193 esc_attr__( 'Published', 'learnpress' ),
1194 esc_attr__( 'Scheduled', 'learnpress' ),
1195 esc_attr( $primary_status_value )
1196 ),
1197 'visibility_row' => sprintf(
1198 '<div class="cb-course-edit-publish__row">
1199 <label for="cb-course-publish-visibility" class="cb-course-edit-publish__label">%1$s</label>
1200 <select id="cb-course-publish-visibility" name="cb_course_publish_visibility" class="cb-course-edit-publish__control">%2$s</select>
1201 </div>',
1202 esc_html__( 'Visibility', 'learnpress' ),
1203 $visibility_options_html
1204 ),
1205 'password_row' => sprintf(
1206 '<div class="cb-course-edit-publish__row cb-course-edit-publish__password-row %1$s">
1207 <label for="cb-course-publish-password" class="cb-course-edit-publish__label">%2$s</label>
1208 <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">
1209 </div>',
1210 'password' === $visibility ? '' : 'lp-hidden',
1211 esc_html__( 'Password', 'learnpress' ),
1212 esc_attr( $current_password )
1213 ),
1214 'published_on_row' => sprintf(
1215 '<div class="cb-course-edit-publish__row">
1216 <label for="cb-course-publish-date" id="cb-course-publish-date-label" class="cb-course-edit-publish__label">%1$s</label>
1217 <input type="datetime-local" id="cb-course-publish-date" name="cb_course_publish_date" class="cb-course-edit-publish__control" value="%2$s">
1218 </div>',
1219 esc_html( $publish_date_label ),
1220 esc_attr( $published_on )
1221 ),
1222 'wrapper_end' => '</div>',
1223 ];
1224
1225 return Template::combine_components( $edit );
1226 }
1227
1228 /**
1229 * Render Course Builder curriculum editor.
1230 *
1231 * Delegates the main curriculum layout to AdminEditCurriculumTemplate with the
1232 * is_course_builder context flag, then appends popup templates and optional AI
1233 * curriculum templates needed by the builder UI.
1234 *
1235 * @param CourseModel $courseModel Course model used to render curriculum sections and popup template IDs.
1236 *
1237 * @return string HTML markup for the curriculum editor plus Course Builder popup and AI templates.
1238 * @since 4.3.0
1239 * @version 1.0.0
1240 */
1241 protected function html_curriculum( CourseModel $courseModel ): string {
1242 ob_start();
1243 AdminEditCurriculumTemplate::instance()->edit_course_curriculum_layout(
1244 $courseModel,
1245 [ 'is_course_builder' => true ]
1246 );
1247 $html_curriculum = ob_get_clean();
1248
1249 return $html_curriculum
1250 . $this->html_curriculum_popup_templates( $courseModel )
1251 . $this->html_curriculum_ai_templates();
1252 }
1253
1254 protected function html_curriculum_popup_templates( CourseModel $courseModel ): string {
1255 $popup_templates = apply_filters(
1256 'learn-press/course-builder/courses/curriculum/popup-templates',
1257 [
1258 'lesson' => sprintf(
1259 '<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>',
1260 esc_attr(
1261 sprintf(
1262 'lp-tmpl-builder-popup-curriculum-lesson-course-%d',
1263 $courseModel->get_id()
1264 )
1265 ),
1266 TemplateAJAX::load_content_via_ajax(
1267 [
1268 'id_url' => 'builder-popup-lesson-curriculum',
1269 'lesson_id' => 0,
1270 'html_no_load_ajax_first' => sprintf(
1271 '<div class="lp-builder-popup__loader"><div class="lp-loading-circle"></div><span>%s</span></div>',
1272 esc_html__( 'Loading...', 'learnpress' )
1273 ),
1274 ],
1275 [
1276 'class' => BuilderPopupTemplate::class,
1277 'method' => 'render_lesson_popup',
1278 ]
1279 )
1280 ),
1281 'quiz' => sprintf(
1282 '<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>',
1283 esc_attr(
1284 sprintf(
1285 'lp-tmpl-builder-popup-curriculum-quiz-course-%d',
1286 $courseModel->get_id()
1287 )
1288 ),
1289 TemplateAJAX::load_content_via_ajax(
1290 [
1291 'id_url' => 'builder-popup-quiz-curriculum',
1292 'quiz_id' => 0,
1293 'html_no_load_ajax_first' => sprintf(
1294 '<div class="lp-builder-popup__loader"><div class="lp-loading-circle"></div><span>%s</span></div>',
1295 esc_html__( 'Loading...', 'learnpress' )
1296 ),
1297 ],
1298 [
1299 'class' => BuilderPopupTemplate::class,
1300 'method' => 'render_quiz_popup',
1301 ]
1302 )
1303 ),
1304 ],
1305 $courseModel
1306 );
1307
1308 return Template::combine_components( $popup_templates );
1309 }
1310
1311
1312 /**
1313 * Add edit popup button for lesson and quiz items in Course Builder curriculum.
1314 * Replace the default edit button with popup button for lesson and quiz items.
1315 *
1316 * @param array $section_action Array of action buttons.
1317 * @param object|null $item Item data.
1318 * @param PostModel|null $itemModel Item model.
1319 * @param CourseModel $courseModel .
1320 * @param array $context_data Context data passed from AJAX.
1321 *
1322 * @return array
1323 * @since 4.3.0
1324 * @version 1.0.2
1325 *
1326 */
1327 public function add_edit_popup_button( array $section_action, $item, $itemModel, $courseModel, $context_data = [] ): array {
1328 // Check if we are in Course Builder context via the flag passed in AJAX args
1329 $is_course_builder = ! empty( $context_data['is_course_builder'] );
1330
1331 if ( ! $is_course_builder ) {
1332 return $section_action;
1333 }
1334
1335 $item_id = $item->item_id ?? 0;
1336 $item_type = $item->item_type ?? '';
1337
1338 $popup_data_attr = '';
1339 $popup_type = '';
1340 if ( $item_type === LP_LESSON_CPT ) {
1341 $popup_data_attr = sprintf( 'data-popup-lesson="%s"', $item_id );
1342 $popup_type = 'lesson';
1343 } elseif ( $item_type === LP_QUIZ_CPT ) {
1344 $popup_data_attr = sprintf( 'data-popup-quiz="%s"', $item_id );
1345 $popup_type = 'quiz';
1346 }
1347
1348 $popup_type = sanitize_key(
1349 (string) apply_filters(
1350 'learn-press/course-builder/courses/curriculum/popup-item-type',
1351 $popup_type,
1352 $item_type,
1353 $item,
1354 $itemModel,
1355 $courseModel,
1356 $context_data
1357 )
1358 );
1359 if ( empty( $popup_type ) ) {
1360 return $section_action;
1361 }
1362
1363 $popup_template_id = sprintf(
1364 'lp-tmpl-builder-popup-curriculum-%1$s-course-%2$d',
1365 $popup_type,
1366 $courseModel->get_id()
1367 );
1368
1369 // Replace edit button with popup button - use lp-icon-edit-square instead of lp-icon-expand
1370 $section_action['edit'] = sprintf(
1371 '<li title="%s" class="lp-btn-edit-item-popup"
1372 data-course-id="%s"
1373 data-template="#%s"
1374 data-popup-type="%s"
1375 data-popup-id="%d"
1376 %s>
1377 <a class="lp-icon-edit-square edit-link edit-popup-link"></a>
1378 </li>',
1379 __( 'Edit in popup', 'learnpress' ),
1380 $courseModel->get_id(),
1381 esc_attr( $popup_template_id ),
1382 esc_attr( $popup_type ),
1383 $item_id,
1384 $popup_data_attr
1385 );
1386
1387 return $section_action;
1388 }
1389
1390 /**
1391 * Keep supported course settings tabs in Course Builder.
1392 *
1393 * @param array $tabs
1394 *
1395 * @return array
1396 */
1397 public function filter_course_builder_settings_tabs( array $tabs ): array {
1398 $allowed_tabs = [ 'general', 'offline', 'price', 'extra', 'assessment', 'author', 'material' ];
1399
1400 foreach ( array_keys( $tabs ) as $tab_key ) {
1401 if ( ! in_array( $tab_key, $allowed_tabs, true ) ) {
1402 unset( $tabs[ $tab_key ] );
1403 }
1404 }
1405
1406 return apply_filters( 'learn-press/course-builder/edit-course/settings/tabs', $tabs );
1407 }
1408
1409 /**
1410 * Convert final quiz edit link to Course Builder quiz settings URL.
1411 *
1412 * @param string $url
1413 * @param int $final_quiz_id
1414 *
1415 * @return string
1416 */
1417 public function filter_course_builder_assessment_final_quiz_edit_link( string $url, int $final_quiz_id ): string {
1418 $final_quiz_id = absint( $final_quiz_id );
1419 if ( ! $final_quiz_id ) {
1420 return $url;
1421 }
1422
1423 return CourseBuilder::get_tab_link( 'quizzes', $final_quiz_id, 'settings' ) . '#_lp_passing_grade';
1424 }
1425 }
1426