Commit c0308658 authored by GuiΩ's avatar GuiΩ
Browse files

feature(Frontend): scroll to selected

on classe changed: scroll to bottom
on question selection change (from tooltip): sroll to selected question
parent a00d0407
......@@ -29,11 +29,12 @@ Events:
</script>
<template lang="pug">
.feuille-list
.FeuilleList
div(v-if="currFeuilles")
template(v-for="feuilleId in currFeuilles")
FeuilleListFeuille(
:feuille-id="feuilleId"
@loaded="$emit('loaded', $event)"
@message="$emit('message', $event)"
@question-edit="$emit('question-edit', $event)"
@question-drop="$emit('question-drop', $event)"
......@@ -44,24 +45,18 @@ Events:
@show-question-tooltip="$emit('show-question-tooltip', $event)"
@show-context-menu="$emit('show-context-menu', $event)")/
button.newfeuille-btn(
button.FeuilleList_newFeuilleBtn(
v-if="classes.hasSelection"
type="button"
@click="feuilles.addFeuille()"
) Ajouter une feuille
span(v-if="! classes.hasSelection") Commencez par ajouter vos classes en cliquant sur "Configuration" en haut à gauche.
a#FeuilleListBottom
</template>
<style scoped>
.feuille-list {
/* !*display: flex;*!*/
/* vertical-align: top;*/
/* overflow-y: scroll;*/
/* scroll-behavior: smooth;*/
/* text-align: center;*/
/* !* width: 1200px; *!*/
/* min-height: 100px;!*display: flex;*!*/
.FeuilleList {
vertical-align: top;
overflow-y: scroll;
scroll-behavior: smooth;
......@@ -75,8 +70,7 @@ Events:
background-color: #ffdffb;
}
.newfeuille-btn {
.FeuilleList_newFeuilleBtn {
margin: 20px;
}
</style>
......@@ -56,6 +56,7 @@ Events:
:key="seanceId")
FeuilleListFeuilleSeance(
:seance-id="seanceId"
@loaded="$emit('loaded', $event)"
@message="$emit('message', $event)"
@feuille-changed="$emit('feuille-changed', $event)"
@question-edit="$emit('question-edit', $event)"
......
......@@ -197,9 +197,11 @@ Events:
:position="position"
v-model:newScore="state.newScores[position]"
:scoreEditing="state.scoresEditing"
@loaded="$emit('loaded', $event)"
@focus-next="focusNext(position)"
@message="$emit('message', $event)"
@question-edit="$emit('question-edit', $event)"
@question-drop="$emit('question-drop', $event)"
@show-question-tooltip="$emit('show-question-tooltip', $event)"
@show-context-menu="$emit('show-context-menu', $event)")/
......
......@@ -233,7 +233,8 @@ Events:
draggable="true")
img(
:alt="'question ' + questionPosee.question"
:src="'/static/thumbs/' + question.thumb_name")
:src="'/static/thumbs/' + question.thumb_name"
@load="$emit('loaded', props.questionId)")
div.question_vide(v-else) Question {{ questionPosee.question }}
</template>
......
......@@ -9,11 +9,11 @@
import QuestionList from "@/components/QuestionList.vue"
import QuestionTooltip from '@/components/QuestionTooltip.vue'
import QuestionEditor from '@/components/QuestionEditor.vue'
import {computed, reactive, ref} from "vue"
import {computed, nextTick, reactive, ref} from "vue"
import {useThemesStore} from "@/store/themes"
import {useClassesStore} from "@/store/classes"
import {useUndoablesStore} from "@/store/undoables";
import type {IQuestionDropEvent, IQuestionPosee, QuestionID} from "@/types";
import type {ClasseID, IQuestionDropEvent, IQuestionPosee, QuestionID, ThemeID} from "@/types";
import ContextMenu from "@/components/ContextMenu.vue";
import type {IContextMenuOption} from "@/components/ContextMenu.vue";
import MessageBox from "@/components/MessageBox.vue";
......@@ -25,17 +25,15 @@
const contextMenu = ref<InstanceType<typeof ContextMenu> | null>(null)
const questionTooltip = ref<InstanceType<typeof QuestionTooltip> | null>(null)
const questionEditor = ref<InstanceType<typeof QuestionEditor> | null>(null)
const questionList = ref<InstanceType<typeof QuestionList> | null>(null)
interface IMainPageState {
questionEditorOptions?: {action: 'edit' | 'duplicate' | 'new', questionId?:QuestionID}
onLoaded?: () => void
}
const state: IMainPageState = reactive({
// classe: null,
// scores: {},
})
const selectionThemeId = computed(() => {
return [themes.getSelectedId]
})
......@@ -56,91 +54,38 @@
contextMenu.value.show(event, options)
}
function showMessage({title:title, message: message}: {title: string, message: string}) {
console.debug('MP.showMessage', title, message)
msgBox.value?.show(title, message);
}
// computed: {
// ...mapState({
// annee: state => state.classes.curr_annee,
// annees: state => state.classes.annees,
// classeId: state => state.classes.curr_classeId,
// classes: state => state.classes.curr_classeIds,
// }),
// ...mapGetters('undoables', [
// 'hasRedoable',
// 'hasUndoable',
// 'undoMessage',
// 'redoMessage',
// ]),
// selection_themeId() {
// return this.$store.state.themes.curr_theme_selection
// },
// }
// onCreated(() => {
// classes.loadAnnees()
// })
// methods: {
// getusername() {
// return username;
// },
// getuserid() {
// return user.id;
// },
function showMessage({title:title, message: message}: {title: string, message: string}) {
console.log(message)
msgBox.value?.show(title,message);
async function onClasseChanged(classeId: ClasseID) {
classes.setSelectedId(classeId)
state.onLoaded = () => document.getElementById('FeuilleListBottom')?.scrollIntoView()
}
async function setQuestionSelection({themeId: themeId, questionsIds: questionsIds}:
{themeId: ThemeID, questionsIds: QuestionID[]}) {
console.debug('MP.setQuestionSelection', themeId, questionsIds)
await themes.setSelectedTheme(themeId)
questionList.value?.setSelection(questionsIds)
}
// ...mapActions('classes', [
// 'setCurrentAnnee',
// 'setCurrentClasse',
// ]),
// ...mapActions('themes', [
// 'setCurrentTheme'
// ]),
// ...mapActions('undoables', [
// 'undo',
// 'redo'
// ]),
// onMenuRemplacer() {
// let source
// if (this.isQuestionSourceMovable)
// source = {
// id: this.dropEvent.questionPoseeSourceId,
// seance: this.dropEvent.seanceSourceId,
// position: this.dropEvent.positionSource,
// question: this.dropEvent.questionSourceId
// }
// else
// source = {question: this.dropEvent.questionSourceId}
// this.$store.dispatch('undoables/replaceQuestionPosee', {
// source: source,
// questionPoseeDest: this.dropEvent.questionPoseeDest
// })
// this.$refs.dropContextMenu.close()
// },
//
// /*
// * questionSourceId // Question à ajouter
// * questionPoseeSourceId // Question à déplacer/supprimer
// * questionDest // si non null : suppression de la QPsource
// * questionPoseeDest // remplacement de la question
// * seanceDest // ajout /déplacement d'une question
// * positionDest //
// */
// const vm = getCurrentInstance()
// const $contextmenu = vm.appContext.config.globalProperties.$contextmenu
function onFeuilleQuestionLoaded() {
if (state.onLoaded !== undefined)
state.onLoaded()
}
function onQuestionDrop(event: IQuestionDropEvent) {
console.debug('MainP.onQuestionDrop', event)
state.onLoaded = undefined
const source = {
id: event.questionPoseeSourceId,
seance: event.seanceSourceId,
position: event.positionSource,
question: event.questionSourceId
} as IQuestionPosee
console.debug('MainP.onQuestionDrop', event)
if (! isNaN(event.questionPoseeSourceId!)) { // déplacement d'une question
if (event.scoreSource && !isNaN(event.scoreSource)) { // questionSource évaluée
if (event.questionPoseeDest) { // remplacement de questionPoseeDest
......@@ -197,6 +142,7 @@
}
function onQuestionEdit({questionId:questionId, action:action}: {questionId: QuestionID, action: 'edit' | 'duplicate'}) {
state.onLoaded = undefined
switch (action) {
case "edit":
questionEditor.value?.edit(questionId)
......@@ -207,10 +153,6 @@
}
}
// function onOptionClicked(event) {
// console.info(event)
// }
</script>
<template lang="pug">
......@@ -228,9 +170,10 @@ QuestionEditor.QuestionEditor(
QuestionTooltip(
ref="questionTooltip"
@set-theme="themes.setSelectedTheme($event.themeId)")/
@set-selection="setQuestionSelection")/
#main-page
Toolbar.Toolbar/
Toolbar.Toolbar(
@classe-changed="onClasseChanged")/
.Mainpanel
.Selector
ThemeTree.Selector_theme(
......@@ -255,10 +198,10 @@ QuestionTooltip(
@question-edit="onQuestionEdit"
@question-drop="onQuestionDrop"
@classe-changed="updateClasseList()"
@loaded="onFeuilleQuestionLoaded"
@score-changed="$refs.questionList.updateScore($event.questionIds)"
@show-question-tooltip="onShowQuestionTooltip"
@show-context-menu="onShowContextMenu"
)/
@show-context-menu="onShowContextMenu")/
</template>
......
......@@ -13,7 +13,7 @@ Events:
import DropContextMenu from "@/components/DropContextMenu.vue";
import QuestionListNode from "@/components/QuestionListNode.vue";
import Score from "@/components/Score.vue";
import {computed, reactive, ref} from "vue";
import {computed, reactive, ref, watchEffect} from "vue";
import {useThemesStore} from "@/store/themes";
import {useQuestionsStore} from "@/store/questions";
import type {ClasseID, IQuestion, QuestionID, ThemeID} from "@/types";
......@@ -26,10 +26,13 @@ Events:
interface IQuestionListState {
questionsIds: QuestionID[],
scores: {},
dropEvent?: MouseEvent,
dropIndex: number,
questionsSel: QuestionID[],
onLoaded?: () => void
}
interface IQuestionListProps {
......@@ -54,10 +57,16 @@ Events:
}>()
const state: IQuestionListState = reactive({
questionsIds: [],
scores: {},
dropEvent: undefined,
dropIndex: 0,
questionsSel: [10],
questionsSel: [],
})
watchEffect(() => {
state.questionsIds = questions.getForCurrTheme
state.onLoaded = undefined
})
const questionsIds = computed(() => { // liste des questions à afficher
......@@ -77,6 +86,24 @@ Events:
return question || permissions.includes('Validateur')
}
async function setSelection(questionsIds?: QuestionID[]) {
console.debug('QL.setSelection', questionsIds)
if (questionsIds?.length) {
state.questionsSel = questionsIds;
state.onLoaded = () => document.getElementById('question_' + questionsIds[0])?.scrollIntoView()
state.onLoaded()
} else {
state.questionsSel = []
state.onLoaded = undefined
}
}
function questionNodeLoaded() {
console.debug('QL.questionNodeLoaded')
if (state.onLoaded !== undefined)
state.onLoaded()
}
function onClick(event: MouseEvent, questionId: QuestionID) {
nClick++
if (nClick === 1) {
......@@ -87,8 +114,11 @@ Events:
}
}
/**
* executes the click event handle (after the timeout)
*/
function execClick(event: MouseEvent, questionId: QuestionID) {
state.onLoaded = undefined
const themeId = props.themeId as ThemeID
if (! event.ctrlKey && ! event.shiftKey)
emit('show-question-tooltip', {event: event, questionId: questionId})
......@@ -100,7 +130,7 @@ Events:
if (last < first)
inc = -1;
for (let i = first+inc; i*inc <= last*inc ; i+= inc) {
let qId = questionsIds.value[i];
let qId = state.questionsIds[i];
if (state.questionsSel.indexOf(qId) < 0)
state.questionsSel.push(qId);
}
......@@ -306,7 +336,7 @@ Events:
}
}
} else if (indexDrop > indexSource) { // after the actual position
if (indexDrop === questionsIds.value.length - 1) {
if (indexDrop === state.questionsIds.length - 1) {
for (let i = 0; i < selLength.value; i++) {
newPosition[i] = positionDrop + 128 * (i + 1) // after the last question
}
......@@ -372,25 +402,12 @@ Events:
index++
return questionNodes.value[index]
}
defineExpose({setSelection})
</script>
<template lang="pug">
#question-list(ref="root") {{ questionsIds.length }} question{{ questionsIds.length > 1 ? 's' : '' }}
//drop-context-menu(ref="dropContextMenu" @on-close="endDrop")
// div(@click="changePosition") Changer l'ordre
// div(@click="endDrop") Annuler
//DropContextMenu(
// ref="rightContextMenu"
// :event="state.dropContexMenuEvent"
// :modelValue="state.dropContexMenuShow"
// @update:modelValue="newValue => state.dropContexMenuShow = newValue")
// div(v-if="editable(state.questionsSel[0])"
// @click="$emit('question-edit', {questionId: contextMenuSelection}); state.contextMenuSelection=null; state.dropContexMenuShow=false"
// ) Modifier
// div(@click="$emit('question-duplicate', {questionId: contextMenuSelection}); state.contextMenuSelection=null; state.dropContexMenuShow=false") Dupliquer
// div(@click="state.contextMenuSelection=null; state.dropContexMenuShow=false") Annuler
div
QuestionListNode(
v-for="(questionId, index) in questionsIds"
......@@ -402,6 +419,7 @@ Events:
@click="onClick($event, questionId)"
@dblclick="onDblClick($event, questionId)"
@contextmenu.prevent="onContextMenu($event, questionId)"
@loaded="questionNodeLoaded"
draggable="draggable"
@dragstart="onDragStart($event, index, questionId)"
@dragenter.prevent="onDragEnter($event, index, questionId)"
......@@ -412,13 +430,8 @@ Events:
@show-question-tooltip="$emit('show-question-tooltip', $event)")/
//question-edit="$emit('question-edit', $event)"
//)/
</template>
<style>
#question-list {
/*overflow-y: scroll;*/
/*scroll-behavior: smooth;*/
/*height: 500px;*/
}
</style>
......@@ -15,6 +15,7 @@
'dragenter',
'dragleave',
'drop',
'loaded'
])
const props = defineProps<{
......@@ -44,16 +45,22 @@
const style = computed(() => root.value!.style)
// resend event to parent
function resend(eventName: 'dragenter' | 'dragleave' | 'drop', event: DragEvent) {
emit(eventName, event)
}
function onImageLoaded(event: Event) {
console.info('question_' + props.questionId + ' loaded')
}
defineExpose({style, question})
</script>
<template lang="pug">
.question(
ref="root"
:id="'question_' + questionId"
:data-testid="'question_' + questionId"
:class="{selected:selected}")
score(
......@@ -69,6 +76,7 @@
avalider: question.avalider && ! question.validee,\
atrier: atrier}"
:src="('../static/thumbs/' + question.thumb_name)"
@load="$emit('loaded')"
@dragenter.prevent="resend('dragenter', $event)"
@dragleave.prevent="resend('dragleave', $event)"
@dragover.prevent
......
......@@ -20,7 +20,7 @@ import type {QuestionID, ThemeID} from "@/types";
const root = ref<HTMLElement | null>(null)
const emit = defineEmits(['set-theme', 'update:show'])
const emit = defineEmits(['set-selection', 'update:show'])
const question = computed(() => {
if (state.questionId)
......@@ -30,8 +30,8 @@ import type {QuestionID, ThemeID} from "@/types";
})
function gotoTheme(themeId: ThemeID) {
emit('set-theme', {themeId: themeId});
function setSelection(themeId: ThemeID, questionId: QuestionID) {
emit('set-selection', {themeId: themeId, questionsIds: [ questionId ]});
close();
}
......@@ -42,7 +42,7 @@ import type {QuestionID, ThemeID} from "@/types";
async function show(event: MouseEvent, questionId: QuestionID) {
state.questionId = questionId
root.value!.style.left = (event.pageX || event.clientX) + 'px'
root.value!.style.top = ((event.pageY || event.clientY) - window.pageYOffset) + 'px'
root.value!.style.top = ((event.pageY || event.clientY) - window.scrollY) + 'px'
state.visible = true
await nextTick()
root.value?.focus()
......@@ -74,9 +74,12 @@ import type {QuestionID, ThemeID} from "@/types";
|{{ question.validee ? 'validée' : (question.avalider ? 'en attente de validation' : 'en cours de rédaction') }})&nbsp;
.themes Themes&nbsp;:
div.link(v-for="question_order in question.themes" @click="gotoTheme(question_order.theme)") {{ themePath(question_order.theme) }}
div.link(
v-for="question_order in question.themes"
@click="setSelection(question_order.theme, state.questionId)")
| {{ themePath(question_order.theme) }}
span(:class="{atrier: question_order.position === null}")
|{{ question_order.position !== null ? ('(position: ' + question_order.position + ')') : '(non positionnée)' }}
| {{ question_order.position !== null ? ('(position: ' + question_order.position + ')') : '(non positionnée)' }}
</template>
......
......@@ -12,10 +12,17 @@
const username = globalThis.username
const emit = defineEmits(['classe-changed'])
classes.loadAnnees()
function onAnneeChange(event: {target: {value: string}}) {
classes.setSelectedAnnee(parseInt(event.target.value))
function onAnneeChange(event: InputEvent) {
classes.setSelectedAnnee(parseInt((event.target as HTMLInputElement).value))
}
function onClasseChange(event: InputEvent) {
emit('classe-changed', parseInt((event.target as HTMLInputElement).value))
// classes.setSelectedId(parseInt((event.target as HTMLInputElement).value))
}
</script>
......@@ -38,7 +45,7 @@
select(
v-if="classes.getAnnees.length"
:value="classes.getSelectedId"
@input="classes.setSelectedId(parseInt($event.target.value))")
@input="onClasseChange")
option(
v-for="classeId of classes.getCurrIds"
:key="classeId"
......
......@@ -256,7 +256,6 @@ export const useFeuillesStore = defineStore('feuillesStore',{
},
_deleteSeance(seance: ISeance) {
console.log('seance', seance.id)
return $axios.delete('api/seance/' + seance.id + '/')
.then(() => {
// for (const questionPosee of state.seances[seanceID].questionsPosees) {
......
......@@ -48,13 +48,14 @@ export const useQuestionsStore = defineStore('questionsStore',{
},
getIndexInTheme: (state: IQuestionsStore) => (questionId: QuestionID, themeId: ThemeID) => {
console.debug('questions.getIndexInTheme', questionId, themeId)
const themes = useThemesStore()
return themes.get(themeId).questions!.findIndex((question) => question === questionId)
},
getPositionInTheme() {
return (questionId: QuestionID, themeId: ThemeID) => {
console.log('getPositioninTheme', questionId, themeId)
console.debug('questions.getPositioninTheme', questionId, themeId)
const question = this.get(questionId)
const themePos = question.themes.find((themePos) => themePos.theme === themeId)
return themePos!.position
......
......@@ -96,8 +96,9 @@ export const useThemesStore = defineStore('themesStore',{
const theme = this.get(themeId)
if (theme.questions === undefined) {
const questions = useQuestionsStore()
questions.loadQuestionsOfTheme(themeId)
return questions.loadQuestionsOfTheme(themeId)
}
return Promise.resolve()
},
getNextPosition(themeId:ThemeID, position: number, before= false) {
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment