diff --git a/README.md b/README.md
index be054c583..6b06f4769 100644
--- a/README.md
+++ b/README.md
@@ -23,10 +23,10 @@
## Frappe Learning
Frappe Learning is an easy-to-use learning system that helps you bring structure to your content.
-## Motivation
+### Motivation
In 2021, we were looking for a Learning Management System to launch [Mon.School](https://mon.school) for FOSS United. We checked out Moodle, but it didn’t feel right. The forms were unnecessarily lengthy and the UI was confusing. It shouldn't be this hard to create a course right? So I started making a learning system for Mon.School which soon became a product in itself. The aim is to have a simple platform that anyone can use to launch a course of their own and make knowledge sharing easier.
-## Key Features
+### Key Features
- **Structured Learning**: Design a course with a 3-level hierarchy, where your courses have chapters and you can group your lessons within these chapters. This ensures that the context of the lesson is set by the chapter.
@@ -67,7 +67,7 @@ In 2021, we were looking for a Learning Management System to launch [Mon.School]
-## Under the Hood
+### Under the Hood
- [**Frappe Framework**](https://github.com/frappe/frappe): A full-stack web application framework.
diff --git a/frontend/src/components/CourseCard.vue b/frontend/src/components/CourseCard.vue
index 50673e82e..790153c4e 100644
--- a/frontend/src/components/CourseCard.vue
+++ b/frontend/src/components/CourseCard.vue
@@ -59,7 +59,7 @@
diff --git a/frontend/src/components/Modals/ExplanationVideos.vue b/frontend/src/components/Modals/ExplanationVideos.vue
index b6e7ceef4..2f362cb49 100644
--- a/frontend/src/components/Modals/ExplanationVideos.vue
+++ b/frontend/src/components/Modals/ExplanationVideos.vue
@@ -26,7 +26,7 @@ const props = defineProps({
required: true,
},
title: {
- type: String,
+ type: [String, null],
required: true,
},
})
diff --git a/frontend/src/pages/LessonForm.vue b/frontend/src/pages/LessonForm.vue
index ed83bf049..ee1aee23f 100644
--- a/frontend/src/pages/LessonForm.vue
+++ b/frontend/src/pages/LessonForm.vue
@@ -132,6 +132,7 @@ const renderEditor = (holder) => {
holder: holder,
tools: getEditorTools(true),
autofocus: true,
+ defaultBlock: 'markdownParser',
})
}
diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js
index d0d9e8a78..789005dcd 100644
--- a/frontend/src/utils/index.js
+++ b/frontend/src/utils/index.js
@@ -13,8 +13,6 @@ import dayjs from '@/utils/dayjs'
import Embed from '@editorjs/embed'
import SimpleImage from '@editorjs/simple-image'
import Table from '@editorjs/table'
-import MDParser from 'editorjs-md-parser'
-import MDImporter from 'editorjs-md-parser'
export function createToast(options) {
toast({
@@ -150,7 +148,12 @@ export function htmlToText(html) {
export function getEditorTools() {
return {
- header: Header,
+ header: {
+ class: Header,
+ config: {
+ placeholder: 'Header',
+ },
+ },
quiz: Quiz,
upload: Upload,
markdownParser: MarkdownParser,
@@ -184,7 +187,7 @@ export function getEditorTools() {
},
embed: {
class: Embed,
- inlineToolbar: false,
+ inlineToolbar: true,
config: {
services: {
youtube: {
diff --git a/frontend/src/utils/markdownParser.js b/frontend/src/utils/markdownParser.js
index 2d8dbcb99..64b383bc3 100644
--- a/frontend/src/utils/markdownParser.js
+++ b/frontend/src/utils/markdownParser.js
@@ -1,143 +1,151 @@
export class MarkdownParser {
constructor({ data, api, readOnly, config }) {
- console.log('markdownParser constructor called')
this.api = api
this.data = data || {}
this.config = config || {}
- this.text = this.data.text || ''
+ this.text = data.text || ''
this.readOnly = readOnly
}
- static get toolbox() {
- const app = createApp({
- render: () =>
- h(UploadIcon, { size: 18, strokeWidth: 1.5, color: 'black' }),
- })
-
- const div = document.createElement('div')
- app.mount(div)
+ static get isReadOnlySupported() {
+ return true
+ }
+ static get conversionConfig() {
return {
- title: 'Upload',
- icon: div.innerHTML,
+ export: 'text',
+ import: 'text',
}
}
- static get isReadOnlySupported() {
- return true
- }
-
render() {
- console.log(' render() called')
- const container = document.createElement('div')
- container.contentEditable = true // Make the div editable like a textarea
- container.classList.add('markdown-parser')
- container.textContent = this.text
-
- container.addEventListener('blur', () => {
- this.text = container.textContent.trim()
- this.parseMarkdown()
- })
-
- this.textArea = container
- return container
+ this.wrapper = document.createElement('div')
+ this.wrapper.classList.add('cdx-block')
+ this.wrapper.classList.add('ce-paragraph')
+ this.wrapper.innerHTML = this.text
+
+ if (!this.readOnly) {
+ this.wrapper.contentEditable = true
+ this.wrapper.innerHTML = this.text
+
+ this.wrapper.addEventListener('keydown', (event) => {
+ const value = event.target.textContent
+ if (event.keyCode === 32 && value.startsWith('#')) {
+ this.convertToHeader(event, value)
+ } else if (event.keyCode === 13) {
+ this.parseContent(event)
+ }
+ })
+
+ this.wrapper.addEventListener('paste', (event) =>
+ this.handlePaste(event)
+ )
+ }
+
+ return this.wrapper
}
- save(blockContent) {
- return {
- text: this.text,
+ convertToHeader(event, value) {
+ event.preventDefault()
+ if (['#', '##', '###', '####', '#####', '######'].includes(value)) {
+ let level = value.length
+ event.target.textContent = ''
+ this.convertBlock('header', {
+ level: level,
+ })
}
}
- /**
- * Parse Markdown text and render Editor.js blocks.
- */
- parseMarkdown() {
- console.log(' parseMarkdown() called')
- const markdown = this.text
- const lines = markdown.split('\n')
-
- const blocks = lines.map((line) => {
- if (line.startsWith('# ')) {
- return {
- type: 'header',
- data: { text: line.replace('# ', ''), level: 1 },
- }
- } else if (line.startsWith('## ')) {
- return {
- type: 'header',
- data: { text: line.replace('## ', ''), level: 2 },
- }
- } else if (line.startsWith('- ')) {
- return {
- type: 'list',
- data: {
- items: [line.replace('- ', '')],
- style: 'unordered',
+ parseContent(event) {
+ event.preventDefault()
+ const previousLine = this.wrapper.textContent
+ if (previousLine && this.hasImage(previousLine)) {
+ this.wrapper.textContent = ''
+ this.convertBlock('image')
+ } else if (previousLine && this.hasLink(previousLine)) {
+ const { text, url } = this.extractLink(previousLine)
+ const anchorTag = `${text}`
+ this.convertBlock('paragraph', {
+ text: previousLine.replace(/\[.+?\]\(.+?\)/, anchorTag),
+ })
+ } else if (previousLine && previousLine.startsWith('- ')) {
+ this.convertBlock('list', {
+ style: 'unordered',
+ items: [
+ {
+ content: previousLine.replace('- ', ''),
},
- }
- } else if (this.isImage(line)) {
- const { alt, url } = this.extractImage(line)
- return {
- type: 'image',
- data: {
- file: { url },
- caption: alt,
- withBorder: false,
- stretched: false,
- withBackground: false,
+ ],
+ })
+ } else if (previousLine && previousLine.startsWith('1. ')) {
+ this.convertBlock('list', {
+ style: 'ordered',
+ items: [
+ {
+ content: previousLine.replace('1. ', ''),
},
- }
- } else if (this.isLink(line)) {
- const { text, url } = this.extractLink(line)
- return {
- type: 'linkTool',
- data: { link: url, meta: { title: text } },
- }
- } else {
- return { type: 'paragraph', data: { text: line } }
- }
- })
+ ],
+ })
+ } else if (previousLine && this.canBeEmbed(previousLine)) {
+ this.wrapper.textContent = ''
+ this.convertBlock('embed', {
+ source: previousLine,
+ })
+ }
+ }
- this.api.blocks.render({ blocks })
+ async convertBlock(type, data, index = null) {
+ const currentIndex = this.api.blocks.getCurrentBlockIndex()
+ const currentBlock = this.api.blocks.getBlockByIndex(currentIndex)
+ await this.api.blocks.convert(currentBlock.id, type, data)
+ this.api.caret.focus(true)
}
- /**
- * Check if the line matches the image syntax.
- * @param {string} line - The line of text.
- * @returns {boolean}
- */
- isImage(line) {
- return /^!\[.*\]\(.*\)$/.test(line)
+ handlePaste(event) {
+ event.preventDefault()
+
+ const clipboardData = event.clipboardData || window.clipboardData
+ const pastedText = clipboardData.getData('text/plain')
+ const sanitizedText = this.processPastedContent(pastedText)
+ document.execCommand('insertText', false, sanitizedText)
+ }
+
+ processPastedContent(text) {
+ return text.trim()
+ }
+
+ save(blockContent) {
+ return {
+ text: blockContent.innerHTML,
+ }
+ }
+
+ hasImage(line) {
+ return /!\[.+?\]\(.+?\)/.test(line)
}
- /**
- * Extract alt text and URL from the image syntax.
- * @param {string} line - The line of text.
- * @returns {Object} { alt, url }
- */
extractImage(line) {
- const match = line.match(/^!\[(.*)\]\((.*)\)$/)
- return { alt: match[1], url: match[2] }
+ const match = line.match(/!\[(.+?)\]\((.+?)\)/)
+ if (match) {
+ return { alt: match[1], url: match[2] }
+ }
+ return { alt: '', url: '' }
}
- /**
- * Check if the line matches the link syntax.
- * @param {string} line - The line of text.
- * @returns {boolean}
- */
- isLink(line) {
- return /^\[.*\]\(.*\)$/.test(line)
+ hasLink(line) {
+ return /\[.+?\]\(.+?\)/.test(line)
}
- /**
- * Extract text and URL from the link syntax.
- * @param {string} line - The line of text.
- * @returns {Object} { text, url }
- */
extractLink(line) {
- const match = line.match(/^\[(.*)\]\((.*)\)$/)
- return { text: match[1], url: match[2] }
+ const match = line.match(/\[(.+?)\]\((.+?)\)/)
+ if (match) {
+ return { text: match[1], url: match[2] }
+ }
+ return { text: '', url: '' }
+ }
+
+ canBeEmbed(line) {
+ return /^https?:\/\/.+/.test(line)
}
}