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"
//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'

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.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()
}
}

View file

@ -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)

View file

@ -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
@ -38,4 +41,24 @@ fun initPosts() {
for (post in postList) {
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;
}
}
.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;
@ -1047,4 +1090,5 @@ form {
width: 100%;
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.routing.routing
import io.ktor.websocket.WebSockets
import kotlinx.serialization.ImplicitReflectionSerializer
@ImplicitReflectionSerializer
fun Application.main() {
install(DefaultHeaders)
install(CallLogging)

View file

@ -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>) {

View file

@ -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") {
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)

View file

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