- ВКонтакте
- РћРТвЂВВВВВВВВнокласснРСвЂВВВВВВВВРєРСвЂВВВВВВВВ
- РњРѕР№ Р В Р’В Р РЋРЎв„ўР В Р’В Р РЋРІР‚ВВВВВВВВРЎР‚
- Viber
- Skype
- Telegram
Верстка (функционал) вложенных коментариев с палочкой относительности к родителю
Пытаюсь сделать задачу - нужно сделать адаптивную палочку от дочерних комментариев к родителю, есть пять уровней вложенности, палочка должна быть до самого верха через компоненты проходить. Vue 3. В компоненте коммента пиксели умножаются на уровень вложенности и получается отступ. Как это лучше сделать, более грамотно? Заранее благодарю!
Приложу компонент родителя и коммента ниже, мб поможет.
Родитель:
<template>
<div class="Comments-List">
<template v-if="get_isReady">
<p class="Comments-List__ttl-separator" v-if="get_isShowLastCommentsTtl">Последние комментарии</p>
<CommentItem
class="Comments-List__item"
v-for="(comment, id, index) in get_comments"
:style="getMargin(comment.level)"
:key="comment.id"
:comment="comment"
:commentsPlace="commentsPlace"
:context="props.context"
/>
<div class="Comments-List__next-comment-ldr" v-if="get_isLoadingNewComments">
<LoaderCircle :color="'var(--blue-600)'" :size="24"/>
</div>
</template>
<div class="Comments-List__ldr" v-else>
<LoaderCircle :color="'var(--blue-600-s)'" :size="32"/>
</div>
</div>
</template>
<script setup>
//---------------------------
const props = defineProps(Props());
const store = useStore();
//---------------------------
onMounted(async () => {
await toLoadComments();
await nextTick();
goToTheComment();
});
//---------------------------
//Получить контекст объекта для комментариев
const get_commentsCtx = computed(() => {
if (props.commentsPlace === 'popup') return 'popupComments';
else if (props.commentsPlace === 'page') return 'contentPageComments';
})
//Готов ли список к отрисовке
const get_isReady = computed(() => store.state.ContentSystem.Comments[get_commentsCtx.value].isReady);
//Получить список комментариев
const get_comments = computed(() => store.state.ContentSystem.Comments[get_commentsCtx.value].list);
//Отображать ли заголовок последних комментариев?
const get_isShowLastCommentsTtl = computed(() => get_comments.value?.length > 0);
//Идет ли подгрузка новых комментов
const get_isLoadingNewComments = computed(() => store.state.ContentSystem.Comments[get_commentsCtx.value].isLoadingNext);
//---------------------------
//Получить комментарии
const toLoadComments = async () => {
await store.dispatch('ContentSystem/Comments/startCommentsLoadingProcess', {postId: props.postId, context: get_commentsCtx.value, area: props.context});
return;
}
//Получить отступ коммента относительно его уровня вложенности
const getMargin = (level) => {
const levelNum = parseInt(level);
return `
margin-top: ${levelNum === 0 ? 22 : 10}px;
margin-left: ${levelNum * 30}px;
`;
}
//Проскролить к конкретному комментарию
const goToTheComment = async () => {
const idCommentScroll = store.state.ContentSystem.Comments[get_commentsCtx.value].targetCommentsId;
if (!idCommentScroll) return;
await nextTick();
const widget = document.querySelector('.Wdgt-Content');
const comment = widget.querySelector(`[data-comment-id="${idCommentScroll}"]`);
if (!comment) return;
comment.scrollIntoView({
behavior: 'auto',
block: 'center'
});
store.commit('ContentSystem/Comments/setScrollTarget', {
isComments: null,
idComment: null,
context: get_commentsCtx.value
});
}
</script>
<style lang="scss">
@import './styles/_styles';
</style>
Коммент:
<template>
<div class="Comment-Item" :data-comment-id="props.comment.id">
<router-link @click="toCloseAllPopups" class="Comment-Item__avt" :to="{ name: PATHS_NAME.OTHER_USERS.PROFILE, params: { username: props.comment.user.username } }">
<div v-if="props.comment.level > 0" class="dependency-line">
</div>
<AvatarImage :src="props.comment.user.avatar" :size="24" :alt="'name'"/>
</router-link>
<router-link @click="toCloseAllPopups" class="Comment-Item__name" :to="{ name: PATHS_NAME.OTHER_USERS.PROFILE, params: { username: props.comment.user.username } }">
{{ props.comment.user.name }}
</router-link>
<CommentMenu class="Comment-Item__menu" :commentsPlace="props.commentsPlace" :commentId="props.comment.id" v-if="get_isShowContextMenu"/>
<div v-if="props.comment.nested_count > 0" class="dependency-line-top">
<div class="line"></div>
</div>
<p :class="{'Comment-Item__txt-hidden': !isExpanded}" class="Comment-Item__txt" v-if="!get_isOpenEditForm && !comment.is_removed">
<span>{{ isExpanded ? props.comment.comment : truncatedComment }}</span>
</p>
<div v-if="get_isTruncated" class="Comment-Item__text-wrapper">
<button class="Comment-Item__show-more" @click="toggleExpand">
{{ isExpanded ? 'Скрыть' : 'Показать полностью' }}
</button>
</div>
<p class="Comment-Item__removed-txt" v-if="comment.is_removed">
<InfoIcon class="Comment-Item__info-icon"/>
Комментарий удалён автором или оракулом клуба...
<button class="Comment-Item__return-comment" @click="returnComment">Восстановить</button>
</p>
<img class="Comment-Item__img" :src="props.comment.image_url" @click="handleClickToContent" v-if="get_isShowImage" alt="img"/>
<template v-if="!BASIC_VIEW">
<CommentsForm
class="Comment-Item__form-edit"
:commentsPlace="props.commentsPlace"
:selfId="props.comment.id"
:isFocus="true"
:mode="'edit_comment'"
:postId="get_objectId"
:commentId="0"
:context="props.context"
v-if="get_isOpenEditForm"
/>
</template>
<div class="Comment-Item__ftr">
<span class="Comment-Item__edited" v-if="props.comment.is_edited">Ред</span>
<time class="Comment-Item__time" :class="get_isWithoutPoint">{{ get_formatedTime }}</time>
<button class="Comment-Item__answer" @click="goToComment" v-if="BASIC_VIEW">Ответить</button>
<button class="Comment-Item__answer" @click="toToggleForm">{{ get_textButton }}</button>
<CommentAddReaction class="Comment-Item__add-react" :comment="props.comment" :commentsPlace="get_commentsCtx" v-if="!BASIC_VIEW"/>
</div>
<CommentReactions class="Comment-Item__reactions" :comment="props.comment" :commentsPlace="get_commentsCtx" v-if="!BASIC_VIEW"/>
<template v-if="!BASIC_VIEW">
<CommentsForm
class="Comment-Item__form"
:commentsPlace="props.commentsPlace"
:selfId="props.comment.level === 5 ? props.comment.parent_id : props.comment.id"
:isFocus="true"
:mode="'answer'"
:postId="get_objectId"
:context="props.context"
v-if="get_isOpenNewCommentForm"
/>
</template>
</div>
</template>
<script setup>
//---------------------------
const props = defineProps(Props());
const store = useStore();
const route = useRoute();
//---------------------------
onBeforeUnmount(() => {
if (!get_isOpenForm.value) return;
store.commit('ContentSystem/Comments/setChildFormCommentId', {id: null, context: get_commentsCtx.value});
})
//---------------------------
const BASIC_VIEW = props.basicView;
const MAX_VISIBLE_SYMBOLS = 500;
const POST = store.state.ContentSystem.Alpha.list[props.comment.object_pk] || store.state.ContentSystem.Infonoise.list[props.comment.object_pk];
//---------------------------
//Состояние, управляющее развернутым видом комментария
const isExpanded = ref(false);
//---------------------------
//Проверяем, нужно ли обрезать текст (более MAX_VISIBLE_SYMBOLS символов)
const get_isTruncated = computed(() => props.comment.comment.length > MAX_VISIBLE_SYMBOLS);
//Если текст нужно обрезать, возвращаем первые MAX_VISIBLE_SYMBOLS символов
const truncatedComment = computed(() => get_isTruncated.value ? `${props.comment.comment.slice(0, MAX_VISIBLE_SYMBOLS)}...` : props.comment.comment);
//Получить контекст объекта для комментариев
const get_commentsCtx = computed(() => {
if (props.commentsPlace === 'popup') return 'popupComments';
else if (props.commentsPlace === 'page') return 'contentPageComments';
})
//Получить id, к которому относится комментарий
const get_objectId = computed(() => parseInt(props.comment.object_pk));
//Получить id комментария, на которого планируется ответ
const get_idCommentForm = computed(() => store.state.ContentSystem.Comments[get_commentsCtx.value].childFormCommentId);
//Комментарий находится в режиме редактирования?
const get_isEditCommentMode = computed(() => store.state.ContentSystem.Comments[get_commentsCtx.value].isEditMode);
//Открыта ли форма для этого элемента
const get_isOpenForm = computed(() => get_idCommentForm.value === props.comment.id);
//Открыта форма для создания коммента?
const get_isOpenNewCommentForm = computed(() => get_isOpenForm.value && !get_isEditCommentMode.value);
//Открыта форма для редактировани?
const get_isOpenEditForm = computed(() => get_isOpenForm.value && get_isEditCommentMode.value);
//Получить форматированное время
const get_formatedTime = computed(() => formatTime_dn_mn_y_h_min(props.comment.submit_date));
//Получить текст для кнопки
const get_textButton = computed(() => get_isOpenForm.value ? 'Скрыть' : 'Ответить');
//Получить адрес перехода
const get_routeUrl = computed(() => `/${POST.model_name.toLowerCase()}/${get_objectId.value}`);
//Отображать ли кнопку ответить?
const get_isShowAnswerBtn = computed(() => props.comment.level <= 5);
//Не ставить точку после элемента
const get_isWithoutPoint = computed(() => BASIC_VIEW || !BASIC_VIEW && get_isShowAnswerBtn.value ? '' : 'Comment-Item__time--without-point');
//Отобразить ли изображение?
const get_isShowImage = computed(() => props.comment.image_url && !get_isOpenEditForm.value);
//Это комментарий пользователя?
const get_isMine = computed(() => {
const userName = store.state.UserSystem.user.username;
return userName === props.comment.user.username;
})
//Отображать ли кнопку удаления?
const get_isShowContextMenu = computed(() => {
return (get_isMine.value || store.getters['UserSystem/hasModeratorPrivileges']) && !BASIC_VIEW;
})
//---------------------------
// Функция для переключения состояния
const toggleExpand = () => {
isExpanded.value = !isExpanded.value;
};
//Переключить видимость формы
const toToggleForm = () => {
get_isOpenForm.value ?
store.commit('ContentSystem/Comments/setChildFormCommentId', {id: null, context: get_commentsCtx.value}) :
store.commit('ContentSystem/Comments/setChildFormCommentId', {
id: props.comment.id,
context: get_commentsCtx.value
});
}
//Восстановить комментарий
const returnComment = async () => {
try {
await restoreComment(props.comment.id);
const comment = store.state.ContentSystem.Comments[get_commentsCtx.value].list.find(item => item.id == props.comment.id);
comment.is_removed = false;
POST.comments_count++;
} catch (error) {
} finally {
}
}
//Переместиться к комментарию
const goToComment = async () => {
store.dispatch('Dropdowns/removeTheDropdown', DROPDOWN_NAMES.contentPage);
await nextTick();
toChangeUrl();
store.commit('ContentSystem/Comments/setScrollTarget', {
isComments: true,
idComment: props.comment.id,
context: get_commentsCtx.value
});
store.dispatch('Dropdowns/addNewDropdown', DROPDOWN_NAMES.contentPage);
}
//Обновить адрес в строке поиска и запомнить последний урл перед переходом
const toChangeUrl = () => {
store.commit('ContentSystem/toRememberLastPageUrl', route.path);
window.history.pushState({}, '', window.location.href);
window.history.replaceState({}, '', get_routeUrl.value);
};
//Клик по контентной области
const handleClickToContent = (e) => {
if (e.target.tagName === 'IMG') {
openSlider([{src: e.target.src, text: null}]);
}
}
//Открыть слайдер с картинкой
const openSlider = (data) => {
store.commit('SliderSystem/toWriteSlides', data);
store.commit('SliderSystem/toChooseSlide', 0);
store.dispatch('Dropdowns/addNewDropdown', DROPDOWN_NAMES.slider);
}
//Очистить все дропдауны при переходе на страницу
const toCloseAllPopups = () => {
store.dispatch('Dropdowns/clearAllDropdowns');
}
</script>
<style lang="scss">
@import './styles/_styles';
</style>