Add live preview for posts
This commit is contained in:
parent
74c297263e
commit
32596228fe
|
@ -88,8 +88,6 @@ kotlin {
|
|||
|
||||
implementation "de.westermann:KObserve-jvm:$observable_version"
|
||||
|
||||
//api 'org.jetbrains:markdown:0.1.28'
|
||||
//implementation 'com.atlassian.commonmark:commonmark:0.12.1'
|
||||
implementation 'com.vladsch.flexmark:flexmark-all:0.42.10'
|
||||
api 'io.github.microutils:kotlin-logging:1.6.23'
|
||||
api 'ch.qos.logback:logback-classic:1.2.3'
|
||||
|
|
|
@ -4,6 +4,7 @@ import de.kif.frontend.views.calendar.initCalendar
|
|||
import de.kif.frontend.views.table.initTableLayout
|
||||
import de.kif.frontend.views.initWorkGroupConstraints
|
||||
import de.kif.frontend.views.overview.initOverviewMain
|
||||
import de.kif.frontend.views.overview.initPostEdit
|
||||
import de.kif.frontend.views.overview.initPosts
|
||||
import de.westermann.kwebview.components.init
|
||||
import kotlin.browser.document
|
||||
|
@ -26,4 +27,7 @@ fun main() = init {
|
|||
if (document.getElementsByClassName("post").length > 0) {
|
||||
initPosts()
|
||||
}
|
||||
if (document.getElementsByClassName("post-edit-right").length > 0) {
|
||||
initPostEdit()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,10 @@ object PostRepository : Repository<Post> {
|
|||
return repositoryRawGet("/api/p/$url")
|
||||
}
|
||||
|
||||
suspend fun render(data: String): String {
|
||||
return repositoryPost("/api/render", data)
|
||||
}
|
||||
|
||||
val handler = object : MessageHandler(RepositoryType.POST) {
|
||||
|
||||
override fun onCreate(id: Long) = onCreate.emit(id)
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package de.kif.frontend.views.overview
|
||||
|
||||
import de.kif.frontend.iterator
|
||||
import de.kif.frontend.launch
|
||||
import de.kif.frontend.repository.PostRepository
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.HTMLTextAreaElement
|
||||
import org.w3c.dom.events.EventListener
|
||||
import org.w3c.dom.get
|
||||
import kotlin.browser.document
|
||||
|
||||
|
@ -39,3 +42,23 @@ fun initPosts() {
|
|||
PostView(post)
|
||||
}
|
||||
}
|
||||
|
||||
fun initPostEdit() {
|
||||
val textArea = document.getElementById("content") as HTMLTextAreaElement
|
||||
val preview = document.getElementsByClassName("post-edit-right")[0] as HTMLElement
|
||||
|
||||
textArea.addEventListener("change", EventListener {
|
||||
launch {
|
||||
preview.innerHTML = PostRepository.render(textArea.value)
|
||||
}
|
||||
})
|
||||
textArea.addEventListener("keyup", EventListener {
|
||||
launch {
|
||||
preview.innerHTML = PostRepository.render(textArea.value)
|
||||
}
|
||||
})
|
||||
|
||||
launch {
|
||||
preview.innerHTML = PostRepository.render(textArea.value)
|
||||
}
|
||||
}
|
|
@ -1001,6 +1001,7 @@ form {
|
|||
color: $text-primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.post-edit {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -1013,26 +1014,68 @@ form {
|
|||
margin: 0.7rem 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
td {
|
||||
border-top: solid 1px rgba($text-primary-color, 0.1);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: rgba($text-primary-color, 0.06);
|
||||
}
|
||||
|
||||
td, th {
|
||||
padding: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.post-edit-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.post-edit-right {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.post-edit-container {
|
||||
flex-direction: row;
|
||||
}
|
||||
.post-edit-right {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
.overview-post {
|
||||
max-height: 20rem;
|
||||
overflow: hidden;
|
||||
|
@ -1048,3 +1091,4 @@ form {
|
|||
background: linear-gradient(0deg, $background-primary-color, transparent);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -12,9 +12,7 @@ import io.ktor.http.content.static
|
|||
import io.ktor.jackson.jackson
|
||||
import io.ktor.routing.routing
|
||||
import io.ktor.websocket.WebSockets
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
|
||||
@ImplicitReflectionSerializer
|
||||
fun Application.main() {
|
||||
install(DefaultHeaders)
|
||||
install(CallLogging)
|
||||
|
|
|
@ -8,10 +8,8 @@ import io.ktor.application.Application
|
|||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.netty.Netty
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
|
||||
object Main {
|
||||
@ImplicitReflectionSerializer
|
||||
@Suppress("UnusedMainParameter")
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
|
|
|
@ -136,63 +136,70 @@ fun Route.overview() {
|
|||
}
|
||||
content {
|
||||
h1 { +"Edit post" }
|
||||
form(method = FormMethod.post) {
|
||||
div("form-group") {
|
||||
label {
|
||||
htmlFor = "name"
|
||||
+"Name"
|
||||
}
|
||||
input(
|
||||
name = "name",
|
||||
classes = "form-control"
|
||||
) {
|
||||
id = "name"
|
||||
placeholder = "Name"
|
||||
value = editPost.name
|
||||
}
|
||||
}
|
||||
div("form-group") {
|
||||
label {
|
||||
htmlFor = "url"
|
||||
+"Url"
|
||||
}
|
||||
input(
|
||||
name = "url",
|
||||
classes = "form-control"
|
||||
) {
|
||||
id = "places"
|
||||
placeholder = "Places"
|
||||
value = editPost.url
|
||||
}
|
||||
}
|
||||
div("post-edit-container") {
|
||||
div("post-edit-left") {
|
||||
form(method = FormMethod.post) {
|
||||
div("form-group") {
|
||||
label {
|
||||
htmlFor = "name"
|
||||
+"Name"
|
||||
}
|
||||
input(
|
||||
name = "name",
|
||||
classes = "form-control"
|
||||
) {
|
||||
id = "name"
|
||||
placeholder = "Name"
|
||||
value = editPost.name
|
||||
}
|
||||
}
|
||||
div("form-group") {
|
||||
label {
|
||||
htmlFor = "url"
|
||||
+"Url"
|
||||
}
|
||||
input(
|
||||
name = "url",
|
||||
classes = "form-control"
|
||||
) {
|
||||
id = "places"
|
||||
placeholder = "Places"
|
||||
value = editPost.url
|
||||
}
|
||||
}
|
||||
|
||||
div("form-group") {
|
||||
label {
|
||||
htmlFor = "content"
|
||||
+"Content"
|
||||
}
|
||||
textArea(rows = "10", classes = "form-control") {
|
||||
name = "content"
|
||||
id = "projector"
|
||||
div("form-group") {
|
||||
label {
|
||||
htmlFor = "content"
|
||||
+"Content"
|
||||
}
|
||||
textArea(rows = "10", classes = "form-control") {
|
||||
name = "content"
|
||||
id = "content"
|
||||
|
||||
+editPost.content
|
||||
}
|
||||
}
|
||||
+editPost.content
|
||||
}
|
||||
}
|
||||
|
||||
div("form-group") {
|
||||
a("/") {
|
||||
button(classes = "form-btn") {
|
||||
+"Cancel"
|
||||
div("form-group") {
|
||||
a("/") {
|
||||
button(classes = "form-btn") {
|
||||
+"Cancel"
|
||||
}
|
||||
}
|
||||
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
|
||||
+"Save"
|
||||
}
|
||||
}
|
||||
}
|
||||
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
|
||||
+"Save"
|
||||
a("/post/${editPost.id}/delete") {
|
||||
button(classes = "form-btn btn-danger") {
|
||||
+"Delete"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
a("/post/${editPost.id}/delete") {
|
||||
button(classes = "form-btn btn-danger") {
|
||||
+"Delete"
|
||||
div("post-edit-right post-content") {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -227,58 +234,65 @@ fun Route.overview() {
|
|||
}
|
||||
content {
|
||||
h1 { +"Create post" }
|
||||
form(method = FormMethod.post) {
|
||||
div("form-group") {
|
||||
label {
|
||||
htmlFor = "name"
|
||||
+"Name"
|
||||
}
|
||||
input(
|
||||
name = "name",
|
||||
classes = "form-control"
|
||||
) {
|
||||
id = "name"
|
||||
placeholder = "Name"
|
||||
value = ""
|
||||
}
|
||||
}
|
||||
div("form-group") {
|
||||
label {
|
||||
htmlFor = "url"
|
||||
+"Url"
|
||||
}
|
||||
input(
|
||||
name = "url",
|
||||
classes = "form-control"
|
||||
) {
|
||||
id = "places"
|
||||
placeholder = "Places"
|
||||
value = Post.generateUrl()
|
||||
}
|
||||
}
|
||||
div("post-edit-container") {
|
||||
div("post-edit-left") {
|
||||
form(method = FormMethod.post) {
|
||||
div("form-group") {
|
||||
label {
|
||||
htmlFor = "name"
|
||||
+"Name"
|
||||
}
|
||||
input(
|
||||
name = "name",
|
||||
classes = "form-control"
|
||||
) {
|
||||
id = "name"
|
||||
placeholder = "Name"
|
||||
value = ""
|
||||
}
|
||||
}
|
||||
div("form-group") {
|
||||
label {
|
||||
htmlFor = "url"
|
||||
+"Url"
|
||||
}
|
||||
input(
|
||||
name = "url",
|
||||
classes = "form-control"
|
||||
) {
|
||||
id = "places"
|
||||
placeholder = "Places"
|
||||
value = Post.generateUrl()
|
||||
}
|
||||
}
|
||||
|
||||
div("form-group") {
|
||||
label {
|
||||
htmlFor = "content"
|
||||
+"Content"
|
||||
}
|
||||
textArea(rows = "10", classes = "form-control") {
|
||||
name = "content"
|
||||
id = "projector"
|
||||
div("form-group") {
|
||||
label {
|
||||
htmlFor = "content"
|
||||
+"Content"
|
||||
}
|
||||
textArea(rows = "10", classes = "form-control") {
|
||||
name = "content"
|
||||
id = "content"
|
||||
|
||||
+""
|
||||
}
|
||||
}
|
||||
+""
|
||||
}
|
||||
}
|
||||
|
||||
div("form-group") {
|
||||
a("/") {
|
||||
button(classes = "form-btn") {
|
||||
+"Cancel"
|
||||
div("form-group") {
|
||||
a("/") {
|
||||
button(classes = "form-btn") {
|
||||
+"Cancel"
|
||||
}
|
||||
}
|
||||
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
|
||||
+"Create"
|
||||
}
|
||||
}
|
||||
}
|
||||
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
|
||||
+"Create"
|
||||
}
|
||||
}
|
||||
div("post-edit-right post-content") {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import io.ktor.application.call
|
|||
import io.ktor.html.respondHtml
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.request.receive
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.request.receiveText
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.routing.get
|
||||
import io.ktor.routing.post
|
||||
|
@ -58,7 +58,7 @@ fun Route.postApi() {
|
|||
|
||||
post("/api/post/{id}") {
|
||||
try {
|
||||
authenticate {
|
||||
authenticate(Permission.POST) {
|
||||
val id = call.parameters["id"]?.toLongOrNull()
|
||||
val post = call.receive<Post>().copy(id = id)
|
||||
|
||||
|
@ -77,7 +77,7 @@ fun Route.postApi() {
|
|||
}
|
||||
post("/api/post/{id}/delete") {
|
||||
try {
|
||||
authenticate {
|
||||
authenticate(Permission.POST) {
|
||||
val id = call.parameters["id"]?.toLongOrNull()
|
||||
|
||||
if (id != null) {
|
||||
|
@ -112,4 +112,18 @@ fun Route.postApi() {
|
|||
call.error(HttpStatusCode.InternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
post("/api/render") {
|
||||
try {
|
||||
authenticate(Permission.POST) {
|
||||
val data = call.receiveText()
|
||||
val html = markdownToHtml(data)
|
||||
call.success(html)
|
||||
} onFailure {
|
||||
call.error(HttpStatusCode.Unauthorized)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
call.error(HttpStatusCode.InternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,59 +2,51 @@ package de.kif.backend.util
|
|||
|
||||
import com.vladsch.flexmark.ext.autolink.AutolinkExtension
|
||||
import com.vladsch.flexmark.ext.emoji.EmojiExtension
|
||||
import com.vladsch.flexmark.ext.emoji.EmojiImageType
|
||||
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension
|
||||
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
|
||||
import com.vladsch.flexmark.ext.gitlab.GitLabExtension
|
||||
import com.vladsch.flexmark.ext.tables.TablesExtension
|
||||
import com.vladsch.flexmark.util.ast.Node;
|
||||
import com.vladsch.flexmark.ext.typographic.TypographicExtension
|
||||
import com.vladsch.flexmark.ext.youtube.embedded.YouTubeLinkExtension
|
||||
import com.vladsch.flexmark.html.HtmlRenderer;
|
||||
import com.vladsch.flexmark.parser.Parser;
|
||||
import com.vladsch.flexmark.util.options.MutableDataSet;
|
||||
import java.util.*
|
||||
|
||||
/*
|
||||
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
|
||||
import org.intellij.markdown.html.HtmlGenerator
|
||||
import org.intellij.markdown.parser.MarkdownParser
|
||||
object ParseMarkdown {
|
||||
|
||||
fun markdownToHtml(content: String): String {
|
||||
val flavour = CommonMarkFlavourDescriptor()
|
||||
val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(content)
|
||||
val html = HtmlGenerator(content, parsedTree, flavour).generateHtml()
|
||||
return html
|
||||
}
|
||||
*/
|
||||
/*
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
private val parser: Parser
|
||||
private val renderer: HtmlRenderer
|
||||
|
||||
fun markdownToHtml(content: String): String {
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse(content)
|
||||
val renderer = HtmlRenderer.builder().build()
|
||||
return renderer.render(document)
|
||||
fun parse(content: String): String {
|
||||
val document = parser.parse(content) ?: return ""
|
||||
return renderer.render(document) ?: ""
|
||||
}
|
||||
|
||||
init {
|
||||
val options = MutableDataSet()
|
||||
|
||||
options.set(
|
||||
Parser.EXTENSIONS, Arrays.asList(
|
||||
TablesExtension.create(),
|
||||
StrikethroughExtension.create(),
|
||||
TaskListExtension.create(),
|
||||
EmojiExtension.create(),
|
||||
AutolinkExtension.create(),
|
||||
GitLabExtension.create(),
|
||||
TypographicExtension.create(),
|
||||
YouTubeLinkExtension.create()
|
||||
)
|
||||
)
|
||||
|
||||
options.set(EmojiExtension.USE_IMAGE_TYPE, EmojiImageType.UNICODE_ONLY)
|
||||
|
||||
//options.set(HtmlRenderer.SOFT_BREAK, "<br />\n");
|
||||
|
||||
parser = Parser.builder(options).build()
|
||||
renderer = HtmlRenderer.builder(options).build()
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
fun markdownToHtml(content: String): String {
|
||||
val options = MutableDataSet()
|
||||
|
||||
options.set(Parser.EXTENSIONS, Arrays.asList(
|
||||
TablesExtension.create(),
|
||||
StrikethroughExtension.create(),
|
||||
TaskListExtension.create(),
|
||||
EmojiExtension.create(),
|
||||
AutolinkExtension.create()
|
||||
));
|
||||
|
||||
//options.set(HtmlRenderer.SOFT_BREAK, "<br />\n");
|
||||
|
||||
val parser = Parser.builder(options).build()
|
||||
val renderer = HtmlRenderer.builder(options).build()
|
||||
|
||||
// You can re-use parser and renderer instances
|
||||
val document = parser.parse(content)
|
||||
val html = renderer.render(document)
|
||||
|
||||
return html
|
||||
}
|
||||
fun markdownToHtml(content: String): String = ParseMarkdown.parse(content)
|
||||
|
|
Loading…
Reference in a new issue