mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
Survey plugin tweaks & bug fixes (#8188)
* Survey plugin tweaks & bug fixes Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com> * ff Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com> --------- Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>
This commit is contained in:
Generated
+2
-1
@@ -5345,7 +5345,7 @@ packages:
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/survey-resources@file:projects/survey-resources.tgz':
|
||||
resolution: {integrity: sha512-GcIw9MyT6uttWSxYSZOdu9Kln2eLlum64WfLRrHoA0/Z6zxXdlKn6IgZwRdwo2iQBbws9Vz3sfEvoa89XCm6dw==, tarball: file:projects/survey-resources.tgz}
|
||||
resolution: {integrity: sha512-OKGMUZJzuG7+t/0yBujRPBsJPraZFObMn+gF0hOPlgUeBwmFhh5fUKhodNvqAQhmaikyUI8wqf1Zt8roq1fMUg==, tarball: file:projects/survey-resources.tgz}
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/survey@file:projects/survey.tgz':
|
||||
@@ -25234,6 +25234,7 @@ snapshots:
|
||||
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
|
||||
fast-equals: 5.2.2
|
||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
|
||||
prettier: 3.2.5
|
||||
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.19)
|
||||
|
||||
@@ -302,6 +302,23 @@ export class ThrottledCaller {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class DebouncedCaller {
|
||||
timeout?: any
|
||||
constructor (readonly delay: number = 50) {}
|
||||
call (op: () => void): void {
|
||||
if (this.timeout !== undefined) {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
this.timeout = setTimeout(() => {
|
||||
op()
|
||||
this.timeout = undefined
|
||||
}, this.delay)
|
||||
}
|
||||
}
|
||||
|
||||
export const testing = (localStorage.getItem('#platform.testing.enabled') ?? 'false') === 'true'
|
||||
|
||||
export const rootBarExtensions = writable<
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"@hcengineering/view": "^0.6.13",
|
||||
"@hcengineering/view-resources": "^0.6.0",
|
||||
"@hcengineering/ui": "^0.6.15",
|
||||
"svelte": "^4.2.19"
|
||||
"svelte": "^4.2.19",
|
||||
"fast-equals": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Question, QuestionKind, Poll, PollData } from '@hcengineering/survey'
|
||||
import PollQuestion from './PollQuestion.svelte'
|
||||
import { Poll, PollData, Question, QuestionKind } from '@hcengineering/survey'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { hasText } from '../utils'
|
||||
import PollQuestion from './PollQuestion.svelte'
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let object: Poll | PollData
|
||||
export let canSubmit: boolean = false
|
||||
@@ -47,16 +47,14 @@
|
||||
return true
|
||||
}
|
||||
|
||||
function isPreviewMode (): boolean {
|
||||
return (object as Poll)._id === undefined
|
||||
function handleChange (patch: Partial<Poll>): void {
|
||||
dispatch('change', patch)
|
||||
}
|
||||
|
||||
async function saveAnswers (): Promise<void> {
|
||||
if (isPreviewMode()) {
|
||||
return
|
||||
}
|
||||
const poll = object as Poll
|
||||
await client.updateDoc(poll._class, poll.space, poll._id, { questions: object.questions })
|
||||
function handleQuestionChange (index: number, patch: Partial<PollQuestion>): Promise<void> | void {
|
||||
const questions = (object.questions ?? []).slice()
|
||||
questions[index] = { ...questions[index], ...patch }
|
||||
handleChange({ questions })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -68,13 +66,13 @@
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#each object.questions ?? [] as question, index}
|
||||
{#each object.questions ?? [] as question, index (index)}
|
||||
{#if isQuestionValid(question)}
|
||||
<PollQuestion
|
||||
bind:this={questionNodes[index]}
|
||||
bind:isAnswered={isAnswered[index]}
|
||||
{readonly}
|
||||
on:answered={saveAnswers}
|
||||
on:change={(e) => handleQuestionChange(index, e.detail)}
|
||||
{question}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -17,34 +17,55 @@
|
||||
<script lang="ts">
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Panel } from '@hcengineering/panel'
|
||||
import { MessageBox, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Poll } from '@hcengineering/survey'
|
||||
import { Button, IconMoreH, showPopup } from '@hcengineering/ui'
|
||||
import { Button, DebouncedCaller, IconMoreH, ThrottledCaller } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { DocNavLink, ParentsNavigator, showMenu } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import EditPoll from './EditPoll.svelte'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import survey from '../plugin'
|
||||
import EditPoll from './EditPoll.svelte'
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
const query = createQuery()
|
||||
|
||||
const throttle = new ThrottledCaller(250)
|
||||
const debounce = new DebouncedCaller(1000)
|
||||
|
||||
export let _id: Ref<Poll>
|
||||
export let embedded: boolean = false
|
||||
export let readonly: boolean = false
|
||||
|
||||
let object: Poll | undefined = undefined
|
||||
interface Patch {
|
||||
id: number
|
||||
patch: Partial<Poll>
|
||||
}
|
||||
|
||||
function combinedPatch (patches: Patch[]): Partial<Poll> {
|
||||
return patches.reduce((r, c) => Object.assign(r, c.patch), {})
|
||||
}
|
||||
|
||||
let patchCounter = 0
|
||||
let patches: Patch[] = []
|
||||
|
||||
let objectState: Poll | undefined = undefined
|
||||
$: object = objectState ? { ...objectState, ...combinedPatch(patches) } : undefined
|
||||
|
||||
let canSubmit = false
|
||||
let requestUpdate = false
|
||||
|
||||
$: isCompleted = object?.isCompleted ?? false
|
||||
$: editable = (!readonly && !isCompleted) || requestUpdate
|
||||
|
||||
$: updateObject(_id)
|
||||
$: void queryObject(_id)
|
||||
|
||||
function updateObject (_id: Ref<Poll>): void {
|
||||
async function queryObject (_id: Ref<Poll>): Promise<void> {
|
||||
await flush()
|
||||
objectState = undefined
|
||||
patches = []
|
||||
query.query(survey.class.Poll, { _id }, (result) => {
|
||||
object = result[0]
|
||||
objectState = result[0]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,8 +89,30 @@
|
||||
// )
|
||||
await getClient().updateDoc(object._class, object.space, object._id, { isCompleted: true })
|
||||
}
|
||||
|
||||
function handleChange (patch: Partial<Poll>): void {
|
||||
patches = [...patches, { id: patchCounter++, patch }]
|
||||
throttle.call(() => {
|
||||
void flush()
|
||||
})
|
||||
}
|
||||
|
||||
async function flush (): Promise<void> {
|
||||
if (!object?._id || patches.length < 1) return
|
||||
|
||||
const patchesToApply = patches.slice()
|
||||
const patchIds = new Set(patchesToApply.map((p) => p.id))
|
||||
const update: Partial<Poll> = combinedPatch(patchesToApply)
|
||||
|
||||
await client.updateDoc(object._class, object.space, object._id, update)
|
||||
debounce.call(() => {
|
||||
patches = patches.filter((p) => !patchIds.has(p.id))
|
||||
})
|
||||
}
|
||||
onDestroy(flush)
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={flush} />
|
||||
{#if object}
|
||||
<Panel
|
||||
isHeader={false}
|
||||
@@ -120,7 +163,14 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="flex-col flex-grow flex-no-shrink">
|
||||
<EditPoll {object} readonly={!editable} bind:canSubmit />
|
||||
<EditPoll
|
||||
{object}
|
||||
readonly={!editable}
|
||||
bind:canSubmit
|
||||
on:change={(e) => {
|
||||
handleChange(e.detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
{/if}
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { MessageBox, getClient } from '@hcengineering/presentation'
|
||||
import { Question, QuestionKind, Survey } from '@hcengineering/survey'
|
||||
import { Question, QuestionKind } from '@hcengineering/survey'
|
||||
import {
|
||||
ButtonIcon,
|
||||
EditBox,
|
||||
@@ -27,138 +26,86 @@
|
||||
showPopup,
|
||||
tooltip
|
||||
} from '@hcengineering/ui'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import survey from '../plugin'
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let parent: Survey
|
||||
export let index: number
|
||||
export let question: Question
|
||||
export let readonly: boolean = false
|
||||
export let isNewQuestion = false
|
||||
|
||||
let inputNameEditBox: EditBox
|
||||
let inputNameSyncState: string = ''
|
||||
let inputName: string = ''
|
||||
|
||||
$: if (inputNameSyncState !== question.name) {
|
||||
inputNameSyncState = question.name
|
||||
inputName = question.name
|
||||
}
|
||||
|
||||
let inputOptionsSyncState: string[] = []
|
||||
let inputOptions: string[] = []
|
||||
|
||||
let inputNewOption = ''
|
||||
|
||||
$: if (!deepEqual(inputOptionsSyncState, question.options ?? [])) {
|
||||
inputOptionsSyncState = question.options ?? []
|
||||
inputOptions = inputOptionsSyncState?.slice() ?? []
|
||||
}
|
||||
|
||||
let editQuestion: EditBox
|
||||
let hovered: boolean = false
|
||||
|
||||
let defaultQuestion: Question = {
|
||||
name: '',
|
||||
kind: QuestionKind.STRING,
|
||||
isMandatory: false,
|
||||
hasCustomOption: false
|
||||
function handleChange (patch: Partial<Question>): void {
|
||||
dispatch('change', patch)
|
||||
}
|
||||
|
||||
$: question = (parent?.questions?.[index] as Question) ?? defaultQuestion
|
||||
$: isNewQuestion = parent?.questions?.[index] === undefined
|
||||
$: options = question?.options ?? []
|
||||
$: questionIcon = isNewQuestion
|
||||
? survey.icon.Question
|
||||
: question.kind === QuestionKind.OPTIONS
|
||||
? survey.icon.QuestionKindOptions
|
||||
: question.kind === QuestionKind.OPTION
|
||||
? survey.icon.QuestionKindOption
|
||||
: survey.icon.QuestionKindString
|
||||
function flush (): void {
|
||||
if (isNewQuestion) return
|
||||
|
||||
let haveNameChanges = false
|
||||
const patch: Partial<Question> = {}
|
||||
let haveChanges = false
|
||||
|
||||
let newOption = ''
|
||||
|
||||
onDestroy(() => {
|
||||
handleExit()
|
||||
})
|
||||
|
||||
function handleExit (): void {
|
||||
void handleNameChange()
|
||||
}
|
||||
|
||||
async function updateParent (): Promise<void> {
|
||||
await client.updateDoc(parent._class, parent.space, parent._id, { questions: parent.questions })
|
||||
}
|
||||
|
||||
$: if (isNewQuestion && question.name.trim() !== '') {
|
||||
void createQuestion()
|
||||
}
|
||||
|
||||
function createQuestion (): Promise<void> {
|
||||
if (parent.questions === undefined) {
|
||||
parent.questions = []
|
||||
if (inputName !== question.name) {
|
||||
patch.name = inputName
|
||||
haveChanges = true
|
||||
}
|
||||
parent.questions.push({ ...question })
|
||||
defaultQuestion = { ...defaultQuestion, name: '' }
|
||||
return updateParent()
|
||||
}
|
||||
|
||||
function handleNameChange (): Promise<void> | void {
|
||||
if (!haveNameChanges) return
|
||||
haveNameChanges = false
|
||||
|
||||
if (isNewQuestion) return createQuestion()
|
||||
return changeName()
|
||||
}
|
||||
|
||||
async function changeName (): Promise<void> {
|
||||
if (!isNewQuestion) {
|
||||
await updateParent()
|
||||
if (!deepEqual(inputOptions, question.options ?? [])) {
|
||||
patch.options = inputOptions
|
||||
haveChanges = true
|
||||
}
|
||||
|
||||
if (haveChanges) handleChange(patch)
|
||||
}
|
||||
onDestroy(flush)
|
||||
|
||||
$: haveNonEmptyName = inputName.trim().length > 0
|
||||
$: if (haveNonEmptyName && isNewQuestion) {
|
||||
handleChange({ name: inputName })
|
||||
}
|
||||
|
||||
async function changeKind (kind: QuestionKind): Promise<void> {
|
||||
if (question.kind !== kind) {
|
||||
question.kind = kind
|
||||
await updateParent()
|
||||
}
|
||||
}
|
||||
|
||||
async function changeMandatory (): Promise<void> {
|
||||
question.isMandatory = !question.isMandatory
|
||||
await updateParent()
|
||||
}
|
||||
|
||||
async function changeCustomOption (): Promise<void> {
|
||||
question.hasCustomOption = !question.hasCustomOption
|
||||
await updateParent()
|
||||
}
|
||||
|
||||
async function changeOption (index: number): Promise<void> {
|
||||
if (options[index].trim().length === 0) {
|
||||
await deleteOption(index)
|
||||
function changeOption (index: number): void {
|
||||
if (inputOptions[index]?.trim()?.length === 0) {
|
||||
deleteOption(index)
|
||||
} else {
|
||||
await updateParent()
|
||||
handleChange({ options: inputOptions })
|
||||
}
|
||||
}
|
||||
|
||||
async function addOption (): Promise<void> {
|
||||
if (newOption.trim().length === 0) {
|
||||
newOption = ''
|
||||
function addOption (): void {
|
||||
if (inputNewOption.trim().length === 0) {
|
||||
inputNewOption = ''
|
||||
return
|
||||
}
|
||||
if (question.options === undefined) {
|
||||
question.options = []
|
||||
}
|
||||
question.options = [...options, newOption]
|
||||
await updateParent()
|
||||
newOption = ''
|
||||
inputOptions = [...inputOptions, inputNewOption]
|
||||
inputNewOption = ''
|
||||
handleChange({ options: inputOptions })
|
||||
}
|
||||
|
||||
async function deleteOption (index: number): Promise<void> {
|
||||
options.splice(index, 1)
|
||||
await updateParent()
|
||||
}
|
||||
|
||||
async function deleteQuestion (): Promise<void> {
|
||||
showPopup(
|
||||
MessageBox,
|
||||
{
|
||||
label: survey.string.DeleteQuestion,
|
||||
message: survey.string.DeleteQuestionConfirm
|
||||
},
|
||||
undefined,
|
||||
async (result?: boolean) => {
|
||||
if (result === true) {
|
||||
parent.questions?.splice(index, 1)
|
||||
await updateParent()
|
||||
}
|
||||
}
|
||||
)
|
||||
function deleteOption (index: number): void {
|
||||
inputOptions = inputOptions.filter((val, idx) => idx !== index)
|
||||
handleChange({ options: inputOptions })
|
||||
}
|
||||
|
||||
function showQuestionParams (ev: MouseEvent): void {
|
||||
@@ -214,27 +161,27 @@
|
||||
async (id) => {
|
||||
switch (id) {
|
||||
case QuestionKind.STRING: {
|
||||
await changeKind(QuestionKind.STRING)
|
||||
handleChange({ kind: QuestionKind.STRING })
|
||||
break
|
||||
}
|
||||
case QuestionKind.OPTION: {
|
||||
await changeKind(QuestionKind.OPTION)
|
||||
handleChange({ kind: QuestionKind.OPTION })
|
||||
break
|
||||
}
|
||||
case QuestionKind.OPTIONS: {
|
||||
await changeKind(QuestionKind.OPTIONS)
|
||||
handleChange({ kind: QuestionKind.OPTIONS })
|
||||
break
|
||||
}
|
||||
case 'mandatory': {
|
||||
await changeMandatory()
|
||||
handleChange({ isMandatory: !question.isMandatory })
|
||||
break
|
||||
}
|
||||
case 'custom-option': {
|
||||
await changeCustomOption()
|
||||
handleChange({ hasCustomOption: !question.hasCustomOption })
|
||||
break
|
||||
}
|
||||
case 'delete': {
|
||||
await deleteQuestion()
|
||||
dispatch('delete')
|
||||
break
|
||||
}
|
||||
case undefined: {
|
||||
@@ -266,7 +213,7 @@
|
||||
async (id) => {
|
||||
switch (id) {
|
||||
case 'delete': {
|
||||
await deleteOption(index)
|
||||
deleteOption(index)
|
||||
break
|
||||
}
|
||||
case undefined: {
|
||||
@@ -285,7 +232,7 @@
|
||||
let draggedOverIndex: number | undefined
|
||||
const draggableElements: HTMLElement[] = []
|
||||
|
||||
function dragStart (ev: DragEvent, index: number): void {
|
||||
function onOptionDragStart (ev: DragEvent, index: number): void {
|
||||
if (readonly || ev.dataTransfer === null) {
|
||||
return
|
||||
}
|
||||
@@ -298,7 +245,7 @@
|
||||
)
|
||||
}
|
||||
|
||||
function dragOver (ev: DragEvent, index: number): void {
|
||||
function onOptionDragOver (ev: DragEvent, index: number): void {
|
||||
if (draggedIndex === undefined || draggedIndex === draggedOverIndex || draggedIndex + 1 === draggedOverIndex) {
|
||||
return
|
||||
}
|
||||
@@ -306,7 +253,7 @@
|
||||
draggedOverIndex = index
|
||||
}
|
||||
|
||||
function dragLeave (ev: DragEvent, index: number): void {
|
||||
function onOptionDragLeave (ev: DragEvent, index: number): void {
|
||||
if (draggedIndex === undefined) {
|
||||
return
|
||||
}
|
||||
@@ -316,32 +263,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function dragDrop (): Promise<void> {
|
||||
function onOptionDrop (): void {
|
||||
if (draggedIndex === undefined || draggedOverIndex === undefined) {
|
||||
return
|
||||
}
|
||||
let modified = false
|
||||
if (draggedOverIndex === options.length && draggedIndex !== options.length - 1) {
|
||||
options.push(options[draggedIndex])
|
||||
options.splice(draggedIndex, 1)
|
||||
modified = true
|
||||
} else if (draggedOverIndex < draggedIndex) {
|
||||
const tmp = options[draggedIndex]
|
||||
options[draggedIndex] = options[draggedOverIndex]
|
||||
options[draggedOverIndex] = tmp
|
||||
modified = true
|
||||
} else if (draggedIndex + 1 !== draggedOverIndex) {
|
||||
const tmp = options[draggedIndex]
|
||||
options[draggedIndex] = options[draggedOverIndex - 1]
|
||||
options[draggedOverIndex - 1] = tmp
|
||||
modified = true
|
||||
}
|
||||
if (modified) {
|
||||
await updateParent()
|
||||
if (draggedIndex === draggedOverIndex || draggedIndex === draggedOverIndex - 1) {
|
||||
return
|
||||
}
|
||||
|
||||
const item = inputOptions[draggedIndex]
|
||||
const other = inputOptions.filter((_, index) => index !== draggedIndex)
|
||||
const index = draggedIndex < draggedOverIndex ? draggedOverIndex - 1 : draggedOverIndex
|
||||
inputOptions = [...other.slice(0, index), item, ...other.slice(index)]
|
||||
|
||||
handleChange({ options: inputOptions })
|
||||
}
|
||||
|
||||
function dragEnd (): void {
|
||||
function onOptionDragEnd (): void {
|
||||
draggedIndex = undefined
|
||||
draggedOverIndex = undefined
|
||||
}
|
||||
@@ -349,7 +287,7 @@
|
||||
let isRootDragging = false
|
||||
let rootElement: HTMLElement
|
||||
|
||||
function rootDragStart (ev: DragEvent): void {
|
||||
function onRootDragStart (ev: DragEvent): void {
|
||||
if (readonly || ev.dataTransfer === null) {
|
||||
return
|
||||
}
|
||||
@@ -363,17 +301,25 @@
|
||||
dispatch('dragStart')
|
||||
}
|
||||
|
||||
function rootDragEnd (): void {
|
||||
function onRootDragEnd (): void {
|
||||
isRootDragging = false
|
||||
dispatch('dragEnd')
|
||||
}
|
||||
|
||||
const focusQuestion = (): void => {
|
||||
editQuestion.focusInput()
|
||||
export function focusQuestion (): void {
|
||||
inputNameEditBox.focusInput()
|
||||
}
|
||||
|
||||
$: questionIcon = isNewQuestion
|
||||
? survey.icon.Question
|
||||
: question.kind === QuestionKind.OPTIONS
|
||||
? survey.icon.QuestionKindOptions
|
||||
: question.kind === QuestionKind.OPTION
|
||||
? survey.icon.QuestionKindOption
|
||||
: survey.icon.QuestionKindString
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={handleExit} />
|
||||
<svelte:window on:beforeunload={flush} />
|
||||
<div
|
||||
bind:this={rootElement}
|
||||
class="question-container flex-col flex-gap-2"
|
||||
@@ -392,22 +338,21 @@
|
||||
class="self-start"
|
||||
role="presentation"
|
||||
draggable={!readonly}
|
||||
on:dragstart={rootDragStart}
|
||||
on:dragend={rootDragEnd}
|
||||
on:dragstart={onRootDragStart}
|
||||
on:dragend={onRootDragEnd}
|
||||
>
|
||||
<ButtonIcon size={'small'} disabled={readonly} icon={questionIcon} on:click={showQuestionParams} />
|
||||
</div>
|
||||
{/if}
|
||||
<EditBox
|
||||
bind:this={editQuestion}
|
||||
bind:this={inputNameEditBox}
|
||||
format={'text-multiline'}
|
||||
disabled={readonly}
|
||||
placeholder={survey.string.QuestionPlaceholderEmpty}
|
||||
bind:value={question.name}
|
||||
on:input={() => {
|
||||
haveNameChanges = true
|
||||
bind:value={inputName}
|
||||
on:change={() => {
|
||||
handleChange({ name: inputName })
|
||||
}}
|
||||
on:change={handleNameChange}
|
||||
/>
|
||||
{#if !isNewQuestion}
|
||||
{#if question.hasCustomOption && question.kind !== QuestionKind.STRING}
|
||||
@@ -423,18 +368,18 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if !isNewQuestion && question.kind !== QuestionKind.STRING}
|
||||
{#each options as option, index (index)}
|
||||
{#each inputOptions as option, index (index)}
|
||||
<div
|
||||
class="flex-row-center flex-gap-3 option"
|
||||
role="listitem"
|
||||
bind:this={draggableElements[index]}
|
||||
on:dragover={(ev) => {
|
||||
dragOver(ev, index)
|
||||
onOptionDragOver(ev, index)
|
||||
}}
|
||||
on:dragleave={(ev) => {
|
||||
dragLeave(ev, index)
|
||||
onOptionDragLeave(ev, index)
|
||||
}}
|
||||
on:drop={dragDrop}
|
||||
on:drop={onOptionDrop}
|
||||
class:is-dragged={index === draggedIndex}
|
||||
class:dragged-over={draggedIndex !== undefined &&
|
||||
draggedOverIndex === index &&
|
||||
@@ -445,9 +390,9 @@
|
||||
role="presentation"
|
||||
draggable={!readonly}
|
||||
on:dragstart={(ev) => {
|
||||
dragStart(ev, index)
|
||||
onOptionDragStart(ev, index)
|
||||
}}
|
||||
on:dragend={dragEnd}
|
||||
on:dragend={onOptionDragEnd}
|
||||
>
|
||||
<ButtonIcon
|
||||
disabled={readonly}
|
||||
@@ -463,9 +408,9 @@
|
||||
<EditBox
|
||||
disabled={readonly}
|
||||
placeholder={survey.string.QuestionPlaceholderOption}
|
||||
bind:value={options[index]}
|
||||
on:change={async () => {
|
||||
await changeOption(index)
|
||||
bind:value={inputOptions[index]}
|
||||
on:change={() => {
|
||||
changeOption(index)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -475,16 +420,20 @@
|
||||
class="flex-row-center flex-gap-3 option"
|
||||
role="listitem"
|
||||
on:dragover={(ev) => {
|
||||
dragOver(ev, options.length)
|
||||
onOptionDragOver(ev, question.options?.length ?? 0)
|
||||
}}
|
||||
on:dragleave={(ev) => {
|
||||
dragLeave(ev, options.length)
|
||||
onOptionDragLeave(ev, question.options?.length ?? 0)
|
||||
}}
|
||||
on:drop={dragDrop}
|
||||
class:dragged-over={draggedOverIndex === options.length && draggedIndex !== options.length - 1}
|
||||
on:drop={onOptionDrop}
|
||||
class:dragged-over={draggedOverIndex === inputOptions.length && draggedIndex !== inputOptions.length - 1}
|
||||
>
|
||||
<ButtonIcon disabled icon={survey.icon.Question} iconSize={'x-small'} kind={'tertiary'} size={'extra-small'} />
|
||||
<EditBox placeholder={survey.string.QuestionPlaceholderOption} bind:value={newOption} on:change={addOption} />
|
||||
<EditBox
|
||||
placeholder={survey.string.QuestionPlaceholderOption}
|
||||
bind:value={inputNewOption}
|
||||
on:change={addOption}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -15,34 +15,76 @@
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Survey } from '@hcengineering/survey'
|
||||
import { EditBox, FocusHandler, Label, createFocusManager, Icon } from '@hcengineering/ui'
|
||||
import EditQuestion from './EditQuestion.svelte'
|
||||
import { MessageBox } from '@hcengineering/presentation'
|
||||
import { Question, QuestionKind, Survey } from '@hcengineering/survey'
|
||||
import { createFocusManager, EditBox, FocusHandler, Icon, Label, showPopup } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import survey from '../plugin'
|
||||
import EditQuestion from './EditQuestion.svelte'
|
||||
import IconQuestion from './icons/Question.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const manager = createFocusManager()
|
||||
const client = getClient()
|
||||
|
||||
export let object: Survey
|
||||
export let readonly: boolean = false
|
||||
|
||||
$: questions = object?.questions ?? []
|
||||
$: questionEditSlots = readonly ? questions : [...questions, {}]
|
||||
|
||||
async function nameChange (): Promise<void> {
|
||||
await client.updateDoc(object._class, object.space, object._id, { name: object.name })
|
||||
const emptyQuestion: Question = {
|
||||
name: '',
|
||||
kind: QuestionKind.STRING,
|
||||
isMandatory: false,
|
||||
hasCustomOption: false
|
||||
}
|
||||
|
||||
async function promptChange (): Promise<void> {
|
||||
await client.updateDoc(object._class, object.space, object._id, { prompt: object.prompt })
|
||||
let newQuestion: Question = { ...emptyQuestion }
|
||||
|
||||
let newQuestionComponent: EditQuestion
|
||||
const questionComponents: EditQuestion[] = []
|
||||
|
||||
function handleChange (patch: Partial<Survey>): void {
|
||||
dispatch('change', patch)
|
||||
}
|
||||
|
||||
function deleteQuestion (index: number): void {
|
||||
if (!object.questions?.[index]) return
|
||||
showPopup(
|
||||
MessageBox,
|
||||
{
|
||||
label: survey.string.DeleteQuestion,
|
||||
message: survey.string.DeleteQuestionConfirm
|
||||
},
|
||||
undefined,
|
||||
async (result?: boolean) => {
|
||||
if (result === true) {
|
||||
let questions = object.questions ?? []
|
||||
questions = questions.filter((q, i) => i !== index)
|
||||
handleChange({ questions })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function handleNewQuestionChange (patch: Partial<Question>): void {
|
||||
const question = { ...newQuestion, ...patch }
|
||||
|
||||
let questions = object.questions ?? []
|
||||
|
||||
questions = [...questions, question]
|
||||
newQuestion = { ...emptyQuestion }
|
||||
|
||||
handleChange({ questions })
|
||||
}
|
||||
|
||||
function handleQuestionChange (index: number, patch: Partial<Question>): Promise<void> | void {
|
||||
const questions = (object.questions ?? []).slice()
|
||||
questions[index] = { ...questions[index], ...patch }
|
||||
handleChange({ questions })
|
||||
}
|
||||
|
||||
let draggedIndex: number | undefined = undefined
|
||||
let draggedOverIndex: number | undefined = undefined
|
||||
|
||||
function dragOver (ev: DragEvent, index: number): void {
|
||||
function onQuestionDragOver (ev: DragEvent, index: number): void {
|
||||
if (draggedIndex === undefined || draggedIndex === draggedOverIndex || draggedIndex + 1 === draggedOverIndex) {
|
||||
return
|
||||
}
|
||||
@@ -50,40 +92,32 @@
|
||||
draggedOverIndex = index
|
||||
}
|
||||
|
||||
function dragLeave (ev: DragEvent, index: number): void {
|
||||
if (draggedIndex === undefined) {
|
||||
return
|
||||
}
|
||||
function onQuestionDragLeave (ev: DragEvent, index: number): void {
|
||||
if (draggedIndex === undefined) return
|
||||
|
||||
ev.preventDefault()
|
||||
if (draggedOverIndex === index) {
|
||||
draggedOverIndex = undefined
|
||||
}
|
||||
if (draggedOverIndex === index) draggedOverIndex = undefined
|
||||
}
|
||||
|
||||
async function dragDrop (): Promise<void> {
|
||||
function onQuestionDrop (): void {
|
||||
if (draggedIndex === undefined || draggedOverIndex === undefined) {
|
||||
return
|
||||
}
|
||||
let modified = false
|
||||
if (draggedOverIndex === questions.length && draggedIndex !== questions.length - 1) {
|
||||
questions.push(questions[draggedIndex])
|
||||
questions.splice(draggedIndex, 1)
|
||||
modified = true
|
||||
} else if (draggedOverIndex < draggedIndex) {
|
||||
const tmp = questions[draggedIndex]
|
||||
questions[draggedIndex] = questions[draggedOverIndex]
|
||||
questions[draggedOverIndex] = tmp
|
||||
modified = true
|
||||
} else if (draggedIndex + 1 !== draggedOverIndex) {
|
||||
const tmp = questions[draggedIndex]
|
||||
questions[draggedIndex] = questions[draggedOverIndex - 1]
|
||||
questions[draggedOverIndex - 1] = tmp
|
||||
modified = true
|
||||
}
|
||||
if (modified) {
|
||||
await client.updateDoc(object._class, object.space, object._id, { questions })
|
||||
if (draggedIndex === draggedOverIndex || draggedIndex === draggedOverIndex - 1) {
|
||||
return
|
||||
}
|
||||
|
||||
let questions = object?.questions ?? []
|
||||
const item = questions[draggedIndex]
|
||||
const other = questions.filter((_, index) => index !== draggedIndex)
|
||||
const index = draggedIndex < draggedOverIndex ? draggedOverIndex - 1 : draggedOverIndex
|
||||
questions = [...other.slice(0, index), item, ...other.slice(index)]
|
||||
questionComponents[index]?.focusQuestion()
|
||||
|
||||
handleChange({ questions })
|
||||
}
|
||||
|
||||
$: questionList = [...(object.questions ?? []), newQuestion]
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
@@ -95,7 +129,9 @@
|
||||
placeholder={survey.string.Name}
|
||||
bind:value={object.name}
|
||||
kind={'large-style'}
|
||||
on:change={nameChange}
|
||||
on:input={() => {
|
||||
handleChange({ name: object.name })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="step-tb-6">
|
||||
@@ -103,7 +139,9 @@
|
||||
disabled={readonly}
|
||||
placeholder={survey.string.PromptPlaceholder}
|
||||
bind:value={object.prompt}
|
||||
on:change={promptChange}
|
||||
on:input={() => {
|
||||
handleChange({ prompt: object.prompt })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="antiSection step-tb-6">
|
||||
@@ -115,42 +153,48 @@
|
||||
<Label label={survey.string.Questions} />
|
||||
</span>
|
||||
</div>
|
||||
{#each questionEditSlots as question, index}
|
||||
{#each questionList as question, index (index)}
|
||||
{@const isNewQuestion = index === questionList.length - 1}
|
||||
<div
|
||||
role="listitem"
|
||||
on:dragover={(ev) => {
|
||||
dragOver(ev, index)
|
||||
onQuestionDragOver(ev, index)
|
||||
}}
|
||||
on:dragleave={(ev) => {
|
||||
dragLeave(ev, index)
|
||||
onQuestionDragLeave(ev, index)
|
||||
}}
|
||||
on:drop={dragDrop}
|
||||
on:drop={onQuestionDrop}
|
||||
class:dragged-over={draggedIndex !== undefined &&
|
||||
draggedOverIndex === index &&
|
||||
draggedOverIndex !== draggedIndex &&
|
||||
draggedOverIndex !== draggedIndex + 1}
|
||||
>
|
||||
{#key index}
|
||||
<EditQuestion
|
||||
{index}
|
||||
{readonly}
|
||||
parent={object}
|
||||
on:dragStart={() => {
|
||||
draggedIndex = index
|
||||
}}
|
||||
on:dragEnd={() => {
|
||||
draggedIndex = undefined
|
||||
draggedOverIndex = undefined
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
<EditQuestion
|
||||
bind:this={questionComponents[index]}
|
||||
{question}
|
||||
{isNewQuestion}
|
||||
on:delete={() => {
|
||||
deleteQuestion(index)
|
||||
}}
|
||||
on:change={(e) => {
|
||||
isNewQuestion ? handleNewQuestionChange(e.detail) : handleQuestionChange(index, e.detail)
|
||||
}}
|
||||
{readonly}
|
||||
on:dragStart={() => {
|
||||
draggedIndex = index
|
||||
}}
|
||||
on:dragEnd={() => {
|
||||
draggedIndex = undefined
|
||||
draggedOverIndex = undefined
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
div[role='listitem'] + div[role='listitem'] {
|
||||
div[role='listitem'] {
|
||||
margin-top: var(--spacing-1);
|
||||
}
|
||||
.dragged-over {
|
||||
|
||||
@@ -17,38 +17,90 @@
|
||||
<script lang="ts">
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Panel } from '@hcengineering/panel'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Survey } from '@hcengineering/survey'
|
||||
import { Button, Icon, IconMoreH, Label, tooltip, Breadcrumb } from '@hcengineering/ui'
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
DebouncedCaller,
|
||||
Icon,
|
||||
IconMoreH,
|
||||
Label,
|
||||
ThrottledCaller,
|
||||
tooltip
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { showMenu } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import EditSurvey from './EditSurvey.svelte'
|
||||
import EditPoll from './EditPoll.svelte'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import survey from '../plugin'
|
||||
import { makePollData } from '../utils'
|
||||
import EditPoll from './EditPoll.svelte'
|
||||
import EditSurvey from './EditSurvey.svelte'
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
const query = createQuery()
|
||||
|
||||
const throttle = new ThrottledCaller(250)
|
||||
const debounce = new DebouncedCaller(1000)
|
||||
|
||||
export let _id: Ref<Survey>
|
||||
export let embedded: boolean = false
|
||||
export let readonly: boolean = false
|
||||
|
||||
let object: Survey | undefined = undefined
|
||||
interface Patch {
|
||||
id: number
|
||||
patch: Partial<Survey>
|
||||
}
|
||||
|
||||
function combinedPatch (patches: Patch[]): Partial<Survey> {
|
||||
return patches.reduce((r, c) => Object.assign(r, c.patch), {})
|
||||
}
|
||||
|
||||
let patchCounter = 0
|
||||
let patches: Patch[] = []
|
||||
|
||||
let objectState: Survey | undefined = undefined
|
||||
$: object = objectState ? { ...objectState, ...combinedPatch(patches) } : undefined
|
||||
|
||||
let preview = false
|
||||
let canSubmit = false
|
||||
|
||||
$: updateObject(_id)
|
||||
$: void queryObject(_id)
|
||||
$: poll = preview && object !== undefined ? makePollData(object) : undefined
|
||||
|
||||
function updateObject (_id: Ref<Survey>): void {
|
||||
async function queryObject (_id: Ref<Survey>): Promise<void> {
|
||||
await flush()
|
||||
objectState = undefined
|
||||
patches = []
|
||||
query.query(survey.class.Survey, { _id }, (result) => {
|
||||
object = result[0]
|
||||
objectState = result[0]
|
||||
})
|
||||
}
|
||||
|
||||
function handleChange (patch: Partial<Survey>): void {
|
||||
patches = [...patches, { id: patchCounter++, patch }]
|
||||
throttle.call(() => {
|
||||
void flush()
|
||||
})
|
||||
}
|
||||
|
||||
async function flush (): Promise<void> {
|
||||
if (!object?._id || patches.length < 1) return
|
||||
|
||||
const patchesToApply = patches.slice()
|
||||
const patchIds = new Set(patchesToApply.map((p) => p.id))
|
||||
const update: Partial<Survey> = combinedPatch(patchesToApply)
|
||||
|
||||
await client.updateDoc(object._class, object.space, object._id, update)
|
||||
debounce.call(() => {
|
||||
patches = patches.filter((p) => !patchIds.has(p.id))
|
||||
})
|
||||
}
|
||||
onDestroy(flush)
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={flush} />
|
||||
{#if object}
|
||||
<Panel
|
||||
isHeader={false}
|
||||
@@ -109,7 +161,13 @@
|
||||
<EditPoll object={poll} bind:canSubmit />
|
||||
{/if}
|
||||
{:else}
|
||||
<EditSurvey {object} {readonly} />
|
||||
<EditSurvey
|
||||
{object}
|
||||
on:change={(e) => {
|
||||
handleChange(e.detail)
|
||||
}}
|
||||
{readonly}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { generateId } from '@hcengineering/core'
|
||||
import { EditBox, Icon, Label, tooltip, ModernCheckbox, ModernRadioButton } from '@hcengineering/ui'
|
||||
import { AnsweredQuestion, QuestionKind } from '@hcengineering/survey'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { EditBox, Icon, Label, ModernCheckbox, ModernRadioButton, tooltip } from '@hcengineering/ui'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import survey from '../plugin'
|
||||
import { hasText } from '../utils'
|
||||
|
||||
@@ -28,113 +29,147 @@
|
||||
export let isAnswered: boolean = false
|
||||
export let readonly: boolean = false
|
||||
|
||||
let answer = ''
|
||||
let selectedOption: number | undefined
|
||||
const selectedOptions: boolean[] = []
|
||||
const customOption = -1
|
||||
|
||||
interface InputState {
|
||||
answer: string
|
||||
option?: number
|
||||
options: boolean[]
|
||||
}
|
||||
|
||||
const input: InputState = {
|
||||
answer: '',
|
||||
option: undefined,
|
||||
options: (question.options ?? []).map(() => false)
|
||||
}
|
||||
|
||||
$: id = generateId()
|
||||
$: showAnswers(question)
|
||||
$: updateIsAnswered(question, selectedOption, selectedOptions)
|
||||
$: syncInputState(question)
|
||||
$: validateAnswer(question, input)
|
||||
|
||||
function showAnswers (question: AnsweredQuestion): void {
|
||||
if (question.kind === QuestionKind.STRING) {
|
||||
answer = question.answer ?? ''
|
||||
} else if (question.kind === QuestionKind.OPTION) {
|
||||
selectedOption = question.answers?.[0]
|
||||
if ((question.answers === undefined || question.answers === null) && typeof question.answer === 'string') {
|
||||
selectedOption = customOption
|
||||
answer = question.answer
|
||||
function flush (): void {
|
||||
const patch: Partial<AnsweredQuestion> = {}
|
||||
let haveChanges = false
|
||||
|
||||
const current = extractCurrentAnswerSet(input)
|
||||
if (!isSameAnswers(current, question)) {
|
||||
patch.answer = current.answer
|
||||
patch.answers = current.answers
|
||||
haveChanges = true
|
||||
}
|
||||
|
||||
if (haveChanges) handleChange(patch)
|
||||
}
|
||||
onDestroy(flush)
|
||||
|
||||
function syncInputState (question: AnsweredQuestion): void {
|
||||
const current = extractCurrentAnswerSet(input)
|
||||
if (isSameAnswers(current, question)) return
|
||||
|
||||
input.answer = question.answer ?? ''
|
||||
input.option =
|
||||
question.kind === QuestionKind.OPTION
|
||||
? typeof question.answer === 'string'
|
||||
? customOption
|
||||
: question.answers?.[0]
|
||||
: undefined
|
||||
|
||||
const answerIds = new Set(question.answers ?? [])
|
||||
input.options = (question.options ?? []).map((_, idx) => answerIds.has(idx))
|
||||
|
||||
if (question.kind === QuestionKind.OPTIONS && typeof question.answer === 'string') {
|
||||
input.options[customOption] = true
|
||||
}
|
||||
}
|
||||
|
||||
function validateAnswer (question: AnsweredQuestion, input: InputState): void {
|
||||
isAnswered = isValidAnswer(question, input)
|
||||
}
|
||||
|
||||
function isValidAnswer (question: AnsweredQuestion, input: InputState): boolean {
|
||||
if (!question.isMandatory) return true
|
||||
|
||||
switch (question.kind) {
|
||||
case QuestionKind.STRING:
|
||||
return hasText(input.answer)
|
||||
case QuestionKind.OPTION:
|
||||
return input.option === customOption ? hasText(input.answer) : input.option !== undefined
|
||||
case QuestionKind.OPTIONS:
|
||||
return input.options[customOption] ? hasText(input.answer) : input.options.some((on) => on)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange (patch: Partial<AnsweredQuestion>): void {
|
||||
dispatch('change', patch)
|
||||
}
|
||||
|
||||
function handleAnswerChange (): void {
|
||||
const patch = extractCurrentAnswerSet(input)
|
||||
handleChange(patch)
|
||||
}
|
||||
|
||||
interface AnswerSet {
|
||||
answer?: string
|
||||
answers?: number[]
|
||||
}
|
||||
|
||||
function isSameAnswers (a1: AnswerSet, a2: AnswerSet): boolean {
|
||||
return (a1.answer ?? null) === (a2.answer ?? null) && deepEqual(a1.answers ?? null, a2.answers ?? null)
|
||||
}
|
||||
|
||||
function extractCurrentAnswerSet (input: InputState): AnswerSet {
|
||||
switch (question.kind) {
|
||||
case QuestionKind.STRING: {
|
||||
const answer = hasText(input.answer) ? input.answer : undefined
|
||||
return { answer, answers: undefined }
|
||||
}
|
||||
} else if (question.kind === QuestionKind.OPTIONS) {
|
||||
question.answers?.forEach((index) => {
|
||||
selectedOptions[index] = true
|
||||
})
|
||||
if (typeof question.answer === 'string') {
|
||||
selectedOptions[customOption] = true
|
||||
answer = question.answer
|
||||
case QuestionKind.OPTION: {
|
||||
const answer: string | undefined = input.option === customOption ? input.answer : undefined
|
||||
const answers: number[] | undefined =
|
||||
input.option !== undefined && input.option !== customOption ? [input.option] : undefined
|
||||
return { answer, answers }
|
||||
}
|
||||
case QuestionKind.OPTIONS: {
|
||||
const toggledOptions: number[] = input.options
|
||||
.map((state, idx) => [state, idx] as const)
|
||||
.filter((a) => a[0])
|
||||
.map((a) => a[1])
|
||||
|
||||
const answers = toggledOptions.length > 0 ? toggledOptions : undefined
|
||||
const answer = input.options[customOption] ? input.answer : undefined
|
||||
return { answer, answers }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateIsAnswered (
|
||||
question: AnsweredQuestion,
|
||||
selectedOption: number | undefined,
|
||||
selectedOptions: boolean[]
|
||||
): void {
|
||||
if (!question.isMandatory) {
|
||||
isAnswered = true
|
||||
return
|
||||
}
|
||||
if (question.kind === QuestionKind.STRING) {
|
||||
isAnswered = hasText(answer)
|
||||
} else if (question.kind === QuestionKind.OPTION) {
|
||||
isAnswered = selectedOption === customOption ? hasText(answer) : selectedOption !== undefined
|
||||
} else if (question.kind === QuestionKind.OPTIONS) {
|
||||
isAnswered = selectedOptions[customOption] ? hasText(answer) : selectedOptions.some((on) => on)
|
||||
}
|
||||
}
|
||||
|
||||
function answerChange (): void {
|
||||
question.answer = hasText(answer) ? answer : undefined
|
||||
dispatch('answered')
|
||||
}
|
||||
|
||||
function optionChange (): void {
|
||||
if (selectedOption === undefined) {
|
||||
question.answer = undefined
|
||||
question.answers = undefined
|
||||
} else if (selectedOption === customOption) {
|
||||
question.answer = answer
|
||||
question.answers = undefined
|
||||
} else {
|
||||
question.answer = undefined
|
||||
question.answers = [selectedOption]
|
||||
}
|
||||
dispatch('answered')
|
||||
}
|
||||
|
||||
function optionsChange (): void {
|
||||
const answers: number[] = []
|
||||
selectedOptions.forEach((on, index) => {
|
||||
if (on) {
|
||||
answers.push(index)
|
||||
}
|
||||
})
|
||||
question.answers = answers.length > 0 ? answers : undefined
|
||||
question.answer = selectedOptions[customOption] ? answer : undefined
|
||||
dispatch('answered')
|
||||
}
|
||||
|
||||
function getReadonlyAnswers (): string[] {
|
||||
if (question.kind === QuestionKind.STRING) {
|
||||
return [answer.trim()]
|
||||
}
|
||||
if (question.kind === QuestionKind.OPTION) {
|
||||
if (selectedOption === undefined) {
|
||||
return []
|
||||
}
|
||||
if (selectedOption === customOption) {
|
||||
return [answer.trim()]
|
||||
}
|
||||
return [question.options?.[selectedOption] ?? '']
|
||||
}
|
||||
if (question.kind === QuestionKind.OPTIONS) {
|
||||
const answers: string[] = []
|
||||
question.options?.forEach((option, index) => {
|
||||
if (selectedOptions[index]) {
|
||||
answers.push(option)
|
||||
switch (question.kind) {
|
||||
case QuestionKind.STRING:
|
||||
return [input.answer.trim()]
|
||||
case QuestionKind.OPTION:
|
||||
if (input.option === undefined) {
|
||||
return []
|
||||
}
|
||||
})
|
||||
if (selectedOptions[customOption]) {
|
||||
answers.push(answer.trim())
|
||||
if (input.option === customOption) {
|
||||
return [input.answer.trim()]
|
||||
}
|
||||
return [question.options?.[input.option] ?? '']
|
||||
case QuestionKind.OPTIONS: {
|
||||
const answers: string[] = (question.options ?? []).filter((_, idx) => input.options[idx])
|
||||
if (input.options[customOption]) {
|
||||
answers.push(input.answer.trim())
|
||||
}
|
||||
return answers
|
||||
}
|
||||
return answers
|
||||
default:
|
||||
return []
|
||||
}
|
||||
return []
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={flush} />
|
||||
<div class="question-answer-container flex-col flex-gap-3">
|
||||
<div class="flex-row-center flex-gap-1 flex-no-shrink">
|
||||
<strong class="text-base caption-color font-medium pre-wrap">{question.name}</strong>
|
||||
@@ -165,8 +200,8 @@
|
||||
id={`${id}-${i}`}
|
||||
value={i}
|
||||
label={option}
|
||||
bind:group={selectedOption}
|
||||
on:change={optionChange}
|
||||
bind:group={input.option}
|
||||
on:change={handleAnswerChange}
|
||||
/>
|
||||
{/each}
|
||||
{#if question.hasCustomOption}
|
||||
@@ -174,17 +209,17 @@
|
||||
id={`${id}-custom`}
|
||||
value={customOption}
|
||||
labelIntl={survey.string.AnswerCustomOption}
|
||||
bind:group={selectedOption}
|
||||
on:change={optionChange}
|
||||
bind:group={input.option}
|
||||
on:change={handleAnswerChange}
|
||||
/>
|
||||
{#if selectedOption === customOption}
|
||||
{#if input.option === customOption}
|
||||
<div class="pl-6">
|
||||
<EditBox
|
||||
bind:value={answer}
|
||||
bind:value={input.answer}
|
||||
placeholder={survey.string.AnswerPlaceholder}
|
||||
focusable
|
||||
autoFocus
|
||||
on:change={answerChange}
|
||||
on:change={handleAnswerChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -193,23 +228,28 @@
|
||||
{:else if question.kind === QuestionKind.OPTIONS}
|
||||
<div class="flex-col flex-gap-2 px-6">
|
||||
{#each question.options ?? [] as option, i}
|
||||
<ModernCheckbox id={`${id}-${i}`} label={option} bind:checked={selectedOptions[i]} on:change={optionsChange} />
|
||||
<ModernCheckbox
|
||||
id={`${id}-${i}`}
|
||||
label={option}
|
||||
bind:checked={input.options[i]}
|
||||
on:change={handleAnswerChange}
|
||||
/>
|
||||
{/each}
|
||||
{#if question.hasCustomOption}
|
||||
<ModernCheckbox
|
||||
id={`${id}-custom`}
|
||||
labelIntl={survey.string.AnswerCustomOption}
|
||||
bind:checked={selectedOptions[customOption]}
|
||||
on:change={optionsChange}
|
||||
bind:checked={input.options[customOption]}
|
||||
on:change={handleAnswerChange}
|
||||
/>
|
||||
{#if selectedOptions[customOption]}
|
||||
{#if input.options[customOption]}
|
||||
<div class="pl-6">
|
||||
<EditBox
|
||||
bind:value={answer}
|
||||
bind:value={input.answer}
|
||||
placeholder={survey.string.AnswerPlaceholder}
|
||||
focusable
|
||||
autoFocus
|
||||
on:change={answerChange}
|
||||
on:change={handleAnswerChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -219,9 +259,9 @@
|
||||
<div>
|
||||
<EditBox
|
||||
format={'text-multiline'}
|
||||
bind:value={answer}
|
||||
bind:value={input.answer}
|
||||
placeholder={survey.string.AnswerPlaceholder}
|
||||
on:change={answerChange}
|
||||
on:change={handleAnswerChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user