Commit 61b0be87 authored by GuiΩ's avatar GuiΩ
Browse files

feat(modal): Modal is draggable and resizable

parent 11e80a01
......@@ -11,11 +11,15 @@
-->
<script lang="ts" setup>
import {ref} from "vue";
interface ICodeEditorProps {
code?: string,
tabsize?: number
}
const codeEditor = ref<HTMLAreaElement | null>(null)
const emit = defineEmits(['input', 'update:code'])
const props = withDefaults(defineProps<ICodeEditorProps>(), {
......@@ -97,17 +101,36 @@
break
}
}
function setHeight(height: string) {
codeEditor.value!.style.height = height
}
function setWidth(width: string) {
codeEditor.value!.style.width = width
}
defineExpose({setHeight, setWidth})
</script>
<template lang="pug">
textarea(
//.CodeEditor__shadowArea(
// @keydown="handleKeyDown"
// @input="handleInput") {{ props.code }}
textarea.CodeEditor__codeArea(
ref="codeEditor"
:value="props.code"
cols="70"
rows="30"
spellcheck="false"
@keydown="handleKeyDown"
@input="handleInput")/
</template>
<style>
<style lang="sass">
.CodeEditor__codeArea
z-index: 0
resize: none
position: relative
.CodeEditor__shadowArea
z-index: -1
</style>
\ No newline at end of file
......@@ -10,15 +10,17 @@ Composant Vuejs qui affiche une fenêtre modale contenant un message
-->
<script lang="ts" setup>
import {computed, nextTick, reactive, ref} from "vue";
import {computed, nextTick, reactive, ref} from "vue";
import Modal from "@/components/Modal.vue";
const bottom = ref<HTMLElement | null>(null)
const modal = ref<InstanceType<typeof Modal> | null>(null)
const body = ref<HTMLDivElement | null>(null)
const bottom = ref<HTMLElement | null>(null)
const state = reactive({
title: '',
message: '', // message à afficher
isVisible: false, // la fenêtre est-elle affichée ?
})
......@@ -35,58 +37,77 @@ const bottom = ref<HTMLElement | null>(null)
async function show(title: string, message: string) {
state.title = title
state.message = message
state.isVisible = true
modal.value!.show()
await nextTick()
bottom.value?.scrollIntoView()
}
function close() {
modal.value!.close()
}
function onResize({width: newWidth, height: newHeight}: { width: number, height: number }) {
console.debug('MB.onResize', newWidth, newHeight)
body.value!.style.height = (newHeight - 129) + 'px'
}
defineExpose({show})
</script>
<template lang="pug">
Modal.MessageBox_modal(v-show="state.isVisible" @close="state.isVisible = false")
Modal(
ref="modal"
@resize="onResize")
template(v-slot:title) {{ state.title }}
template(v-slot:body)
.MessageBox__body(ref="body")
pre(v-if="! html.value") {{ state.message }}
span(v-if="html.value" v-html="state.message")
div(ref="bottom")
//transition(v-if="visible")
// .message-mask(@click="visible = false")
// .message-wrapper
.message-container(data-textid="message")
pre(v-if="! html.value") {{ state.message }}
span(v-if="html.value" v-html="state.message")
div(ref="bottom")
template(v-slot:footer)
button(data-testid="cancel" type="button" @click="close") Fermer
</template>
<style>
.message-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: table;
transition: opacity 0.3s ease;
}
.message-wrapper {
vertical-align: middle;
display: table-cell;
width: 100%;
}
.message-container {
overflow-y: scroll;
max-width: 40%;
max-height: 600px;
margin: 0 auto;
padding: 20px 30px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
font-family: Helvetica, Arial, sans-serif;
}
<style lang="sass">
.MessageBox__body
overflow: auto
/*.message-mask {*/
/* position: fixed;*/
/* z-index: 9998;*/
/* top: 0;*/
/* left: 0;*/
/* width: 100%;*/
/* height: 100%;*/
/* background-color: rgba(0, 0, 0, 0.5);*/
/* display: table;*/
/* transition: opacity 0.3s ease;*/
/* overflow: auto;*/
/*}*/
/*.message-wrapper {*/
/* vertical-align: middle;*/
/* display: table-cell;*/
/* width: 100%;*/
/*}*/
/*.message-container {*/
/* overflow-y: scroll;*/
/* max-width: 40%;*/
/* max-height: 600px;*/
/* margin: 0 auto;*/
/* padding: 20px 30px;*/
/* background-color: #fff;*/
/* border-radius: 2px;*/
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);*/
/* transition: all 0.3s ease;*/
/* font-family: Helvetica, Arial, sans-serif;*/
/*}*/
</style>
......@@ -4,93 +4,215 @@
-->
<script lang="ts" setup>
import {nextTick, reactive, ref} from "vue";
interface IModalProps {
minHeight?: number
minWidth?: number
}
interface IModalState {
isVisible: boolean,
isDragging: boolean,
isWaiting: boolean,
resizingDirection?: 's' | 'e' | 'se',
lastX: number,
lastY: number,
firstOpening: boolean,
}
const emit = defineEmits([
'close'
'resize'
])
const backdrop = ref<HTMLDivElement | null>(null)
const modal = ref<HTMLDivElement | null>(null)
const props = withDefaults(defineProps<IModalProps>(), {
minHeight: 100,
minWidth: 150,
})
const state: IModalState = reactive({
isVisible: false,
isDragging: false,
isWaiting: false,
lastX: 0,
lastY: 0,
firstOpening: true,
})
function onMouseDownOnHeader(event: MouseEvent) {
console.debug('Modal.onMouseDownOnHeader',event)
state.isDragging = true
state.lastX = event.clientX
state.lastY = event.clientY
}
function onMouseDown(event: MouseEvent) {
console.debug('Modal.onMouseDown', event, modal.value!.getBoundingClientRect())
const bb = modal.value!.getBoundingClientRect()
const x = event.clientX
const y = event.clientY
if ( bb.right - 15 < x && x < bb.right && bb.top < y && y < bb.bottom - 20)
state.resizingDirection = 'e'
else if (bb.left < x && x < bb.right - 20 && bb.bottom - 15 < y && y <bb.bottom)
state.resizingDirection = 's'
else if (bb.right - 20 < x && x < bb.right && bb.bottom - 20 < y && y <bb.bottom)
state.resizingDirection = 'se'
}
function onMouseMove(event:MouseEvent) {
console.debug('Modal.onMouseMove', event)
const bb = modal.value!.getBoundingClientRect()
const x = event.clientX
const y = event.clientY
if (state.isDragging) {
modal.value!.style.left = (bb.left + x - state.lastX) + 'px'
modal.value!.style.top = (bb.top + y - state.lastY) + 'px'
} else if (state.resizingDirection) {
const newSize = {width: bb.width, height: bb.height}
const w = bb.width + x - state.lastX
const h = bb.height + y - state.lastY
switch (state.resizingDirection) {
case 'e':
newSize.width = w < props.minWidth ? props.minWidth : w
modal.value!.style.width = newSize.width + 'px'
break
case 's':
newSize.height = h < props.minHeight ? props.minHeight : h
modal.value!.style.height = newSize.height + 'px'
break
case 'se':
newSize.height = h < props.minHeight ? props.minHeight : h
newSize.width = w < props.minWidth ? props.minWidth : w
modal.value!.style.width = newSize.width + 'px'
modal.value!.style.height = newSize.height + 'px'
}
emit('resize', newSize)
} else if ( bb.right-10 < x && x < bb.right && bb.top < y && y < bb.bottom-10)
backdrop.value!.style.cursor = 'e-resize'
else if (bb.left < x && x < bb.right-10 && bb.bottom - 10 < y && y <bb.bottom)
backdrop.value!.style.cursor = 's-resize'
else if (bb.right -10 < x && x < bb.right && bb.bottom - 10 < y && y <bb.bottom)
backdrop.value!.style.cursor = 'se-resize'
else if (state.isWaiting)
backdrop.value!.style.cursor = 'wait'
else
backdrop.value!.style.cursor = 'default'
state.lastX = x
state.lastY = y
}
function onMouseUp(event: MouseEvent) {
state.isDragging = false
state.resizingDirection = undefined
}
async function show() {
state.isVisible = true
await nextTick()
if (state.firstOpening) {
const bb = modal.value!.getBoundingClientRect()
modal.value!.style.left = ((backdrop.value!.getBoundingClientRect().right - bb.right + bb.left) / 2) + 'px'
modal.value!.style.top = ((backdrop.value!.getBoundingClientRect().bottom - bb.bottom + bb.top) / 2) + 'px'
state.firstOpening = false
emit('resize', {width: bb.width, height: bb.height})
}
}
function setWaiting(wait: boolean) {
state.isWaiting = wait
if (state.isWaiting)
modal.value!.style.cursor = 'wait'
else
modal.value!.style.cursor = 'default'
}
function close() {
emit('close')
state.isVisible = false
state.isWaiting = false
}
defineExpose({show, close, setWaiting})
</script>
<template lang="pug">
.Modal_backdrop
.Modal
header.Modal_header
slot(name="title") default title
button.Modal_closeBtn(
.Modal__backdrop(
ref="backdrop"
v-show="state.isVisible"
@mousemove.stop="onMouseMove"
@mouseup="onMouseUp")
.Modal(
ref="modal"
@mousedown="onMouseDown")
header.Modal__header(
@mousedown.stop="onMouseDownOnHeader")
.Modal__title
slot(name="title") default title
button.Modal__closeBtn(
type="button"
title="Fermer"
@click="close") X
section.Modal_body
@click="close") &#128473;
section.Modal__body
slot(name="body") default body
footer.Modal_footer
footer.Modal__footer
slot(name="footer")
</template>
<style>
.Modal_backdrop {
z-index: 1;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
resize: both;
overflow: scroll;
}
<style lang="sass">
.Modal__backdrop
z-index: 1
position: fixed
top: 0
bottom: 0
left: 0
right: 0
background-color: rgba(0, 0, 0, 0.3)
.Modal {
background: #FFFFFF;
box-shadow: 2px 2px 20px 1px;
display: flex;
flex-direction: column;
max-height: 100%;
}
.Modal
position: absolute
box-shadow: 2px 2px 20px 1px
background: #FFFFFF
border-radius: 10px
display: flex
flex-direction: column
max-height: 100%
width: 50%
.Modal_header,
.Modal_footer {
flex: 0 0 auto;
padding: 15px;
display: flex;
}
.Modal__header
padding: 5px 5px 0 5px
display: grid
grid-template-columns: auto min-content
align-items: center
border-bottom: 1px solid #eeeeee
font-weight: bold
cursor: move
.Modal_header {
position: relative;
border-bottom: 1px solid #eeeeee;
color: #4AAE9B;
justify-content: space-between;
}
.Modal__title
text-align: center
.Modal_footer {
border-top: 1px solid #eeeeee;
flex-direction: column;
justify-content: flex-end;
}
.Modal__closeBtn
border: none
cursor: pointer
font-size: larger
//color: #4AAE9B
background: transparent
.Modal_body {
flex: 1 1 auto;
position: relative;
padding: 20px 10px;
overflow: auto;
}
.Modal__body
flex: 1 1 auto
//position: relative
padding: 20px 10px
//overflow: hidden
.Modal_closeBtn {
position: absolute;
top: 0;
right: 0;
border: none;
font-size: 20px;
padding: 10px;
cursor: pointer;
font-weight: bold;
color: #4AAE9B;
background: transparent;
}
.Modal__footer
flex: 0 0 auto
padding: 10px
border-top: 1px solid #eeeeee
display: grid
justify-items: center
</style>
\ No newline at end of file
......@@ -7,29 +7,24 @@ Events:
<script lang="ts" setup>
import Modal from '@/components/Modal.vue'
import CodeEditor from "@/components/CodeEditor.vue";
import ThemeTree from "@/components/ThemeTree.vue";
// import VModal from 'vue-js-modal';
import {computed, nextTick, reactive, ref, watch} from 'vue';
import {useQuestionsStore} from "@/store/questions";
import type {IQuestion, IThemePos, QuestionID, ThemeID} from "@/types";
import {useThemesStore} from "@/store/themes";
import ThemeTree from "@/components/ThemeTree.vue"
import {computed, nextTick, reactive, ref} from 'vue'
import {useQuestionsStore} from "@/store/questions"
import type {IQuestion, IThemePos, QuestionID, ThemeID} from "@/types"
import {useThemesStore} from "@/store/themes"
const questions = useQuestionsStore()
const themes = useThemesStore()
//
// interface IQuestionEditorProps {
// show: boolean,
// options: {action: 'edit' | 'duplicate' | 'new', questionId: QuestionID}
// }
interface IQuestionEditorState {
themesSelection: ThemeID[],
question: Partial<IQuestion>,
oldQuestion: IQuestion | null,
// visible: boolean,
isModalVisible: boolean
}
const modal = ref<InstanceType<typeof Modal> | null>(null)
const root = ref<HTMLElement | null>(null)
const codeEditor = ref<InstanceType<typeof CodeEditor> | null>(null)
const themeTree = ref<InstanceType<typeof ThemeTree> | null>(null)
const emit = defineEmits([
......@@ -42,9 +37,7 @@ Events:
const state: IQuestionEditorState = reactive({
themesSelection: [],
question: {themes: []},
oldQuestion: null,
// visible: false,
isModalVisible: false
oldQuestion: null
})
const errors = computed(() => questions.getSaveErrors)
......@@ -88,7 +81,7 @@ Events:
state.question.avalider = false
state.question.validee = false
setThemesSelection(state.question.themes!);
show();
modal.value.show();
await nextTick()
themeTree.value?.initOpenThemes()
}
......@@ -103,9 +96,9 @@ Events:
state.question = JSON.parse(JSON.stringify(state.oldQuestion))
}
setThemesSelection(state.question.themes!)
show()
await nextTick()
themeTree.value?.initOpenThemes()
modal.value.show()
// await nextTick()
// themeTree.value?.initOpenThemes()
}
// function opened(event) {
......@@ -120,7 +113,7 @@ Events:
}
function save() {
root.value!.style['cursor'] = 'wait';
modal.value!.setWaiting(true)
let themes = [];
for (let i = 0; i < state.themesSelection.length; i++) {
const themeId = state.themesSelection[i];
......@@ -140,161 +133,167 @@ Events:
.then(() => {
if (questions.getSaveStatus)
close()
root.value!.style.cursor = 'default';
modal.value.setWaiting(false)
})
}
function show() {
state.isModalVisible = true
function onResize({width: newWidth, height: newHeight}: { width: number, height: number }) {
console.debug('QE.onResize', newWidth, newHeight)
themeTree.value!.setHeight((newHeight - 139) + 'px')
codeEditor.value!.setHeight((newHeight - 188) + 'px')
codeEditor.value!.setWidth((newWidth - 480) + 'px')
}
function close() {
state.isModalVisible = false
modal.value.close()
}
defineExpose({edit, duplicate})
</script>
<template lang="pug">
//modal(
// classes="question-editor"
// name="question-editor"
// adaptative="true"
// height="550"
// width="750"
// :min-width="650"
// :min-height=550
// :draggable="true"
// :resizable="true"
// :scrollable="true"
// :clickToClose="false"
// @opened="opened")
Modal(v-show="state.isModalVisible" @close="state.isModalVisible = false")
template(v-slot:title) {{ state.question.id ? 'Question ' + state.question.id : 'Nouvelle question' }}
template(v-slot:body)
.content(ref="root")
#code-panel
table
tr
td
label(for="code") Code LaTeX
td
.error(v-if="errors && errors.code" data-testid="code-error") {{ errors.code.join('\n') }}
.link(
v-if="errors && errors.latexlog"
data-testid="latexlog"
@click="$emit('message', {title: 'Erreur de compilation', message:errors.latexlog[0]})"
) Voir le log LaTeX