Skip to content

Commit

Permalink
fix(KtComment): fix/enhance/refact KtComments
Browse files Browse the repository at this point in the history
- fix: stop avatar from getting squished
- fix: prefix all class names with `kt-`
- fix: stop the inline edit from resizing
- feat: add translations to edit/delete options
- fix: options not skipping stacking context (by using tippy)
- refact: introduce common components for reply button
- refact: common component for inline edit
- refact: common component for options
- refact: migrate to ts where possible
- refact: delete dead classes and move style sheets to
..relevant components rather than _comments.scss
carsoli committed May 25, 2022
1 parent a63dffc commit 2ed4bc8
Showing 19 changed files with 539 additions and 485 deletions.
48 changes: 29 additions & 19 deletions packages/documentation/pages/usage/components/comment.vue
Original file line number Diff line number Diff line change
@@ -146,22 +146,6 @@ methods: {
/>
</div>

### Props

| Attribute | Description | Type | Accepted values | Default |
| :---------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------- | :-------------------------------- | :--------------------- |
| `createdTime` | The Time that appears in the comment | string | "20-12-2008" | - |
| `id` | the id to track the comment | number, string | "1" | - |
| `replies` | array of comment props to be nested under the coment | [CommentProps] | [{id: "1", message: "hello"}] | - |
| `userAvatar` | url to image thumbnail | string | "https://someimage.com/image.png" | - |
| `userId` | id of user who made the comment to reply too | number, string | "2" | - |
| `userName` | name of user to display | string | "Jhone Doe" | - |
| `message` | the actual comment | string | "Hello" | - |
| `dangerDefaultParserOverride` | A function that processes and escapes the comment message before it is passed to the div that render it, as the name implies you're responsible for escaping if you use this | (string) => string | Function | lodash escape function |
| `postEscapeParser` | A function that processes the message after is has been escaped use this instead of `dangerDefaultParserOverride` | (string) => string | Function | (_) => _ |
| `isDeletable` | whether this comment is deletable | boolean | true,false | false |
| `isEditable` | whether this comment is editable | boolean | true,false | false |

### Event

| Event Name | Component | Payload | Description |
@@ -251,9 +235,35 @@ export default {
methods: {
dangerouslyOverrideParser: (msg) => escape(msg),
postEscapeParser: (msg) => msg.replace(/\n/g, '</br>'),
handleEdit(payload) {
// eslint-disable-next-line
console.log(payload)
handleEdit({ id, message, parentId }) {
if (parentId === null) {
const commentIndex = this.comments.findIndex(
(comment) => comment.id === id,
)
return (this.comments = [
...this.comments.slice(0, commentIndex),
{ ...this.comments[commentIndex], message },
...this.comments.slice(commentIndex + 1),
])
}
const parentCommentIndex = this.comments.findIndex(
(comment) => comment.id === parentId,
)
const oldReplies = this.comments[parentCommentIndex].replies
const replyIndex = oldReplies.findIndex((reply) => reply.id === id)
const newReplies = [
...oldReplies.slice(0, replyIndex),
{ ...oldReplies[replyIndex], message },
...oldReplies.slice(replyIndex + 1),
]
this.comments = [
...this.comments.slice(0, parentCommentIndex),
{ ...this.comments[parentCommentIndex], replies: newReplies },
...this.comments.slice(parentCommentIndex + 1),
]
},
handleSubmit(payload) {
const _message = {
169 changes: 77 additions & 92 deletions packages/kotti-ui/source/kotti-comment/KtComment.vue
Original file line number Diff line number Diff line change
@@ -1,65 +1,39 @@
<template>
<div class="comment">
<div class="kt-comment">
<KtAvatar size="sm" :src="userAvatar" />
<div class="comment__wrapper">
<div class="comment__info">
<div class="kt-comment__content">
<div class="kt-comment__content__info">
<div class="info__name" v-text="userName" />
<div class="info__time" v-text="createdTime" />
</div>
<!-- eslint-disable vue/no-v-html -->
<div
v-if="!isInlineEdit"
class="comment__message"
v-html="postEscapeParser(dangerouslyOverrideParser(inlineMessage))"

<CommentInlineEdit
:id="id"
:dangerouslyOverrideParser="dangerouslyOverrideParser"
:isEditing="isEditing"
:message="message"
:postEscapeParser="postEscapeParser"
@edit="handleEdit($event)"
@update:isEditing="($event) => (isEditing = $event)"
/>
<!-- eslint-enable vue/no-v-html -->
<div v-else class="comment-inline-edit form-group">
<textarea
v-model="inlineMessageValue"
class="comment-inline-edit-input form-input"
></textarea>
<KtButtonGroup class="comment-inline-edit-buttons">
<KtButton icon="close" @click="cancelInlineEdit" />
<KtButton icon="check" @click="handleEditConfirm" />
</KtButtonGroup>
</div>
<div class="comment__action">
<div
class="action__reply"
@click="handleInlineReplyClick({ userName, userId })"
>
<i class="yoco" v-text="'comment'" /> {{ replyButton }}
</div>
<div v-if="actionOptions.length > 0" class="action__more">
<i class="yoco">dots</i>
<div class="action__options">
<a
v-for="option in actionOptions"
:key="option.type"
@click="option.onClick"
>
<li>{{ option.label }}</li>
</a>
</div>
</div>
</div>

<CommentActions
:options="actionOptions"
:userData="{ userId, userName }"
@replyClick="handleReplyClick"
/>

<div v-for="reply in replies" :key="reply.id">
<CommentReply
:id="reply.id"
:createdTime="reply.createdTime"
v-bind="reply"
:dangerouslyOverrideParser="dangerouslyOverrideParser"
:isDeletable="reply.isDeletable"
:isEditable="reply.isEditable"
:message="reply.message"
:postEscapeParser="postEscapeParser"
:userAvatar="reply.userAvatar"
:userId="reply.userId"
:userName="reply.userName"
@_inlineDeleteClick="(commentId) => handleDelete(commentId, true)"
@_inlineEditSubmit="$emit('edit', $event)"
@_inlineReplyClick="handleInlineReplyClick"
@click="handleReplyClick"
@delete="(commentId) => handleDelete(commentId, true)"
@edit="(editPayload) => handleEdit(editPayload, true)"
/>
</div>

<KtCommentInput
v-if="userBeingRepliedTo"
isInline
@@ -76,87 +50,80 @@
<script lang="ts">
import { computed, defineComponent, ref } from '@vue/composition-api'
import { KtAvatar } from '../kotti-avatar'
import { KtButton } from '../kotti-button'
import { KtButtonGroup } from '../kotti-button-group'
import { KottiButton } from '../kotti-button/types'
import { useTranslationNamespace } from '../kotti-i18n/hooks'
import { makeProps } from '../make-props'
import { Kotti } from '../types'
import CommentActions from './components/CommentActions.vue'
import CommentInlineEdit from './components/CommentInlineEdit.vue'
import CommentReply from './components/CommentReply.vue'
import KtCommentInput from './KtCommentInput.vue'
import { KottiComment } from './types'
type UserData = Pick<KottiComment.PropsInternal, 'userName' | 'userId'>
export default defineComponent<KottiComment.PropsInternal>({
name: 'KtComment',
components: {
KtAvatar,
KtButton,
KtButtonGroup,
CommentActions,
CommentReply,
CommentInlineEdit,
KtCommentInput,
},
props: makeProps(KottiComment.propsSchema),
setup(props, { emit }) {
const isInlineEdit = ref(false)
const inlineMessageValue = ref<string | null>(null)
const userBeingRepliedTo = ref<UserData | null>(null)
const isEditing = ref(false)
const userBeingRepliedTo = ref<Kotti.Comment.UserData | null>(null)
const translations = useTranslationNamespace('KtComment')
const handleDelete = (commentId: number | string, isInline?: boolean) => {
const handleDelete = (commentId: number | string, isReply = false) => {
const payload: KottiComment.Events.Delete = {
id: commentId,
parentId: isInline ? props.id : null,
parentId: isReply ? props.id : null,
}
emit('delete', payload)
}
const handleEdit = (
{ id, message }: KottiComment.Events.InternalEdit,
isReply = false,
) => {
const payload: KottiComment.Events.Edit = {
id,
message,
parentId: isReply ? props.id : null,
}
emit('edit', payload)
}
return {
actionOptions: computed<Array<KottiButton.Props>>(() => {
actionOptions: computed<Kotti.Popover.Props['options']>(() => {
const options = []
if (isInlineEdit.value) return options
if (isEditing.value) return options
if (props.isEditable)
options.push({
label: 'Edit',
label: translations.value.editButton,
onClick: () => {
inlineMessageValue.value = props.message
isInlineEdit.value = true
isEditing.value = true
},
type: KottiButton.Type.PRIMARY,
})
if (props.isDeletable)
options.push({
label: 'Delete',
label: translations.value.deleteButton,
onClick: () => handleDelete(props.id),
type: KottiButton.Type.DANGER,
})
return options
}),
cancelInlineEdit: () => {
inlineMessageValue.value = null
isInlineEdit.value = false
},
handleDelete,
handleEditConfirm: () => {
isInlineEdit.value = false
if (inlineMessageValue.value === null) return
const payload: KottiComment.Events.Edit = {
id: props.id,
message: inlineMessageValue.value,
}
emit('edit', payload)
},
handleInlineReplyClick: (replyUserData: UserData) => {
handleEdit,
handleReplyClick: (replyUserData: Kotti.Comment.UserData) => {
userBeingRepliedTo.value = replyUserData
},
handleInlineSubmit: (commentData: KottiComment.Events.Submit) => {
userBeingRepliedTo.value = null
emit('submit', commentData)
},
inlineMessage: computed(() => inlineMessageValue.value ?? props.message),
inlineMessageValue,
isInlineEdit,
isEditing,
Kotti,
placeholder: computed(() =>
userBeingRepliedTo.value === null
? null
@@ -165,16 +132,34 @@ export default defineComponent<KottiComment.PropsInternal>({
userBeingRepliedTo.value.userName,
].join(' '),
),
replyButton: computed(() => translations.value.replyButton),
userBeingRepliedTo,
}
},
})
</script>

<style lang="scss" scoped>
.action__reply {
.kt-comment {
display: flex;
align-items: center;
flex-flow: row;
+ .kt-comment {
padding-top: var(--unit-1);
border-top: 1px solid var(--ui-02);
}
&__content {
display: flex;
flex: 1;
flex-direction: column;
margin-left: var(--unit-2);
&__info {
display: flex;
width: 100%;
font-size: var(--font-size);
line-height: 1.2rem;
}
}
}
</style>
75 changes: 60 additions & 15 deletions packages/kotti-ui/source/kotti-comment/KtCommentInput.vue
Original file line number Diff line number Diff line change
@@ -3,19 +3,19 @@
<div :class="wrapperClass">
<KtAvatar
v-if="!isInline"
class="comment-input__avatar"
class="kt-comment-input__avatar"
size="sm"
:src="userAvatar"
/>
<textarea
ref="textarea"
v-model="text"
class="comment-input__textarea"
class="kt-comment-input__textarea"
:placeholder="placeholder"
@blur="textFocused = false"
@focus="textFocused = true"
@input="updateHeight"
></textarea>
/>
<KtButton
:disabled="!text"
type="text"
@@ -29,19 +29,13 @@
<script lang="ts">
import { computed, defineComponent, ref } from '@vue/composition-api'
import { KtAvatar } from '../kotti-avatar'
import { KtButton } from '../kotti-button'
import { useTranslationNamespace } from '../kotti-i18n/hooks'
import { makeProps } from '../make-props'
import { KottiCommentInput } from './types'
export default defineComponent<KottiCommentInput.PropsInternal>({
name: 'KtCommentInput',
components: {
KtAvatar,
KtButton,
},
props: makeProps(KottiCommentInput.propsSchema),
setup(props, { emit }) {
const translations = useTranslationNamespace('KtComment')
@@ -51,8 +45,8 @@ export default defineComponent<KottiCommentInput.PropsInternal>({
const textFocused = ref(false)
return {
containerClass: computed(() => ({
'comment-input': true,
'comment-input--inline': props.isInline,
'kt-comment-input': true,
'kt-comment-input--inline': props.isInline,
})),
handleSubmitClick: () => {
if (text.value === null) return
@@ -77,13 +71,64 @@ export default defineComponent<KottiCommentInput.PropsInternal>({
const height = textarea.value.scrollHeight
textarea.value.style.height = `${height}px`
},
wrapperClass: computed(() => ({
'comment-input__wrapper': true,
'comment-input__wrapper--focus': textFocused.value,
'comment-input__wrapper--inline': props.isInline,
'kt-comment-input__wrapper': true,
'kt-comment-input__wrapper--focus': textFocused.value,
'kt-comment-input__wrapper--inline': props.isInline,
})),
}
},
})
</script>

<style lang="scss" scoped>
.kt-comment-input {
box-sizing: border-box;
display: flex;
&--inline {
margin: 0 0 var(--unit-1) 0;
}
&__wrapper {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
padding: var(--unit-2);
background-color: var(--white);
border: 1px solid #dbdbdb;
border-radius: var(--border-radius);
&--focus {
border: 1px solid #bbb;
}
&--inline {
padding: var(--unit-1);
}
}
&__avatar {
margin-right: var(--unit-1);
}
&__textarea {
flex: 1 1;
width: 100%;
height: 1.2rem;
padding: 0;
margin: 0 0.2rem;
resize: none;
border: 0;
&:focus {
outline: none;
}
}
.kt-button {
cursor: pointer;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<template>
<div class="kt-comment__content__actions">
<CommentActionsReply @click="handleReplyClick" />
<CommentActionsOptions :options="options" />
</div>
</template>

<script lang="ts">
import { defineComponent } from '@vue/composition-api'
import { Kotti } from '../../types'
import CommentActionsOptions from './CommentActionsOptions.vue'
import CommentActionsReply from './CommentActionsReply.vue'
export default defineComponent<{
options: Kotti.Popover.Props['options']
userData: Kotti.Comment.UserData
}>({
name: 'CommentActions',
props: {
options: { type: Array, required: true },
userData: { type: Object, required: true },
},
components: {
CommentActionsOptions,
CommentActionsReply,
},
setup(props, { emit }) {
return {
handleReplyClick: () => emit('replyClick', props.userData),
}
},
})
</script>

<style lang="scss" scoped>
.kt-comment__content__actions {
display: flex;
justify-content: space-between;
margin: 0.2rem 0;
font-size: 0.7rem;
font-weight: 600;
line-height: 1.2rem;
color: var(--link-02);
> * {
&:hover {
color: var(--link-03);
cursor: pointer;
}
}
.yoco {
font-size: 0.9rem;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<template>
<KtPopover
v-if="options.length > 0"
class="kt-comment__actions__more-options"
:clickBehavior="clickBehavior"
:options="options"
placement="bottom"
trigger="click"
>
<i class="yoco" v-text="'dots'" />
</KtPopover>
</template>

<script lang="ts">
import { defineComponent } from '@vue/composition-api'
import { Kotti } from '../../types'
export default defineComponent<{
options: Array<Kotti.Popover.Props['options']>
}>({
name: 'CommentActionsOptions',
props: {
options: { type: Array, required: true },
},
setup() {
return {
clickBehavior: Kotti.Popover.ClickBehavior.HIDE_ON_CLICK_AWAY,
}
},
})
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<template>
<div class="kt-comment__actions__reply" @click="$emit('click')">
<i class="yoco" v-text="'comment'" />
<span v-text="replyButtonText" />
</div>
</template>

<script lang="ts">
import { computed, defineComponent } from '@vue/composition-api'
import { useTranslationNamespace } from '../../kotti-i18n/hooks'
export default defineComponent({
name: 'CommentAcionsReply',
props: {},
setup() {
const translations = useTranslationNamespace('KtComment')
return {
replyButtonText: computed(() => translations.value.replyButton),
}
},
})
</script>

<style lang="scss" scoped>
.kt-comment__actions__reply {
display: flex;
flex: 0 0 auto;
align-items: center;
padding: 0 var(--unit-01);
> *:not(:last-child) {
margin-right: var(--unit-01);
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<template>
<div v-if="!isEditing" class="kt-comment-inline-edit__message">
<!-- eslint-disable vue/no-v-html -->
<span v-html="postEscapeParser(dangerouslyOverrideParser(message))" />
<!-- eslint-enable vue/no-v-html -->
</div>
<div v-else class="kt-comment-inline-edit">
<textarea v-model="inlineValue" class="form-input" />
<KtButtonGroup class="kt-comment-inline-edit__buttons">
<KtButton icon="close" @click="handleCancel" />
<KtButton icon="check" @click="handleConfirm" />
</KtButtonGroup>
</div>
</template>

<script lang="ts">
import { defineComponent, ref, watch } from '@vue/composition-api'
import { Kotti } from '../../types'
export default defineComponent<{
dangerouslyOverrideParser: () => void
id: Kotti.Comment.PropsInternal['id']
isEditing: boolean
postEscapeParser: () => void
message: string
}>({
name: 'CommentInlineEdit',
props: {
dangerouslyOverrideParser: { type: Function, required: true },
id: { type: Number, required: true },
isEditing: { type: Boolean, required: true },
postEscapeParser: { type: Function, required: true },
message: { type: String, required: true },
},
setup(props, { emit }) {
const inlineValue = ref<string | null>(null)
watch(
() => props.isEditing,
(newValue) => {
if (newValue === true) {
inlineValue.value = props.message
}
},
{ immediate: true },
)
return {
handleConfirm: () => {
emit('update:isEditing', false)
if (inlineValue.value === null) return
const payload: Kotti.Comment.Events.InternalEdit = {
id: props.id,
message: inlineValue.value,
}
emit('edit', payload)
},
handleCancel: () => {
emit('update:isEditing', false)
inlineValue.value === null
},
inlineValue,
}
},
})
</script>

<style lang="scss" scoped>
@import '../../kotti-field/mixins';
.kt-comment-inline-edit {
position: relative;
width: 100%;
textarea {
resize: vertical;
@include prettify-scrollbar;
}
&__buttons {
position: absolute;
right: 0;
z-index: 9999; // use tippy
}
&__message {
display: flex;
align-items: center;
word-break: break-word;
}
}
</style>
176 changes: 74 additions & 102 deletions packages/kotti-ui/source/kotti-comment/components/CommentReply.vue
Original file line number Diff line number Diff line change
@@ -1,131 +1,103 @@
<template>
<div class="comment-reply">
<KtAvatar class="comment-reply__avatar" size="sm" :src="userAvatar" />
<div class="comment-reply__wrapper">
<div class="comment-reply__info">
<div class="comment-reply__name" v-text="userName" />
<div class="comment-reply__time" v-text="createdTime" />
</div>
<div class="comment-reply__body">
<div
v-if="!isInlineEdit"
class="comment-reply__message"
@click="$emit('_inlineReplyClick', { userName, userId })"
>
<!-- eslint-disable vue/no-v-html -->
<span
v-html="postEscapeParser(dangerouslyOverrideParser(inlineMessage))"
/>
<!-- eslint-enable vue/no-v-html -->
<i class="yoco" v-text="'comment'" />
</div>
<div v-else class="comment-inline-edit form-group">
<textarea
v-model="inlineMessageValue"
class="comment-inline-edit-input form-input"
></textarea>
<KtButtonGroup class="comment-inline-edit-buttons">
<KtButton icon="close" @click="cancelInlineEdit" />
<KtButton icon="check" @click="handleConfirm" />
</KtButtonGroup>
</div>
<div
v-if="!isInlineEdit & (actionOptions.length > 0)"
class="comment-reply__action action__more"
>
<i class="yoco">dots</i>
<div class="action__options">
<a
v-for="(option, index) in actionOptions"
:key="index"
@click="option.onClick"
>
<li v-text="option.label" />
</a>
</div>
</div>
<div class="comment-reply__content">
<div class="comment-reply__content__info">
<div class="info__name" v-text="userName" />
<div class="info__time" v-text="createdTime" />
</div>

<CommentInlineEdit
:id="id"
:dangerouslyOverrideParser="dangerouslyOverrideParser"
:isEditing="isEditing"
:message="message"
:postEscapeParser="postEscapeParser"
@edit="($event) => $emit('edit', $event)"
@update:isEditing="($event) => (isEditing = $event)"
/>

<CommentActions
:options="actionOptions"
:userData="{ userId, userName }"
@replyClick="($event) => $emit('click', $event)"
/>
</div>
</div>
</template>

<script>
<script lang="ts">
import { computed, defineComponent, ref } from '@vue/composition-api'
import escape from 'lodash/escape'
import { KtAvatar } from '../../kotti-avatar'
import { KtButton } from '../../kotti-button'
import { KtButtonGroup } from '../../kotti-button-group'
import { useTranslationNamespace } from '../../kotti-i18n/hooks'
import { Kotti } from '../../types'
export default {
import CommentActions from './CommentActions.vue'
import CommentInlineEdit from './CommentInlineEdit.vue'
export default defineComponent<Kotti.Comment.Reply.PropsInternal>({
name: 'CommentReply',
components: {
KtAvatar,
KtButton,
KtButtonGroup,
CommentActions,
CommentInlineEdit,
},
props: {
createdTime: String,
createdTime: { default: () => null, type: String },
dangerouslyOverrideParser: { default: escape, type: Function },
isDeletable: { default: false, type: Boolean },
isEditable: { default: false, type: Boolean },
id: [Number, String],
message: String,
id: { default: () => null, type: [Number, String] },
message: { type: String, required: true },
parser: { default: escape, type: Function },
postEscapeParser: { default: (_) => _, type: Function },
userAvatar: String,
userId: [Number, String],
userName: String,
userAvatar: { default: () => null, type: String },
userId: { default: () => null, type: Number },
userName: { default: () => null, type: String },
},
data() {
setup(props, { emit }) {
const isEditing = ref<boolean>(false)
const translations = useTranslationNamespace('KtComment')
return {
inlineMessageValue: '',
isInlineEdit: false,
actionOptions: computed(() => {
const options = []
if (props.isEditable)
options.push({
label: translations.value.editButton,
onClick: () => (isEditing.value = true),
})
if (props.isDeletable)
options.push({
label: translations.value.deleteButton,
onClick: () => emit('delete', props.id),
})
return options
}),
isEditing,
}
},
computed: {
inlineMessage() {
return this.inlineMessageValue || this.message
},
actionOptions() {
const options = []
if (this.isEditable)
options.push({
label: 'Edit',
onClick: () => {
this.inlineMessageValue = this.inlineMessage
this.isInlineEdit = true
},
})
if (this.isDeletable)
options.push({
label: 'Delete',
onClick: () => this.$emit('_inlineDeleteClick', this.id),
})
return options
},
},
methods: {
cancelInlineEdit() {
this.inlineMessageValue = ''
this.isInlineEdit = false
},
handleInlineInput(event) {
this.inlineMessageValue = event.target.value
},
handleConfirm() {
this.isInlineEdit = false
if (!this.inlineMessageValue) return
this.$emit('_inlineEditSubmit', {
message: this.inlineMessageValue,
id: this.id,
})
},
},
}
})
</script>

<style lang="scss" scoped>
.comment-reply__message {
.comment-reply {
display: flex;
align-items: center;
padding: var(--unit-1) 0;
&__content {
display: flex;
flex-direction: column;
width: 100%;
margin-left: var(--unit-2);
&__info {
display: flex;
flex-direction: row;
margin-bottom: var(--unit-h);
font-size: calc(var(--unit-3) + var(--unit-h));
line-height: calc(var(--unit-3) + var(--unit-h));
}
}
}
</style>
46 changes: 32 additions & 14 deletions packages/kotti-ui/source/kotti-comment/types.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,72 @@
import { z } from 'zod'

import { Kotti } from '../types'

import { defaultParser, defaultPostEscapeParser } from './utilities'

const commentIdSchema = z.union([z.number(), z.string()])

export namespace KottiComment {
const parseFunctionSchema = z.function().args(z.string()).returns(z.string())

export const commentSchema = z.object({
createdTime: z.string().optional(),
id: commentIdSchema.optional(),
createdTime: z.string().nullable().default(null),
id: commentIdSchema.nullable().default(null),
isDeletable: z.boolean().default(false),
isEditable: z.boolean().default(false),
message: z.string(),
userAvatar: z.string().optional(),
userId: z.number().optional(),
userAvatar: z.string().nullable().default(null),
userId: z.number().nullable().default(null),
userName: z.string().optional(),
})

export const propsSchema = commentSchema.extend({
const sharedSchema = commentSchema.extend({
dangerouslyOverrideParser: parseFunctionSchema.default(defaultParser),
postEscapeParser: parseFunctionSchema.default(defaultPostEscapeParser),
})

export const propsSchema = sharedSchema.extend({
replies: z.array(commentSchema).optional(),
})

export type Props = z.input<typeof propsSchema>
export type PropsInternal = z.output<typeof propsSchema>

export namespace Reply {
export type Props = z.input<typeof sharedSchema>
export type PropsInternal = z.output<typeof sharedSchema>
}

export namespace Events {
export type Delete = {
id: string | number
parentId: string | number | null
type CommonPayload = {
id: NonNullable<KottiComment.PropsInternal['id']>
parentId: KottiComment.PropsInternal['id']
}

export type Edit = {
id: string | number
message: string
export type Delete = CommonPayload

export type Edit = CommonPayload & {
message: Kotti.Comment.PropsInternal['message']
}

export type InternalEdit = Pick<Kotti.Comment.Events.Edit, 'message' | 'id'>

export type Submit = {
message: string
replyToUserId: number
parentId: string | number
message: KottiComment.PropsInternal['message']
parentId: CommonPayload['parentId']
replyToUserId: KottiComment.PropsInternal['userId']
}
}

export type Translations = {
deleteButton: string
editButton: string
postButton: string
replyButton: string
replyPlaceholder: string
}

export type UserData = Pick<PropsInternal, 'userName' | 'userId'>
}

export namespace KottiCommentInput {
3 changes: 2 additions & 1 deletion packages/kotti-ui/source/kotti-comment/utilities.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import escape from 'lodash/escape'

export const defaultParser = (message: string) => escape(message)
export const defaultPostEscapeParser = (message: string) => message
export const defaultPostEscapeParser = (message: string) =>
message.replace(/\n/g, '</br>')
Original file line number Diff line number Diff line change
@@ -74,31 +74,7 @@ export default defineComponent<KottiFieldTextArea.PropsInternal>({
border: 1px solid var(--ui-02);
border-radius: var(--field-border-radius);
&::-webkit-scrollbar {
opacity: 0;
}
scrollbar-width: thin;
scrollbar-color: var(--ui-background) var(--ui-background);
&:active,
&:hover {
scrollbar-color: var(--ui-03) var(--ui-background);
transition: scrollbar-color var(--transition-medium) ease-out;
&::-webkit-scrollbar {
width: 5px;
opacity: 1;
transition: opacity var(--transition-medium) ease-out;
}
&::-webkit-scrollbar-thumb {
cursor: all-scroll;
background-color: var(--ui-03);
border-radius: var(--field-border-radius);
}
}
@include prettify-scrollbar;
}
}
28 changes: 28 additions & 0 deletions packages/kotti-ui/source/kotti-field/mixins.scss
Original file line number Diff line number Diff line change
@@ -391,6 +391,34 @@
}
}

@mixin prettify-scrollbar {
&::-webkit-scrollbar {
opacity: 0;
}

scrollbar-width: thin;
scrollbar-color: var(--ui-background) var(--ui-background);

&:active,
&:hover {
scrollbar-color: var(--ui-03) var(--ui-background);
transition: scrollbar-color var(--transition-medium) ease-out;

&::-webkit-scrollbar {
width: 5px;
opacity: 1;
transition: opacity var(--transition-medium) ease-out;
}

&::-webkit-scrollbar-thumb {
cursor: all-scroll;

background-color: var(--ui-03);
border-radius: var(--field-border-radius);
}
}
}

@mixin sizes {
$fontSizes: (
'small': var(--font-size-small),
2 changes: 2 additions & 0 deletions packages/kotti-ui/source/kotti-i18n/locales/de-DE.ts
Original file line number Diff line number Diff line change
@@ -21,6 +21,8 @@ export const deDE: KottiI18n.Messages = {
expandCloseLabel: 'Schließen',
},
KtComment: {
deleteButton: 'Löschen',
editButton: 'Bearbeiten',
postButton: 'Beitragen',
replyButton: 'Antworten',
replyPlaceholder: 'Antwort an',
2 changes: 2 additions & 0 deletions packages/kotti-ui/source/kotti-i18n/locales/en-US.ts
Original file line number Diff line number Diff line change
@@ -21,6 +21,8 @@ export const enUS: KottiI18n.Messages = {
expandCloseLabel: 'Close',
},
KtComment: {
deleteButton: 'Delete',
editButton: 'Edit',
postButton: 'Post',
replyButton: 'Reply',
replyPlaceholder: 'Reply to',
2 changes: 2 additions & 0 deletions packages/kotti-ui/source/kotti-i18n/locales/es-ES.ts
Original file line number Diff line number Diff line change
@@ -21,6 +21,8 @@ export const esES: KottiI18n.Messages = {
expandCloseLabel: 'Cerrar',
},
KtComment: {
deleteButton: 'Borrar',
editButton: 'Editar',
postButton: 'Publicar',
replyButton: 'Responder',
replyPlaceholder: 'Responder a',
2 changes: 2 additions & 0 deletions packages/kotti-ui/source/kotti-i18n/locales/fr-FR.ts
Original file line number Diff line number Diff line change
@@ -21,6 +21,8 @@ export const frFR: KottiI18n.Messages = {
expandCloseLabel: 'Fermer',
},
KtComment: {
deleteButton: 'Effacer',
editButton: 'Modifier',
postButton: 'Commenter',
replyButton: 'Répondre',
replyPlaceholder: 'Répondre à',
2 changes: 2 additions & 0 deletions packages/kotti-ui/source/kotti-i18n/locales/ja-JP.ts
Original file line number Diff line number Diff line change
@@ -21,6 +21,8 @@ export const jaJP: KottiI18n.Messages = {
expandCloseLabel: '閉じる',
},
KtComment: {
deleteButton: '消す',
editButton: 'エディット',
postButton: '送信',
replyButton: '返信',
replyPlaceholder: '返信',
4 changes: 0 additions & 4 deletions packages/kotti-ui/source/kotti-popover/KtPopover.vue
Original file line number Diff line number Diff line change
@@ -123,10 +123,6 @@ export default defineComponent<KottiPopover.PropsInternal>({
&__content {
margin: 3px -1px; // tippy theme applies 5px 9px padding, therefore this equals 8px 8px
&--has-options {
min-width: 200px;
}
&--size {
&-auto {
width: auto;
216 changes: 3 additions & 213 deletions packages/kotti-ui/source/kotti-style/_comments.scss
Original file line number Diff line number Diff line change
@@ -1,222 +1,12 @@
.comment {
display: flex;
flex-flow: row;

+ .comment {
padding-top: var(--unit-1);
border-top: 1px solid var(--ui-02);
}
}

.comment__avatar {
flex: 0 0 2.4rem;
height: 2.4rem;
}

.comment__wrapper {
display: flex;
flex: 1 1 auto;
flex-direction: column;
margin-left: 0.4rem;
}

.comment__info {
display: flex;
flex-direction: row;
width: 100%;
font-size: 0.7rem;
line-height: 1.2rem;
}

.info__name {
flex: 1 1;
font-weight: 600;
}

.info__time {
flex: 1 1;
color: $lightgray-500;
text-align: right;
}

.comment__message {
font-size: 0.75rem;
line-height: 1.2rem;
word-break: break-word;
}

.comment__action,
.comment-reply__action {
display: flex;
flex-direction: row;
justify-content: space-between;
margin: 0.2rem 0;
font-size: 0.7rem;
font-weight: 600;
line-height: 1.2rem;
color: $lightgray-500;
.yoco {
font-size: 0.9rem;
}
}

.comment-inline-edit {
position: relative;
width: 100%;
}
.comment-inline-edit-buttons {
position: absolute;
right: 0;
z-index: $zindex-4;
}

.action__options {
position: absolute;
top: 1.2rem;
right: 0;
z-index: $zindex-4;
display: none;
height: auto;
padding: 0.4rem 0;
line-height: 1.4rem;
text-align: left;
background: #ffffff;
border-radius: var(--border-radius);
box-shadow: $box-shadow;
li {
padding: 0.2rem 1rem;
list-style: none;
}
li:hover {
background: $lightgray-300;
}
}

.action__reply {
flex: 0 0 auto;
padding: 0 0.2rem;
border-radius: 0.2rem;
&:hover {
color: $darkgray-400;
cursor: pointer;
background: $lightgray-400;
}
}
.action__more {
position: relative;
display: inline-block;
flex: 0 0 2rem;
padding: 0 0.2rem;
text-align: center;
border-radius: 0.2rem;
&:hover {
color: $darkgray-400;
cursor: pointer;
background: $lightgray-400;
.action__options {
display: block;
}
}
}

// Comment Reply => KtCommentReply

.comment-reply {
display: flex;
flex-direction: row;
padding: 0.2rem 0;
&__wrapper {
display: flex;
flex-direction: column;
width: 100%;
margin-left: 0.4rem;
}
&__avatar {
flex: 0 0 1.6rem;
}
&__info {
display: flex;
flex-direction: row;
margin-bottom: 0.1rem;
font-size: 0.7rem;
line-height: 0.7rem;
}
.info {
&__name {
flex: 1 1;
font-weight: 600;
}

&__time {
flex: 1 1 auto;
flex: 1 1;
color: $lightgray-500;
text-align: right;
}
&__body {
display: flex;
flex-direction: row;
justify-content: space-between;
}
&__message {
width: 100%;
font-size: 0.65rem;
line-height: 1rem;
word-break: break-word;
.yoco {
display: none;
font-size: 0.8rem;
color: #a8a8a8;
}
&:hover {
cursor: pointer;
.yoco {
display: inline;
color: $darkgray-400;
}
}
}
}

// KtCommentInput

.comment-input {
box-sizing: border-box;
display: flex;
flex-direction: row;
&--inline {
margin: 0;
}
&__wrapper {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
padding: 0.4rem;
background: #fff;
border: 1px solid #dbdbdb;
border-radius: var(--border-radius);
&--focus {
border: 1px solid #bbb;
}
&--inline {
padding: 0.2rem;
}
}
&__avatar {
flex: 0 0 1.6rem;
align-self: flex-start;
}
&__textarea {
flex: 1 1;
width: 100%;
height: 1.2rem;
padding: 0;
margin: 0 0.2rem;
resize: none;
border: 0;
&:focus {
outline: none;
}
}
&__button {
height: 1.6rem;
}
}

0 comments on commit 2ed4bc8

Please sign in to comment.