PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.3.9
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.3.9
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.3.9, at inc/TemplateHooks/CourseBuilder/Course/BuilderEditCourseTemplate.php

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