Add live preview for posts

This commit is contained in:
Lars Westermann 2019-05-27 14:45:34 +02:00
parent 74c297263e
commit 32596228fe
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
11 changed files with 241 additions and 152 deletions

View file

@ -88,8 +88,6 @@ kotlin {
implementation "de.westermann:KObserve-jvm:$observable_version" 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' implementation 'com.vladsch.flexmark:flexmark-all:0.42.10'
api 'io.github.microutils:kotlin-logging:1.6.23' api 'io.github.microutils:kotlin-logging:1.6.23'
api 'ch.qos.logback:logback-classic:1.2.3' api 'ch.qos.logback:logback-classic:1.2.3'

View file

@ -4,6 +4,7 @@ import de.kif.frontend.views.calendar.initCalendar
import de.kif.frontend.views.table.initTableLayout import de.kif.frontend.views.table.initTableLayout
import de.kif.frontend.views.initWorkGroupConstraints import de.kif.frontend.views.initWorkGroupConstraints
import de.kif.frontend.views.overview.initOverviewMain import de.kif.frontend.views.overview.initOverviewMain
import de.kif.frontend.views.overview.initPostEdit
import de.kif.frontend.views.overview.initPosts import de.kif.frontend.views.overview.initPosts
import de.westermann.kwebview.components.init import de.westermann.kwebview.components.init
import kotlin.browser.document import kotlin.browser.document
@ -26,4 +27,7 @@ fun main() = init {
if (document.getElementsByClassName("post").length > 0) { if (document.getElementsByClassName("post").length > 0) {
initPosts() initPosts()
} }
if (document.getElementsByClassName("post-edit-right").length > 0) {
initPostEdit()
}
} }

View file

