Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fill in the blanks: Final preparation for release #3176

Merged
merged 32 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9570249
wip: draft drag & drop implementation
LarsTheGlidingSquirrel Dec 1, 2023
81734a9
Change persisted state to use `correctAnswers` and `incorrectAnswers`
LarsTheGlidingSquirrel Dec 5, 2023
e3640f5
Make draggables display correct text
LarsTheGlidingSquirrel Dec 5, 2023
a7db1de
Rename plugin type to `textWithBlanksExercise`
LarsTheGlidingSquirrel Dec 8, 2023
270e88a
Merge remote-tracking branch 'origin/staging' into fill-in-the-blank-…
LarsTheGlidingSquirrel Dec 8, 2023
14d6fc9
Comment out properties that concern future features/state.
LarsTheGlidingSquirrel Dec 8, 2023
f220097
Move DndProvider and make nesting them work
LarsTheGlidingSquirrel Dec 8, 2023
1354526
Enable fill-in-the-blanks plugin also in production environment
LarsTheGlidingSquirrel Dec 8, 2023
ff8b8a8
Change type from `textWithBlanksExercise` to `blanksExercise`
LarsTheGlidingSquirrel Dec 11, 2023
6b46cd6
Disable formatting options 'heading' and 'link' in fill-in-the-blanks
LarsTheGlidingSquirrel Dec 11, 2023
c5b1915
Disable 'math' element in fill-in-the-blanks because toolbar is broken.
LarsTheGlidingSquirrel Dec 11, 2023
723825f
fix lint error
LarsTheGlidingSquirrel Dec 11, 2023
4fa4921
Add placeholder text
LarsTheGlidingSquirrel Dec 11, 2023
6f46729
Rename `defaultLearnerFeedback` to `defaultIncorrectAnswerFeedback`
LarsTheGlidingSquirrel Dec 11, 2023
50f404b
refactor(blank-exercise): Delete an `as` statement in toggleBlank()
kulla Dec 11, 2023
4778d5f
refactor(blank-exercise): Remove unused getBlankElement()
kulla Dec 11, 2023
a350c52
refactor(text-plugin): Use more concrete type in trimSelection()
kulla Dec 11, 2023
1e3621f
refactor(blank-exercise): Remove `as` statements
kulla Dec 11, 2023
d49d93b
refactor(blank-exercise): Shorten toggleBlank()
kulla Dec 11, 2023
9398163
refactor(blank-exercise): Shorten toggleBlank()
kulla Dec 11, 2023
7258163
Fix eslint error
kulla Dec 11, 2023
415414c
Fix toggleBlank when blank->text->blank are selected
LarsTheGlidingSquirrel Dec 12, 2023
3293c34
Merge pull request #3191 from serlo/kulla-patch-fill
LarsTheGlidingSquirrel Dec 12, 2023
0ba1b83
Use `textBlank` instead of `blank` as type.
LarsTheGlidingSquirrel Dec 12, 2023
2326b5f
Prevent toggleBlan k affecting non-selected blanks
LarsTheGlidingSquirrel Dec 12, 2023
2df6598
Give copy and pasted blanks a unique id
LarsTheGlidingSquirrel Dec 12, 2023
7fb4833
refactor(editor): Use list descrution in closures
kulla Dec 14, 2023
37a88d5
refactor: Shorten code in normalizeNode()
kulla Dec 14, 2023
a7088d8
refactor: Shorten if statement
kulla Dec 14, 2023
bf102cb
refactor
LarsTheGlidingSquirrel Dec 14, 2023
8b16490
format
LarsTheGlidingSquirrel Dec 14, 2023
e725353
Merge pull request #3210 from serlo/patch-kulla-fill
LarsTheGlidingSquirrel Dec 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 29 additions & 24 deletions src/components/frontend-client-base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Cookies from 'js-cookie'
import { Router, useRouter } from 'next/router'
import NProgress from 'nprogress'
import { useState, useEffect } from 'react'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { default as ToastNotice } from 'react-notify-toast'
import { getInstanceDataByLang } from 'src/helper/feature-i18n'

