feat: editor js for instructor notes
This commit is contained in:
@@ -135,13 +135,13 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "instructor_notes",
|
||||
"fieldtype": "Text",
|
||||
"fieldtype": "Markdown Editor",
|
||||
"label": "Instructor Notes"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-31 21:47:06.314995",
|
||||
"modified": "2023-09-27 15:45:54.738573",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Course Lesson",
|
||||
|
||||
@@ -152,11 +152,6 @@ def get_lesson_details(chapter):
|
||||
)
|
||||
lesson_details.number = flt(f"{chapter.idx}.{row.idx}")
|
||||
lesson_details.icon = get_lesson_icon(lesson_details.body)
|
||||
if lesson_details.instructor_notes:
|
||||
lesson_details.instructor_notes_html = markdown_to_html(
|
||||
lesson_details.instructor_notes
|
||||
)
|
||||
|
||||
lessons.append(lesson_details)
|
||||
return lessons
|
||||
|
||||
|
||||
@@ -160,6 +160,10 @@ textarea.field-input {
|
||||
position: unset;
|
||||
}
|
||||
|
||||
.codex-editor--narrow .ce-toolbar__actions {
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
.lesson-editor {
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--border-radius-md);
|
||||
@@ -2453,4 +2457,16 @@ select {
|
||||
.batch-details {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-section {
|
||||
font-size: var(--text-lg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.collapse-section.collapsed .icon {
|
||||
transition: all 0.5s;
|
||||
-webkit-transform: rotate(180deg);
|
||||
-moz-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@@ -75,41 +75,54 @@
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="published" class="vertically-center">
|
||||
<label for="preview" class="vertically-center">
|
||||
<input type="checkbox" id="preview" {% if lesson.include_in_preview %} checked {% endif %}>
|
||||
<span>{{ _("Show preview of this lesson to Guest users.") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div class="field-label">
|
||||
{{ _("Instructor Notes") }}
|
||||
<div class="collapse-section collapsed" data-toggle="collapse" data-target="#lesson-content-section">
|
||||
<svg class="icon icon-sm pull-right">
|
||||
<use href="#icon-up-line"></use>
|
||||
</svg>
|
||||
<div class="field-label">
|
||||
{{ _("Content") }}
|
||||
</div>
|
||||
<div class="field-description mb-2">
|
||||
{{ _("Add your lesson content here") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("These notes will only be visible to the Course Creator, Course Evaluaor and Moderator.") }}
|
||||
|
||||
<div id="lesson-content-section" class="collapse">
|
||||
<div id="lesson-content" class="lesson-editor"></div>
|
||||
</div>
|
||||
<div id="instructor-notes"></div>
|
||||
{% if lesson.instructor_notes %}
|
||||
<div id="current-instructor-notes" class="hide">{{ lesson.instructor_notes }}</div>
|
||||
|
||||
{% if lesson.body %}
|
||||
<div id="current-lesson-content" class="hide">{{ lesson.body }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div>
|
||||
<div class="collapse-section collapsed" data-toggle="collapse" data-target="#instructor-notes-section">
|
||||
<svg class="icon icon-sm pull-right">
|
||||
<use href="#icon-up-line"></use>
|
||||
</svg>
|
||||
<div class="field-label">
|
||||
{{ _("Content") }}
|
||||
{{ _("Instructor Notes") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("Add your lesson content here") }}
|
||||
<div class="field-description mb-2">
|
||||
{{ _("These notes will only be visible to the Course Creator, Course Evaluaor and Moderator.") }}
|
||||
</div>
|
||||
</div>
|
||||
<div id="lesson-content" class="lesson-editor"></div>
|
||||
{% if lesson.body %}
|
||||
<div id="current-lesson-content" class="hide">{{ lesson.body }}</div>
|
||||
</div>
|
||||
<div id="instructor-notes-section" class="collapse">
|
||||
<div id="instructor-notes" class="lesson-editor"></div>
|
||||
</div>
|
||||
{% if lesson.instructor_notes %}
|
||||
<div id="current-instructor-notes" class="hide">{{ lesson.instructor_notes }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
@@ -1,110 +1,129 @@
|
||||
frappe.ready(() => {
|
||||
let self = this;
|
||||
|
||||
frappe.require("controls.bundle.js");
|
||||
frappe.telemetry.capture("on_lesson_creation_page", "lms");
|
||||
|
||||
if ($("#instructor-notes").length) {
|
||||
frappe.require("controls.bundle.js", () => {
|
||||
make_instructor_notes_component();
|
||||
});
|
||||
}
|
||||
|
||||
if ($("#current-lesson-content").length) {
|
||||
parse_string_to_lesson();
|
||||
parse_string_to_lesson("lesson");
|
||||
}
|
||||
|
||||
setup_editor();
|
||||
if ($("#current-instructor-notes").length) {
|
||||
parse_string_to_lesson("notes");
|
||||
}
|
||||
|
||||
setup_editor_for_lesson_content();
|
||||
setup_editor_for_instructor_notes();
|
||||
|
||||
$("#save-lesson").click((e) => {
|
||||
save_lesson(e);
|
||||
});
|
||||
});
|
||||
|
||||
const setup_editor = () => {
|
||||
const setup_editor_for_lesson_content = () => {
|
||||
self.editor = new EditorJS({
|
||||
holder: "lesson-content",
|
||||
tools: {
|
||||
embed: {
|
||||
class: Embed,
|
||||
config: {
|
||||
services: {
|
||||
youtube: true,
|
||||
vimeo: true,
|
||||
codepen: true,
|
||||
slides: {
|
||||
regex: /https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/,
|
||||
embedUrl:
|
||||
"https://docs.google.com/presentation/d/e/<%= remote_id %>/embed",
|
||||
html: "<iframe width='100%' height='300' frameborder='0' allowfullscreen='true'></iframe>",
|
||||
},
|
||||
pdf: {
|
||||
regex: /(https?:\/\/.*\.pdf)/,
|
||||
embedUrl: "<%= remote_id %>",
|
||||
html: "<iframe width='100%' height='600px' frameborder='0'></iframe>",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
header: {
|
||||
class: Header,
|
||||
inlineToolbar: ["bold", "italic", "link"],
|
||||
config: {
|
||||
levels: [4, 5, 6],
|
||||
defaultLevel: 5,
|
||||
},
|
||||
icon: `<svg class="icon icon-sm" style="">
|
||||
<use class="" href="#icon-header"></use>
|
||||
</svg>`,
|
||||
},
|
||||
paragraph: {
|
||||
class: Paragraph,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
preserveBlank: true,
|
||||
},
|
||||
},
|
||||
youtube: YouTubeVideo,
|
||||
quiz: Quiz,
|
||||
upload: Upload,
|
||||
},
|
||||
tools: get_tools(),
|
||||
data: {
|
||||
blocks: self.blocks ? self.blocks : [],
|
||||
blocks: self.lesson_blocks || [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const parse_string_to_lesson = () => {
|
||||
let lesson_content = $("#current-lesson-content").html();
|
||||
let lesson_blocks = [];
|
||||
const setup_editor_for_instructor_notes = () => {
|
||||
self.instructor_notes_editor = new EditorJS({
|
||||
holder: "instructor-notes",
|
||||
tools: get_tools(),
|
||||
data: {
|
||||
blocks: self.notes_blocks || [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
lesson_content.split("\n").forEach((block) => {
|
||||
const get_tools = () => {
|
||||
return {
|
||||
embed: {
|
||||
class: Embed,
|
||||
config: {
|
||||
services: {
|
||||
youtube: true,
|
||||
vimeo: true,
|
||||
codepen: true,
|
||||
slides: {
|
||||
regex: /https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/,
|
||||
embedUrl:
|
||||
"https://docs.google.com/presentation/d/e/<%= remote_id %>/embed",
|
||||
html: "<iframe width='100%' height='300' frameborder='0' allowfullscreen='true'></iframe>",
|
||||
},
|
||||
pdf: {
|
||||
regex: /(https?:\/\/.*\.pdf)/,
|
||||
embedUrl: "<%= remote_id %>",
|
||||
html: "<iframe width='100%' height='600px' frameborder='0'></iframe>",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
header: {
|
||||
class: Header,
|
||||
inlineToolbar: ["bold", "italic", "link"],
|
||||
config: {
|
||||
levels: [4, 5, 6],
|
||||
defaultLevel: 5,
|
||||
},
|
||||
icon: `<svg class="icon icon-sm" style="">
|
||||
<use class="" href="#icon-header"></use>
|
||||
</svg>`,
|
||||
},
|
||||
paragraph: {
|
||||
class: Paragraph,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
preserveBlank: true,
|
||||
},
|
||||
},
|
||||
youtube: YouTubeVideo,
|
||||
quiz: Quiz,
|
||||
upload: Upload,
|
||||
};
|
||||
};
|
||||
|
||||
const parse_string_to_lesson = (type) => {
|
||||
let content;
|
||||
let blocks = [];
|
||||
|
||||
if (type == "lesson") {
|
||||
content = $("#current-lesson-content").html();
|
||||
} else if (type == "notes") {
|
||||
content = $("#current-instructor-notes").html();
|
||||
}
|
||||
|
||||
content.split("\n").forEach((block) => {
|
||||
if (block.includes("{{ YouTubeVideo")) {
|
||||
let youtube_id = block.match(/'([^']+)'/)[1];
|
||||
lesson_blocks.push({
|
||||
let youtube_id = block.match(/\(["']([^"']+?)["']\)/)[1];
|
||||
blocks.push({
|
||||
type: "youtube",
|
||||
data: {
|
||||
youtube: youtube_id,
|
||||
},
|
||||
});
|
||||
} else if (block.includes("{{ Quiz")) {
|
||||
let quiz = block.match(/'([^']+)'/)[1];
|
||||
lesson_blocks.push({
|
||||
let quiz = block.match(/\(["']([^"']+?)["']\)/)[1];
|
||||
blocks.push({
|
||||
type: "quiz",
|
||||
data: {
|
||||
quiz: quiz,
|
||||
},
|
||||
});
|
||||
} else if (block.includes("{{ Video")) {
|
||||
let video = block.match(/'([^']+)'/)[1];
|
||||
lesson_blocks.push({
|
||||
let video = block.match(/\(["']([^"']+?)["']\)/)[1];
|
||||
blocks.push({
|
||||
type: "upload",
|
||||
data: {
|
||||
file_url: video,
|
||||
},
|
||||
});
|
||||
} else if (block.includes("{{ Embed")) {
|
||||
let embed = block.match(/'([^']+)'/)[1];
|
||||
lesson_blocks.push({
|
||||
let embed = block.match(/\(["']([^"']+?)["']\)/)[1];
|
||||
blocks.push({
|
||||
type: "embed",
|
||||
data: {
|
||||
service: embed.split("|||")[0],
|
||||
@@ -113,7 +132,7 @@ const parse_string_to_lesson = () => {
|
||||
});
|
||||
} else if (block.includes("![]")) {
|
||||
let image = block.match(/\((.*?)\)/)[1];
|
||||
lesson_blocks.push({
|
||||
blocks.push({
|
||||
type: "upload",
|
||||
data: {
|
||||
file_url: image,
|
||||
@@ -121,7 +140,7 @@ const parse_string_to_lesson = () => {
|
||||
});
|
||||
} else if (block.includes("#")) {
|
||||
let level = (block.match(/#/g) || []).length;
|
||||
lesson_blocks.push({
|
||||
blocks.push({
|
||||
type: "header",
|
||||
data: {
|
||||
text: block.replace(/#/g, "").trim(),
|
||||
@@ -129,7 +148,7 @@ const parse_string_to_lesson = () => {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
lesson_blocks.push({
|
||||
blocks.push({
|
||||
type: "paragraph",
|
||||
data: {
|
||||
text: block,
|
||||
@@ -138,16 +157,25 @@ const parse_string_to_lesson = () => {
|
||||
}
|
||||
});
|
||||
|
||||
this.blocks = lesson_blocks;
|
||||
if (type == "lesson") {
|
||||
this.lesson_blocks = blocks;
|
||||
} else if (type == "notes") {
|
||||
this.notes_blocks = blocks;
|
||||
}
|
||||
};
|
||||
|
||||
const save_lesson = (e) => {
|
||||
self.editor.save().then((outputData) => {
|
||||
parse_lesson_to_string(outputData);
|
||||
parse_content_to_string(outputData, "lesson");
|
||||
|
||||
self.instructor_notes_editor.save().then((outputData) => {
|
||||
parse_content_to_string(outputData, "notes");
|
||||
save();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const parse_lesson_to_string = (data) => {
|
||||
const parse_content_to_string = (data, type) => {
|
||||
let lesson_content = "";
|
||||
data.blocks.forEach((block) => {
|
||||
if (block.type == "youtube") {
|
||||
@@ -175,24 +203,28 @@ const parse_lesson_to_string = (data) => {
|
||||
}|||${block.data.embed.replace(/&/g, "&")}") }}\n`;
|
||||
}
|
||||
});
|
||||
save(lesson_content);
|
||||
if (type == "lesson") {
|
||||
this.lesson_content_data = lesson_content;
|
||||
} else if (type == "notes") {
|
||||
this.instructor_notes_data = lesson_content;
|
||||
}
|
||||
};
|
||||
|
||||
const save = (lesson_content) => {
|
||||
validate_mandatory(lesson_content);
|
||||
const save = () => {
|
||||
console.log(this.instructor_notes_data);
|
||||
console.log(this.lesson_content_data);
|
||||
validate_mandatory(this.lesson_content_data);
|
||||
let lesson = $("#lesson-title").data("lesson");
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course.lms_course.save_lesson",
|
||||
args: {
|
||||
title: $("#lesson-title").val(),
|
||||
body: lesson_content,
|
||||
body: this.lesson_content_data,
|
||||
chapter: $("#lesson-title").data("chapter"),
|
||||
preview: $("#preview").prop("checked") ? 1 : 0,
|
||||
idx: $("#lesson-title").data("index"),
|
||||
lesson: lesson ? lesson : "",
|
||||
instructor_notes:
|
||||
this.instructor_notes.get_values().instructor_notes,
|
||||
instructor_notes: this.instructor_notes_data,
|
||||
},
|
||||
callback: (data) => {
|
||||
frappe.show_alert({
|
||||
|
||||
@@ -156,12 +156,17 @@
|
||||
{% endif %}
|
||||
|
||||
{% if lesson.instructor_notes and (is_moderator or instructor or is_evaluator) %}
|
||||
<div class="alert alert-info mb-4">
|
||||
<div class="bold-heading mb-2">
|
||||
{{ _("Instructor Notes") }}
|
||||
<div class="alert alert-secondary mb-4">
|
||||
<div class="bold-heading collapse-section collapsed" data-toggle="collapse" data-target="#instructor-notes">
|
||||
<svg class="icon icon-md pull-right">
|
||||
<use href="#icon-up-line"></use>
|
||||
</svg>
|
||||
<div>
|
||||
{{ _("Instructor Notes") }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{{ lesson.instructor_notes_html }}
|
||||
<div class="collapse" id="instructor-notes">
|
||||
{{ instructor_notes }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -227,36 +232,6 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Help Article -->
|
||||
{% macro HelpArticle() %}
|
||||
<div class="medium">
|
||||
<h3> {{ _("Embed Components") }} </h3>
|
||||
<p>
|
||||
{{ _("You can add additional content to the lesson using a special syntax. The table below mentions
|
||||
all types of dynamic content that you can add to the lessons and the syntax for the same.") }}
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
<b> {{ _("YouTube Video") }} </b>
|
||||
<p> To get the YouTube Video ID, follow the steps mentioned below. </p>
|
||||
<ul class="px-4">
|
||||
<li>
|
||||
{{ _("Upload the video on youtube.") }}
|
||||
</li>
|
||||
<li>
|
||||
{{ _("When you share a youtube video, it shows a URL") }}
|
||||
</li>
|
||||
<li>
|
||||
{{ _("Copy the last parameter of the URL and paste it here.") }}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Discussions Component -->
|
||||
{% macro Discussions() %}
|
||||
{% set topics_count = frappe.db.count("Discussion Topic", {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cstr, flt
|
||||
from lms.lms.md import markdown_to_html
|
||||
|
||||
from lms.lms.utils import (
|
||||
get_lesson_url,
|
||||
@@ -46,6 +47,9 @@ def get_context(context):
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
context.is_evaluator = has_course_evaluator_role()
|
||||
|
||||
if context.lesson.instructor_notes:
|
||||
context.instructor_notes = markdown_to_html(context.lesson.instructor_notes)
|
||||
|
||||
context.show_lesson = (
|
||||
context.membership
|
||||
or (context.lesson and context.lesson.include_in_preview)
|
||||
|
||||
Reference in New Issue
Block a user