@ -45,6 +45,10 @@ object PostRepository : Repository<Post> {
return repositoryRawGet("/api/p/$url") return repositoryRawGet("/api/p/$url")
} }
suspend fun render(data: String): String {
return repositoryPost("/api/render", data)
}
val handler = object : MessageHandler(RepositoryType.POST) { val handler = object : MessageHandler(RepositoryType.POST) {
override fun onCreate(id: Long) = onCreate.emit(id) override fun onCreate(id: Long) = onCreate.emit(id)

View file

@ -1,8 +1,11 @@
package de.kif.frontend.views.overview package de.kif.frontend.views.overview
import de.kif.frontend.iterator import de.kif.frontend.iterator
import de.kif.frontend.launch
import de.kif.frontend.repository.PostRepository import de.kif.frontend.repository.PostRepository
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLTextAreaElement
import org.w3c.dom.events.EventListener
import org.w3c.dom.get import org.w3c.dom.get
import kotlin.browser.document import kotlin.browser.document
@ -38,4 +41,24 @@ fun initPosts() {
for (post in postList) { for (post in postList) {
PostView(post) 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)
}
} }

View file

@ -1001,6 +1001,7 @@ form {
color: $text-primary-color; color: $text-primary-color;
} }
} }
.post-edit { .post-edit {
position: absolute; position: absolute;
top: 0; top: 0;
@ -1013,26 +1014,68 @@ form {
margin: 0.7rem 0; margin: 0.7rem 0;
padding: 0; padding: 0;
} }
h1 { h1 {
font-size: 1rem; font-size: 1rem;
} }
h2 { h2 {
font-size: 1rem; font-size: 1rem;
} }
h3 { h3 {
font-size: 1rem; font-size: 1rem;
} }
h4 { h4 {
font-size: 1rem; font-size: 1rem;
} }
h5 { h5 {
font-size: 1rem; font-size: 1rem;
} }
h6 { h6 {
font-size: 1rem; 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 { .overview-post {
max-height: 20rem; max-height: 20rem;
overflow: hidden; overflow: hidden;
@ -1047,4 +1090,5 @@ form {
width: 100%; width: 100%;
background: linear-gradient(0deg, $background-primary-color, transparent); background: linear-gradient(0deg, $background-primary-color, transparent);
} }
} }
*/

View file

@ -12,9 +12,7 @@ import io.ktor.http.content.static
import io.ktor.jackson.jackson import io.ktor.jackson.jackson
import io.ktor.routing.routing import io.ktor.routing.routing
import io.ktor.websocket.WebSockets import io.ktor.websocket.WebSockets
import kotlinx.serialization.ImplicitReflectionSerializer
@ImplicitReflectionSerializer
fun Application.main() { fun Application.main() {
install(DefaultHeaders) install(DefaultHeaders)
install(CallLogging) install(CallLogging)

View file

@ -8,10 +8,8 @@ import io.ktor.application.Application
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ImplicitReflectionSerializer
object Main { object Main {
@ImplicitReflectionSerializer
@Suppress("UnusedMainParameter") @Suppress("UnusedMainParameter")
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {

View file

@ -136,63 +136,70 @@ fun Route.overview() {
} }
content { content {
h1 { +"Edit post" } h1 { +"Edit post" }
form(method = FormMethod.post) { div("post-edit-container") {
div("form-group") { div("post-edit-left") {
label { form(method = FormMethod.post) {
htmlFor = "name" div("form-group") {
+"Name" label {
} htmlFor = "name"
input( +"Name"
name = "name", }
classes = "form-control" input(
) { name = "name",
id = "name" classes = "form-control"
placeholder = "Name" ) {
value = editPost.name id = "name"
} placeholder = "Name"
} value = editPost.name
div("form-group") { }
label { }
htmlFor = "url" div("form-group") {
+"Url" label {
} htmlFor = "url"
input( +"Url"
name = "url", }
classes = "form-control" input(
) { name = "url",
id = "places" classes = "form-control"
placeholder = "Places" ) {
value = editPost.url id = "places"
} placeholder = "Places"
} value = editPost.url
}
}
div("form-group") { div("form-group") {
label { label {
htmlFor = "content" htmlFor = "content"
+"Content" +"Content"
} }
textArea(rows = "10", classes = "form-control") { textArea(rows = "10", classes = "form-control") {
name = "content" name = "content"
id = "projector" id = "content"
+editPost.content +editPost.content
} }
} }
div("form-group") { div("form-group") {
a("/") { a("/") {
button(classes = "form-btn") { button(classes = "form-btn") {
+"Cancel" +"Cancel"
}
}
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
+"Save"
}
} }
} }
button(type = ButtonType.submit, classes = "form-btn btn-primary") { a("/post/${editPost.id}/delete") {
+"Save" button(classes = "form-btn btn-danger") {
+"Delete"
}
} }
} }
} div("post-edit-right post-content") {
a("/post/${editPost.id}/delete") {
button(classes = "form-btn btn-danger") {
+"Delete"
} }
} }
} }
@ -227,58 +234,65 @@ fun Route.overview() {
} }
content { content {
h1 { +"Create post" } h1 { +"Create post" }
form(method = FormMethod.post) { div("post-edit-container") {
div("form-group") { div("post-edit-left") {
label { form(method = FormMethod.post) {
htmlFor = "name" div("form-group") {
+"Name" label {
} htmlFor = "name"
input( +"Name"
name = "name", }
classes = "form-control" input(
) { name = "name",
id = "name" classes = "form-control"
placeholder = "Name" ) {
value = "" id = "name"
} placeholder = "Name"
} value = ""
div("form-group") { }
label { }
htmlFor = "url" div("form-group") {
+"Url" label {
} htmlFor = "url"
input( +"Url"
name = "url", }
classes = "form-control" input(
) { name = "url",
id = "places" classes = "form-control"
placeholder = "Places" ) {
value = Post.generateUrl() id = "places"
} placeholder = "Places"
} value = Post.generateUrl()
}
}
div("form-group") { div("form-group") {
label { label {
htmlFor = "content" htmlFor = "content"
+"Content" +"Content"
} }
textArea(rows = "10", classes = "form-control") { textArea(rows = "10", classes = "form-control") {
name = "content" name = "content"
id = "projector" id = "content"
+"" +""
} }
} }
div("form-group") { div("form-group") {
a("/") { a("/") {
button(classes = "form-btn") { button(classes = "form-btn") {
+"Cancel" +"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") {
}
} }
} }
} }

View file

@ -9,7 +9,7 @@ import io.ktor.application.call
import io.ktor.html.respondHtml import io.ktor.html.respondHtml
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.request.receive import io.ktor.request.receive
import io.ktor.response.respond import io.ktor.request.receiveText
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.routing.get import io.ktor.routing.get
import io.ktor.routing.post import io.ktor.routing.post
@ -58,7 +58,7 @@ fun Route.postApi() {
post("/api/post/{id}") { post("/api/post/{id}") {
try { try {
authenticate { authenticate(Permission.POST) {
val id = call.parameters["id"]?.toLongOrNull() val id = call.parameters["id"]?.toLongOrNull()
val post = call.receive<Post>().copy(id = id) val post = call.receive<Post>().copy(id = id)
@ -77,7 +77,7 @@ fun Route.postApi() {
} }
post("/api/post/{id}/delete") { post("/api/post/{id}/delete") {
try { try {
authenticate { authenticate(Permission.POST) {
val id = call.parameters["id"]?.toLongOrNull() val id = call.parameters["id"]?.toLongOrNull()
if (id != null) { if (id != null) {
@ -112,4 +112,18 @@ fun Route.postApi() {
call.error(HttpStatusCode.InternalServerError) 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)
}
}
} }

View file

@ -2,59 +2,51 @@ package de.kif.backend.util
import com.vladsch.flexmark.ext.autolink.AutolinkExtension import com.vladsch.flexmark.ext.autolink.AutolinkExtension
import com.vladsch.flexmark.ext.emoji.EmojiExtension 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.strikethrough.StrikethroughExtension
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension 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.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.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.options.MutableDataSet; import com.vladsch.flexmark.util.options.MutableDataSet;
import java.util.* import java.util.*
/* object ParseMarkdown {
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
import org.intellij.markdown.html.HtmlGenerator
import org.intellij.markdown.parser.MarkdownParser
fun markdownToHtml(content: String): String { private val parser: Parser
val flavour = CommonMarkFlavourDescriptor() private val renderer: HtmlRenderer
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
fun markdownToHtml(content: String): String { fun parse(content: String): String {
val parser = Parser.builder().build() val document = parser.parse(content) ?: return ""
val document = parser.parse(content) return renderer.render(document) ?: ""
val renderer = HtmlRenderer.builder().build() }
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 = ParseMarkdown.parse(content)
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
}

View file

@ -51,4 +51,4 @@ fun Route.pushService() {
UserRepository.registerPushService() UserRepository.registerPushService()
WorkGroupRepository.registerPushService() WorkGroupRepository.registerPushService()
PostRepository.registerPushService() PostRepository.registerPushService()
} }