Expand Down Expand Up @@ -116,33 +118,36 @@ export function FrontendClientBase({
//console.dir(initialProps)

return (
<InstanceDataProvider value={instanceData}>
<PrintMode />
<AuthProvider unauthenticatedAuthorizationPayload={authorization}>
<LoggedInDataProvider value={loggedInData}>
<UuidsProvider value={{ entityId, revisionId }}>
<ConditionalWrap
condition={!noHeaderFooter}
wrapper={(kids) => <HeaderFooter>{kids}</HeaderFooter>}
>
// TODO: Move DndProvider to better location
<DndProvider backend={HTML5Backend}>
<InstanceDataProvider value={instanceData}>
<PrintMode />
<AuthProvider unauthenticatedAuthorizationPayload={authorization}>
<LoggedInDataProvider value={loggedInData}>
<UuidsProvider value={{ entityId, revisionId }}>
<ConditionalWrap
condition={!noContainers}
wrapper={(kids) => (
<div className="relative">
<MaxWidthDiv showNav={showNav}>
<main id="content">{kids}</main>
</MaxWidthDiv>
</div>
)}
condition={!noHeaderFooter}
wrapper={(kids) => <HeaderFooter>{kids}</HeaderFooter>}
>
{children}
<ConditionalWrap
condition={!noContainers}
wrapper={(kids) => (
<div className="relative">
<MaxWidthDiv showNav={showNav}>
<main id="content">{kids}</main>
</MaxWidthDiv>
</div>
)}
>
{children}
</ConditionalWrap>
</ConditionalWrap>
</ConditionalWrap>
<ToastNotice />
</UuidsProvider>
</LoggedInDataProvider>
</AuthProvider>
</InstanceDataProvider>
<ToastNotice />
</UuidsProvider>
</LoggedInDataProvider>
</AuthProvider>
</InstanceDataProvider>
</DndProvider>
)

function getCachedLoggedInData() {
Expand Down
12 changes: 3 additions & 9 deletions src/serlo-editor/core/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { useEffect, ReactNode, useRef, useState } from 'react'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { HotkeysProvider, useHotkeys } from 'react-hotkeys-hook'
import { Provider } from 'react-redux'

Expand All @@ -26,13 +24,9 @@ import { ROOT } from '../store/root/constants'
export function Editor(props: EditorProps) {
return (
<Provider store={store}>
<DndProvider backend={HTML5Backend}>
<HotkeysProvider
initiallyActiveScopes={['global', 'root-up-down-enter']}
>
<InnerDocument {...props} />
</HotkeysProvider>
</DndProvider>
<HotkeysProvider initiallyActiveScopes={['global', 'root-up-down-enter']}>
<InnerDocument {...props} />
</HotkeysProvider>
</Provider>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ export function toggleBlank(editor: SlateEditor) {
},
})
if (text) {
Transforms.insertNodes(editor, { text: (text as Blank).correctAnswer })
const blank = text as Blank
Transforms.insertNodes(editor, {
text: blank.correctAnswers[0]?.answer ?? '',
})
}
return
}
Expand All @@ -47,9 +50,7 @@ export function toggleBlank(editor: SlateEditor) {
Transforms.insertNodes(editor, {
type: 'blank',
blankId: uuid_v4(),
correctAnswer: '',
// Disabled alternative correct solutions for now
// alternativeSolutions: [],
correctAnswers: [{ answer: '' }],
children: [{ text: '' }],
})
return
Expand All @@ -62,10 +63,14 @@ export function toggleBlank(editor: SlateEditor) {
{
type: 'blank',
blankId: uuid_v4(),
correctAnswer:
SlateEditor.string(editor, trimmedSelection as Location) || '',
// Disabled alternative correct solutions for now
// alternativeSolutions: [],
correctAnswers: [
{
answer: SlateEditor.string(
editor,
trimmedSelection as Location
).trim(),
},
],
children: [{ text: '' }],
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
// import { useDroppable } from '@dnd-kit/core'
import { ChangeEventHandler, useContext } from 'react'
import { ChangeEventHandler, ReactNode, useContext } from 'react'
import { useDrop } from 'react-dnd'

// import { BlankSolution } from './components/blank-solution'
// import { BlankDragAndDropSolutions } from './renderer'
import type { FillInTheBlanksMode } from '.'
import type { BlankId, DraggableId, FillInTheBlanksMode } from '.'
import { DraggableSolution } from './components/blank-solution'
import { FillInTheBlanksContext } from './context/blank-context'
import { cn } from '@/helper/cn'

/** Renders either an input element (where user can type into) or a drop area (where user can drop draggable answers) depending on the mode */
export function BlankRenderer(props: {
correctAnswer: string
blankId: string
onChange?: ChangeEventHandler<HTMLInputElement>
forceMode?: FillInTheBlanksMode
}) {
// const dragAndDropSolutions = useContext(BlankDragAndDropSolutions)
// const draggableElementInBlank = dragAndDropSolutions?.find(
// (entry) => entry.inDroppableId === props.blankId
// )

const fillInTheBlanksContext = useContext(FillInTheBlanksContext)
if (fillInTheBlanksContext === null) {
// blankStates was not provided by FillInTheBlanksRenderer -> cannot continue
Expand All @@ -32,6 +25,18 @@ export function BlankRenderer(props: {
const textInBlank =
fillInTheBlanksContext.textInBlanks.get(props.blankId)?.text ?? ''

const draggableSolutionInBlank = [
...fillInTheBlanksContext.locationOfDraggables.value,
].find((entry) => entry[1] === props.blankId)

const draggableIdInThisBlank = draggableSolutionInBlank
? draggableSolutionInBlank[0]
: null

const draggableText = fillInTheBlanksContext.draggables.find(
(draggable) => draggable.draggableId === draggableIdInThisBlank
)?.text

return (
<>
{mode === 'typing' ? (
Expand All @@ -56,14 +61,17 @@ export function BlankRenderer(props: {
/>
) : (
<>
{/* {draggableElementInBlank ? (
<BlankSolution
text={draggableElementInBlank.text}
draggableId={draggableElementInBlank.draggableId}
/>
) : (
<EmptyBlankWithDropZone id={props.blankId} />
)} */}
<DroppableBlank
blankId={props.blankId}
disable={draggableIdInThisBlank !== null}
>
{draggableIdInThisBlank ? (
<DraggableSolution
text={draggableText ?? ''}
draggableId={draggableIdInThisBlank}
/>
) : null}
</DroppableBlank>
</>
)}
</>
Expand All @@ -85,18 +93,40 @@ export function BlankRenderer(props: {
}
}

// function EmptyBlankWithDropZone(props: { id: string }) {
// const { setNodeRef, isOver } = useDroppable({
// id: props.id,
// })
function DroppableBlank(props: {
blankId: BlankId
disable: boolean
children: ReactNode
}) {
const fillInTheBlanksContext = useContext(FillInTheBlanksContext)
const [{ isOver }, dropRef] = useDrop({
accept: 'blank-solution',
drop: (item) => {
if (!fillInTheBlanksContext) return
const newMap = new Map<DraggableId, BlankId>(
fillInTheBlanksContext.locationOfDraggables.value
)
newMap.set(
(item as { draggableId: DraggableId }).draggableId,
props.blankId
)
fillInTheBlanksContext.locationOfDraggables.set(newMap)
},
collect: (monitor) => ({
isOver: monitor.isOver(),
}),
canDrop: () => !props.disable,
})

// return (
// <span
// className={cn(
// 'rounded-full border border-editor-primary-300 bg-editor-primary-100 px-2',
// isOver && 'bg-slate-400'
// )}
// ref={setNodeRef}
// ></span>
// )
// }
return (
<span
className={cn(
'rounded-full border border-editor-primary-300 bg-editor-primary-100 px-2',
isOver && !props.disable && 'bg-slate-400'
)}
ref={dropRef}
>
{props.children}
</span>
)
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
// import { useDroppable } from '@dnd-kit/core'
// import { ReactNode } from 'react'
import { ReactNode } from 'react'
import { useDrop } from 'react-dnd'

// export function BlankSolutionsArea(props: { children: ReactNode }) {
// const { setNodeRef } = useDroppable({
// id: 'blank-solutions-area',
// })
import type { BlankId, DraggableId } from '..'
import { cn } from '@/helper/cn'

// return (
// <div
// className="mt-5 min-h-8 w-full rounded-full bg-slate-100"
// ref={setNodeRef}
// >
// {props.children}
// </div>
// )
// }
export function DraggableSolutionArea(props: {
children: ReactNode
locationOfDraggables: {
value: Map<DraggableId, BlankId>
set: React.Dispatch<React.SetStateAction<Map<DraggableId, BlankId>>>
}
}) {
const [{ isOver }, dropRef] = useDrop({
accept: 'blank-solution',
drop: (item: { draggableId: DraggableId }) => {
const newMap = new Map<DraggableId, BlankId>(
props.locationOfDraggables.value
)
newMap.delete(item.draggableId)
props.locationOfDraggables.set(newMap)
},
collect: (monitor) => ({
isOver: monitor.isOver(),
}),
})
return (
<div
className={cn(
'mt-5 min-h-8 w-full rounded-full bg-slate-100',
isOver ? 'bg-slate-200' : ''
)}
ref={dropRef}
>
{props.children}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
// import { UniqueIdentifier, useDraggable } from '@dnd-kit/core'
import { useDrag } from 'react-dnd'

// export function BlankSolution(props: {
// text: string
// draggableId: UniqueIdentifier
// }) {
// const { attributes, listeners, setNodeRef, transform } = useDraggable({
// id: props.draggableId,
// })
// const style = transform
// ? {
// transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
// }
// : undefined
import type { DraggableId } from '..'

// return (
// <div
// ref={setNodeRef}
// style={style}
// {...listeners}
// {...attributes}
// className="inline-block h-full rounded-full border border-editor-primary-300 bg-editor-primary-100 px-2"
// >
// {props.text}
// </div>
// )
// }
export function DraggableSolution(props: {
text: string
draggableId: DraggableId
}) {
const [, dragRef] = useDrag({
type: 'blank-solution',
item: { draggableId: props.draggableId },
})

return (
<div
className="inline-block h-full rounded-full border border-editor-primary-300 bg-editor-primary-100 px-2"
ref={dragRef}
>
{props.text}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { createContext } from 'react'

import type { FillInTheBlanksMode } from '..'

type BlankId = string
import type { BlankId, DraggableId, FillInTheBlanksMode } from '..'

// Used to pass state to BlankRenderer from FillInTheBlanksRenderer
// BlankRenderer will use this state alongside the state stored in the slate custom element 'blank' to render.
Expand All @@ -14,4 +12,12 @@ export const FillInTheBlanksContext = createContext<{
value: Map<BlankId, { text: string }>
set: React.Dispatch<React.SetStateAction<Map<BlankId, { text: string }>>>
}
draggables: {
draggableId: DraggableId
text: string
}[]
locationOfDraggables: {
value: Map<DraggableId, BlankId>
set: React.Dispatch<React.SetStateAction<Map<DraggableId, BlankId>>>
}
} | null>(null)
Loading