-
Notifications
You must be signed in to change notification settings - Fork 75
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2051 from zooniverse/flexible-survey
Flexible survey
- Loading branch information
Showing
13 changed files
with
990 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
React = require 'react' | ||
|
||
module.exports = React.createClass | ||
displayName: 'SurveyAnnotationView' | ||
|
||
getDefaultProperties: -> | ||
task: null | ||
classification: null | ||
annotation: null | ||
|
||
render: -> | ||
<div> | ||
{for identification, i in @props.annotation.value | ||
identification._key ?= Math.random() | ||
|
||
answersByQuestion = @props.task.questionsOrder.map (questionID) => | ||
if questionID of identification.answers | ||
answerLabels = [].concat(identification.answers[questionID]).map (answerID) => | ||
@props.task.questions[questionID].answers[answerID].label | ||
answerLabels.join ', ' | ||
answersList = answersByQuestion.filter(Boolean).join '; ' | ||
|
||
<span key={identification._key}> | ||
<span className="survey-identification-proxy" title={answersList}> | ||
{@props.task.choices[identification.choice].label} | ||
{' '} | ||
<button type="button" className="survey-identification-remove" title="Remove" onClick={@handleRemove.bind this, i}>×</button> | ||
</span> | ||
{' '} | ||
</span>} | ||
</div> | ||
|
||
handleRemove: (index) -> | ||
@props.annotation.value.splice index, 1 | ||
@props.classification.update 'annotations' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
React = require 'react' | ||
TriggeredModalForm = require 'modal-form/triggered' | ||
{Markdown} = require 'markdownz' | ||
Utility = require './utility' | ||
|
||
ImageFlipper = React.createClass | ||
displayName: 'ImageFlipper' | ||
|
||
getDefaultProps: -> | ||
images: [] | ||
|
||
getInitialState: -> | ||
frame: 0 | ||
|
||
PRELOAD_STYLE: | ||
height: 0 | ||
overflow: 'hidden' | ||
position: 'fixed' | ||
right: 0 | ||
width: 0 | ||
|
||
render: -> | ||
<span className="survey-task-image-flipper"> | ||
{@renderPreload()} | ||
<img src={@props.images[@state.frame]} className="survey-task-image-flipper-image" /> | ||
<span className="survey-task-image-flipper-pips"> | ||
{unless @props.images.length is 1 | ||
for index in [0...@props.images.length] | ||
<span key={@props.images[index]}> | ||
<button type="button" className="survey-task-image-flipper-pip" disabled={index is @state.frame} onClick={@handleFrameChange.bind this, index}>{index + 1}</button> | ||
{' '} | ||
</span>} | ||
</span> | ||
</span> | ||
|
||
renderPreload: -> | ||
<div style={@PRELOAD_STYLE}> | ||
{for image in @props.images | ||
<img src={image} key={image} />} | ||
</div> | ||
|
||
handleFrameChange: (frame) -> | ||
@setState {frame} | ||
|
||
module.exports = React.createClass | ||
displayName: 'FlexibleChoice' | ||
|
||
getDefaultProps: -> | ||
task: null | ||
choiceID: '' | ||
onSwitch: Function.prototype | ||
onConfirm: Function.prototype | ||
onCancel: Function.prototype | ||
|
||
getInitialState: -> | ||
answers: {} | ||
|
||
allFilledIn: -> | ||
for questionID in Utility.getQuestionIDs(@props.task, @props.choiceID) | ||
question = @props.task.questions[questionID] | ||
if question.required | ||
answer = @state.answers[questionID] | ||
if (not answer?) or (question.multiple and answer.length is 0) | ||
return false | ||
true | ||
|
||
render: -> | ||
choice = @props.task.choices[@props.choiceID] | ||
<div className="survey-task-choice"> | ||
{unless choice.images.length is 0 | ||
<ImageFlipper images={@props.task.images[filename] for filename in choice.images} />} | ||
<div className="survey-task-choice-content"> | ||
<div className="survey-task-choice-label">{choice.label}</div> | ||
<div className="survey-task-choice-description">{choice.description}</div> | ||
|
||
{unless choice.confusionsOrder.length is 0 | ||
<div className="survey-task-choice-confusions"> | ||
Often confused with | ||
{' '} | ||
{for otherChoiceID in choice.confusionsOrder | ||
otherChoice = @props.task.choices[otherChoiceID] | ||
<span key={otherChoiceID}> | ||
<TriggeredModalForm className="survey-task-confusions-modal" trigger={ | ||
<span className="survey-task-choice-confusion"> | ||
{otherChoice.label} | ||
</span> | ||
} style={maxWidth: '60ch'}> | ||
<ImageFlipper images={@props.task.images[filename] for filename in otherChoice.images} /> | ||
<Markdown content={choice.confusions[otherChoiceID]} /> | ||
<div className="survey-task-choice-confusion-buttons" style={textAlign: 'center'}> | ||
<button type="submit" className="major-button identfiy">Dismiss</button> | ||
{' '} | ||
<button type="button" className="standard-button cancel" onClick={@props.onSwitch.bind null, otherChoiceID}>I think it’s this</button> | ||
</div> | ||
</TriggeredModalForm> | ||
{' '} | ||
</span>} | ||
</div>} | ||
|
||
<hr /> | ||
|
||
{unless choice.noQuestions | ||
for questionID in Utility.getQuestionIDs(@props.task, @props.choiceID) | ||
question = @props.task.questions[questionID] | ||
inputType = if question.multiple | ||
'checkbox' | ||
else | ||
'radio' | ||
<div key={questionID} className="survey-task-choice-question" data-multiple={question.multiple || null}> | ||
<div className="survey-task-choice-question-label">{question.label}</div> | ||
{for answerID in question.answersOrder | ||
answer = question.answers[answerID] | ||
isChecked = if question.multiple | ||
answerID in (@state.answers[questionID] ? []) | ||
else | ||
answerID is @state.answers[questionID] | ||
<span key={answerID}> | ||
<label className="survey-task-choice-answer" data-checked={isChecked || null}> | ||
<input type={inputType} checked={isChecked} onChange={@handleAnswer.bind this, questionID, answerID} /> | ||
{answer.label} | ||
</label> | ||
{' '} | ||
</span>} | ||
</div>} | ||
|
||
{unless choice.noQuestions or Utility.getQuestionIDs(@props.task, @props.choiceID).length is 0 | ||
<hr />} | ||
</div> | ||
<div style={textAlign: 'center'}> | ||
<button type="button" className="minor-button" onClick={@props.onCancel}>Cancel</button> | ||
{' '} | ||
<button type="button" className="standard-button" disabled={not @allFilledIn()} onClick={@handleIdentification}> | ||
<strong>Identify</strong> | ||
</button> | ||
</div> | ||
</div> | ||
|
||
handleAnswer: (questionID, answerID, e) -> | ||
if @props.task.questions[questionID].multiple | ||
@state.answers[questionID] ?= [] | ||
if e.target.checked | ||
@state.answers[questionID].push answerID | ||
else | ||
@state.answers[questionID].splice @state.answers[questionID].indexOf(answerID), 1 | ||
else | ||
@state.answers[questionID] = if e.target.checked | ||
answerID | ||
else | ||
null | ||
@setState answers: @state.answers | ||
|
||
handleIdentification: -> | ||
@props.onConfirm @props.choiceID, @state.answers |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
React = require 'react' | ||
TriggeredModalForm = require 'modal-form/triggered' | ||
|
||
THUMBNAIL_BREAKPOINTS = [Infinity, 40, 20, 10, 5, 0] | ||
|
||
module.exports = React.createClass | ||
displayName: 'Chooser' | ||
|
||
getDefaultProps: -> | ||
task: null | ||
filters: {} | ||
onFilter: Function.prototype | ||
onChoose: Function.prototype | ||
|
||
getFilteredChoices: -> | ||
for choiceID in @props.task.choicesOrder | ||
choice = @props.task.choices[choiceID] | ||
rejected = false | ||
for characteristicID, valueID of @props.filters | ||
if valueID not in choice.characteristics[characteristicID] | ||
rejected = true | ||
break | ||
if rejected | ||
continue | ||
else | ||
choiceID | ||
|
||
render: -> | ||
filteredChoices = @getFilteredChoices() | ||
|
||
for point in THUMBNAIL_BREAKPOINTS | ||
if filteredChoices.length <= point | ||
breakpoint = point | ||
|
||
<div className="survey-task-chooser"> | ||
<div className="survey-task-chooser-characteristics"> | ||
{for characteristicID in @props.task.characteristicsOrder | ||
characteristic = @props.task.characteristics[characteristicID] | ||
selectedValue = characteristic.values[@props.filters[characteristicID]] | ||
hasBeenAutoFocused = false | ||
|
||
<TriggeredModalForm key={characteristicID} ref="#{characteristicID}-dropdown" className="survey-task-chooser-characteristic-menu" trigger={ | ||
<span className="survey-task-chooser-characteristic" data-is-active={selectedValue? || null}> | ||
<span className="survey-task-chooser-characteristic-label">{selectedValue?.label ? characteristic.label}</span> | ||
</span> | ||
}> | ||
<div className="survey-task-chooser-characteristic-menu-container"> | ||
{for valueID in characteristic.valuesOrder | ||
value = characteristic.values[valueID] | ||
|
||
disabled = valueID is @props.filters[characteristicID] | ||
autoFocus = not disabled and not hasBeenAutoFocused | ||
selected = valueID is @props.filters[characteristicID] | ||
|
||
if autoFocus | ||
hasBeenAutoFocused = true | ||
|
||
<button key={valueID} type="submit" title={value.label} className="survey-task-chooser-characteristic-value" disabled={disabled} data-selected={selected} autoFocus={autoFocus} onClick={@handleFilter.bind this, characteristicID, valueID}> | ||
{if value.image? | ||
<img src={@props.task.images[value.image]} alt={value.label} className="survey-task-chooser-characteristic-value-icon" />} | ||
</button>} | ||
|
||
<button type="submit" className="survey-task-chooser-characteristic-clear-button" disabled={characteristicID not of @props.filters} autoFocus={not hasBeenAutoFocused} onClick={@handleFilter.bind this, characteristicID, undefined}> | ||
Clear | ||
</button> | ||
</div> | ||
<div className="survey-task-chooser-characteristic-value-label"> | ||
{label = "" | ||
for valueID in characteristic.valuesOrder | ||
value = characteristic.values[valueID] | ||
|
||
if valueID is @props.filters[characteristicID] | ||
label = value.label | ||
if label then label else "Make a selection" | ||
}</div> | ||
</TriggeredModalForm>} | ||
</div> | ||
|
||
<div className="survey-task-chooser-choices" data-breakpoint={breakpoint}> | ||
{if filteredChoices.length is 0 | ||
<div> | ||
<em>No matches.</em> | ||
</div> | ||
else | ||
for choiceID, i in filteredChoices | ||
choice = @props.task.choices[choiceID] | ||
<button key={choiceID + i} type="button" className="survey-task-chooser-choice" onClick={@props.onChoose.bind null, choiceID}> | ||
{unless choice.images.length is 0 | ||
<span className="survey-task-chooser-choice-thumbnail-container"> | ||
<img src={@props.task.images[choice.images[0]]} alt={choice.label} className="survey-task-chooser-choice-thumbnail" /> | ||
</span>} | ||
<span className="survey-task-chooser-choice-label">{choice.label}</span> | ||
</button>} | ||
</div> | ||
<div style={textAlign: 'center'}> | ||
Showing {filteredChoices.length} of {@props.task.choicesOrder.length}. | ||
  | ||
<button type="button" className="survey-task-chooser-characteristic-clear-button" disabled={Object.keys(@props.filters).length is 0} onClick={@handleClearFilters}> | ||
<i className="fa fa-ban"></i> Clear filters | ||
</button> | ||
</div> | ||
</div> | ||
|
||
handleFilter: (characteristicID, valueID) -> | ||
@props.onFilter characteristicID, valueID | ||
|
||
handleClearFilters: -> | ||
for characteristicID in @props.task.characteristicsOrder | ||
@props.onFilter characteristicID, undefined |
Oops, something went wrong.