diff --git a/demo.cjsx b/demo.cjsx index bfab290..e1b9e52 100644 --- a/demo.cjsx +++ b/demo.cjsx @@ -1,4 +1,5 @@ React = require 'react' +ReactDom = require 'react-dom' Demo = require './src/components/demo' {startMathJax} = require './src/helpers/mathjax' @@ -10,7 +11,7 @@ loadApp = -> mainDiv = document.createElement('div') mainDiv.id = 'react-root-container' document.body.appendChild(mainDiv) - React.render(, mainDiv) + ReactDom.render(, mainDiv) true loadApp() or document.addEventListener('readystatechange', loadApp) diff --git a/index.cjsx b/index.cjsx index 432512f..96015dc 100644 --- a/index.cjsx +++ b/index.cjsx @@ -13,6 +13,7 @@ SmartOverflow = require './src/components/smart-overflow' RefreshButton = require './src/components/buttons/refresh-button' AsyncButton = require './src/components/buttons/async-button' CloseButton = require './src/components/buttons/close-button' +ExercisePreview = require './src/components/exercise/preview' ExerciseIdentifierLink = require './src/components/exercise/identifier-link' ChapterSectionMixin = require './src/components/chapter-section-mixin' GetPositionMixin = require './src/components/get-position-mixin' @@ -44,6 +45,7 @@ module.exports = { ResizeListenerMixin, SpyMode, + ExercisePreview, ExerciseIdentifierLink, KeysHelper } diff --git a/package.json b/package.json index 0344c2d..2ee5096 100644 --- a/package.json +++ b/package.json @@ -21,82 +21,83 @@ }, "homepage": "https://github.com/openstax/react-components", "dependencies": { - "bootstrap": "3.3.5", - "camelcase": "1.2.1", - "classnames": "2.1.5", - "eventemitter2": "0.4.14", - "font-awesome": "4.4.0", + "bootstrap": "3.3.6", + "camelcase": "2.1.0", + "classnames": "2.2.3", + "eventemitter2": "1.0.0", + "font-awesome": "4.5.0", "keymaster": "1.6.2", - "markdown-it": "4.4.0", - "moment": "2.10.6", + "markdown-it": "6.0.0", + "moment": "2.11.2", "underscore": "1.8.3" }, "devDependencies": { - "blanket": "1.1.7", - "chai": "2.1.2", - "chai-as-promised": "5.1.0", - "chromedriver": "2.19.0", - "cjsxify": "0.2.6", + "blanket": "1.2.2", + "chai": "3.5.0", + "chai-as-promised": "5.2.0", + "chromedriver": "2.21.2", + "cjsxify": "0.3.0", "coffee-jsx-loader": "0.1.3", "coffee-loader": "0.7.2", "coffee-react-transform": "3.3.0", - "coffee-script": "1.9.3", - "coffeelint": "1.13.0", + "coffee-script": "1.10.0", + "coffeelint": "1.14.2", "coffeelint-loader": "0.1.1", - "css-loader": "0.22.0", - "del": "1.1.1", - "es6-promise": "2.3.0", - "extract-text-webpack-plugin": "0.8.2", - "file-exists": "0.1.1", - "file-loader": "0.8.4", - "fixed-data-table": "0.4.1", - "gulp": "3.9.0", - "gulp-coffeelint": "0.4.0", - "gulp-env": "0.2.0", - "gulp-gzip": "1.0.0", - "gulp-rev": "3.0.1", - "gulp-tar": "1.4.0", - "gulp-util": "3.0.6", + "css-loader": "0.23.1", + "del": "2.2.0", + "es6-promise": "3.1.2", + "extract-text-webpack-plugin": "1.0.1", + "file-exists": "1.0.0", + "file-loader": "0.8.5", + "fixed-data-table": "0.6.0", + "gulp": "3.9.1", + "gulp-coffeelint": "0.6.0", + "gulp-env": "0.4.0", + "gulp-gzip": "1.2.0", + "gulp-rev": "7.0.0", + "gulp-tar": "1.8.0", + "gulp-util": "3.0.7", "gulp-watch": "4.3.5", - "istanbul-instrumenter-loader": "0.1.3", - "json-loader": "0.5.3", - "karma": "0.13.19", + "istanbul-instrumenter-loader": "0.2.0", + "json-loader": "0.5.4", + "karma": "0.13.21", "karma-chai": "0.1.0", "karma-chai-sinon": "0.1.5", - "karma-chrome-launcher": "0.2.1", - "karma-coverage": "0.5.2", - "karma-mocha": "0.1.10", - "karma-mocha-reporter": "1.0.4", - "karma-nyan-reporter": "0.2.2", - "karma-phantomjs-launcher": "0.2.1", - "karma-phantomjs-shim": "1.1.1", - "karma-sourcemap-loader": "0.3.6", + "karma-chrome-launcher": "0.2.2", + "karma-coverage": "0.5.3", + "karma-mocha": "0.2.2", + "karma-mocha-reporter": "1.2.2", + "karma-nyan-reporter": "0.2.3", + "karma-phantomjs-launcher": "1.0.0", + "karma-phantomjs-shim": "1.2.0", + "karma-sourcemap-loader": "0.3.7", "karma-webpack": "1.7.0", - "less": "2.5.3", - "less-loader": "2.2.1", - "lodash": "3.10.1", "mocha": "2.2.5", "phantomjs": "1.9.18", - "react": "0.13.1", - "react-bootstrap": "0.23.0", + "react-bootstrap": "0.28.3", + "less": "2.6.0", + "less-loader": "2.2.2", + "lodash": "4.5.1", + "react": "0.14.7", + "react-addons-test-utils": "0.14.7", "react-hot-loader": "1.3.0", "react-scroll-components": "0.2.2", - "selenium-webdriver": "2.47.0", - "sinon": "1.17.1", + "selenium-webdriver": "2.52.0", + "sinon": "1.17.3", "sinon-chai": "2.8.0", - "style-loader": "0.12.4", - "url-loader": "0.5.6", - "webpack": "1.12.2", - "webpack-dev-server": "1.12.0", - "webpack-module-hot-accept": "1.0.3", + "style-loader": "0.13.0", + "url-loader": "0.5.7", + "webpack": "1.12.14", + "webpack-dev-server": "1.14.1", + "webpack-module-hot-accept": "1.0.4", "webpack-umd-external": "1.0.2", "when": "3.7.3" }, "peerDependencies": { - "react": "^0.13.1", - "react-bootstrap": "^0.23.0", - "react-scroll-components": "^0.2.2" + "react": "0.14.7", + "react-bootstrap": "0.28.3" }, + "peerDependencies": {}, "config": { "blanket": { "pattern": "src" diff --git a/resources/styles/_components.less b/resources/styles/_components.less index f245d4e..27d25f3 100644 --- a/resources/styles/_components.less +++ b/resources/styles/_components.less @@ -5,6 +5,7 @@ @import './components/html/index'; @import './components/exercise/step-card'; @import './components/exercise/group'; +@import './components/exercise/preview'; @import './components/breadcrumbs/step'; @import './components/question'; @import './components/close'; diff --git a/resources/styles/components/breadcrumbs/coach.less b/resources/styles/components/breadcrumbs/coach.less new file mode 100644 index 0000000..19cfa59 --- /dev/null +++ b/resources/styles/components/breadcrumbs/coach.less @@ -0,0 +1,141 @@ +@concept-coach-breadcrumb-background: @openstax-white; +@concept-coach-breadcrumb-color: @openstax-neutral; +@concept-coach-breadcrumb-border-radius: 0; + +.make-breadcrumb-clickable() { + &.active { + color: @openstax-white; + background: @openstax-info; + border-color: @openstax-info; + cursor: default; + font-weight: 400; + } + + &:hover, + &:focus { + border-color: @openstax-info; + outline: none; + } +} + +.concept-coach-breadcrumbs() { + .task-breadcrumbs { + margin: 1rem 0; + } + + .openstax-breadcrumbs-step { + border-width: 2px; + border-style: solid; + border-radius: @concept-coach-breadcrumb-border-radius; + background: @concept-coach-breadcrumb-background; + border-color: @concept-coach-breadcrumb-background; + color: @concept-coach-breadcrumb-color; + margin-right: 1rem; + font-size: 2.5rem; + line-height: 3.8rem; + width: auto; + position: relative; + + &:hover, + &.active { + box-shadow: none; + border-radius: @concept-coach-breadcrumb-border-radius; + .scale(1); + } + + .transition(~'background 0.1s ease-in-out, border-color 0.1s ease-in-out'); + .make-breadcrumb-clickable(); + + &::before { + font-weight: 300; + position: relative; + } + + &.disabled { + background: transparent; + border-color: transparent; + color: lighten(@concept-coach-breadcrumb-color, 25%); + cursor: default; + + &:hover { + border-color: transparent; + } + } + + &.end:not(.disabled) { + background: @openstax-white; + border-color: @openstax-white; + color: @concept-coach-breadcrumb-color; + cursor: pointer; + } + + &:not(.active) { + &.status-incorrect, + &.status-correct { + background: @concept-coach-breadcrumb-background; + } + &.status-incorrect { + color: @openstax-incorrect-color; + } + + &.status-correct { + color: @openstax-correct-color; + } + } + + + &.breadcrumb-end[data-label] { + top: -0.5em; + + &::before { + text-align: center; + width: 100%; + padding: 0 1rem; + font-style: italic; + text-transform: capitalize; + content: attr(data-label); + } + } + } + + .icon-end { + display: none; + } + + .icon-stack { + > .icon-sm, + > .icon-lg { + top: -1.5rem; + left: 2.2rem; + width: 26px; + height: 26px; + border-radius: 50%; + border: 3px solid @openstax-neutral-light; + } + } + + + .icon-incorrect, + .icon-correct { + .fa-icon(); + font-size: 2.4rem; + line-height: 1.8rem; + background: @openstax-white; + } + + .icon-incorrect { + color: @openstax-incorrect-color; + + &::after { + content: @fa-var-times-circle; + } + } + + .icon-correct { + color: @openstax-correct-color; + + &::after { + content: @fa-var-check-circle; + } + } +} \ No newline at end of file diff --git a/resources/styles/components/exercise/preview.less b/resources/styles/components/exercise/preview.less new file mode 100644 index 0000000..e6be5af --- /dev/null +++ b/resources/styles/components/exercise/preview.less @@ -0,0 +1,191 @@ +&-exercise-preview { + + position: relative; + + .sans(@size: 1.5rem, @line-height: 3rem) { + font-family: 'Lato', Helvetica, sans-serif; + font-weight: 400; + font-style: normal; + font-size: @size; + line-height: @line-height; + } + + img { + max-width: 100%; + display: block; + margin: 0 auto 20px auto; + } + + // Selected card + &.panel-info { + background-color: @pale-yellow; + } + + &.is-selectable { + .panel-body { + position: relative; + &:hover { + .toggle-mask { + visibility: visible; + } + } + } + + .toggle-mask { + cursor: pointer; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + visibility: hidden; + z-index: 1; + display: flex; + border: none; + justify-content: center; + align-items: center; + .transition(all .25s ease-in-out); + + .message { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + font-size: 2.5rem; + font-weight: 700; + + &::before { + .fa-icon(); + font-weight: 300; + text-align: center; + font-size: 50px; + border-radius: 60px; + width: 60px; + height: 60px; + padding-top: 5px; + } + } + } + } + + .panel-body { + + .question-stem { + font-weight: 700; + margin-bottom: 1rem; + font-size: 1.6rem; + line-height: 1.4em; + } + + .answers-table { + counter-reset: answer; + font-size: 1.4rem; + line-height: 1.4em; + margin-bottom: 2rem; + + .answers-answer { + padding-left: 15px; + counter-increment: answer; + + &.answer-correct { + color: @openstax-secondary; + padding-left: 0; + + &::before { + .fa-icon(); + float: left; + margin-top: 0.15em; + margin-right: .125em; + content: @fa-var-check; + } + } + + .answer-label { + font-weight: 400; + display: table; + } + + .answer-letter { + margin-right: 1rem; + float: left; + + &::after { + content: counter(answer, lower-latin) ')'; + } + } + + .answer-answer { + display: table; + margin-bottom: 5px; + } + } + + .question-feedback-content { + display: none; + margin-bottom: 5px; + color: @openstax-neutral; + margin-left: 0; + .sans(1.1rem, 1.1rem); + font-style: italic; + } + + } + + .detailed-solution { + display: none; + margin-bottom: 1.5rem; + + .header { + color: @openstax-neutral-darker; + margin-bottom: 0.5rem; + .sans(1.3rem, 1.3rem); + } + + .solution { + color: @openstax-neutral; + .sans(1.1rem, 1.1rem); + font-style: italic; + } + } + + .exercise-tags { + color: @openstax-neutral; + font-size: 1.2rem; + line-height: 1.4em; + + .exercise-tag + .exercise-tag::before { + content: ','; + margin-right: 1rem; + } + + .lo-tag { + margin-right: 1rem; + display: block; + content: ''; + } + } + } + + + &.is-displaying-feedback { + .panel-body .answers-table .question-feedback-content { display: table; } + .detailed-solution{ display: block; } + } + + // use absolute positioning for the identifer so it's link is clickable through the overlay + .panel-footer .controls { + display: flex; + justify-content: space-between; + font-size: 1.1rem; + color: @openstax-neutral; + + .exercise-identifier-link a { color: @openstax-neutral; } + + .feedback-toggle { + border: 0; + background-color: transparent; + font-size: 1.2rem; + } + } + +} // end of exercise.card styles diff --git a/resources/styles/components/html/index.less b/resources/styles/components/html/index.less index 1e92802..29477e4 100644 --- a/resources/styles/components/html/index.less +++ b/resources/styles/components/html/index.less @@ -1,3 +1,9 @@ &-has-html { .openstax-tables(); -} \ No newline at end of file + + .frame-wrapper { + margin: 1rem 0; + } +} + + diff --git a/resources/styles/components/question.less b/resources/styles/components/question.less index 3c4ff18..acb11ef 100644 --- a/resources/styles/components/question.less +++ b/resources/styles/components/question.less @@ -182,132 +182,135 @@ font-style: italic; } - // answers - counter-reset: answer 0; - .answers-answer { - counter-increment: answer 1; - width: initial; - } + &:not(.openstax-question-preview) { + // answers + counter-reset: answer 0; - .answer-content { - width: calc(~'100% - @{answer-label-spacing}'); - margin-left: @answer-horizontal-spacing; - margin-top: 0.5 * @answer-horizontal-spacing; - .answer-left-block(); - } + .answers-answer { + counter-increment: answer 1; + width: initial; + } - .answer-letter { - .answer-left-block(); - &::after { - content: counter(answer, lower-latin); + .answer-content { + width: calc(~'100% - @{answer-label-spacing}'); + margin-left: @answer-horizontal-spacing; + margin-top: 0.5 * @answer-horizontal-spacing; + .answer-left-block(); } - } - .answer-label { - font-weight: normal; - width: 100%; - padding: @answer-vertical-spacing 0 0 0; - margin: 0; + .answer-letter { + .answer-left-block(); + &::after { + content: counter(answer, lower-latin); + } + } - .transition(~'color @{answer-transition}'); - .answer(); - } + .answer-label { + font-weight: normal; + width: 100%; + padding: @answer-vertical-spacing 0 0 0; + margin: 0; - // a selectable answer - .answer-input-box:not([disabled]) ~ .answer-label { - cursor: pointer; + .transition(~'color @{answer-transition}'); + .answer(); + } + + // a selectable answer + .answer-input-box:not([disabled]) ~ .answer-label { + cursor: pointer; - &:hover { - .answer(hover); + &:hover { + .answer(hover); + } } - } - // a selected answer - &:not(.has-correct-answer){ - .answer-input-box { - display: none; + // a selected answer + &:not(.has-correct-answer){ + .answer-input-box { + display: none; - &:checked { - + .answer-label, - + .answer-label:hover { - .answer(checked); + &:checked { + + .answer-label, + + .answer-label:hover { + .answer(checked); + } } } - } - .answer-checked{ - .answer-label { - .answer(checked); + .answer-checked{ + .answer-label { + .answer(checked); + } } } - } - // answer that has been checked - &.has-correct-answer { - .answer-checked { - &:not(.answer-correct) { - .answer-label { - .answer(wrong); + // answer that has been checked + &.has-correct-answer { + .answer-checked { + &:not(.answer-correct) { + .answer-label { + .answer(wrong); + } + } + + &.answer-correct { + .answer-label { + .answer(correct); + } } } - &.answer-correct { + .answer-correct:not(.answer-checked) { .answer-label { - .answer(correct); + .answer(correct-answer); } } } - .answer-correct:not(.answer-checked) { - .answer-label { - .answer(correct-answer); + .question-feedback { + .popover(); + + font-style: italic; + color: @answer-label-color; + + position: relative; + display: block; + width: calc(~'100% + @{feedback-horizontal-buffer}'); + max-width: calc(~'100% + @{feedback-horizontal-buffer}'); + margin-left: -1 * @feedback-horizontal-spacing; + margin-top: -1 * (@answer-vertical-spacing - @popover-arrow-width/10); + margin-bottom: @answer-horizontal-spacing; + z-index: 1; + .box-shadow(0 0 10px rgba(0, 0, 0, .25)); + border: none; + + .arrow { + &::after { + border-width: @popover-arrow-width; + content: ""; + } + .popover > .arrow(); } - } - } - - .question-feedback { - .popover(); - font-style: italic; - color: @answer-label-color; + &.bottom, + &.top { + > .arrow { + left: @answer-bubble-size/2 + @feedback-horizontal-spacing; + } - position: relative; - display: block; - width: calc(~'100% + @{feedback-horizontal-buffer}'); - max-width: calc(~'100% + @{feedback-horizontal-buffer}'); - margin-left: -1 * @feedback-horizontal-spacing; - margin-top: -1 * (@answer-vertical-spacing - @popover-arrow-width/10); - margin-bottom: @answer-horizontal-spacing; - z-index: 1; - .box-shadow(0 0 10px rgba(0, 0, 0, .25)); - border: none; - - .arrow { - &::after { - border-width: @popover-arrow-width; - content: ""; } - .popover > .arrow(); - } - - &.bottom, - &.top { - > .arrow { - left: @answer-bubble-size/2 + @feedback-horizontal-spacing; + &.bottom { + margin-top: -5px; + } + &.top { + margin-bottom: -5px; } - } - &.bottom { - margin-top: -5px; - } - &.top { - margin-bottom: -5px; - } - - .question-feedback-content { - .popover-content(); - padding: @feedback-vertical-spacing @feedback-horizontal-spacing; + .question-feedback-content { + .popover-content(); + padding: @feedback-vertical-spacing @feedback-horizontal-spacing; + } } } } diff --git a/resources/styles/globals/openstax-bootstrap.less b/resources/styles/globals/openstax-bootstrap.less index 6734d77..0b535c8 100644 --- a/resources/styles/globals/openstax-bootstrap.less +++ b/resources/styles/globals/openstax-bootstrap.less @@ -35,7 +35,7 @@ // @import "@{bootstrap-path}/media"; @import "@{bootstrap-path}/list-group"; @import "@{bootstrap-path}/panels"; -// @import "@{bootstrap-path}/responsive-embed"; +@import "@{bootstrap-path}/responsive-embed"; // @import "@{bootstrap-path}/wells"; @import "@{bootstrap-path}/close"; @@ -47,4 +47,4 @@ // Utility classes @import "@{bootstrap-path}/utilities"; -@import "@{bootstrap-path}/responsive-utilities"; \ No newline at end of file +@import "@{bootstrap-path}/responsive-utilities"; diff --git a/src/components/breadcrumb/index.cjsx b/src/components/breadcrumb/index.cjsx index 54673a7..63a926b 100644 --- a/src/components/breadcrumb/index.cjsx +++ b/src/components/breadcrumb/index.cjsx @@ -45,7 +45,7 @@ Breadcrumb = React.createClass {step, crumb, goToStep, className} = @props {isCorrect, isIncorrect, isCurrent, isCompleted, isEnd, crumbType} = @state - propsToPassOn = _.omit(@props, 'onClick', 'title', 'className', 'data-chapter', 'key') + propsToPassOn = _.omit(@props, 'onClick', 'title', 'className', 'data-chapter', 'key', 'step') if isCurrent title = "Current Step (#{crumbType})" diff --git a/src/components/demo.cjsx b/src/components/demo.cjsx index ca9d92b..babdc2d 100644 --- a/src/components/demo.cjsx +++ b/src/components/demo.cjsx @@ -2,6 +2,7 @@ React = require 'react' BS = require 'react-bootstrap' _ = require 'lodash' EventEmitter2 = require 'eventemitter2' +classnames = require 'classnames' Exercise = require './exercise' @@ -12,6 +13,10 @@ STEP_ID = exerciseStub['free-response'].content.questions[0].id steps = [] steps[STEP_ID] = {} + +ExercisePreview = require './exercise/preview' +exercisePreviewStub = require '../../stubs/exercise-preview/data' + Breadcrumb = require './breadcrumb' breadcrumbStub = require '../../stubs/breadcrumbs/steps' @@ -63,6 +68,25 @@ ExerciseDemo = React.createClass {exerciseProps} = @state +ExercisePreviewDemo = React.createClass + displayName: 'ExercisePreviewDemo' + getInitialState: -> + displayFeedback: false + toggleFeedbackDisplay: (ev) -> + @setState(displayFeedback: not @state?.displayFeedback) + render: -> + {displayFeedback} = @state + + displayFeedbackIconClasses = classnames 'fa', + 'fa-check-square-o': displayFeedback + 'fa-square-o': not displayFeedback + + + + + BreadcrumbDemo = React.createClass displayName: 'BreadcrumbDemo' @@ -84,22 +108,24 @@ BreadcrumbDemo = React.createClass type: 'step' ) - crumbs.push(type: 'end', key: crumbs.length, data: {}) + crumbs.push(type: 'end', key: crumbs.length + 1, data: {}) breadcrumbsNoReview = _.map(crumbs, (crumb) => ) - breadcrumbsReview = _.map(crumbs, (crumb) => + breadcrumbsReview = _.map(crumbs, (crumb, key) => if crumb.type is 'step' and crumb.data.is_completed crumb.data.correct_answer_id = "3" demos = exercise: + exercisePreview: breadcrumbs: demos = _.map(demos, (demo, name) -> - +

{"#{name}"}

{demo}
diff --git a/src/components/exercise/mode.cjsx b/src/components/exercise/mode.cjsx index 3da64eb..7a98ca5 100644 --- a/src/components/exercise/mode.cjsx +++ b/src/components/exercise/mode.cjsx @@ -1,10 +1,13 @@ React = require 'react' +ReactDom = require 'react-dom' _ = require 'underscore' ArbitraryHtmlAndMath = require '../html' Question = require '../question' FreeResponse = require './free-response' +RESPONSE_CHAR_LIMIT = 10000 + {propTypes, props} = require './props' modeType = propTypes.ExerciseStepCard.panel modeProps = _.extend {}, propTypes.ExFreeResponse, propTypes.ExMulitpleChoice, propTypes.ExReview, mode: modeType @@ -31,23 +34,25 @@ ExMode = React.createClass @focusBox() if mode is 'free-response' componentWillReceiveProps: (nextProps) -> - {free_response, answer_id} = nextProps + {free_response, answer_id, freeResponseValue} = nextProps nextAnswers = {} + freeResponse = free_response or freeResponseValue or '' - nextAnswers.freeResponse = free_response if @state.freeResponse isnt free_response + nextAnswers.freeResponse = freeResponse if @state.freeResponse isnt freeResponse nextAnswers.answerId = answer_id if @state.answerId isnt answer_id @setState(nextAnswers) unless _.isEmpty(nextAnswers) focusBox: -> {focus, mode} = @props - @refs.freeResponse?.getDOMNode?().focus?() if focus and mode is 'free-response' + ReactDom.findDOMNode(@refs.freeResponse)?.focus?() if focus and mode is 'free-response' onFreeResponseChange: -> - freeResponse = @refs.freeResponse?.getDOMNode()?.value - @setState({freeResponse}) - @props.onFreeResponseChange?(freeResponse) + freeResponse = ReactDom.findDOMNode(@refs.freeResponse)?.value + if freeResponse.length <= RESPONSE_CHAR_LIMIT + @setState({freeResponse}) + @props.onFreeResponseChange?(freeResponse) onAnswerChanged: (answer) -> return if answer.id is @state.answerId or @props.mode isnt 'multiple-choice' @@ -55,9 +60,8 @@ ExMode = React.createClass @props.onAnswerChanged?(answer) getFreeResponse: -> - freeResponseValue = @state.freeResponse {mode, free_response, disabled} = @props - {freeResponseValue} = @props unless freeResponseValue + {freeResponse} = @state if mode is 'free-response' @@ -65,7 +69,7 @@ ExMode = React.createClass disabled={disabled} ref='freeResponse' placeholder='Enter your response' - value={freeResponseValue or ''} + value={freeResponse} onChange={@onFreeResponseChange} /> else @@ -76,8 +80,6 @@ ExMode = React.createClass {answerId} = @state answerKeySet = null unless choicesEnabled - question = content.questions[0] - question = _.omit(question, 'answers') if mode is 'free-response' questionProps = _.pick(@props, 'processHtmlAndMath', 'choicesEnabled', 'correct_answer_id', 'feedback_html', 'type') if mode is 'multiple-choice' @@ -90,21 +92,26 @@ ExMode = React.createClass htmlAndMathProps = _.pick(@props, 'processHtmlAndMath') {stimulus_html} = content -
- + questions = _.map(content.questions, (question) => + question = _.omit(question, 'answers') if mode is 'free-response' {@getFreeResponse()} + ) + +
+ + {questions}
diff --git a/src/components/exercise/preview.cjsx b/src/components/exercise/preview.cjsx new file mode 100644 index 0000000..bd99cab --- /dev/null +++ b/src/components/exercise/preview.cjsx @@ -0,0 +1,109 @@ +React = require 'react' +_ = require 'underscore' +classnames = require 'classnames' +BS = require 'react-bootstrap' + +ArbitraryHtmlAndMath = require '../html' +ExerciseIdentifierLink = require './identifier-link' +Question = require '../question' + +ExercisePreview = React.createClass + + propTypes: + extractTag: React.PropTypes.func + displayFeedback: React.PropTypes.bool + panelStyle: React.PropTypes.string + className: React.PropTypes.string + header: React.PropTypes.element + displayAllTags: React.PropTypes.bool + hideAnswers: React.PropTypes.bool + toggleExercise: React.PropTypes.func + isSelected: React.PropTypes.bool + hoverMessage: React.PropTypes.string + exercise: React.PropTypes.shape( + content: React.PropTypes.object + tags: React.PropTypes.array + ).isRequired + + getDefaultProps: -> + panelStyle: 'default' + extractTag: (tag) -> + content = _.compact([tag.name, tag.description]).join(' ') or tag.id + isLO = _.include(['lo', 'aplo'], tag.type) + {content, isLO} + + renderTag: (tag) -> + {content, isLO} = @props.extractTag(tag) + classes = if isLO + content = "LO: #{content}" if isLO + 'lo-tag' + else + 'exercise-tag' + {content} + + onOverlayClick: (ev) -> + @props.toggleExercise(ev, not @props.isSelected) + + renderFooter: -> +
+ {@props.children} + +
+ + renderToggleOverlay: -> +
+
+ {@props.hoverMessage} +
+
+ + render: -> + content = @props.exercise.content + question = content.questions[0] + + tags = _.clone @props.exercise.tags + unless @props.displayAllTags + tags = _.where tags, is_visible: true + renderedTags = _.map(_.sortBy(tags, 'name'), @renderTag) + classes = classnames( 'openstax-exercise-preview', @props.className, { + 'answers-hidden': @props.hideAnswers, + 'is-selectable' : @props.toggleExercise? + 'is-selected': @props.isSelected + 'is-displaying-feedback': @props.displayFeedback + }) + + questions = _.map(content.questions, (question, questionIter) => + question = _.omit(question, 'answers') if @props.hideAnswers + + details =
+
Detailed solution
+ +
+ + + {@props.questionFooters?[questionIter]} + + ) + + + {@renderToggleOverlay() if @props.toggleExercise?} + + {questions} +
{renderedTags}
+
+ +module.exports = ExercisePreview diff --git a/src/components/exercise/step-card.cjsx b/src/components/exercise/step-card.cjsx index 90c8ecc..25911c0 100644 --- a/src/components/exercise/step-card.cjsx +++ b/src/components/exercise/step-card.cjsx @@ -1,4 +1,4 @@ -React = require 'react/addons' +React = require 'react' _ = require 'underscore' classnames = require 'classnames' @@ -61,7 +61,7 @@ ExerciseStepCard = React.createClass @isContinueEnabled(@props, @state) is @isContinueEnabled(nextProps, nextState)) componentWillReceiveProps: (nextProps) -> - unless _.isEqual(@getStepState(@props), @getStepState(nextProps)) + unless _.isEqual(@getStepState(@props, @state), @getStepState(nextProps)) nextStepState = @getStepState(nextProps) @setState(nextStepState) @@ -79,9 +79,10 @@ ExerciseStepCard = React.createClass keymaster.unbind('enter', 'multiple-choice') keymaster.deleteScope('multiple-choice') - getStepState: (props) -> + getStepState: (props, state = {}) -> + {freeResponse} = state {step} = props - freeResponse: step.free_response or '' + freeResponse: step.free_response or props.freeResponseValue or freeResponse or '' answerId: step.answer_id or '' isContinueEnabled: (props, state) -> @@ -134,7 +135,7 @@ ExerciseStepCard = React.createClass footerProps = _.pick(@props, props.StepFooter) footerProps.controlButtons = controlButtons or - footer = React.addons.cloneWithProps(footer, footerProps) + footer = React.cloneElement(footer, footerProps) cardClasses = classnames 'task-step', 'openstax-exercise-card', className diff --git a/src/components/html.cjsx b/src/components/html.cjsx index 670a962..702cec6 100644 --- a/src/components/html.cjsx +++ b/src/components/html.cjsx @@ -1,8 +1,10 @@ React = require 'react' +ReactDom = require 'react-dom' _ = require 'underscore' classnames = require 'classnames' -{typesetMath} = require '../helpers/mathjax' +{ typesetMath } = require '../helpers/mathjax' +{ wrapFrames } = require '../helpers/html-videos' module.exports = React.createClass displayName: 'ArbitraryHtmlAndMath' @@ -51,8 +53,9 @@ module.exports = React.createClass # Perform manipulation on HTML contained inside the components node. updateDOMNode: -> # External links should open in a new window - root = @getDOMNode() + root = ReactDom.findDOMNode(@) links = root.querySelectorAll('a') _.each links, (link) -> link.setAttribute('target', '_blank') unless link.getAttribute('href')?[0] is '#' @props.processHtmlAndMath?(root) or typesetMath(root) + wrapFrames(root) diff --git a/src/components/pinned-header-footer-card/index.cjsx b/src/components/pinned-header-footer-card/index.cjsx index 409aa31..aa5e2ed 100644 --- a/src/components/pinned-header-footer-card/index.cjsx +++ b/src/components/pinned-header-footer-card/index.cjsx @@ -1,4 +1,5 @@ React = require 'react' +ReactDom = require 'react-dom' _ = require 'underscore' {ScrollListenerMixin} = require 'react-scroll-components' @@ -46,7 +47,7 @@ module.exports = React.createClass if @props.fixedOffset? offset = @props.fixedOffset else if @refs.header? - offset = @getTopPosition(@refs.header.getDOMNode()) + offset = @getTopPosition(ReactDom.findDOMNode(@refs.header)) offset @@ -116,18 +117,18 @@ module.exports = React.createClass @setState(shouldBeShy: true) getHeaderHeight: -> - header = @refs.header?.getDOMNode() + header = ReactDom.findDOMNode(@refs.header) headerHeight = header?.offsetHeight or 0 setOriginalContainerMargin: -> - container = @refs.container?.getDOMNode() + container = ReactDom.findDOMNode(@refs.container) return unless container @setState(containerMarginTop: window.getComputedStyle(container).marginTop) if window.getComputedStyle? setContainerMargin: -> headerHeight = @getHeaderHeight() - container = @refs.container?.getDOMNode() + container = ReactDom.findDOMNode(@refs.container) return unless container @setState(headerHeight: headerHeight) diff --git a/src/components/question/answer.cjsx b/src/components/question/answer.cjsx index cf2054c..5e4805b 100644 --- a/src/components/question/answer.cjsx +++ b/src/components/question/answer.cjsx @@ -5,7 +5,7 @@ keymaster = require 'keymaster' keysHelper = require '../../helpers/keys' ArbitraryHtmlAndMath = require '../html' -{Feedback} = require './feedback' +{SimpleFeedback} = require './feedback' idCounter = 0 @@ -98,7 +98,7 @@ Answer = React.createClass 'answer-checked': isChecked 'answer-correct': isCorrect - unless (hasCorrectAnswer or type is 'teacher-review') + unless (hasCorrectAnswer or type is 'teacher-review' or type is 'teacher-preview') radioBox = if @props.show_all_feedback and answer.feedback_html - feedback = + feedback = {answer.feedback_html} htmlAndMathProps = _.pick(@context, 'processHtmlAndMath') @@ -130,13 +130,15 @@ Answer = React.createClass htmlFor="#{qid}-option-#{iter}" className='answer-label'>
- +
+ + {feedback} +
- {feedback}
module.exports = {Answer} diff --git a/src/components/question/feedback.cjsx b/src/components/question/feedback.cjsx index ad991cd..f5605bf 100644 --- a/src/components/question/feedback.cjsx +++ b/src/components/question/feedback.cjsx @@ -4,6 +4,22 @@ _ = require 'underscore' ArbitraryHtmlAndMath = require '../html' +SimpleFeedback = React.createClass + displayName: 'SimpleFeedback' + propTypes: + children: React.PropTypes.string.isRequired + contextTypes: + processHtmlAndMath: React.PropTypes.func + render: -> + wrapperClasses = classnames 'question-feedback-content', 'has-html', @props.className + htmlAndMathProps = _.pick(@context, 'processHtmlAndMath') + + + Feedback = React.createClass displayName: 'Feedback' propTypes: @@ -19,11 +35,7 @@ Feedback = React.createClass
- + {@props.children}
-module.exports = {Feedback} \ No newline at end of file +module.exports = {Feedback, SimpleFeedback} \ No newline at end of file diff --git a/src/components/question/index.cjsx b/src/components/question/index.cjsx index 1a66164..e596b3e 100644 --- a/src/components/question/index.cjsx +++ b/src/components/question/index.cjsx @@ -3,8 +3,6 @@ _ = require 'underscore' classnames = require 'classnames' {AnswersTable} = require './answers-table' -{Answer} = require './answer' -{Feedback} = require './feedback' ArbitraryHtmlAndMath = require '../html' QuestionHtml = React.createClass @@ -44,19 +42,22 @@ Question = React.createClass processHtmlAndMath: @props.processHtmlAndMath render: -> - {model, correct_answer_id, exercise_uid} = @props + {model, correct_answer_id, exercise_uid, details, className} = @props {stem_html, stimulus_html} = model hasCorrectAnswer = !! correct_answer_id - classes = classnames 'openstax-question', + classes = classnames 'openstax-question', className, 'has-correct-answer': hasCorrectAnswer + exerciseUid =
{exercise_uid}
if exercise_uid? +
- + {@props.children} -
{exercise_uid}
+ {details} + {exerciseUid}
module.exports = Question diff --git a/src/components/resize-listener-mixin.cjsx b/src/components/resize-listener-mixin.cjsx index 6527638..5ba2221 100644 --- a/src/components/resize-listener-mixin.cjsx +++ b/src/components/resize-listener-mixin.cjsx @@ -1,4 +1,5 @@ React = require 'react' +ReactDom = require 'react-dom' _ = require 'underscore' module.exports = @@ -41,7 +42,7 @@ module.exports = _getComponentSize: -> return {height: 0, width: 0} unless @isMounted() - componentNode = @getDOMNode() + componentNode = ReactDom.findDOMNode(@) width: componentNode.offsetWidth height: componentNode.offsetHeight diff --git a/src/components/smart-overflow.cjsx b/src/components/smart-overflow.cjsx index 0b232fa..4942e18 100644 --- a/src/components/smart-overflow.cjsx +++ b/src/components/smart-overflow.cjsx @@ -1,4 +1,5 @@ React = require 'react' +ReactDom = require 'react-dom' _ = require 'underscore' classnames = require 'classnames' @@ -21,7 +22,7 @@ SmartOverflow = React.createClass mixins: [ResizeListenerMixin] getOffset: -> - componentNode = @getDOMNode() + componentNode = ReactDom.findDOMNode(@) topOffset = componentNode.getBoundingClientRect().top getTriggerHeight: -> diff --git a/src/helpers/html-videos.coffee b/src/helpers/html-videos.coffee new file mode 100644 index 0000000..6b8461b --- /dev/null +++ b/src/helpers/html-videos.coffee @@ -0,0 +1,26 @@ +_ = require 'underscore' + +getRatioClass = (frame) -> + if (not frame.width or not frame.height) + return "embed-responsive-16by9" + + ratio = frame.width / frame.height + if (Math.abs(ratio - 16 / 9) > Math.abs(ratio - 4 / 3)) + return "embed-responsive-4by3" + else + return "embed-responsive-16by9" + + +wrapFrames = (dom) -> + _.each(dom.getElementsByTagName('iframe'), (frame) -> + if (frame.parentNode?.classList.contains('embed-responsive')) then return + + wrapper = document.createElement("div") + wrapper.className = "frame-wrapper embed-responsive #{getRatioClass(frame)}" + dom.replaceChild(wrapper, frame) + wrapper.appendChild(frame) + ) + + dom + +module.exports = { wrapFrames, getRatioClass } diff --git a/stubs/exercise-preview/data.json b/stubs/exercise-preview/data.json new file mode 100644 index 0000000..fc26d28 --- /dev/null +++ b/stubs/exercise-preview/data.json @@ -0,0 +1,197 @@ +{ + "id": "3", + "url": "https://exercises-dev.openstax.org/exercises/3@43", + "content": { + "tags": [ + "book:stax-apbio", + "apbio-ch01", + "apbio-ch01-s01", + "apbio-ch01-s01-lo02", + "apbio-ch01-ex003", + "ost-chapter-review", + "review", + "dok1", + "time-short", + "blooms-2", + "blooms:2", + "time:short", + "dok:1", + "exid:stax-apbio:3", + "cnxmod:4d5fc58c-cdea-4950-a91d-a5f141a38744", + "type:conceptual-or-recall", + "type:practice", + "filter-type:chapter-review" + ], + "uid": "3@43", + "number": 3, + "version": 43, + "published_at": "2015-12-16T20:46:58.862Z", + "editors": [], + "authors": [{ + "user_id": 1, + "name": "OpenStax" + }], + "copyright_holders": [{ + "user_id": 2, + "name": "Rice University" + }], + "derived_from": [], + "stimulus_html": "", + "questions": [{ + "id": 5882, + "is_answer_order_important": true, + "stimulus_html": "", + "stem_html": "What is a suggested and testable explanation for an event called?", + "answers": [{ + "id": 23238, + "content_html": "discovery", + "correctness": "0.0", + "feedback_html": "A hypothesis is a suggested and testable explanation for an event." + }, { + "id": 23239, + "content_html": "hypothesis", + "correctness": "1.0", + "feedback_html": "A hypothesis is a proposed explanation for a phenomenon that can be tested." + }, { + "id": 23237, + "content_html": "scientific method", + "correctness": "0.0", + "feedback_html": "A hypothesis refers to the suggested and testable explanation of an event." + }, { + "id": 23236, + "content_html": "theory", + "correctness": "0.0", + "feedback_html": "A theory is a broad explanation for a long number of observations and tested hypotheses while a hypothesis refers to the suggested and testable explanation for an event." + }], + "collaborator_solutions": [{ + "attachments": [], + "solution_type": "detailed", + "content_html": "A hypothesis is a proposed explanation for an occurance or event based on previous observations that can be tested." + }], + "solutions": [{ + "uid": "1@1", + "number": 1, + "version": 1, + "published_at": "2015-09-16T20:13:32.533Z", + "editors": [], + "authors": [{ + "user_id": 1, + "name": "OpenStax" + }], + "copyright_holders": [{ + "user_id": 2, + "name": "Rice University" + }], + "derived_from": [], + "attachments": [], + "solution_type": "detailed", + "content_html": "
    \n
  1. C
  2. \n
  3. F
  4. \n
  5. A
  6. \n
  7. B
  8. \n
  9. D
  10. \n
  11. E
  12. \n
\n\n

The original hypothesis is incorrect, as the coffeemaker works when plugged into the outlet. Alternative hypotheses include that the toaster might be broken or that the toaster wasn’t turned on.

\n" + }], + "community_solutions": [], + "hints": [], + "formats": [ + "free-response", + "multiple-choice" + ], + "combo_choices": [] + }] + }, + "tags": [{ + "id": "cnxmod:4d5fc58c-cdea-4950-a91d-a5f141a38744", + "type": "cnxmod", + "is_visible": false, + "data": "4d5fc58c-cdea-4950-a91d-a5f141a38744" + }, { + "id": "apbio-ch01-s01-lo02", + "type": "lo", + "description": "What are the steps of the scientific method?", + "chapter_section": [ + 1, + 1 + ], + "is_visible": true + }, { + "id": "book:stax-apbio", + "type": "generic", + "is_visible": false + }, { + "id": "apbio-ch01", + "type": "generic", + "is_visible": false + }, { + "id": "apbio-ch01-s01", + "type": "generic", + "chapter_section": [ + 1, + 1 + ], + "is_visible": false + }, { + "id": "type:conceptual-or-recall", + "type": "generic", + "is_visible": false + }, { + "id": "apbio-ch01-ex003", + "type": "generic", + "is_visible": false + }, { + "id": "ost-chapter-review", + "type": "generic", + "is_visible": false + }, { + "id": "review", + "type": "generic", + "is_visible": false + }, { + "id": "dok1", + "type": "dok", + "name": "DOK: 1", + "is_visible": true, + "data": "1" + }, { + "id": "time-short", + "type": "length", + "name": "Length: S", + "is_visible": true, + "data": "short" + }, { + "id": "blooms-2", + "type": "blooms", + "name": "Blooms: 2", + "is_visible": true, + "data": "2" + }, { + "id": "blooms:2", + "type": "blooms", + "name": "Blooms: 2", + "is_visible": true, + "data": "2" + }, { + "id": "time:short", + "type": "length", + "name": "Length: S", + "is_visible": true, + "data": "short" + }, { + "id": "dok:1", + "type": "dok", + "name": "DOK: 1", + "is_visible": true, + "data": "1" + }, { + "id": "exid:stax-apbio:3", + "type": "generic", + "is_visible": false + }, { + "id": "type:practice", + "type": "generic", + "is_visible": false + }, { + "id": "filter-type:chapter-review", + "type": "generic", + "is_visible": false + }], + "pool_types": [ + "homework_core" + ] +} \ No newline at end of file diff --git a/stubs/exercise/review.json b/stubs/exercise/review.json index 596bc36..b40e0c1 100644 --- a/stubs/exercise/review.json +++ b/stubs/exercise/review.json @@ -52,16 +52,20 @@ "stem_html": "

In the example below, the scientific method is used to solve an everyday problem. Order the scientific method steps (numbered items) with the process of solving the everyday problem (lettered items). Based on the results of the experiment, is the hypothesis correct? If it is incorrect, propose some alternative hypotheses.

\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Scientific Method Everyday process
1ObservationAThere is something wrong with the electrical outlet.
2QuestionBIf something is wrong with the outlet, my coffeemaker also won’t work when plugged into it.
3Hypothesis (answer)CMy toaster doesn’t toast my bread.
4PredictionDI plug my coffee maker into the outlet.
5ExperimentEMy coffeemaker works.
6ResultFWhat is preventing my coffeemaker from working?
\n", "answers": [{ "id": "4", - "content_html": "The original hypothesis is incorrect. Alternative hypotheses includes that both coffee maker and toaster were broken. -4.81 \\times 10^{-19}\\,\\text{C}." + "content_html": "The original hypothesis is incorrect. Alternative hypotheses includes that both coffee maker and toaster were broken.", + "feedback_html": "Why wouldn't this be true?" }, { "id": "3", - "content_html": "The original hypothesis is correct. The coffee maker and the toaster do not work when plugged into the outlet. -1.602 \\times 10^{-19}\\,\\text{C}." + "content_html": "The original hypothesis is correct. The coffee maker and the toaster do not work when plugged into the outlet.", + "feedback_html": "Correct" }, { "id": "2", - "content_html": "The original hypothesis is incorrect. Alternative hypothesis includes that toaster wasn’t turned on. 1.602 \\times 10^{-19}\\,\\text{C}." + "content_html": "The original hypothesis is incorrect. Alternative hypothesis includes that toaster wasn’t turned on.", + "feedback_html": "Why wouldn't this be true? are you sure?" }, { "id": "1", - "content_html": "The original hypothesis is correct. There is something wrong with the electrical outlet and therefore the toaster doesn’t work. 4.81 \\times 10^{-19}\\,\\text{C}." + "content_html": "The original hypothesis is correct. There is something wrong with the electrical outlet and therefore the toaster doesn’t work.", + "feedback_html": "Why wouldn't this be true? The electrical outlet is good." }], "solutions": [{ "uid": "1@1", @@ -96,4 +100,4 @@ "feedback_html": "The original hypothesis is incorrect because when the coffee maker was plugged in it worked. Therefore, it is incorrect to hypothesize that there is something wrong with the outlet. Alternative hypothesis includes that the toaster wasn’t turned on.", "correct_answer_id": "4", "is_correct": false -} \ No newline at end of file +} diff --git a/test/components/exercise/preview.spec.coffee b/test/components/exercise/preview.spec.coffee new file mode 100644 index 0000000..29d7026 --- /dev/null +++ b/test/components/exercise/preview.spec.coffee @@ -0,0 +1,35 @@ +{Testing, expect, sinon, _} = require 'test/helpers' + +ExercisePreview = require 'components/exercise/preview' + +EXERCISE = require '../../../stubs/exercise/review' +ANSWERS = EXERCISE.content.questions[0].answers + +describe 'Exercise Preview Component', -> + + beforeEach -> + @props = { + exercise: EXERCISE + } + + it 'displays the exercise answers', -> + Testing.renderComponent( ExercisePreview, props: @props ).then ({dom}) -> + for answer, i in _.pluck(dom.querySelectorAll('.answers-answer .answer .choice'), 'textContent') + expect(answer).to.equal( ANSWERS[i].content_html ) + + + it 'renders the feedback', -> + Testing.renderComponent( ExercisePreview, props: @props ).then ({dom}) -> + for answer, i in _.pluck(dom.querySelectorAll('.answers-answer .answer .feedback'), 'textContent') + expect(answer).to.equal( ANSWERS[i].feedback_html ) + + it 'sets the className when displaying feedback', -> + _.extend(@props, displayFeedback: true) + Testing.renderComponent( ExercisePreview, props: @props ).then ({dom}) -> + expect(dom.classList.contains('is-displaying-feedback')).to.be.true + + it 'can hide the answers', -> + _.extend(@props, hideAnswers: true) + Testing.renderComponent( ExercisePreview, props: @props ).then ({dom}) -> + expect(dom.querySelector('.answers-table')).to.be.not.ok + expect(dom.classList.contains('answers-hidden')).to.be.true diff --git a/test/components/html.spec.coffee b/test/components/html.spec.coffee index de237d0..4cae026 100644 --- a/test/components/html.spec.coffee +++ b/test/components/html.spec.coffee @@ -11,6 +11,13 @@ describe 'Arbitrary Html Component', -> processHtmlAndMath: sinon.spy() block: true + @frameProps = + className: 'html' + html: """""" + processHtmlAndMath: sinon.spy() + block: true + it 'renders html', -> Testing.renderComponent( Html, props: @props ).then ({dom}) -> expect(dom.tagName).equal('DIV') @@ -24,3 +31,7 @@ describe 'Arbitrary Html Component', -> @props.block = false Testing.renderComponent( Html, props: @props ).then ({dom}) -> expect(dom.tagName).equal('SPAN') + + it 'wraps iframes with embed classes', -> + Testing.renderComponent( Html, props: @frameProps ).then ({dom}) -> + expect(dom.getElementsByClassName('embed-responsive').length).equal(1) diff --git a/test/components/pinned-header-footer-card/index.spec.coffee b/test/components/pinned-header-footer-card/index.spec.coffee index bc18c32..ad22734 100644 --- a/test/components/pinned-header-footer-card/index.spec.coffee +++ b/test/components/pinned-header-footer-card/index.spec.coffee @@ -29,6 +29,5 @@ describe 'Pinned Header/Footer Card Component', -> Testing.renderComponent( PinnedHeaderFooterCard, unmountAfter: 20, props: @props ).then ({dom, element}) -> expect(document.body.classList.contains('pinned-shy')).to.be.false element.setState(scrollTop: 400) # imitate react-scroll-components - _.defer -> - expect(document.body.classList.contains('pinned-shy')).to.be.true - done() + expect(document.body.classList.contains('pinned-shy')).to.be.true + done() diff --git a/test/components/question/answer.spec.coffee b/test/components/question/answer.spec.coffee index 638b133..c6c5465 100644 --- a/test/components/question/answer.spec.coffee +++ b/test/components/question/answer.spec.coffee @@ -14,11 +14,13 @@ describe 'Answer Component', -> @props = answer: ANSWER type: 'student' + chosenAnswer: [] @propsWithFeedback = answer: ANSWER type: 'student' show_all_feedback: true + chosenAnswer: [] it 'renders answer', -> Testing.renderComponent( Answer, props: @props ).then ({dom}) -> @@ -28,5 +30,5 @@ describe 'Answer Component', -> it 'renders answer feedback based on props', -> Testing.renderComponent( Answer, props: @propsWithFeedback ).then ({dom}) -> - answers = _.pluck dom.querySelectorAll('.question-feedback'), 'textContent' + answers = _.pluck dom.querySelectorAll('.question-feedback-content'), 'textContent' expect(answers).to.deep.equal(['feedback yo']) diff --git a/test/helpers/html-videos.coffee b/test/helpers/html-videos.coffee new file mode 100644 index 0000000..9a33525 --- /dev/null +++ b/test/helpers/html-videos.coffee @@ -0,0 +1,47 @@ +HtmlVideo = require 'helpers/html-videos' + +describe 'Html Video Helper', -> + it 'can wrap an html video frame in a div', -> + dom = document.createElement('div') + html = """""" + + dom.innerHTML = html + dom = HtmlVideo.wrapFrames(dom) + expect(dom.getElementsByClassName('embed-responsive').length).to.equal(1) + + it 'can wrap multiple html videos frame each in a div', -> + dom = document.createElement('div') + html = """ + """ + + dom.innerHTML = html + dom = HtmlVideo.wrapFrames(dom) + expect(dom.getElementsByClassName('embed-responsive').length).to.equal(2) + + it 'can will not wrap frames if a wrapper already exists', -> + dom = document.createElement('div') + html = """
+
""" + + dom.innerHTML = html + dom = HtmlVideo.wrapFrames(dom) + expect(dom.getElementsByClassName('embed-responsive').length).to.equal(1) + + it 'can add responsive embed classes with correct aspect ratio', -> + dom = document.createElement('div') + + html = """""" + dom.innerHTML = html + dom = HtmlVideo.wrapFrames(dom) + expect(dom.getElementsByClassName('embed-responsive-16by9').length).to.equal(1) + + html = """""" + dom.innerHTML = html + dom = HtmlVideo.wrapFrames(dom) + expect(dom.getElementsByClassName('embed-responsive-4by3').length).to.equal(1) diff --git a/test/helpers/index.coffee b/test/helpers/index.coffee index f793a32..6506fb8 100644 --- a/test/helpers/index.coffee +++ b/test/helpers/index.coffee @@ -1,7 +1,8 @@ -_ = require 'lodash' +_ = require 'underscore' expect = chai.expect -React = require 'react/addons' -ReactTestUtils = React.addons.TestUtils +React = require 'react' +ReactDom = require 'react-dom' +ReactTestUtils = require 'react-addons-test-utils' {Promise} = require 'es6-promise' {commonActions} = require './utilities' sandbox = null @@ -23,18 +24,18 @@ Testing = { promise = new Promise( (resolve, reject) -> props = _.clone(options.props) props._wrapped_component = component - wrapper = React.render( React.createElement(Wrapper, props), root ) + wrapper = ReactDom.render( React.createElement(Wrapper, props), root ) resolve({ root, wrapper, element: wrapper.refs.element, - dom: React.findDOMNode(wrapper.refs.element) + dom: ReactDom.findDOMNode(wrapper.refs.element) }) ) # defer adding the then callback so it'll be called after whatever is attached after the return _.defer -> promise.then -> _.delay( -> - React.unmountComponentAtNode(root) + ReactDom.unmountComponentAtNode(root) , unmountAfter ) return arguments promise @@ -43,4 +44,7 @@ Testing = { } +_.pluck = (array, key) -> + _.map(array, _.property(key)) + module.exports = {Testing, expect, sinon, React, _, ReactTestUtils} diff --git a/test/helpers/utilities.cjsx b/test/helpers/utilities.cjsx index 3183410..c2b44c8 100644 --- a/test/helpers/utilities.cjsx +++ b/test/helpers/utilities.cjsx @@ -1,4 +1,6 @@ -React = require 'react/addons' +React = require 'react' +ReactDom = require 'react-dom' +ReactTestUtils = require 'react-addons-test-utils' {Promise} = require 'es6-promise' _ = require 'underscore' @@ -8,7 +10,7 @@ routerStub = container: document.createElement('div') unmount: -> - React.unmountComponentAtNode(@container) + ReactDom.unmountComponentAtNode(@container) @container = document.createElement('div') forceUpdate: (component, args...) -> @@ -29,7 +31,7 @@ componentStub = promise = new Promise (resolve, reject) -> try - React.render(component, div, -> + ReactDom.render(component, div, -> component = @ # merge in custom results with the default kitchen sink of results result = _.defaults({div, component}, result) @@ -44,7 +46,7 @@ componentStub = @_render(@container, component, result) unmount: -> - React.unmountComponentAtNode(@container) + ReactDom.unmountComponentAtNode(@container) @container = document.createElement('div') commonActions = @@ -55,20 +57,20 @@ commonActions = button = div.querySelector(selector) click: (clickElementNode, eventData = {}) -> - React.addons.TestUtils.Simulate.click(clickElementNode, eventData) + ReactTestUtils.Simulate.click(clickElementNode, eventData) # http://stackoverflow.com/questions/24140773/could-not-simulate-mouseenter-event-using-react-test-utils mouseEnter: (clickElementNode, eventData = {}) -> - React.addons.TestUtils.SimulateNative.mouseOver(clickElementNode, eventData) + ReactTestUtils.SimulateNative.mouseOver(clickElementNode, eventData) mouseLeave: (clickElementNode, eventData = {}) -> - React.addons.TestUtils.SimulateNative.mouseOut(clickElementNode, eventData) + ReactTestUtils.SimulateNative.mouseOut(clickElementNode, eventData) blur: (clickElementNode, eventData = {}) -> - React.addons.TestUtils.Simulate.blur(clickElementNode, eventData) + ReactTestUtils.Simulate.blur(clickElementNode, eventData) select: (selectElementNode) -> - React.addons.TestUtils.Simulate.select(selectElementNode) + ReactTestUtils.Simulate.select(selectElementNode) _clickMatch: (selector, args...) -> {div} = args[0] @@ -80,13 +82,13 @@ commonActions = Promise.resolve(commonActions._clickMatch(selector, args...)) _clickComponent: (target, args...) -> - targetNode = React.findDOMNode(target) + targetNode = ReactDom.findDOMNode(target) commonActions.click(targetNode) args[0] _clickComponentOfType: (targetComponent, args...) -> {div, component} = args[0] - target = React.addons.TestUtils.findRenderedComponentWithType(component, targetComponent) + target = ReactTestUtils.findRenderedComponentWithType(component, targetComponent) commonActions._clickComponent(target) clickComponentOfType: (targetComponent) -> @@ -108,7 +110,7 @@ commonActions = _focusMatch: (selector, args...) -> {div} = args[0] elementNode = div.querySelector(selector) - React.addons.TestUtils.Simulate.focus(elementNode) + ReactTestUtils.Simulate.focus(elementNode) args[0] focusMatch: (selector) -> @@ -116,7 +118,7 @@ commonActions = Promise.resolve(commonActions._focusMatch(selector, args...)) _changeDOMNode: (targetNode, eventData, args...) -> - React.addons.TestUtils.Simulate.change(targetNode, eventData) + ReactTestUtils.Simulate.change(targetNode, eventData) args[0] _changeMatch: (selector, eventData, args...) -> @@ -132,7 +134,7 @@ commonActions = _hoverMatch: (selector, args...) -> {div} = args[0] elementNode = div.querySelector(selector) - React.addons.TestUtils.Simulate.mouseOver(elementNode) + ReactTestUtils.Simulate.mouseOver(elementNode) args[0] hoverMatch: (selector) -> @@ -146,9 +148,9 @@ commonActions = textarea = div.querySelector(selector) textarea.value = response - React.addons.TestUtils.Simulate.focus(textarea) - React.addons.TestUtils.Simulate.keyDown(textarea, {key: 'Enter'}) - React.addons.TestUtils.Simulate.change(textarea) + ReactTestUtils.Simulate.focus(textarea) + ReactTestUtils.Simulate.keyDown(textarea, {key: 'Enter'}) + ReactTestUtils.Simulate.change(textarea) _.defaults(args[0], {textarea}) diff --git a/webpack-helper/configs.coffee b/webpack-helper/configs.coffee index d919b27..3d49d9f 100644 --- a/webpack-helper/configs.coffee +++ b/webpack-helper/configs.coffee @@ -2,7 +2,7 @@ webpack = require 'webpack' ExtractTextPlugin = require 'extract-text-webpack-plugin' webpackUMDExternal = require 'webpack-umd-external' -DEV_PORT = process.env['PORT'] or 8000 +DEV_PORT = process.env['DEV_PORT'] or 8000 DEV_LOADERS = ['react-hot', 'webpack-module-hot-accept'] # base config, true for all builds no matter what conditions diff --git a/webpack-helper/index.coffee b/webpack-helper/index.coffee index 2162f47..b9d30fc 100644 --- a/webpack-helper/index.coffee +++ b/webpack-helper/index.coffee @@ -3,7 +3,7 @@ _ = require 'lodash' {base, optionConfigs, devServer} = require './configs' mergeWebpackConfig = (baseConfig, config) -> - _.merge {}, baseConfig, config, (a, b) -> + _.mergeWith {}, baseConfig, config, (a, b) -> if _.isArray(a) return a.concat(b)