Add image upload

This commit is contained in:
Lars Westermann 2019-05-30 12:32:40 +02:00
parent 997f374fe4
commit 67f24adfdf
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
24 changed files with 614 additions and 92 deletions

View file

@ -5,9 +5,13 @@ import de.kif.frontend.launch
import de.kif.frontend.repository.PostRepository
import de.westermann.kobserve.event.subscribe
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLTextAreaElement
import org.w3c.dom.events.EventListener
import org.w3c.dom.get
import org.w3c.files.File
import org.w3c.files.FileReader
import org.w3c.files.get
import kotlin.browser.document
import kotlin.dom.clear
@ -50,6 +54,7 @@ fun initPosts() {
}
fun initPostEdit() {
// Content preview
val textArea = document.getElementById("content") as HTMLTextAreaElement
val preview = document.getElementsByClassName("post-edit-right")[0] as HTMLElement
@ -67,4 +72,45 @@ fun initPostEdit() {
launch {
preview.innerHTML = PostRepository.render(textArea.value)
}
// Image preview
val imageView = document.getElementsByClassName("post-edit-image")[0] as HTMLElement
val uploadButton = document.getElementById("image") as HTMLInputElement
val deleteSwitch = document.getElementById("image-delete") as? HTMLInputElement
var file: File? = null
val original = imageView.style.backgroundImage
fun updateImage() {
val deleteState = deleteSwitch?.checked == true
val f = file
when {
deleteState -> {
imageView.removeAttribute("style")
uploadButton.value = ""
file = null
}
f == null -> imageView.style.backgroundImage = original
else -> {
val reader = FileReader()
reader.onload = {
val dataUrl = it.target.asDynamic().result as String
imageView.style.backgroundImage = "url(\"$dataUrl\")"
Unit
}
reader.readAsDataURL(f)
}
}
}
uploadButton.addEventListener("change", EventListener {
val files = uploadButton.files ?: return@EventListener
file = files[0]
updateImage()
})
deleteSwitch?.addEventListener("change", EventListener {
updateImage()
})
}

View file

@ -1,5 +1,6 @@
package de.kif.frontend.views.overview
import de.kif.common.formatDate
import de.kif.frontend.launch
import de.kif.frontend.repository.PostRepository
import de.westermann.kobserve.event.emit
@ -25,37 +26,44 @@ class PostView(
}
private val nameView: Link
private val contentView: View
private val contentView: HTMLElement
private val footerView: HTMLElement
private val imageView: HTMLElement
private fun reload() {
launch {
val p = PostRepository.get(postId) ?: return@launch
classList["post-no-image"] = p.image == null
nameView.text = p.name
nameView.target = "/p/${p.url}"
pinned = p.pinned
contentView.html.innerHTML = PostRepository.htmlByUrl(p.url)
if (p.image == null) {
imageView.removeAttribute("style")
} else {
imageView.style.backgroundImage = "url(\"/images/${p.image}\")"
}
contentView.innerHTML = PostRepository.htmlByUrl(p.url)
footerView.innerText = formatDate(p.createdAt)
emit(PostChangeEvent(postId))
}
}
init {
val nameHtml = view.getElementsByClassName("post-name")[0]
nameView = nameHtml?.let { Link.wrap(it as HTMLAnchorElement) } ?: Link().also {
html.appendChild(it.html)
it.classList += "post-name"
}
nameView = Link.wrap(view.getByClassOrCreate("post-name"))
// val editHtml = view.getElementsByClassName("post-edit")[0]
// editView = editHtml?.let { Link.wrap(it as HTMLAnchorElement) } ?: Link()
val postColumn = view.getByClassOrCreate<HTMLElement>("post-column")
val postColumnLeft = postColumn.getByClassOrCreate<HTMLElement>("post-column-left")
val postColumnRight = postColumn.getByClassOrCreate<HTMLElement>("post-column-right")
val contentHtml = view.getElementsByClassName("post-content")[0]
contentView = contentHtml?.let { wrap(it as HTMLElement) } ?: wrap(createHtmlView()).also {
html.appendChild(it.html)
it.classList += "post-content"
}
imageView = postColumnLeft.getByClassOrCreate("post-image", "figure")
contentView = postColumnRight.getByClassOrCreate("post-content")
footerView = postColumnRight.getByClassOrCreate("post-footer")
PostRepository.onUpdate {
if (it == postId) {
@ -69,7 +77,7 @@ class PostView(
companion object {
fun create(postId: Long): PostView {
val div = document.createElement("div") as HTMLElement
val div = createHtmlView<HTMLElement>()
div.classList.add("post")
div.dataset["id"] = postId.toString()
return PostView(div).also(PostView::reload)
@ -78,3 +86,16 @@ class PostView(
}
data class PostChangeEvent(val id: Long)
inline fun <reified T : HTMLElement> HTMLElement.getByClassOrCreate(name: String, newTagName: String? = null): T {
val v = this.getElementsByClassName(name)[0] as? T
if (v != null) {
return v
}
val h = createHtmlView<T>(newTagName)
h.classList.add(name)
this.appendChild(h)
return h
}

View file

@ -11,6 +11,7 @@ $background-primary-color: #fff;
$background-secondary-color: #fcfcfc;
$text-primary-color: #333;
$text-secondary-color: rgba($text-primary-color, 0.5);
$primary-color: #B11D33;
$primary-text-color: #fff;
@ -18,7 +19,9 @@ $primary-text-color: #fff;
$error-color: #D55225;
$error-text-color: #fff;
$border-color: #888;
$input-border-color: #888;
$table-border-color: rgba($text-primary-color, 0.1);
$table-header-color: rgba($text-primary-color, 0.06);
$transitionTime: 150ms;
@ -40,6 +43,10 @@ body, html {
height: 100%;
margin: 0;
padding: 0;
& > *:last-child {
margin-bottom: 1rem;
}
}
.no-select {
@ -128,7 +135,7 @@ a {
}
&:hover {
background-color: rgba($primary-text-color, 0.1);
background-color: $table-border-color;
&::after {
background: $primary-color;
@ -165,8 +172,8 @@ a {
position: absolute;
background-color: $background-secondary-color;
z-index: 5;
left: 1rem;
right: 1rem;
left: 0;
right: 0;
&::before {
content: '';
@ -183,7 +190,11 @@ a {
line-height: 3rem;
&:after {
bottom: 0.2rem;
bottom: 0.55rem;
}
&:last-child {
margin-bottom: 0.5rem;
}
}
}
@ -220,7 +231,7 @@ a {
}
.main {
padding: 0 1rem;
//padding: 0 1rem;
}
.table-layout-search {
@ -252,14 +263,14 @@ a {
}
tr {
border-top: solid 1px rgba($text-primary-color, 0.1);
border-top: solid 1px $table-border-color;
&:nth-child(odd) {
//background-color: rgba($text-primary-color, 0.01);
}
&:first-child {
background-color: rgba($text-primary-color, 0.06);
background-color: $table-header-color;
height: 2.5rem;
line-height: 2.5rem;
}
@ -269,7 +280,7 @@ a {
line-height: 2rem;
&:hover {
background-color: rgba($text-primary-color, 0.06);
background-color: $table-header-color;
}
}
}
@ -292,20 +303,20 @@ a {
z-index: 1;
background: $background-primary-color;
width: 100%;
border: solid 1px rgba($text-primary-color, 0.1);
border: solid 1px $table-border-color;
span {
padding: 0 0.5rem;
&:hover {
background-color: rgba($text-primary-color, 0.06);
background-color: $table-header-color;
}
}
}
}
.form-control {
border: solid 1px $border-color;
border: solid 1px $input-border-color;
outline: none;
padding: 0 1rem;
line-height: 2.5rem;
@ -412,7 +423,7 @@ select:-moz-focusring {
}
.form-btn {
border: solid 1px $border-color;
border: solid 1px $input-border-color;
outline: none;
padding: 0 1rem;
line-height: 2rem;
@ -588,7 +599,7 @@ form {
.calendar[data-editable = "true"].edit {
.calendar-table {
width: calc(100% - 16rem);
border-right: solid 1px rgba($text-primary-color, 0.1);
border-right: solid 1px $table-border-color;
}
.calendar-edit-main {
@ -608,8 +619,8 @@ form {
display: none;
z-index: 10;
border: solid 1px $border-color;
box-shadow: 0 0.1rem 0.2rem rgba($primary-text-color);
border: solid 1px $input-border-color;
box-shadow: 0 0.1rem 0.2rem $primary-text-color;
a {
padding: 0.2rem;
@ -707,14 +718,14 @@ form {
height: 100%;
left: 0;
top: 0;
border-left: solid 1px rgba($text-primary-color, 0.1);
border-left: solid 1px $table-border-color;
position: absolute;
}
}
}
.calendar-row {
border-top: solid 1px rgba($text-primary-color, 0.1);
border-top: solid 1px $table-border-color;
line-height: 3rem;
height: 3rem;
@ -727,12 +738,12 @@ form {
height: 100%;
left: 0;
top: 0;
border-left: solid 1px rgba($text-primary-color, 0.1);
border-left: solid 1px $table-border-color;
position: absolute;
}
&:hover {
background-color: rgba($text-primary-color, 0.06);
background-color: $table-header-color;
}
.calendar-entry {
@ -749,7 +760,7 @@ form {
width: 6rem;
left: 0;
text-align: center;
border-right: solid 1px rgba($text-primary-color, 0.1);
border-right: solid 1px $table-border-color;
}
.calendar-link {
@ -780,7 +791,7 @@ form {
height: 100%;
left: 0;
top: 0;
border-left: solid 1px rgba($text-primary-color, 0.1);
border-left: solid 1px $table-border-color;
position: absolute;
}
}
@ -800,12 +811,12 @@ form {
width: 100%;
left: 0;
top: 0;
border-left: solid 1px rgba($text-primary-color, 0.1);
border-left: solid 1px $table-border-color;
position: absolute;
}
&:hover {
background-color: rgba($text-primary-color, 0.06);
background-color: $table-header-color;
}
.calendar-entry {
@ -826,7 +837,7 @@ form {
}
&:nth-child(4n + 2) .calendar-cell::before {
border-top: solid 1px rgba($text-primary-color, 0.1);
border-top: solid 1px $table-border-color;
}
}
@ -939,13 +950,13 @@ form {
display: none;
background: $background-primary-color;
border: solid 1px rgba($text-primary-color, 0.1);
border: solid 1px $table-border-color;
span {
padding: 0 0.5rem;
&:hover {
background-color: rgba($text-primary-color, 0.06);
background-color: $table-header-color;
}
}
@ -956,11 +967,13 @@ form {
.overview {
display: flex;
flex-direction: column;
}
.overview-main {
flex-grow: 4;
margin-right: 1rem;
width: 100%;
}
.overview-side {
@ -985,6 +998,7 @@ form {
font-size: 1.2rem;
color: $primary-color;
line-height: 2rem;
padding: 0 1rem;
&:empty::before {
display: block;
@ -999,11 +1013,30 @@ form {
.post-edit {
position: absolute;
top: 0;
right: 0;
right: 1rem;
line-height: 2rem;
}
.post-column {
display: flex;
flex-direction: column;
}
.post-image {
width: 100%;
margin: 0.4rem 0 0;
padding-top: 75%;
background-position: center;
background-size: cover;
}
.post-no-image .post-column-left {
display: none;
}
.post-content {
padding: 0 1rem;
h1, h2, h3, h4, h5, h6, p {
margin: 0.7rem 0;
padding: 0;
@ -1039,11 +1072,11 @@ form {
}
td {
border-top: solid 1px rgba($text-primary-color, 0.1);
border-top: solid 1px $table-border-color;
}
th {
background-color: rgba($text-primary-color, 0.06);
background-color: $table-header-color;
}
td, th {
@ -1051,6 +1084,14 @@ form {
}
}
.post-footer {
color: $text-secondary-color;
padding: 0 1rem;
font-size: 0.8rem;
font-weight: 400;
margin-top: -0.3rem;
}
.post-edit-container {
display: flex;
flex-direction: column;
@ -1060,29 +1101,84 @@ form {
margin-left: 0;
}
.post-edit-image-box {
width: 100%;
display: flex;
margin-top: 0.4rem;
& > div:nth-child(1) {
width: 20%;
}
& > div:nth-child(2) {
width: 80%;
padding-left: 1rem;
}
}
.post-edit-image {
width: 100%;
padding-top: 75%;
border: solid 1px $input-border-color;
margin: 0;
background-color: $background-primary-color;
background-position: center;
background-size: cover;
}
.overview-post {
margin-bottom: 2rem;
&::after {
content: '';
position: absolute;
display: block;
bottom: -1rem;
height: 1px;
left: 0;
right: 0;
background: $table-border-color;
}
}
@media (min-width: 768px) {
.post-column {
padding: 0 1rem;
}
.overview-post::after {
left: 1rem;
right: 1rem;
}
.post-content, .post-footer {
padding: 0;
}
.overview-post {
.post-column {
flex-direction: row;
}
.post-column-left {
width: 30%;
margin-right: 1rem;
}
.post-column-right {
width: 70%;
}
}
}
@media (min-width: 992px) {
.post-edit-container {
flex-direction: row;
}
.post-edit-right {
margin-left: 2rem;
}
}
/*
.overview-post {
max-height: 20rem;
overflow: hidden;
margin-bottom: 1rem;
&::after {
content: '';
position: absolute;
display: block;
height: 1.5rem;
top: 18.5rem;
width: 100%;
background: linear-gradient(0deg, $background-primary-color, transparent);
.overview {
flex-direction: row;
}
}
*/