diff --git a/.gitmodules b/.gitmodules index 1c55615c88..fb114e4b76 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "ke"] path = ke - url = https://github.com/Khan/khan-exercises.git + url = https://github.com/junyiacademy/junyiexercise diff --git a/build/perseus-1.css b/build/perseus-1.css new file mode 100644 index 0000000000..80b0c5080d --- /dev/null +++ b/build/perseus-1.css @@ -0,0 +1,1193 @@ +#perseus { + position: relative; +} +.no-select { + -moz-user-select: -moz-none; + -khtml-user-select: none; + -webkit-user-select: none; + -o-user-select: none; + user-select: none; +} +.blank-background { + background-color: #FDFDFD; +} +#answer_area .blank-background { + background-color: transparent; +} +.above-scratchpad { + position: relative; + z-index: 2; +} +.graphie.above-scratchpad, +.graphie-container.above-scratchpad { + background-color: #FDFDFD; +} +.graphie { + -moz-user-select: -moz-none; + -khtml-user-select: none; + -webkit-user-select: none; + -o-user-select: none; + user-select: none; +} +.interactive-component, +.interactive-component.above-scratchpad { + position: relative; + z-index: 3; +} +#answercontent input[type=text].perseus-input-size-normal, +#answercontent input[type=number].perseus-input-size-normal, +.framework-perseus input[type=text].perseus-input-size-normal, +.framework-perseus input[type=number].perseus-input-size-normal { + width: 80px; +} +#answercontent input[type=text].perseus-input-size-small, +#answercontent input[type=number].perseus-input-size-small, +.framework-perseus input[type=text].perseus-input-size-small, +.framework-perseus input[type=number].perseus-input-size-small { + width: 40px; +} +#examples > div > ul { + list-style-type: disc; +} +.framework-perseus #problemarea input, +.framework-perseus #problemarea button { + position: relative; + z-index: 3; +} +.framework-perseus div.paragraph { + font-size: 20px; + margin: 22px 0px; +} +.framework-perseus div.instructions { + display: block; + font-style: italic; + font-weight: bold; +} +.framework-perseus .paragraph > ul:not(.perseus-widget-radio) { + font-size: 20px; + margin: -11px 0px 22px 0px; + padding-left: 35px; +} +.framework-perseus .paragraph > ul:not(.perseus-widget-radio) > li { + list-style-type: disc; + line-height: 1.5; +} +.framework-perseus table { + font-size: 20px; +} +.framework-perseus table th, +.framework-perseus table td { + padding: 5px 20px; + text-align: left; +} +.framework-perseus table th[align=center], +.framework-perseus table td[align=center] { + text-align: center; +} +.framework-perseus table th[align=right], +.framework-perseus table td[align=right] { + text-align: right; +} +.framework-perseus table th { + border-bottom: 2px solid #ccc; + font-weight: bold; + padding-bottom: 2px; +} +.framework-perseus table tr:nth-child(odd) td { + background-color: #ededed; +} +.framework-perseus .range-input { + border: 1px solid #ccc; + border-radius: 5px; + display: inline-block; + padding: 0px 5px; +} +.framework-perseus .range-input > input { + border: 0; + display: inline; + text-align: center; + width: 30px; +} +.framework-perseus .range-input > span { + color: #999; + font-size: 14px; +} +.framework-perseus .number-input { + border: 1px solid #ccc; + border-radius: 5px; + margin: 0; + padding: 5px 0; + text-align: center; + width: 40px; +} +.framework-perseus .number-input.invalid-input { + background-color: #ffbaba; + outline-color: red; +} +.framework-perseus .number-input.number-input-label { + margin-left: 5px; +} +.framework-perseus .number-input.mini { + width: 40px; +} +.framework-perseus .number-input.small { + width: 60px; +} +.framework-perseus .number-input.normal { + width: 80px; +} +.framework-perseus .graph-settings .graph-settings-axis-label { + border: 1px solid #ccc; + border-radius: 5px; + display: inline-block; + padding: 5px 5px; + width: 70px; + float: right; + margin: 0 5px; +} +.framework-perseus .graph-settings .graph-settings-background-url { + width: 250px; +} +.framework-perseus .graph-settings, +.framework-perseus .image-settings, +.framework-perseus .misc-settings { + padding-bottom: 5px; +} +.framework-perseus .image-settings, +.framework-perseus .misc-settings, +.framework-perseus .type-settings { + border-top: 1px solid black; + padding-top: 5px; +} +.framework-perseus div #solutionarea div.paragraph { + font-size: inherit; + margin: 0px; +} +.framework-perseus table.non-markdown tr:nth-child(odd) td { + background-color: transparent; +} +.framework-perseus table.non-markdown th, +.framework-perseus table.non-markdown td { + border-width: 0; +} +/* Widget CSS */ +.perseus-widget-expression { + position: relative; +} +.perseus-widget-expression > span, +.perseus-widget-expression .error-tooltip { + display: inline-block; + vertical-align: middle; +} +.perseus-widget-expression .error-tooltip { + position: absolute; + right: 6px; + top: -2px; +} +.perseus-widget-expression .error-icon { + color: #fcc335; + cursor: pointer; + font-size: 20px; +} +.perseus-widget-expression .error-text { + background-color: #fff; + padding: 5px; + width: 210px; +} +.perseus-widget-expression.show-error-tooltip .perseus-math-input.mq-editable-field.mq-math-mode > .mq-root-block { + padding-right: 25px; +} +.perseus-widget-expression .perseus-formats-tooltip { + width: 190px; +} +#answer_area .perseus-widget-expression .perseus-math-input.mq-editable-field.mq-math-mode { + min-width: 130px; +} +#answer_area .perseus-widget-expression .error-tooltip .error-text-container { + left: -125px !important; + top: -17px !important; +} +#answer_area .perseus-widget-expression .error-tooltip .error-text { + font-size: 12px; + width: 90px; +} +#answer_area .perseus-widget-expression .error-tooltip .tooltipContainer > div:first-child { + visibility: hidden !important; +} +.perseus-widget-expression-old, +.perseus-widget-expression-old > .output, +.perseus-widget-expression-old > .output > .tex, +.perseus-widget-expression-old > .output > .placeholder { + display: block; +} +.perseus-widget-expression-old > input, +#answer_area .perseus-widget-expression-old > input { + font-size: 14px; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + direction: ltr; + margin-bottom: 5px; + max-width: 240px; + width: 100%; +} +.perseus-widget-expression-old > .output > .tex { + overflow-x: auto; +} +.perseus-widget-expression-old > .output > .placeholder { + position: relative; + height: 40px; + overflow-y: hidden; +} +.perseus-widget-expression-old > .output > .placeholder > .error { + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + background: #f7f7f7; + border: 1px solid #ddd; + box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5); + color: #000; + display: none; + font-weight: normal; + min-height: 22px; + width: 168px; + position: absolute; + top: 0px; + left: 40px; + margin: auto; +} +.perseus-widget-expression-old > .output > .placeholder > .error > .buddy { + background-image: url(/images/perseus/error-buddy.png); + background-size: 100%; + height: 36px; + width: 40px; + position: absolute; + top: -3px; + left: -42px; +} +.perseus-widget-expression-old > .output > .placeholder > .error > .message { + font: 12px "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 1.4em; + margin: 0px 4px; +} +.perseus-widget-radio li span.checkbox input[type=radio], +#answer_area .perseus-widget-radio li span.checkbox input[type=radio] { + float: none; + margin: 3px; +} +.perseus-widget-radio div, +.perseus-widget-radio div > p { + /* TODO(alpert): Find a better way of doing inline renderers */ + display: inline; +} +.perseus-widget-radio li { + padding: 7px 0; + margin-left: 20px; + cursor: pointer; +} +.perseus-widget-radio li.inline { + display: inline-block; + padding-left: 20px; +} +.perseus-widget-radio li span.checkbox { + position: relative; + z-index: 3; + display: inline-block; + width: 20px; + margin-left: -20px; + margin-right: 0; + height: 0px; +} +.perseus-widget-radio li span.checkbox input { + margin: 0px; +} +.perseus-widget-radio li div.instructions { + margin-bottom: 5px; +} +.perseus-widget-radio li .value { + display: block; + margin-left: 18px; + min-height: 22px; +} +.perseus-widget-radio li img { + vertical-align: middle; +} +.perseus-widget-radio .perseus-radio-clue { + color: #007aff; + display: block; +} +.perseus-widget-radio .perseus-textarea-pair, +.perseus-widget-radio .perseus-textarea-underlay { + display: block; +} +body.mobile .framework-perseus #problemarea input[type=radio], +body.mobile .framework-perseus #problemarea input[type=checkbox] { + float: left; + margin-right: 7px; +} +body.mobile .perseus-widget-radio { + color: #aaaaaa; + margin-left: 0; +} +body.mobile .perseus-widget-radio .perseus-radio-option { + margin-left: 0; + padding: 0; +} +body.mobile .perseus-widget-radio span.checkbox { + position: relative; + z-index: 3; + margin-right: 26px; + display: block; + margin: 0; + width: auto; +} +body.mobile .perseus-widget-radio .perseus-radio-option { + border: 2px solid #aaaaaa; + border-radius: 28px; + box-sizing: border-box; + cursor: pointer; + display: block; + font: 700 14pt/30px "Avenir", "Helvetica", "Arial", sans-serif; + margin-bottom: 10px; + overflow: hidden; + padding: 8px 10px; +} +body.mobile .perseus-widget-radio .perseus-radio-option.perseus-radio-selected { + border-color: #1c758a; + color: #1c758a; + font-weight: bold; +} +body.mobile .perseus-widget-radio input[type=radio], +body.mobile .perseus-widget-radio input[type=checkbox] { + -webkit-appearance: none; + appearance: none; + border: 2px solid #aaaaaa; + border-radius: 25px; + float: left; +} +body.mobile .perseus-widget-radio input[type=radio] { + height: 25px; + margin-right: 10px; + outline: none; + width: 25px; +} +body.mobile .perseus-widget-radio input[type=radio]:checked { + background: #1c758a; + border: 2px solid #1c758a; +} +body.mobile .perseus-widget-radio input[type=checkbox] { + position: absolute; + outline: none; +} +body.mobile .perseus-widget-radio input[type=checkbox]:before { + color: #aaaaaa; + content: "\f00c"; + font: 15pt "FontAwesome"; + left: 0; + position: relative; + top: 1px; +} +body.mobile .perseus-widget-radio input[type=checkbox]:checked { + border-color: #1c758a; +} +body.mobile .perseus-widget-radio input[type=checkbox]:checked:before { + color: #1c758a; +} +.perseus-widget-interactive-graph { + padding: 25px 25px 0 0; +} +.graphie-container { + position: relative; +} +.graphie-container img { + position: absolute; +} +.categorization-container { + position: relative; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; +} +.categorization-container .categories, +.categorization-container .bank { + float: left; +} +.categorization-container .categories { + width: 231px; +} +.categorization-container .cards-list { + padding: 13px; +} +.categorization-container .category { + float: left; + padding: 8px; + width: 215px; + text-align: center; +} +.categorization-container .category .card-container { + float: left; + width: 100%; + display: block; +} +.categorization-container .category.target .cards-list.cards-area { + border-color: #999999; + box-shadow: 0 0 4px #7d7d7d; + -moz-box-shadow: 0 0 4px #7d7d7d; + -ms-box-shadow: 0 0 4px #7d7d7d; + -o-box-shadow: 0 0 4px #7d7d7d; + -webkit-box-shadow: 0 0 4px #7d7d7d; +} +.categorization-container .header { + font-weight: bold; + margin-bottom: 5px; +} +.categorization-container .target .header { + color: black; +} +.categorization-container .card { + min-height: 26px; + text-align: center; + line-height: 24px; + width: 167px; + padding: 6px 8px; + margin: 0 auto; + transition-property: border-color, box-shadow; + transition-duration: 150ms; + -moz-transition-property: border-color, -moz-box-shadow; + -moz-transition-duration: 150ms; + -webkit-transition-property: border-color, -webkit-box-shadow; + -webkit-transition-duration: 150ms; +} +.categorization-container .card:hover { + transition-property: border-color, box-shadow; + transition-duration: 150ms; + -moz-transition-property: border-color, -moz-box-shadow; + -moz-transition-duration: 150ms; + -webkit-transition-property: border-color, -webkit-box-shadow; + -webkit-transition-duration: 150ms; +} +.categorization-container .card-container { + float: left; + padding: 4px 0; +} +.categorization-container .card-hidden { + visibility: hidden; + z-index: 0; +} +.categorization-container .card.dragging { + position: absolute; + display: none; + z-index: 4; + border-color: #ffa500; + box-shadow: 0 0 4px #c78100; + -moz-box-shadow: 0 0 4px #c78100; + -ms-box-shadow: 0 0 4px #c78100; + -o-box-shadow: 0 0 4px #c78100; + -webkit-box-shadow: 0 0 4px #c78100; +} +.categorization-container .card.placeholder > * { + visibility: hidden; +} +.categorization-container.currently-dragging .card { + opacity: 0.4; +} +.categorization-container.currently-dragging .card.dragging { + display: block; + background-color: white; + opacity: 1; + filter: opacity(1); +} +.categorization-container div.paragraph { + margin: 0; +} +.categorizer-container { + margin-top: 20px; +} +.categorizer-container div.paragraph { + margin: 10px 0px; +} +.categorizer-container .category { + text-align: center; +} +.categorizer-container th.category { + vertical-align: bottom; +} +.categorizer-container label { + position: relative; + z-index: 2; +} +.categorizer-container td.category { + padding: 0; + color: #ccc; + vertical-align: bottom; +} +.categorizer-container td.category input[type="radio"] { + display: none; +} +.categorizer-container td.category input[type="radio"] + span:before { + display: inline-block; + position: relative; + font-family: 'FontAwesome'; + font-size: 30px; + width: 30px; + padding-right: 3px; + bottom: 9px; + content: "\f1db"; +} +.categorizer-container td.category label span:hover { + color: #999; +} +.categorizer-container td.category input[type="radio"]:checked + span:before { + content: "\f111"; + color: #333; +} +.perseus-widget-plotter svg, +.perseus-widget-plotter vml { + position: absolute; +} +.perseus-widget-plotter span.rotate { + -moz-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + -webkit-transform: rotate(-90deg); + transform: rotate(-90deg); + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; +} +.perseus-widget-plotter body.ie span.rotate { + left: 60px !important; + top: 140px !important; +} +.set-from-scale-box { + border: 2px solid #EEEEEE; + border-radius: 3px; + padding: 3px; +} +.categories-title { + font-size: 14px; +} +.framework-perseus table.perseus-widget-table-of-values.non-markdown { + text-align: left; + margin: 20px auto 0; + border-collapse: collapse; +} +.framework-perseus table.perseus-widget-table-of-values.non-markdown tr { + height: 23px; +} +.framework-perseus table.perseus-widget-table-of-values.non-markdown th, +.framework-perseus table.perseus-widget-table-of-values.non-markdown td { + border: 2px solid black; + border-width: 0 2px; +} +.framework-perseus table.perseus-widget-table-of-values.non-markdown th:first-child, +.framework-perseus table.perseus-widget-table-of-values.non-markdown td:first-child { + border-left: 0; +} +.framework-perseus table.perseus-widget-table-of-values.non-markdown th:last-child, +.framework-perseus table.perseus-widget-table-of-values.non-markdown td:last-child { + border-right: 0; +} +.framework-perseus table.perseus-widget-table-of-values.non-markdown th { + font-weight: normal; + padding: 5px; + width: 80px; + text-align: left; + border-bottom: 2px solid black; +} +.framework-perseus table.perseus-widget-table-of-values.non-markdown th .paragraph { + margin: 0; +} +.framework-perseus table.perseus-widget-table-of-values.non-markdown td { + padding: 0px 5px; + text-align: right; +} +.framework-perseus table.perseus-widget-table-of-values.non-markdown tbody tr:first-child td { + padding-top: 5px; +} +.framework-perseus table.perseus-widget-table-of-values input, +#answer_area table.perseus-widget-table-of-values input { + width: 80px; +} +.perseus-widget-dropdown { + position: relative; +} +.perseus-widget-dropdown .fancy-select { + border: 3px solid #358fa4; + border-radius: 40px; + color: #358fa4; + font: 700 23pt avenir, sans-serif; + z-index: 3; + padding: 0 60px 0 35px; + min-width: 40px; + min-height: 40px; + cursor: pointer; + white-space: nowrap; + user-select: none; + -webkit-user-select: none; +} +.perseus-widget-dropdown .fancy-select:before { + font-size: 0; + visibility: hidden; + content: "_"; +} +.perseus-widget-dropdown .fancy-select > .placeholder { + opacity: 0.5; +} +.perseus-widget-dropdown .fancy-select.active { + opacity: 0.5; +} +.perseus-widget-dropdown .fancy-select:after { + color: #358fa4; + content: "\f0dd"; + font: 24pt "FontAwesome"; + position: absolute; + top: 0px; + right: 17px; +} +.perseus-widget-dropdown .fancy-select-options { + color: #358fa4; + position: absolute; + z-index: 4; + min-width: 100%; + overflow-y: hidden; +} +.perseus-widget-dropdown .fancy-option { + cursor: pointer; + padding: 3px 20px 3px 38px; + font-size: 17pt; + line-height: 22pt; + background: rgba(255, 255, 255, 0.95); + border: 3px solid #358fa4; + display: block; + border-radius: 20px; + min-width: 40px; + min-height: 30px; + margin-top: -70px; +} +.perseus-widget-dropdown .fancy-option:before { + content: ""; + width: 16px; + height: 16px; + position: absolute; + border: 3px solid #358fa4; + margin-top: 4px; + margin-left: -30px; + border-radius: 25px; +} +.perseus-widget-dropdown .fancy-option.selected:before { + background: #358fa4; +} +.perseus-widget-dropdown .fancy-option.active { + margin-top: 6px; + -webkit-animation-name: perseus-fancy-dropdown-slideout; + -webkit-animation-duration: 0.35s; + -webkit-animation-timing-function: ease-out; +} +.perseus-widget-dropdown .fancy-option.closed { + margin-top: -70px; + -webkit-animation-name: perseus-fancy-dropdown-slidein; + -webkit-animation-duration: 0.2s; + -webkit-animation-timing-function: ease-in; +} +@-webkit-keyframes perseus-fancy-dropdown-slideout { + 0% { + margin-top: -70px; + } + 100% { + margin-top: 6px; + } +} +@-webkit-keyframes perseus-fancy-dropdown-slidein { + 0% { + margin-top: 6px; + } + 100% { + margin-top: -70px; + } +} +.orderer { + position: relative; + min-width: 480px; +} +.orderer .draggable-box { + padding: 13px; + min-height: 69px; + margin-top: 30px; +} +.orderer .card { + padding: 0 10px; + cursor: pointer; + position: relative; + user-select: none; + width: auto; +} +.orderer .card img { + vertical-align: middle; +} +.orderer.height-normal.layout-horizontal .card { + line-height: 65px; + height: 65px; +} +.orderer.height-normal.layout-vertical .card { + padding: 5px; +} +.orderer.height-large .card { + line-height: 160px; + height: 160px; +} +.orderer.height-auto .card { + padding: 0; +} +.orderer.height-auto.layout-horizontal .drag-hint { + min-height: 65px; + min-width: 22px; +} +.orderer .card-wrap { + position: relative; + z-index: 3; + margin-right: 4px; + width: auto; + float: left; +} +.orderer .bank { + margin: 0px 13px; +} +.orderer .bank .card-wrap { + margin-right: 10px; + margin-bottom: 8px; +} +.orderer div.paragraph { + margin: 0; +} +.orderer.layout-vertical .card-wrap { + float: none; + text-align: center; +} +.orderer.layout-vertical .bank, +.orderer.layout-vertical .draggable-box { + box-sizing: border-box; + -moz-box-sizing: border-box; + float: left; + max-width: 50%; +} +.orderer.layout-vertical .bank { + padding: 10px 0 0 0; + margin: 0; +} +.orderer.layout-vertical .bank .card-wrap { + margin-right: 20px; +} +.orderer.layout-vertical .draggable-box { + margin-top: 0; + min-height: 170px; + padding: 10px; +} +.orderer.layout-vertical .draggable-box .card-wrap:first-child { + margin: 0; +} +.orderer.layout-vertical .draggable-box .card-wrap { + margin: 8px 0 0 0; +} +.orderer.layout-vertical .draggable-box .drag-hint { + box-sizing: border-box; + min-width: 140px; + min-height: 34px; +} +.orderer.layout-vertical .draggable-box .placeholder { + box-sizing: border-box; +} +.perseus-widget-measurer { + position: relative; +} +.perseus-widget-measurer img { + position: absolute; +} +.perseus-widget-measurer-url { + width: 70%; +} +.perseus-widget-transformer .highlighted-tool-button { + text-shadow: 1px 1px rgba(0, 0, 0, 0.25); +} +.perseus-widget-transformer .graphie-container { + margin-bottom: 10px; +} +.perseus-widget-transformer .transformer-undo-button { + float: right; +} +.perseus-widget-transformer .perseus-transformation-list { + margin-top: 10px; + margin-bottom: 10px; +} +.perseus-widget-transformer .perseus-transformation-list input { + width: 40px; +} +.perseus-widget-matcher .column { + float: left; + max-width: 50%; +} +.perseus-widget-matcher .column:first-child .column-label, +.perseus-widget-matcher .column:first-child .perseus-sortable { + padding-right: 5px; +} +.perseus-widget-matcher .column + .column .column-label, +.perseus-widget-matcher .column + .column .perseus-sortable { + border-left: 1px solid #444444; + padding-left: 5px; +} +.perseus-widget-matcher .column-label { + border-bottom: 1px solid #444444; + padding-bottom: 5px; + text-align: center; +} +.perseus-widget-matcher .column-label div.paragraph { + margin: 0; +} +.perseus-widget-matcher .perseus-sortable { + padding-top: 5px; +} +.draggy-boxy-thing .draggable-box, +.draggy-boxy-thing .cards-area { + background: #eee; + border: 1px solid #ccc; + border-bottom: 1px solid #aaa; + box-shadow: 0 1px 2px #ccc; + -moz-box-shadow: 0 1px 2px #ccc; + -webkit-box-shadow: 0 1px 2px #ccc; +} +.draggy-boxy-thing .cards-area { + position: relative; + z-index: 2; +} +.draggy-boxy-thing .card { + position: relative; + z-index: 3; + background-color: #fff; + border: 1px solid #b9b9b9; + border-bottom-color: #939393; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + cursor: pointer; +} +.draggy-boxy-thing .card.placeholder { + background: #ddd; + border: 1px solid #ccc; +} +.draggy-boxy-thing .card.drag-hint { + background: none; + border: 1px dashed #aaa; + cursor: auto; +} +.draggy-boxy-thing .card.drag-hint:hover { + border-color: #aaa; + box-shadow: none; +} +.draggy-boxy-thing .card.dragging { + background-color: #ffedcd; + opacity: 0.8; + filter: opacity(0.8); +} +.draggy-boxy-thing .card.stack { + z-index: auto; +} +.draggy-boxy-thing .card.stack:after { + content: " "; + background-color: #fff; + border: 1px solid #b9b9b9; + border-bottom-color: #939393; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + height: 100%; + width: 100%; + z-index: -1; + top: 1px; + left: 1px; + position: absolute; +} +.draggy-boxy-thing .card:hover { + border-color: #ffa500; + box-shadow: 0 0 4px #c78100; + -moz-box-shadow: 0 0 4px #c78100; + -ms-box-shadow: 0 0 4px #c78100; + -o-box-shadow: 0 0 4px #c78100; + -webkit-box-shadow: 0 0 4px #c78100; +} +.perseus-sortable { + position: relative; + z-index: 3; + box-sizing: border-box; + -moz-box-sizing: border-box; + float: left; +} +.perseus-sortable .perseus-sortable-card { + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + box-sizing: border-box; + -moz-box-sizing: border-box; + cursor: pointer; + min-width: 25px; + min-height: 44px; + padding: 10px; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; +} +.perseus-sortable .perseus-sortable-placeholder { + background: #ddd; + border: 1px solid #ccc; +} +.perseus-sortable .perseus-sortable-draggable { + font-size: 0; + text-align: center; +} +.perseus-sortable .perseus-sortable-draggable:before { + content: ""; + display: inline-block; + height: 100%; + vertical-align: middle; +} +.perseus-sortable .perseus-sortable-draggable > div { + display: inline-block; + font-size: 14px; + max-width: 100%; + vertical-align: middle; +} +.perseus-sortable .perseus-sortable-draggable div.paragraph { + margin: 0; +} +.perseus-sortable .perseus-sortable-draggable.perseus-sortable-static { + cursor: pointer; +} +.perseus-sortable .perseus-sortable-draggable.perseus-sortable-static:hover { + border-color: #ffa500; + box-shadow: 0 0 4px #c78100; + -moz-box-shadow: 0 0 4px #c78100; + -ms-box-shadow: 0 0 4px #c78100; + -o-box-shadow: 0 0 4px #c78100; + -webkit-box-shadow: 0 0 4px #c78100; +} +.perseus-sortable .perseus-sortable-draggable.perseus-sortable-dragging { + background-color: #ffedcd; + filter: opacity(0.8); + opacity: 0.8; +} +.perseus-sortable .perseus-sortable-draggable.perseus-sortable-disabled { + background-color: inherit; + border: 1px solid transparent; + cursor: default; +} +.perseus-sortable.layout-horizontal .perseus-sortable-card { + float: left; +} +.perseus-sortable.layout-horizontal .perseus-sortable-dragging { + cursor: ew-resize; +} +.perseus-sortable.layout-vertical, +.perseus-sortable.layout-vertical .perseus-sortable-card { + max-width: 100%; +} +.perseus-sortable.layout-vertical .perseus-sortable-dragging { + cursor: ns-resize; +} +.perseus-sortable.unpadded .perseus-sortable-card { + padding: 0; +} +.perseus-sortable.unpadded .perseus-sortable-card img { + vertical-align: bottom; +} +.perseus-widget-image .label-settings td { + padding: 5px 4px; + text-align: center; +} +.perseus-widget-image .label-settings tr:nth-child(odd) td { + background-color: transparent; +} +.perseus-widget-image .label-settings th, +.perseus-widget-image .label-settings td { + border-width: 0; +} +.perseus-diff { + margin: 0 10px; +} +.perseus-diff .diff-header { + font-size: 18px; + padding: 10px 0; + width: 50%; + display: inline-block; +} +.perseus-diff .diff-header.collapsed { + font-size: 14px; + padding: 0px; +} +.perseus-diff .diff-body { + border-top: 1px solid #e4e4e4; + border-bottom: 1px solid #e4e4e4; +} +.perseus-diff .diff-row { + width: 50%; + white-space: pre-wrap; + box-sizing: border-box; + -moz-box-sizing: border-box; + font-size: 14px; + padding-right: 20px; + padding-left: 10px; + overflow: hidden; +} +.perseus-diff .diff-row.collapsed { + color: #888; + cursor: pointer; +} +.perseus-diff .diff-row.collapsed:hover { + color: #666; +} +.perseus-diff .diff-row.collapsed:active { + color: #444; +} +.perseus-diff .diff-row .diff-line { + margin-bottom: 20px; +} +.perseus-diff .before { + float: left; + border-left: 1px solid #e4e4e4; +} +.perseus-diff .after { + float: right; + border-right: 1px solid #e4e4e4; + border-left: 1px solid #e4e4e4; +} +.perseus-diff .inner-value { + height: 100%; + padding: 3px; +} +.perseus-diff .not-present { + display: none; +} +.perseus-diff .blank-space { + visibility: hidden; +} +.perseus-diff .added { + background-color: #EEFFEE; +} +.perseus-diff .added.dark { + background-color: #AAFFAA; +} +.perseus-diff .removed { + background-color: #FFEEEE; +} +.perseus-diff .removed.dark { + background-color: #FFAAAA; +} +.perseus-diff .image { + margin-left: 20px; + margin-bottom: 10px; +} +.perseus-diff .image-unchanged { + border: 1px solid #AAAAAA; +} +.perseus-diff .image-added { + border: 2px solid #AAFFAA; +} +.perseus-diff .image-removed { + border: 2px solid #FFAAAA; +} +.perseus-widget-container { + font-size: 14px; +} +.perseus-widget-container.widget-nohighlight { + transition: all 0.15s; +} +.perseus-widget-container.widget-highlight { + -webkit-box-shadow: 0px 0px 9px 2px #ffa500; + -moz-box-shadow: 0px 0px 9px 2px #ffa500; + box-shadow: 0px 0px 9px 2px #ffa500; + transition: all 0.15s; +} +.perseus-tooltip { + background: #fff; + padding: 5px 10px; + width: 240px; +} +.perseus-formats-tooltip { + background: #fff; + padding: 5px 10px; + width: 240px; + color: #777; +} +.framework-perseus .perseus-formats-tooltip .paragraph > ul { + padding: 0; + margin: -20px 0 -16px 0; +} +.framework-perseus .perseus-formats-tooltip .paragraph > ul > li { + list-style-type: none; +} +.perseus-math-input.mq-editable-field.mq-math-mode { + background: #fff; + font-size: 18px; + min-width: 100px; + position: relative; + z-index: 3; +} +.perseus-math-input.mq-editable-field.mq-math-mode > .mq-root-block { + border-color: #a4a4a4; + border-radius: 5px; + padding: 4px; +} +.perseus-math-input.mq-editable-field.mq-math-mode .mq-cursor { + padding-left: 0; +} +.perseus-math-input.mq-editable-field.mq-math-mode .mq-paren.mq-ghost { + color: inherit; +} +.perseus-math-input.mq-editable-field.mq-math-mode .mq-paren + span { + margin: 0; +} +.math-input-buttons { + background-color: rgba(255, 255, 255, 0.7); + border-radius: 5px; + border: 1px solid #ddd; + box-sizing: border-box; + margin-top: 5px; + padding: 2px; + width: 280px; +} +.math-input-buttons.absolute { + left: -2px; + position: absolute; + top: -3px; + z-index: 5; + width: 360px; +} +.tex-button { + display: block; + float: left; + width: 41px; + height: 41px; + margin: 2px; + border: 1px solid #1c758a; + background-color: white; + border-radius: 5px; +} +.tex-button:hover { + cursor: pointer; + background-color: #f0f0f0; +} +.tex-button:focus { + border: 2px solid #1c758a; + outline: none; +} +.tex-button-row { + margin: 5px 0; +} +.tex-button-row:first-child, +.tex-button-row:last-child { + margin: 0; +} diff --git a/build/perseus-1.js b/build/perseus-1.js index 4e12cf83cf..f1cdc69cd5 100644 --- a/build/perseus-1.js +++ b/build/perseus-1.js @@ -1,7 +1,7 @@ /*! Perseus | http://github.com/Khan/perseus */ -// commit c3a79e5f47a88d2e38c141b3c6206c32dc5538f9 -// branch gh-pages -!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.Perseus=e():"undefined"!=typeof global?global.Perseus=e():"undefined"!=typeof self&&(self.Perseus=e())}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;oa.VOICESUPPORT_ATTEMPTLIMIT&&(clearInterval(b),null!=window.speechSynthesis?a.iOS?(a.iOS9?a.systemVoicesReady(a.cache_ios9_voices):a.systemVoicesReady(a.cache_ios_voices),console.log("RV: Voice support ready (cached)")):(console.log("RV: speechSynthesis present but no system voices found"),a.enableFallbackMode()):a.enableFallbackMode()))},100)},100);a.Dispatch("OnLoad")};a.systemVoicesReady=function(b){a.systemvoices=b;a.mapRVs();null!=a.OnVoiceReady&& +a.OnVoiceReady.call();a.Dispatch("OnReady");window.hasOwnProperty("dispatchEvent")&&window.dispatchEvent(new Event("ResponsiveVoice_OnReady"))};a.enableFallbackMode=function(){a.fallbackMode=!0;console.log("RV: Enabling fallback mode");a.mapRVs();null!=a.OnVoiceReady&&a.OnVoiceReady.call();a.Dispatch("OnReady");window.hasOwnProperty("dispatchEvent")&&window.dispatchEvent(new Event("ResponsiveVoice_OnReady"))};a.getVoices=function(){for(var b=[],c=0;ca.CHARACTER_LIMIT){for(var e=b;e.length>a.CHARACTER_LIMIT;){var g= +e.search(/[:!?.;]+/),d="";if(-1==g||g>=a.CHARACTER_LIMIT)g=e.search(/[,]+/);-1==g&&-1==e.search(" ")&&(g=99);if(-1==g||g>=a.CHARACTER_LIMIT)for(var k=e.split(" "),g=0;ga.CHARACTER_LIMIT);g++)d+=(0!=g?" ":"")+k[g];else d=e.substr(0,g+1);e=e.substr(d.length,e.length-d.length);h.push(d)}0=f)){var h=b.split(/\s+/).length,e=(b.match(/[^ ]/igm)||b).length,f=60/a.WORDS_PER_MINUTE*f*1E3*(e/h/5.1)*h;3>h&&(f=4E3);3E3>f&&(f=3E3);a.timeoutId=setTimeout(c,f)}};a.checkAndCancelTimeout=function(){null!=a.timeoutId&&(clearTimeout(a.timeoutId),a.timeoutId=null)};a.speech_timedout=function(){a.cancel();a.cancelled=!1;a.speech_onend()};a.speech_onend=function(){a.checkAndCancelTimeout(); +!0===a.cancelled?a.cancelled=!1:null!=a.msgparameters&&null!=a.msgparameters.onend&&1!=a.msgparameters.onendcalled&&(a.msgparameters.onendcalled=!0,a.msgparameters.onend())};a.speech_onstart=function(){if(!a.onstartFired){a.onstartFired=!0;if(a.iOS||a.is_safari||a.useTimer)a.fallbackMode||a.startTimeout(a.msgtext,a.speech_timedout);a.msgparameters.onendcalled=!1;if(null!=a.msgparameters&&null!=a.msgparameters.onstart)a.msgparameters.onstart()}};a.fallback_startPart=function(){0==a.fallback_part_index&& +a.speech_onstart();a.fallback_audio=a.fallback_parts[a.fallback_part_index];if(null==a.fallback_audio)console.log("RV: Fallback Audio is not available");else{var b=a.fallback_audio;a.fallback_audiopool.push(b);setTimeout(function(){b.playbackRate=a.fallback_playbackrate},50);b.onloadedmetadata=function(){b.play();b.playbackRate=a.fallback_playbackrate};a.fallback_audio.play();a.fallback_audio.addEventListener("ended",a.fallback_finishPart);a.useTimer&&a.startTimeout(a.multipartText[a.fallback_part_index], +a.fallback_finishPart)}};a.fallback_finishPart=function(b){a.checkAndCancelTimeout();a.fallback_part_indexa[e])&&clearTimeout(a[h])},50));return!1};a.AddEventListener=function(b,c){a.hasOwnProperty(b+"_callbacks")||(a[b+"_callbacks"]=[]);a[b+"_callbacks"].push(c)};a.addEventListener=a.AddEventListener;a.clickEvent=function(){if(a.iOS&& +!a.iOS_initialized){console.log("Initializing iOS click event");var b=new SpeechSynthesisUtterance(" ");speechSynthesis.speak(b);a.iOS_initialized=!0}};a.isPlaying=function(){return a.fallbackMode?null!=a.fallback_audio&&!a.fallback_audio.ended&&!a.fallback_audio.paused:speechSynthesis.speaking};a.clearFallbackPool=function(){for(var b=0;b + * Build: `lodash modularize modern exports="npm" -o ./npm/` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.5.2 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license + */ - ':focus': { - zIndex: '2' - } -}; +/** Used to pool arrays and objects used internally */ +var arrayPool = []; -var selectedStyle = { - backgroundColor: '#ddd' -}; +module.exports = arrayPool; -RCSS.createClass(outerStyle); -RCSS.createClass(buttonStyle); -RCSS.createClass(selectedStyle); +},{}],5:[function(require,module,exports){ +/** + * Lo-Dash 2.4.1 (Custom Build) + * Build: `lodash modularize modern exports="npm" -o ./npm/` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.5.2 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license + */ +var baseCreate = require('lodash._basecreate'), + isObject = require('lodash.isobject'), + setBindData = require('lodash._setbinddata'), + slice = require('lodash._slice'); -/* ButtonGroup is an aesthetically pleasing group of buttons. - * - * The class requires these properties: - * buttons - an array of objects with keys: - * "value": this is the value returned when the button is selected - * "text": this is the text shown on the button - * "title": this is the title-text shown on hover - * onChange - a function that is provided with the updated value - * (which it then is responsible for updating) - * - * The class has these optional properties: - * value - the initial value of the button selected, defaults to null. - * allowEmpty - if false, exactly one button _must_ be selected; otherwise - * it defaults to true and _at most_ one button (0 or 1) may be selected. +/** + * Used for `Array` method references. * - * Requires stylesheets/perseus-admin-package/editor.less to look nice. + * Normally `Array.prototype` would suffice, however, using an array literal + * avoids issues in Narwhal. */ +var arrayRef = []; -var ButtonGroup = React.createClass({displayName: 'ButtonGroup', - propTypes: { - value: React.PropTypes.any, - buttons: React.PropTypes.arrayOf(React.PropTypes.shape({ - value: React.PropTypes.any.isRequired, - text: React.PropTypes.renderable, - title: React.PropTypes.string - })).isRequired, - onChange: React.PropTypes.func.isRequired, - allowEmpty: React.PropTypes.bool - }, - - getDefaultProps: function() { - return { - value: null, - allowEmpty: true - }; - }, - - render: function() { - var value = this.props.value; - var buttons = _(this.props.buttons).map(function(button, i) { - var maybeSelected = button.value === value ? - selectedStyle.className : - ""; - return React.DOM.button( {title:button.title, - id:"" + i, - ref:"button" + i, - key:"" + i, - className:(buttonStyle.className + " " + maybeSelected), - onClick:this.toggleSelect.bind(this, button.value)}, - button.text || "" + button.value - ); - }.bind(this)); - - return React.DOM.div( {className:outerStyle.className}, - buttons - ); - }, - - focus: function() { - this.getDOMNode().focus(); - return true; - }, +/** Native method shortcuts */ +var push = arrayRef.push; - toggleSelect: function(newValue) { - var value = this.props.value; +/** + * The base implementation of `_.bind` that creates the bound function and + * sets its meta data. + * + * @private + * @param {Array} bindData The bind data array. + * @returns {Function} Returns the new bound function. + */ +function baseBind(bindData) { + var func = bindData[0], + partialArgs = bindData[2], + thisArg = bindData[4]; - if (this.props.allowEmpty) { - // Select the new button or unselect if it's already selected - this.props.onChange(value !== newValue ? newValue : null); - } else { - this.props.onChange(newValue); - } + function bound() { + // `Function#bind` spec + // http://es5.github.io/#x15.3.4.5 + if (partialArgs) { + // avoid `arguments` object deoptimizations by using `slice` instead + // of `Array.prototype.slice.call` and not assigning `arguments` to a + // variable as a ternary expression + var args = slice(partialArgs); + push.apply(args, arguments); } -}); + // mimic the constructor's `return` behavior + // http://es5.github.io/#x13.2.2 + if (this instanceof bound) { + // ensure `new bound` is an instance of `func` + var thisBinding = baseCreate(func.prototype), + result = func.apply(thisBinding, args || arguments); + return isObject(result) ? result : thisBinding; + } + return func.apply(thisArg, args || arguments); + } + setBindData(bound, bindData); + return bound; +} -module.exports = ButtonGroup; +module.exports = baseBind; -},{"rcss":6,"react":115,"underscore":116}],4:[function(require,module,exports){ -/** @jsx React.DOM */ +},{"lodash._basecreate":6,"lodash._setbinddata":19,"lodash._slice":21,"lodash.isobject":31}],6:[function(require,module,exports){ +(function (global){ +/** + * Lo-Dash 2.4.1 (Custom Build) + * Build: `lodash modularize modern exports="npm" -o ./npm/` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.5.2 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license + */ +var isNative = require('lodash._isnative'), + isObject = require('lodash.isobject'), + noop = require('lodash.noop'); -var React = require('react'); +/* Native method shortcuts for methods with the same name as other `lodash` methods */ +var nativeCreate = isNative(nativeCreate = Object.create) && nativeCreate; -/* This component makes its children a drag target. Example: - * - * Drag to me - * - * ... - * - * handleDrop: function(e) { - * this.addImages(e.nativeEvent.dataTransfer.files); - * } +/** + * The base implementation of `_.create` without support for assigning + * properties to the created object. * - * Now "Drag to me" will be a drag target - when something is dragged over it, - * the element will become partially transparent as a visual indicator that - * it's a target. + * @private + * @param {Object} prototype The object to inherit from. + * @returns {Object} Returns the new object. */ -// TODO(joel) - indicate before the hover is over the target that it's possible -// to drag into the target. This would (I think) require a high level handler - -// like on Perseus itself, waiting for onDragEnter, then passing down the -// event. Sounds like a pain. Possible workaround - create a div covering the -// entire page... -// -// Other extensions: -// * custom styles for global drag and dragOver -// * only respond to certain types of drags (only images for instance)! -var DragTarget = React.createClass({displayName: 'DragTarget', - propTypes: { - onDrop: React.PropTypes.func.isRequired, - component: React.PropTypes.component, - shouldDragHighlight: React.PropTypes.func - }, - render: function() { - var opacity = this.state.dragHover ? { "opacity": 0.3 } : {}; - var component = this.props.component; - - return this.transferPropsTo( - component( {style:opacity, - onDrop:this.handleDrop, - onDragEnd:this.handleDragEnd, - onDragOver:this.handleDragOver, - onDragEnter:this.handleDragEnter, - onDragLeave:this.handleDragLeave}, - this.props.children - ) - ); - }, - getInitialState: function() { - return { dragHover: false }; - }, - getDefaultProps: function() { - return { - component: React.DOM.div, - shouldDragHighlight: function() {return true;} - }; - }, - handleDrop: function(e) { - e.stopPropagation(); - e.preventDefault(); - this.setState({ dragHover: false }); - this.props.onDrop(e); - }, - handleDragEnd: function() { - this.setState({ dragHover: false }); - }, - handleDragOver: function(e) { - e.preventDefault(); - }, - handleDragLeave: function() { - this.setState({ dragHover: false }); - }, - handleDragEnter: function(e) { - this.setState({ dragHover: this.props.shouldDragHighlight(e) }); - } -}); - -module.exports = DragTarget; - -},{"react":115}],5:[function(require,module,exports){ -/** @jsx React.DOM */ - -var React = require('react'); -var RCSS = require('rcss'); -var _ = require('underscore'); +function baseCreate(prototype, properties) { + return isObject(prototype) ? nativeCreate(prototype) : {}; +} +// fallback for browsers without `Object.create` +if (!nativeCreate) { + baseCreate = (function() { + function Object() {} + return function(prototype) { + if (isObject(prototype)) { + Object.prototype = prototype; + var result = new Object; + Object.prototype = null; + } + return result || global.Object(); + }; + }()); +} -var colors = { - grayLight: '#aaa', - basicBorderColor: '#ccc', - white: '#fff' -}; +module.exports = baseCreate; -var infoTip = { - display: 'inline-block', - marginLeft: '5px', - position: 'relative' -}; +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"lodash._isnative":14,"lodash.isobject":31,"lodash.noop":33}],7:[function(require,module,exports){ +/** + * Lo-Dash 2.4.1 (Custom Build) + * Build: `lodash modularize modern exports="npm" -o ./npm/` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.5.2 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license + */ +var bind = require('lodash.bind'), + identity = require('lodash.identity'), + setBindData = require('lodash._setbinddata'), + support = require('lodash.support'); -var infoTipI = { - cursor: 'pointer' -}; +/** Used to detected named functions */ +var reFuncName = /^\s*function[ \n\r\t]+\w/; -var infoTipContainer = { - position: 'absolute', - 'top': '-12px', - left: '22px', - zIndex: '1000' -}; +/** Used to detect functions containing a `this` reference */ +var reThis = /\bthis\b/; -var triangleBeforeAfter = { - borderBottom: '9px solid transparent', - borderTop: '9px solid transparent', - content: ' ', - height: '0', - position: 'absolute', - 'top': '0', - width: '0' -}; +/** Native method shortcuts */ +var fnToString = Function.prototype.toString; -var infoTipTriangle = { - height: '10px', - left: '0', - position: 'absolute', - 'top': '8px', - width: '0', - zIndex: '1', +/** + * The base implementation of `_.createCallback` without support for creating + * "_.pluck" or "_.where" style callbacks. + * + * @private + * @param {*} [func=identity] The value to convert to a callback. + * @param {*} [thisArg] The `this` binding of the created callback. + * @param {number} [argCount] The number of arguments the callback accepts. + * @returns {Function} Returns a callback function. + */ +function baseCreateCallback(func, thisArg, argCount) { + if (typeof func != 'function') { + return identity; + } + // exit early for no `thisArg` or already bound by `Function#bind` + if (typeof thisArg == 'undefined' || !('prototype' in func)) { + return func; + } + var bindData = func.__bindData__; + if (typeof bindData == 'undefined') { + if (support.funcNames) { + bindData = !func.name; + } + bindData = bindData || !support.funcDecomp; + if (!bindData) { + var source = fnToString.call(func); + if (!support.funcNames) { + bindData = !reFuncName.test(source); + } + if (!bindData) { + // checks if `func` references the `this` keyword and stores the result + bindData = reThis.test(source); + setBindData(func, bindData); + } + } + } + // exit early if there are no `this` references or `func` is bound + if (bindData === false || (bindData !== true && bindData[1] & 1)) { + return func; + } + switch (argCount) { + case 1: return function(value) { + return func.call(thisArg, value); + }; + case 2: return function(a, b) { + return func.call(thisArg, a, b); + }; + case 3: return function(value, index, collection) { + return func.call(thisArg, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(thisArg, accumulator, value, index, collection); + }; + } + return bind(func, thisArg); +} - ':before': _.extend({}, triangleBeforeAfter, { - borderRight: '9px solid #bbb', - right: '0', - }), +module.exports = baseCreateCallback; - ':after': _.extend({}, triangleBeforeAfter, { - borderRight: ("9px solid " + colors.white), - right: '-1px' - }) -}; +},{"lodash._setbinddata":19,"lodash.bind":23,"lodash.identity":29,"lodash.support":35}],8:[function(require,module,exports){ +/** + * Lo-Dash 2.4.1 (Custom Build) + * Build: `lodash modularize modern exports="npm" -o ./npm/` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.5.2 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license + */ +var baseCreate = require('lodash._basecreate'), + isObject = require('lodash.isobject'), + setBindData = require('lodash._setbinddata'), + slice = require('lodash._slice'); -var basicBorder = { - border: ("1px solid " + colors.basicBorderColor) -}; +/** + * Used for `Array` method references. + * + * Normally `Array.prototype` would suffice, however, using an array literal + * avoids issues in Narwhal. + */ +var arrayRef = []; -var boxShadow = function(str) { return { boxShadow: str }; }; +/** Native method shortcuts */ +var push = arrayRef.push; -var verticalShadow = RCSS.merge( - basicBorder, - boxShadow(("0 1px 3px " + colors.basicBorderColor)), - { borderBottom: ("1px solid " + colors.grayLight) } -); +/** + * The base implementation of `createWrapper` that creates the wrapper and + * sets its meta data. + * + * @private + * @param {Array} bindData The bind data array. + * @returns {Function} Returns the new function. + */ +function baseCreateWrapper(bindData) { + var func = bindData[0], + bitmask = bindData[1], + partialArgs = bindData[2], + partialRightArgs = bindData[3], + thisArg = bindData[4], + arity = bindData[5]; -var infoTipContentContainer = RCSS.merge(verticalShadow, { - background: colors.white, - padding: '5px 10px', - width: '240px' -}); + var isBind = bitmask & 1, + isBindKey = bitmask & 2, + isCurry = bitmask & 4, + isCurryBound = bitmask & 8, + key = func; -RCSS.createClass(infoTip); -RCSS.createClass(infoTipI); -RCSS.createClass(infoTipTriangle); -RCSS.createClass(verticalShadow); -RCSS.createClass(infoTipContainer); -RCSS.createClass(infoTipContentContainer); - -var questionMark = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3NpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NDkxMSwgMjAxMy8xMC8yOS0xMTo0NzoxNiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo2N2M3NTAxYS04YmVlLTQ0M2MtYmRiNS04OGM2N2IxN2NhYzEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUJCRTk4Qjc4NjAwMTFFMzg3QUJDNEI4Mzk2QTRGQkQiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUJCRTk4QjY4NjAwMTFFMzg3QUJDNEI4Mzk2QTRGQkQiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NGE5ZDI0OTMtODk1NC00OGFkLTlhMTgtZDAwM2MwYWNjNDJlIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjY3Yzc1MDFhLThiZWUtNDQzYy1iZGI1LTg4YzY3YjE3Y2FjMSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pqm89uYAAADMSURBVHjaXJA9DoJAEIUH1M4TUHIFsCMGen9OwCGw1YRGW2ntKel0exsojHIBC0ouQAyUviFDstmXfNmZeS+zm7XSNCXRFiRgJf0bXIHixpbhGdxBBJYC1w/xaA424MhNEATkui71fU9KqfEU78UbD9PdbJRlOdae55GmhIP+1NV1TcMwkOM41DSNHvRtMhTHMRVFQW3b6mOLgx99kue5GRp/gIOZuZGvNpTNwjD8oliANU+qqqKu6/TQBdymN57AHjzBT+B6Jx79BRgAvc49kQA4yxgAAAAASUVORK5CYII='; // @NoLint + function bound() { + var thisBinding = isBind ? thisArg : this; + if (partialArgs) { + var args = slice(partialArgs); + push.apply(args, arguments); + } + if (partialRightArgs || isCurry) { + args || (args = slice(arguments)); + if (partialRightArgs) { + push.apply(args, partialRightArgs); + } + if (isCurry && args.length < arity) { + bitmask |= 16 & ~32; + return baseCreateWrapper([func, (isCurryBound ? bitmask : bitmask & ~3), args, null, thisArg, arity]); + } + } + args || (args = arguments); + if (isBindKey) { + func = thisBinding[key]; + } + if (this instanceof bound) { + thisBinding = baseCreate(func.prototype); + var result = func.apply(thisBinding, args); + return isObject(result) ? result : thisBinding; + } + return func.apply(thisBinding, args); + } + setBindData(bound, bindData); + return bound; +} -var InfoTip = React.createClass({displayName: 'InfoTip', - getInitialState: function() { - return { - hover: false - }; - }, +module.exports = baseCreateWrapper; - render: function() { - return React.DOM.div( {className:infoTip.className}, - React.DOM.img( {width:10, - height:10, - src:questionMark, - onMouseEnter:this.handleMouseEnter, - onMouseLeave:this.handleMouseLeave} ), - React.DOM.div( {className:infoTipContainer.className, - style:{display: this.state.hover ? 'block' : 'none'}}, - React.DOM.div( {className:infoTipTriangle.className} ), - /* keep the classes here - used for selectors on KA */ - React.DOM.div( {className:infoTipContentContainer.className}, - this.props.children - ) - ) - ); - }, +},{"lodash._basecreate":6,"lodash._setbinddata":19,"lodash._slice":21,"lodash.isobject":31}],9:[function(require,module,exports){ +/** + * Lo-Dash 2.4.1 (Custom Build) + * Build: `lodash modularize modern exports="npm" -o ./npm/` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.5.2 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license + */ +var forIn = require('lodash.forin'), + getArray = require('lodash._getarray'), + isFunction = require('lodash.isfunction'), + objectTypes = require('lodash._objecttypes'), + releaseArray = require('lodash._releasearray'); - handleMouseEnter: function() { - this.setState({hover: true}); - }, +/** `Object#toString` result shortcuts */ +var argsClass = '[object Arguments]', + arrayClass = '[object Array]', + boolClass = '[object Boolean]', + dateClass = '[object Date]', + numberClass = '[object Number]', + objectClass = '[object Object]', + regexpClass = '[object RegExp]', + stringClass = '[object String]'; - handleMouseLeave: function() { - this.setState({hover: false}); - } -}); +/** Used for native method references */ +var objectProto = Object.prototype; -module.exports = InfoTip; +/** Used to resolve the internal [[Class]] of values */ +var toString = objectProto.toString; -},{"rcss":6,"react":115,"underscore":116}],6:[function(require,module,exports){ -var assign = require('lodash.assign'); +/** Native method shortcuts */ +var hasOwnProperty = objectProto.hasOwnProperty; -var styleRuleValidator = require('./styleRuleValidator'); -var styleRuleConverter = require('./styleRuleConverter'); -var mediaQueryValidator = require('valid-media-queries'); +/** + * The base implementation of `_.isEqual`, without support for `thisArg` binding, + * that allows partial "_.where" style comparisons. + * + * @private + * @param {*} a The value to compare. + * @param {*} b The other value to compare. + * @param {Function} [callback] The function to customize comparing values. + * @param {Function} [isWhere=false] A flag to indicate performing partial comparisons. + * @param {Array} [stackA=[]] Tracks traversed `a` objects. + * @param {Array} [stackB=[]] Tracks traversed `b` objects. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + */ +function baseIsEqual(a, b, callback, isWhere, stackA, stackB) { + // used to indicate that when comparing objects, `a` has at least the properties of `b` + if (callback) { + var result = callback(a, b); + if (typeof result != 'undefined') { + return !!result; + } + } + // exit early for identical values + if (a === b) { + // treat `+0` vs. `-0` as not equal + return a !== 0 || (1 / a == 1 / b); + } + var type = typeof a, + otherType = typeof b; -var existingClasses = {}; -var styleTag = createStyleTag(); + // exit early for unlike primitive values + if (a === a && + !(a && objectTypes[type]) && + !(b && objectTypes[otherType])) { + return false; + } + // exit early for `null` and `undefined` avoiding ES3's Function#call behavior + // http://es5.github.io/#x15.3.4.4 + if (a == null || b == null) { + return a === b; + } + // compare [[Class]] names + var className = toString.call(a), + otherClass = toString.call(b); -var classNameId = 0; -var randomSuffix = Math.random().toString(36).slice(-5); + if (className == argsClass) { + className = objectClass; + } + if (otherClass == argsClass) { + otherClass = objectClass; + } + if (className != otherClass) { + return false; + } + switch (className) { + case boolClass: + case dateClass: + // coerce dates and booleans to numbers, dates to milliseconds and booleans + // to `1` or `0` treating invalid dates coerced to `NaN` as not equal + return +a == +b; -function generateValidCSSClassName() { - // CSS classNames can't start with a number. - return 'c' + (classNameId++) + '-' + randomSuffix; -} + case numberClass: + // treat `NaN` vs. `NaN` as equal + return (a != +a) + ? b != +b + // but treat `+0` vs. `-0` as not equal + : (a == 0 ? (1 / a == 1 / b) : a == +b); -function objToCSS(style) { - var serialized = ''; - for (var propName in style) { - // we put that ourselves - if (propName == 'className') continue; + case regexpClass: + case stringClass: + // coerce regexes to strings (http://es5.github.io/#x15.10.6.4) + // treat string primitives and their corresponding object instances as equal + return a == String(b); + } + var isArr = className == arrayClass; + if (!isArr) { + // unwrap any `lodash` wrapped values + var aWrapped = hasOwnProperty.call(a, '__wrapped__'), + bWrapped = hasOwnProperty.call(b, '__wrapped__'); - var cssPropName = styleRuleConverter.hyphenateProp(propName); - if (!styleRuleValidator.isValidProp(cssPropName)) { - console.warn( - '%s (transformed into %s) is not a valid CSS property name.', propName, cssPropName - ); - continue; + if (aWrapped || bWrapped) { + return baseIsEqual(aWrapped ? a.__wrapped__ : a, bWrapped ? b.__wrapped__ : b, callback, isWhere, stackA, stackB); } + // exit for functions and DOM nodes + if (className != objectClass) { + return false; + } + // in older versions of Opera, `arguments` objects have `Array` constructors + var ctorA = a.constructor, + ctorB = b.constructor; - var styleValue = style[propName]; - if (!styleRuleValidator.isValidValue(styleValue)) continue; - - if (styleValue !== null) { - serialized += cssPropName + ':'; - serialized += styleRuleConverter.escapeValueForProp(styleValue, - cssPropName) + ';'; + // non `Object` object instances with different constructors are not equal + if (ctorA != ctorB && + !(isFunction(ctorA) && ctorA instanceof ctorA && isFunction(ctorB) && ctorB instanceof ctorB) && + ('constructor' in a && 'constructor' in b) + ) { + return false; } } - return serialized || null; -} - -function createStyleTag() { - var tag = document.createElement('style'); - document.getElementsByTagName('head')[0].appendChild(tag); - return tag; -} - -function styleToCSS(style) { - var styleStr = '.' + style.className + '{'; - styleStr += objToCSS(style.value); - styleStr += '}'; + // assume cyclic structures are equal + // the algorithm for detecting cyclic structures is adapted from ES 5.1 + // section 15.12.3, abstract operation `JO` (http://es5.github.io/#x15.12.3) + var initedStack = !stackA; + stackA || (stackA = getArray()); + stackB || (stackB = getArray()); - if (style.media) { - if (!mediaQueryValidator(style.media)) { - console.log('%s is not a valid media query.', style.media); + var length = stackA.length; + while (length--) { + if (stackA[length] == a) { + return stackB[length] == b; } - styleStr = style.media + '{' + styleStr + '}'; } + var size = 0; + result = true; - return styleStr; -} - -// TODO: support media queries -function parseStyles(className, styleObj) { - var mainStyle = { - className: className, - value: {} - }; - var styles = [mainStyle]; + // add `a` and `b` to the stack of traversed objects + stackA.push(a); + stackB.push(b); - Object.keys(styleObj).forEach(function(k){ - // pseudo-selector, insert a new rule - if (k[0] === ':') { - styles.push({ - className: className+k, - value: styleObj[k] - }); - return; - } else if (k.substring(0, 6) === '@media') { - styles.push({ - className: className, - value: styleObj[k], - media: k - }); - return; - } - - // normal rule, insert into main one - mainStyle.value[k] = styleObj[k]; - }); - - return styles; -} - -function insertStyle(className, styleObj) { - var styles = parseStyles(className, styleObj); - var styleStr = styles.map(styleToCSS).join(''); - styleTag.innerHTML += styleStr; -} + // recursively compare objects and arrays (susceptible to call stack limits) + if (isArr) { + // compare lengths to determine if a deep comparison is necessary + length = a.length; + size = b.length; + result = size == length; -var RCSS = { - merge: function(a, b, c, d, e) { - return assign({}, a, b, c, d, e); - }, + if (result || isWhere) { + // deep compare the contents, ignoring non-numeric properties + while (size--) { + var index = length, + value = b[size]; - createClass: function(styleObj) { - var styleId = JSON.stringify(styleObj); - var className; + if (isWhere) { + while (index--) { + if ((result = baseIsEqual(a[index], value, callback, isWhere, stackA, stackB))) { + break; + } + } + } else if (!(result = baseIsEqual(a[size], value, callback, isWhere, stackA, stackB))) { + break; + } + } + } + } + else { + // deep compare objects using `forIn`, instead of `forOwn`, to avoid `Object.keys` + // which, in this case, is more costly + forIn(b, function(value, key, b) { + if (hasOwnProperty.call(b, key)) { + // count the number of properties. + size++; + // deep compare each property value. + return (result = hasOwnProperty.call(a, key) && baseIsEqual(a[key], value, callback, isWhere, stackA, stackB)); + } + }); - if (existingClasses[styleId]) { - // already exists, use the existing className - className = existingClasses[styleId]; - } else { - // generate a new class and insert it - className = generateValidCSSClassName(); - existingClasses[styleId] = className; - insertStyle(className, styleObj); + if (result && !isWhere) { + // ensure both objects have the same number of properties + forIn(a, function(value, key, a) { + if (hasOwnProperty.call(a, key)) { + // `size` will be `-1` if `a` has more properties than `b` + return (result = --size > -1); + } + }); } + } + stackA.pop(); + stackB.pop(); - styleObj.className = className; - return styleObj; + if (initedStack) { + releaseArray(stackA); + releaseArray(stackB); } -}; + return result; +} -module.exports = RCSS; +module.exports = baseIsEqual; -},{"./styleRuleConverter":111,"./styleRuleValidator":112,"lodash.assign":7,"valid-media-queries":46}],7:[function(require,module,exports){ +},{"lodash._getarray":12,"lodash._objecttypes":16,"lodash._releasearray":17,"lodash.forin":27,"lodash.isfunction":30}],10:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -916,70 +1030,106 @@ module.exports = RCSS; * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ -var baseCreateCallback = require('lodash._basecreatecallback'), - keys = require('lodash.keys'), - objectTypes = require('lodash._objecttypes'); +var baseBind = require('lodash._basebind'), + baseCreateWrapper = require('lodash._basecreatewrapper'), + isFunction = require('lodash.isfunction'), + slice = require('lodash._slice'); /** - * Assigns own enumerable properties of source object(s) to the destination - * object. Subsequent sources will overwrite property assignments of previous - * sources. If a callback is provided it will be executed to produce the - * assigned values. The callback is bound to `thisArg` and invoked with two - * arguments; (objectValue, sourceValue). - * - * @static - * @memberOf _ - * @type Function - * @alias extend - * @category Objects - * @param {Object} object The destination object. - * @param {...Object} [source] The source objects. - * @param {Function} [callback] The function to customize assigning values. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns the destination object. - * @example - * - * _.assign({ 'name': 'fred' }, { 'employer': 'slate' }); - * // => { 'name': 'fred', 'employer': 'slate' } + * Used for `Array` method references. * - * var defaults = _.partialRight(_.assign, function(a, b) { - * return typeof a == 'undefined' ? b : a; - * }); + * Normally `Array.prototype` would suffice, however, using an array literal + * avoids issues in Narwhal. + */ +var arrayRef = []; + +/** Native method shortcuts */ +var push = arrayRef.push, + unshift = arrayRef.unshift; + +/** + * Creates a function that, when called, either curries or invokes `func` + * with an optional `this` binding and partially applied arguments. * - * var object = { 'name': 'barney' }; - * defaults(object, { 'name': 'fred', 'employer': 'slate' }); - * // => { 'name': 'barney', 'employer': 'slate' } + * @private + * @param {Function|string} func The function or method name to reference. + * @param {number} bitmask The bitmask of method flags to compose. + * The bitmask may be composed of the following flags: + * 1 - `_.bind` + * 2 - `_.bindKey` + * 4 - `_.curry` + * 8 - `_.curry` (bound) + * 16 - `_.partial` + * 32 - `_.partialRight` + * @param {Array} [partialArgs] An array of arguments to prepend to those + * provided to the new function. + * @param {Array} [partialRightArgs] An array of arguments to append to those + * provided to the new function. + * @param {*} [thisArg] The `this` binding of `func`. + * @param {number} [arity] The arity of `func`. + * @returns {Function} Returns the new function. */ -var assign = function(object, source, guard) { - var index, iterable = object, result = iterable; - if (!iterable) return result; - var args = arguments, - argsIndex = 0, - argsLength = typeof guard == 'number' ? 2 : args.length; - if (argsLength > 3 && typeof args[argsLength - 2] == 'function') { - var callback = baseCreateCallback(args[--argsLength - 1], args[argsLength--], 2); - } else if (argsLength > 2 && typeof args[argsLength - 1] == 'function') { - callback = args[--argsLength]; - } - while (++argsIndex < argsLength) { - iterable = args[argsIndex]; - if (iterable && objectTypes[typeof iterable]) { - var ownIndex = -1, - ownProps = objectTypes[typeof iterable] && keys(iterable), - length = ownProps ? ownProps.length : 0; +function createWrapper(func, bitmask, partialArgs, partialRightArgs, thisArg, arity) { + var isBind = bitmask & 1, + isBindKey = bitmask & 2, + isCurry = bitmask & 4, + isCurryBound = bitmask & 8, + isPartial = bitmask & 16, + isPartialRight = bitmask & 32; - while (++ownIndex < length) { - index = ownProps[ownIndex]; - result[index] = callback ? callback(result[index], iterable[index]) : iterable[index]; + if (!isBindKey && !isFunction(func)) { + throw new TypeError; + } + if (isPartial && !partialArgs.length) { + bitmask &= ~16; + isPartial = partialArgs = false; + } + if (isPartialRight && !partialRightArgs.length) { + bitmask &= ~32; + isPartialRight = partialRightArgs = false; + } + var bindData = func && func.__bindData__; + if (bindData && bindData !== true) { + // clone `bindData` + bindData = slice(bindData); + if (bindData[2]) { + bindData[2] = slice(bindData[2]); + } + if (bindData[3]) { + bindData[3] = slice(bindData[3]); + } + // set `thisBinding` is not previously bound + if (isBind && !(bindData[1] & 1)) { + bindData[4] = thisArg; + } + // set if previously bound but not currently (subsequent curried functions) + if (!isBind && bindData[1] & 1) { + bitmask |= 8; + } + // set curried arity if not yet set + if (isCurry && !(bindData[1] & 4)) { + bindData[5] = arity; + } + // append partial left arguments + if (isPartial) { + push.apply(bindData[2] || (bindData[2] = []), partialArgs); } + // append partial right arguments + if (isPartialRight) { + unshift.apply(bindData[3] || (bindData[3] = []), partialRightArgs); } + // merge flags + bindData[1] |= bitmask; + return createWrapper.apply(null, bindData); } - return result -}; + // fast path for `_.bind` + var creater = (bitmask == 1 || bitmask === 17) ? baseBind : baseCreateWrapper; + return creater([func, bitmask, partialArgs, partialRightArgs, thisArg, arity]); +} -module.exports = assign; +module.exports = createWrapper; -},{"lodash._basecreatecallback":8,"lodash._objecttypes":29,"lodash.keys":30}],8:[function(require,module,exports){ +},{"lodash._basebind":5,"lodash._basecreatewrapper":8,"lodash._slice":21,"lodash.isfunction":30}],11:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -988,80 +1138,22 @@ module.exports = assign; * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ -var bind = require('lodash.bind'), - identity = require('lodash.identity'), - setBindData = require('lodash._setbinddata'), - support = require('lodash.support'); - -/** Used to detected named functions */ -var reFuncName = /^\s*function[ \n\r\t]+\w/; - -/** Used to detect functions containing a `this` reference */ -var reThis = /\bthis\b/; - -/** Native method shortcuts */ -var fnToString = Function.prototype.toString; +var htmlEscapes = require('lodash._htmlescapes'); /** - * The base implementation of `_.createCallback` without support for creating - * "_.pluck" or "_.where" style callbacks. + * Used by `escape` to convert characters to HTML entities. * * @private - * @param {*} [func=identity] The value to convert to a callback. - * @param {*} [thisArg] The `this` binding of the created callback. - * @param {number} [argCount] The number of arguments the callback accepts. - * @returns {Function} Returns a callback function. + * @param {string} match The matched character to escape. + * @returns {string} Returns the escaped character. */ -function baseCreateCallback(func, thisArg, argCount) { - if (typeof func != 'function') { - return identity; - } - // exit early for no `thisArg` or already bound by `Function#bind` - if (typeof thisArg == 'undefined' || !('prototype' in func)) { - return func; - } - var bindData = func.__bindData__; - if (typeof bindData == 'undefined') { - if (support.funcNames) { - bindData = !func.name; - } - bindData = bindData || !support.funcDecomp; - if (!bindData) { - var source = fnToString.call(func); - if (!support.funcNames) { - bindData = !reFuncName.test(source); - } - if (!bindData) { - // checks if `func` references the `this` keyword and stores the result - bindData = reThis.test(source); - setBindData(func, bindData); - } - } - } - // exit early if there are no `this` references or `func` is bound - if (bindData === false || (bindData !== true && bindData[1] & 1)) { - return func; - } - switch (argCount) { - case 1: return function(value) { - return func.call(thisArg, value); - }; - case 2: return function(a, b) { - return func.call(thisArg, a, b); - }; - case 3: return function(value, index, collection) { - return func.call(thisArg, value, index, collection); - }; - case 4: return function(accumulator, value, index, collection) { - return func.call(thisArg, accumulator, value, index, collection); - }; - } - return bind(func, thisArg); +function escapeHtmlChar(match) { + return htmlEscapes[match]; } -module.exports = baseCreateCallback; +module.exports = escapeHtmlChar; -},{"lodash._setbinddata":9,"lodash.bind":12,"lodash.identity":26,"lodash.support":27}],9:[function(require,module,exports){ +},{"lodash._htmlescapes":13}],12:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1070,43 +1162,49 @@ module.exports = baseCreateCallback; * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ -var isNative = require('lodash._isnative'), - noop = require('lodash.noop'); +var arrayPool = require('lodash._arraypool'); -/** Used as the property descriptor for `__bindData__` */ -var descriptor = { - 'configurable': false, - 'enumerable': false, - 'value': null, - 'writable': false -}; +/** + * Gets an array from the array pool or creates a new one if the pool is empty. + * + * @private + * @returns {Array} The array from the pool. + */ +function getArray() { + return arrayPool.pop() || []; +} -/** Used to set meta data on functions */ -var defineProperty = (function() { - // IE 8 only accepts DOM elements - try { - var o = {}, - func = isNative(func = Object.defineProperty) && func, - result = func(o, o, o) && func; - } catch(e) { } - return result; -}()); +module.exports = getArray; +},{"lodash._arraypool":4}],13:[function(require,module,exports){ /** - * Sets `this` binding data on a given function. + * Lo-Dash 2.4.1 (Custom Build) + * Build: `lodash modularize modern exports="npm" -o ./npm/` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.5.2 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license + */ + +/** + * Used to convert characters to HTML entities: * - * @private - * @param {Function} func The function to set data on. - * @param {Array} value The data array to set. + * Though the `>` character is escaped for symmetry, characters like `>` and `/` + * don't require escaping in HTML and have no special meaning unless they're part + * of a tag or an unquoted attribute value. + * http://mathiasbynens.be/notes/ambiguous-ampersands (under "semi-related fun fact") */ -var setBindData = !defineProperty ? noop : function(func, value) { - descriptor.value = value; - defineProperty(func, '__bindData__', descriptor); +var htmlEscapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' }; -module.exports = setBindData; +module.exports = htmlEscapes; -},{"lodash._isnative":10,"lodash.noop":11}],10:[function(require,module,exports){ +},{}],14:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1142,7 +1240,7 @@ function isNative(value) { module.exports = isNative; -},{}],11:[function(require,module,exports){ +},{}],15:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1152,25 +1250,34 @@ module.exports = isNative; * Available under MIT license */ +/** Used as the max size of the `arrayPool` and `objectPool` */ +var maxPoolSize = 40; + +module.exports = maxPoolSize; + +},{}],16:[function(require,module,exports){ /** - * A no-operation function. - * - * @static - * @memberOf _ - * @category Utilities - * @example - * - * var object = { 'name': 'fred' }; - * _.noop(object) === undefined; - * // => true + * Lo-Dash 2.4.1 (Custom Build) + * Build: `lodash modularize modern exports="npm" -o ./npm/` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.5.2 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license */ -function noop() { - // no operation performed -} -module.exports = noop; +/** Used to determine if values are of the language type Object */ +var objectTypes = { + 'boolean': false, + 'function': true, + 'object': true, + 'number': false, + 'string': false, + 'undefined': false +}; -},{}],12:[function(require,module,exports){ +module.exports = objectTypes; + +},{}],17:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1179,40 +1286,25 @@ module.exports = noop; * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ -var createWrapper = require('lodash._createwrapper'), - slice = require('lodash._slice'); +var arrayPool = require('lodash._arraypool'), + maxPoolSize = require('lodash._maxpoolsize'); /** - * Creates a function that, when called, invokes `func` with the `this` - * binding of `thisArg` and prepends any additional `bind` arguments to those - * provided to the bound function. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to bind. - * @param {*} [thisArg] The `this` binding of `func`. - * @param {...*} [arg] Arguments to be partially applied. - * @returns {Function} Returns the new bound function. - * @example - * - * var func = function(greeting) { - * return greeting + ' ' + this.name; - * }; + * Releases the given array back to the array pool. * - * func = _.bind(func, { 'name': 'fred' }, 'hi'); - * func(); - * // => 'hi fred' + * @private + * @param {Array} [array] The array to release. */ -function bind(func, thisArg) { - return arguments.length > 2 - ? createWrapper(func, 17, slice(arguments, 2), null, thisArg) - : createWrapper(func, 1, null, null, thisArg); +function releaseArray(array) { + array.length = 0; + if (arrayPool.length < maxPoolSize) { + arrayPool.push(array); + } } -module.exports = bind; +module.exports = releaseArray; -},{"lodash._createwrapper":13,"lodash._slice":25}],13:[function(require,module,exports){ +},{"lodash._arraypool":4,"lodash._maxpoolsize":15}],18:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1221,106 +1313,60 @@ module.exports = bind; * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ -var baseBind = require('lodash._basebind'), - baseCreateWrapper = require('lodash._basecreatewrapper'), - isFunction = require('lodash.isfunction'), - slice = require('lodash._slice'); +var htmlEscapes = require('lodash._htmlescapes'), + keys = require('lodash.keys'); + +/** Used to match HTML entities and HTML characters */ +var reUnescapedHtml = RegExp('[' + keys(htmlEscapes).join('') + ']', 'g'); + +module.exports = reUnescapedHtml; +},{"lodash._htmlescapes":13,"lodash.keys":32}],19:[function(require,module,exports){ /** - * Used for `Array` method references. - * - * Normally `Array.prototype` would suffice, however, using an array literal - * avoids issues in Narwhal. + * Lo-Dash 2.4.1 (Custom Build) + * Build: `lodash modularize modern exports="npm" -o ./npm/` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.5.2 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license */ -var arrayRef = []; +var isNative = require('lodash._isnative'), + noop = require('lodash.noop'); -/** Native method shortcuts */ -var push = arrayRef.push, - unshift = arrayRef.unshift; +/** Used as the property descriptor for `__bindData__` */ +var descriptor = { + 'configurable': false, + 'enumerable': false, + 'value': null, + 'writable': false +}; + +/** Used to set meta data on functions */ +var defineProperty = (function() { + // IE 8 only accepts DOM elements + try { + var o = {}, + func = isNative(func = Object.defineProperty) && func, + result = func(o, o, o) && func; + } catch(e) { } + return result; +}()); /** - * Creates a function that, when called, either curries or invokes `func` - * with an optional `this` binding and partially applied arguments. + * Sets `this` binding data on a given function. * * @private - * @param {Function|string} func The function or method name to reference. - * @param {number} bitmask The bitmask of method flags to compose. - * The bitmask may be composed of the following flags: - * 1 - `_.bind` - * 2 - `_.bindKey` - * 4 - `_.curry` - * 8 - `_.curry` (bound) - * 16 - `_.partial` - * 32 - `_.partialRight` - * @param {Array} [partialArgs] An array of arguments to prepend to those - * provided to the new function. - * @param {Array} [partialRightArgs] An array of arguments to append to those - * provided to the new function. - * @param {*} [thisArg] The `this` binding of `func`. - * @param {number} [arity] The arity of `func`. - * @returns {Function} Returns the new function. + * @param {Function} func The function to set data on. + * @param {Array} value The data array to set. */ -function createWrapper(func, bitmask, partialArgs, partialRightArgs, thisArg, arity) { - var isBind = bitmask & 1, - isBindKey = bitmask & 2, - isCurry = bitmask & 4, - isCurryBound = bitmask & 8, - isPartial = bitmask & 16, - isPartialRight = bitmask & 32; - - if (!isBindKey && !isFunction(func)) { - throw new TypeError; - } - if (isPartial && !partialArgs.length) { - bitmask &= ~16; - isPartial = partialArgs = false; - } - if (isPartialRight && !partialRightArgs.length) { - bitmask &= ~32; - isPartialRight = partialRightArgs = false; - } - var bindData = func && func.__bindData__; - if (bindData && bindData !== true) { - // clone `bindData` - bindData = slice(bindData); - if (bindData[2]) { - bindData[2] = slice(bindData[2]); - } - if (bindData[3]) { - bindData[3] = slice(bindData[3]); - } - // set `thisBinding` is not previously bound - if (isBind && !(bindData[1] & 1)) { - bindData[4] = thisArg; - } - // set if previously bound but not currently (subsequent curried functions) - if (!isBind && bindData[1] & 1) { - bitmask |= 8; - } - // set curried arity if not yet set - if (isCurry && !(bindData[1] & 4)) { - bindData[5] = arity; - } - // append partial left arguments - if (isPartial) { - push.apply(bindData[2] || (bindData[2] = []), partialArgs); - } - // append partial right arguments - if (isPartialRight) { - unshift.apply(bindData[3] || (bindData[3] = []), partialRightArgs); - } - // merge flags - bindData[1] |= bitmask; - return createWrapper.apply(null, bindData); - } - // fast path for `_.bind` - var creater = (bitmask == 1 || bitmask === 17) ? baseBind : baseCreateWrapper; - return creater([func, bitmask, partialArgs, partialRightArgs, thisArg, arity]); -} +var setBindData = !defineProperty ? noop : function(func, value) { + descriptor.value = value; + defineProperty(func, '__bindData__', descriptor); +}; -module.exports = createWrapper; +module.exports = setBindData; -},{"lodash._basebind":14,"lodash._basecreatewrapper":19,"lodash._slice":25,"lodash.isfunction":24}],14:[function(require,module,exports){ +},{"lodash._isnative":14,"lodash.noop":33}],20:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1329,63 +1375,39 @@ module.exports = createWrapper; * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ -var baseCreate = require('lodash._basecreate'), - isObject = require('lodash.isobject'), - setBindData = require('lodash._setbinddata'), - slice = require('lodash._slice'); +var objectTypes = require('lodash._objecttypes'); -/** - * Used for `Array` method references. - * - * Normally `Array.prototype` would suffice, however, using an array literal - * avoids issues in Narwhal. - */ -var arrayRef = []; +/** Used for native method references */ +var objectProto = Object.prototype; /** Native method shortcuts */ -var push = arrayRef.push; +var hasOwnProperty = objectProto.hasOwnProperty; /** - * The base implementation of `_.bind` that creates the bound function and - * sets its meta data. + * A fallback implementation of `Object.keys` which produces an array of the + * given object's own enumerable property names. * * @private - * @param {Array} bindData The bind data array. - * @returns {Function} Returns the new bound function. + * @type Function + * @param {Object} object The object to inspect. + * @returns {Array} Returns an array of property names. */ -function baseBind(bindData) { - var func = bindData[0], - partialArgs = bindData[2], - thisArg = bindData[4]; - - function bound() { - // `Function#bind` spec - // http://es5.github.io/#x15.3.4.5 - if (partialArgs) { - // avoid `arguments` object deoptimizations by using `slice` instead - // of `Array.prototype.slice.call` and not assigning `arguments` to a - // variable as a ternary expression - var args = slice(partialArgs); - push.apply(args, arguments); - } - // mimic the constructor's `return` behavior - // http://es5.github.io/#x13.2.2 - if (this instanceof bound) { - // ensure `new bound` is an instance of `func` - var thisBinding = baseCreate(func.prototype), - result = func.apply(thisBinding, args || arguments); - return isObject(result) ? result : thisBinding; +var shimKeys = function(object) { + var index, iterable = object, result = []; + if (!iterable) return result; + if (!(objectTypes[typeof object])) return result; + for (index in iterable) { + if (hasOwnProperty.call(iterable, index)) { + result.push(index); + } } - return func.apply(thisArg, args || arguments); - } - setBindData(bound, bindData); - return bound; -} + return result +}; -module.exports = baseBind; +module.exports = shimKeys; -},{"lodash._basecreate":15,"lodash._setbinddata":9,"lodash._slice":25,"lodash.isobject":18}],15:[function(require,module,exports){ -var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};/** +},{"lodash._objecttypes":16}],21:[function(require,module,exports){ +/** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` * Copyright 2012-2013 The Dojo Foundation @@ -1393,46 +1415,38 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ -var isNative = require('lodash._isnative'), - isObject = require('lodash.isobject'), - noop = require('lodash.noop'); - -/* Native method shortcuts for methods with the same name as other `lodash` methods */ -var nativeCreate = isNative(nativeCreate = Object.create) && nativeCreate; /** - * The base implementation of `_.create` without support for assigning - * properties to the created object. + * Slices the `collection` from the `start` index up to, but not including, + * the `end` index. + * + * Note: This function is used instead of `Array#slice` to support node lists + * in IE < 9 and to ensure dense arrays are returned. * * @private - * @param {Object} prototype The object to inherit from. - * @returns {Object} Returns the new object. + * @param {Array|Object|string} collection The collection to slice. + * @param {number} start The start index. + * @param {number} end The end index. + * @returns {Array} Returns the new array. */ -function baseCreate(prototype, properties) { - return isObject(prototype) ? nativeCreate(prototype) : {}; -} -// fallback for browsers without `Object.create` -if (!nativeCreate) { - baseCreate = (function() { - function Object() {} - return function(prototype) { - if (isObject(prototype)) { - Object.prototype = prototype; - var result = new Object; - Object.prototype = null; - } - return result || global.Object(); - }; - }()); +function slice(array, start, end) { + start || (start = 0); + if (typeof end == 'undefined') { + end = array ? array.length : 0; + } + var index = -1, + length = end - start || 0, + result = Array(length < 0 ? 0 : length); + + while (++index < length) { + result[index] = array[start + index]; + } + return result; } -module.exports = baseCreate; +module.exports = slice; -},{"lodash._isnative":16,"lodash.isobject":18,"lodash.noop":17}],16:[function(require,module,exports){ -module.exports=require(10) -},{}],17:[function(require,module,exports){ -module.exports=require(11) -},{}],18:[function(require,module,exports){ +},{}],22:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1441,39 +1455,70 @@ module.exports=require(11) * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ -var objectTypes = require('lodash._objecttypes'); +var baseCreateCallback = require('lodash._basecreatecallback'), + keys = require('lodash.keys'), + objectTypes = require('lodash._objecttypes'); /** - * Checks if `value` is the language type of Object. - * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * Assigns own enumerable properties of source object(s) to the destination + * object. Subsequent sources will overwrite property assignments of previous + * sources. If a callback is provided it will be executed to produce the + * assigned values. The callback is bound to `thisArg` and invoked with two + * arguments; (objectValue, sourceValue). * * @static * @memberOf _ + * @type Function + * @alias extend * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is an object, else `false`. + * @param {Object} object The destination object. + * @param {...Object} [source] The source objects. + * @param {Function} [callback] The function to customize assigning values. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns the destination object. * @example * - * _.isObject({}); - * // => true + * _.assign({ 'name': 'fred' }, { 'employer': 'slate' }); + * // => { 'name': 'fred', 'employer': 'slate' } * - * _.isObject([1, 2, 3]); - * // => true + * var defaults = _.partialRight(_.assign, function(a, b) { + * return typeof a == 'undefined' ? b : a; + * }); * - * _.isObject(1); - * // => false + * var object = { 'name': 'barney' }; + * defaults(object, { 'name': 'fred', 'employer': 'slate' }); + * // => { 'name': 'barney', 'employer': 'slate' } */ -function isObject(value) { - // check if the value is the ECMAScript language type of Object - // http://es5.github.io/#x8 - // and avoid a V8 bug - // http://code.google.com/p/v8/issues/detail?id=2291 - return !!(value && objectTypes[typeof value]); -} +var assign = function(object, source, guard) { + var index, iterable = object, result = iterable; + if (!iterable) return result; + var args = arguments, + argsIndex = 0, + argsLength = typeof guard == 'number' ? 2 : args.length; + if (argsLength > 3 && typeof args[argsLength - 2] == 'function') { + var callback = baseCreateCallback(args[--argsLength - 1], args[argsLength--], 2); + } else if (argsLength > 2 && typeof args[argsLength - 1] == 'function') { + callback = args[--argsLength]; + } + while (++argsIndex < argsLength) { + iterable = args[argsIndex]; + if (iterable && objectTypes[typeof iterable]) { + var ownIndex = -1, + ownProps = objectTypes[typeof iterable] && keys(iterable), + length = ownProps ? ownProps.length : 0; -module.exports = isObject; + while (++ownIndex < length) { + index = ownProps[ownIndex]; + result[index] = callback ? callback(result[index], iterable[index]) : iterable[index]; + } + } + } + return result +}; + +module.exports = assign; -},{"lodash._objecttypes":29}],19:[function(require,module,exports){ +},{"lodash._basecreatecallback":7,"lodash._objecttypes":16,"lodash.keys":32}],23:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1482,86 +1527,123 @@ module.exports = isObject; * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ -var baseCreate = require('lodash._basecreate'), - isObject = require('lodash.isobject'), - setBindData = require('lodash._setbinddata'), +var createWrapper = require('lodash._createwrapper'), slice = require('lodash._slice'); /** - * Used for `Array` method references. + * Creates a function that, when called, invokes `func` with the `this` + * binding of `thisArg` and prepends any additional `bind` arguments to those + * provided to the bound function. * - * Normally `Array.prototype` would suffice, however, using an array literal - * avoids issues in Narwhal. + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to bind. + * @param {*} [thisArg] The `this` binding of `func`. + * @param {...*} [arg] Arguments to be partially applied. + * @returns {Function} Returns the new bound function. + * @example + * + * var func = function(greeting) { + * return greeting + ' ' + this.name; + * }; + * + * func = _.bind(func, { 'name': 'fred' }, 'hi'); + * func(); + * // => 'hi fred' */ -var arrayRef = []; +function bind(func, thisArg) { + return arguments.length > 2 + ? createWrapper(func, 17, slice(arguments, 2), null, thisArg) + : createWrapper(func, 1, null, null, thisArg); +} -/** Native method shortcuts */ -var push = arrayRef.push; +module.exports = bind; +},{"lodash._createwrapper":10,"lodash._slice":21}],24:[function(require,module,exports){ /** - * The base implementation of `createWrapper` that creates the wrapper and - * sets its meta data. + * Lo-Dash 2.4.4 (Custom Build) + * Build: `lodash modularize modern exports="npm" -o ./npm/` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.5.2 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license + */ +var baseCreateCallback = require('lodash._basecreatecallback'), + baseIsEqual = require('lodash._baseisequal'), + isObject = require('lodash.isobject'), + keys = require('lodash.keys'), + property = require('lodash.property'); + +/** + * Produces a callback bound to an optional `thisArg`. If `func` is a property + * name the created callback will return the property value for a given element. + * If `func` is an object the created callback will return `true` for elements + * that contain the equivalent object properties, otherwise it will return `false`. * - * @private - * @param {Array} bindData The bind data array. - * @returns {Function} Returns the new function. + * @static + * @memberOf _ + * @category Utilities + * @param {*} [func=identity] The value to convert to a callback. + * @param {*} [thisArg] The `this` binding of the created callback. + * @param {number} [argCount] The number of arguments the callback accepts. + * @returns {Function} Returns a callback function. + * @example + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * // wrap to create custom callback shorthands + * _.createCallback = _.wrap(_.createCallback, function(func, callback, thisArg) { + * var match = /^(.+?)__([gl]t)(.+)$/.exec(callback); + * return !match ? func(callback, thisArg) : function(object) { + * return match[2] == 'gt' ? object[match[1]] > match[3] : object[match[1]] < match[3]; + * }; + * }); + * + * _.filter(characters, 'age__gt38'); + * // => [{ 'name': 'fred', 'age': 40 }] */ -function baseCreateWrapper(bindData) { - var func = bindData[0], - bitmask = bindData[1], - partialArgs = bindData[2], - partialRightArgs = bindData[3], - thisArg = bindData[4], - arity = bindData[5]; +function createCallback(func, thisArg, argCount) { + var type = typeof func; + if (func == null || type == 'function') { + return baseCreateCallback(func, thisArg, argCount); + } + // handle "_.pluck" style callback shorthands + if (type != 'object') { + return property(func); + } + var props = keys(func), + key = props[0], + a = func[key]; - var isBind = bitmask & 1, - isBindKey = bitmask & 2, - isCurry = bitmask & 4, - isCurryBound = bitmask & 8, - key = func; + // handle "_.where" style callback shorthands + if (props.length == 1 && a === a && !isObject(a)) { + // fast path the common case of providing an object with a single + // property containing a primitive value + return function(object) { + var b = object[key]; + return a === b && (a !== 0 || (1 / a == 1 / b)); + }; + } + return function(object) { + var length = props.length, + result = false; - function bound() { - var thisBinding = isBind ? thisArg : this; - if (partialArgs) { - var args = slice(partialArgs); - push.apply(args, arguments); - } - if (partialRightArgs || isCurry) { - args || (args = slice(arguments)); - if (partialRightArgs) { - push.apply(args, partialRightArgs); - } - if (isCurry && args.length < arity) { - bitmask |= 16 & ~32; - return baseCreateWrapper([func, (isCurryBound ? bitmask : bitmask & ~3), args, null, thisArg, arity]); + while (length--) { + if (!(result = baseIsEqual(object[props[length]], func[props[length]], null, true))) { + break; } } - args || (args = arguments); - if (isBindKey) { - func = thisBinding[key]; - } - if (this instanceof bound) { - thisBinding = baseCreate(func.prototype); - var result = func.apply(thisBinding, args); - return isObject(result) ? result : thisBinding; - } - return func.apply(thisBinding, args); - } - setBindData(bound, bindData); - return bound; + return result; + }; } -module.exports = baseCreateWrapper; +module.exports = createCallback; -},{"lodash._basecreate":20,"lodash._setbinddata":9,"lodash._slice":25,"lodash.isobject":23}],20:[function(require,module,exports){ -module.exports=require(15) -},{"lodash._isnative":21,"lodash.isobject":23,"lodash.noop":22}],21:[function(require,module,exports){ -module.exports=require(10) -},{}],22:[function(require,module,exports){ -module.exports=require(11) -},{}],23:[function(require,module,exports){ -module.exports=require(18) -},{"lodash._objecttypes":29}],24:[function(require,module,exports){ +},{"lodash._basecreatecallback":7,"lodash._baseisequal":9,"lodash.isobject":31,"lodash.keys":32,"lodash.property":34}],25:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1570,27 +1652,31 @@ module.exports=require(18) * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ +var escapeHtmlChar = require('lodash._escapehtmlchar'), + keys = require('lodash.keys'), + reUnescapedHtml = require('lodash._reunescapedhtml'); /** - * Checks if `value` is a function. + * Converts the characters `&`, `<`, `>`, `"`, and `'` in `string` to their + * corresponding HTML entities. * * @static * @memberOf _ - * @category Objects - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the `value` is a function, else `false`. + * @category Utilities + * @param {string} string The string to escape. + * @returns {string} Returns the escaped string. * @example * - * _.isFunction(_); - * // => true + * _.escape('Fred, Wilma, & Pebbles'); + * // => 'Fred, Wilma, & Pebbles' */ -function isFunction(value) { - return typeof value == 'function'; +function escape(string) { + return string == null ? '' : String(string).replace(reUnescapedHtml, escapeHtmlChar); } -module.exports = isFunction; +module.exports = escape; -},{}],25:[function(require,module,exports){ +},{"lodash._escapehtmlchar":11,"lodash._reunescapedhtml":18,"lodash.keys":32}],26:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1599,38 +1685,74 @@ module.exports = isFunction; * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ +var createCallback = require('lodash.createcallback'), + forOwn = require('lodash.forown'); /** - * Slices the `collection` from the `start` index up to, but not including, - * the `end` index. + * Checks if the given callback returns truey value for **all** elements of + * a collection. The callback is bound to `thisArg` and invoked with three + * arguments; (value, index|key, collection). * - * Note: This function is used instead of `Array#slice` to support node lists - * in IE < 9 and to ensure dense arrays are returned. + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. * - * @private - * @param {Array|Object|string} collection The collection to slice. - * @param {number} start The start index. - * @param {number} end The end index. - * @returns {Array} Returns the new array. + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias all + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {boolean} Returns `true` if all elements passed the callback check, + * else `false`. + * @example + * + * _.every([true, 1, null, 'yes']); + * // => false + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * // using "_.pluck" callback shorthand + * _.every(characters, 'age'); + * // => true + * + * // using "_.where" callback shorthand + * _.every(characters, { 'age': 36 }); + * // => false */ -function slice(array, start, end) { - start || (start = 0); - if (typeof end == 'undefined') { - end = array ? array.length : 0; - } +function every(collection, callback, thisArg) { + var result = true; + callback = createCallback(callback, thisArg, 3); + var index = -1, - length = end - start || 0, - result = Array(length < 0 ? 0 : length); + length = collection ? collection.length : 0; - while (++index < length) { - result[index] = array[start + index]; + if (typeof length == 'number') { + while (++index < length) { + if (!(result = !!callback(collection[index], index, collection))) { + break; + } + } + } else { + forOwn(collection, function(value, index, collection) { + return (result = !!callback(value, index, collection)); + }); } return result; } -module.exports = slice; +module.exports = every; -},{}],26:[function(require,module,exports){ +},{"lodash.createcallback":24,"lodash.forown":28}],27:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1639,29 +1761,55 @@ module.exports = slice; * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ +var baseCreateCallback = require('lodash._basecreatecallback'), + objectTypes = require('lodash._objecttypes'); /** - * This method returns the first argument provided to it. + * Iterates over own and inherited enumerable properties of an object, + * executing the callback for each property. The callback is bound to `thisArg` + * and invoked with three arguments; (value, key, object). Callbacks may exit + * iteration early by explicitly returning `false`. * * @static * @memberOf _ - * @category Utilities - * @param {*} value Any value. - * @returns {*} Returns `value`. + * @type Function + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns `object`. * @example * - * var object = { 'name': 'fred' }; - * _.identity(object) === object; - * // => true + * function Shape() { + * this.x = 0; + * this.y = 0; + * } + * + * Shape.prototype.move = function(x, y) { + * this.x += x; + * this.y += y; + * }; + * + * _.forIn(new Shape, function(value, key) { + * console.log(key); + * }); + * // => logs 'x', 'y', and 'move' (property order is not guaranteed across environments) */ -function identity(value) { - return value; -} +var forIn = function(collection, callback, thisArg) { + var index, iterable = collection, result = iterable; + if (!iterable) return result; + if (!objectTypes[typeof iterable]) return result; + callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3); + for (index in iterable) { + if (callback(iterable[index], index, collection) === false) return result; + } + return result +}; -module.exports = identity; +module.exports = forIn; -},{}],27:[function(require,module,exports){ -var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};/** +},{"lodash._basecreatecallback":7,"lodash._objecttypes":16}],28:[function(require,module,exports){ +/** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` * Copyright 2012-2013 The Dojo Foundation @@ -1669,42 +1817,109 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ -var isNative = require('lodash._isnative'); - -/** Used to detect functions containing a `this` reference */ -var reThis = /\bthis\b/; +var baseCreateCallback = require('lodash._basecreatecallback'), + keys = require('lodash.keys'), + objectTypes = require('lodash._objecttypes'); /** - * An object used to flag environments features. + * Iterates over own enumerable properties of an object, executing the callback + * for each property. The callback is bound to `thisArg` and invoked with three + * arguments; (value, key, object). Callbacks may exit iteration early by + * explicitly returning `false`. * * @static * @memberOf _ - * @type Object + * @type Function + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns `object`. + * @example + * + * _.forOwn({ '0': 'zero', '1': 'one', 'length': 2 }, function(num, key) { + * console.log(key); + * }); + * // => logs '0', '1', and 'length' (property order is not guaranteed across environments) */ -var support = {}; +var forOwn = function(collection, callback, thisArg) { + var index, iterable = collection, result = iterable; + if (!iterable) return result; + if (!objectTypes[typeof iterable]) return result; + callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3); + var ownIndex = -1, + ownProps = objectTypes[typeof iterable] && keys(iterable), + length = ownProps ? ownProps.length : 0; + + while (++ownIndex < length) { + index = ownProps[ownIndex]; + if (callback(iterable[index], index, collection) === false) return result; + } + return result +}; + +module.exports = forOwn; +},{"lodash._basecreatecallback":7,"lodash._objecttypes":16,"lodash.keys":32}],29:[function(require,module,exports){ /** - * Detect if functions can be decompiled by `Function#toString` - * (all but PS3 and older Opera mobile browsers & avoided in Windows 8 apps). + * Lo-Dash 2.4.1 (Custom Build) + * Build: `lodash modularize modern exports="npm" -o ./npm/` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.5.2 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license + */ + +/** + * This method returns the first argument provided to it. * - * @memberOf _.support - * @type boolean + * @static + * @memberOf _ + * @category Utilities + * @param {*} value Any value. + * @returns {*} Returns `value`. + * @example + * + * var object = { 'name': 'fred' }; + * _.identity(object) === object; + * // => true */ -support.funcDecomp = !isNative(global.WinRTError) && reThis.test(function() { return this; }); +function identity(value) { + return value; +} + +module.exports = identity; +},{}],30:[function(require,module,exports){ /** - * Detect if `Function#name` is supported (all but IE). + * Lo-Dash 2.4.1 (Custom Build) + * Build: `lodash modularize modern exports="npm" -o ./npm/` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.5.2 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license + */ + +/** + * Checks if `value` is a function. * - * @memberOf _.support - * @type boolean + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true */ -support.funcNames = typeof Function.name == 'string'; +function isFunction(value) { + return typeof value == 'function'; +} -module.exports = support; +module.exports = isFunction; -},{"lodash._isnative":28}],28:[function(require,module,exports){ -module.exports=require(10) -},{}],29:[function(require,module,exports){ +},{}],31:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1713,20 +1928,39 @@ module.exports=require(10) * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ +var objectTypes = require('lodash._objecttypes'); -/** Used to determine if values are of the language type Object */ -var objectTypes = { - 'boolean': false, - 'function': true, - 'object': true, - 'number': false, - 'string': false, - 'undefined': false -}; +/** + * Checks if `value` is the language type of Object. + * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(1); + * // => false + */ +function isObject(value) { + // check if the value is the ECMAScript language type of Object + // http://es5.github.io/#x8 + // and avoid a V8 bug + // http://code.google.com/p/v8/issues/detail?id=2291 + return !!(value && objectTypes[typeof value]); +} -module.exports = objectTypes; +module.exports = isObject; -},{}],30:[function(require,module,exports){ +},{"lodash._objecttypes":16}],32:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1764,9 +1998,7 @@ var keys = !nativeKeys ? shimKeys : function(object) { module.exports = keys; -},{"lodash._isnative":31,"lodash._shimkeys":32,"lodash.isobject":33}],31:[function(require,module,exports){ -module.exports=require(10) -},{}],32:[function(require,module,exports){ +},{"lodash._isnative":14,"lodash._shimkeys":20,"lodash.isobject":31}],33:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1775,40 +2007,26 @@ module.exports=require(10) * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ -var objectTypes = require('lodash._objecttypes'); - -/** Used for native method references */ -var objectProto = Object.prototype; - -/** Native method shortcuts */ -var hasOwnProperty = objectProto.hasOwnProperty; /** - * A fallback implementation of `Object.keys` which produces an array of the - * given object's own enumerable property names. + * A no-operation function. * - * @private - * @type Function - * @param {Object} object The object to inspect. - * @returns {Array} Returns an array of property names. + * @static + * @memberOf _ + * @category Utilities + * @example + * + * var object = { 'name': 'fred' }; + * _.noop(object) === undefined; + * // => true */ -var shimKeys = function(object) { - var index, iterable = object, result = []; - if (!iterable) return result; - if (!(objectTypes[typeof object])) return result; - for (index in iterable) { - if (hasOwnProperty.call(iterable, index)) { - result.push(index); - } - } - return result -}; +function noop() { + // no operation performed +} -module.exports = shimKeys; +module.exports = noop; -},{"lodash._objecttypes":29}],33:[function(require,module,exports){ -module.exports=require(18) -},{"lodash._objecttypes":29}],34:[function(require,module,exports){ +},{}],34:[function(require,module,exports){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1817,31 +2035,41 @@ module.exports=require(18) * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ -var escapeHtmlChar = require('lodash._escapehtmlchar'), - keys = require('lodash.keys'), - reUnescapedHtml = require('lodash._reunescapedhtml'); /** - * Converts the characters `&`, `<`, `>`, `"`, and `'` in `string` to their - * corresponding HTML entities. + * Creates a "_.pluck" style function, which returns the `key` value of a + * given object. * * @static * @memberOf _ * @category Utilities - * @param {string} string The string to escape. - * @returns {string} Returns the escaped string. + * @param {string} key The name of the property to retrieve. + * @returns {Function} Returns the new function. * @example * - * _.escape('Fred, Wilma, & Pebbles'); - * // => 'Fred, Wilma, & Pebbles' + * var characters = [ + * { 'name': 'fred', 'age': 40 }, + * { 'name': 'barney', 'age': 36 } + * ]; + * + * var getName = _.property('name'); + * + * _.map(characters, getName); + * // => ['barney', 'fred'] + * + * _.sortBy(characters, getName); + * // => [{ 'name': 'barney', 'age': 36 }, { 'name': 'fred', 'age': 40 }] */ -function escape(string) { - return string == null ? '' : String(string).replace(reUnescapedHtml, escapeHtmlChar); +function property(key) { + return function(object) { + return object[key]; + }; } -module.exports = escape; +module.exports = property; -},{"lodash._escapehtmlchar":35,"lodash._reunescapedhtml":37,"lodash.keys":39}],35:[function(require,module,exports){ +},{}],35:[function(require,module,exports){ +(function (global){ /** * Lo-Dash 2.4.1 (Custom Build) * Build: `lodash modularize modern exports="npm" -o ./npm/` @@ -1850,1561 +2078,858 @@ module.exports = escape; * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license */ -var htmlEscapes = require('lodash._htmlescapes'); +var isNative = require('lodash._isnative'); + +/** Used to detect functions containing a `this` reference */ +var reThis = /\bthis\b/; /** - * Used by `escape` to convert characters to HTML entities. + * An object used to flag environments features. * - * @private - * @param {string} match The matched character to escape. - * @returns {string} Returns the escaped character. - */ -function escapeHtmlChar(match) { - return htmlEscapes[match]; -} - -module.exports = escapeHtmlChar; + * @static + * @memberOf _ + * @type Object + */ +var support = {}; -},{"lodash._htmlescapes":36}],36:[function(require,module,exports){ /** - * Lo-Dash 2.4.1 (Custom Build) - * Build: `lodash modularize modern exports="npm" -o ./npm/` - * Copyright 2012-2013 The Dojo Foundation - * Based on Underscore.js 1.5.2 - * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license + * Detect if functions can be decompiled by `Function#toString` + * (all but PS3 and older Opera mobile browsers & avoided in Windows 8 apps). + * + * @memberOf _.support + * @type boolean */ +support.funcDecomp = !isNative(global.WinRTError) && reThis.test(function() { return this; }); /** - * Used to convert characters to HTML entities: + * Detect if `Function#name` is supported (all but IE). * - * Though the `>` character is escaped for symmetry, characters like `>` and `/` - * don't require escaping in HTML and have no special meaning unless they're part - * of a tag or an unquoted attribute value. - * http://mathiasbynens.be/notes/ambiguous-ampersands (under "semi-related fun fact") + * @memberOf _.support + * @type boolean */ -var htmlEscapes = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''' +support.funcNames = typeof Function.name == 'string'; + +module.exports = support; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"lodash._isnative":14}],36:[function(require,module,exports){ +/** @jsx React.DOM */ + +var React = require("react"); + +/* You know when you want to propagate input to a parent... + * but then that parent does something with the input... + * then changing the props of the input... + * on every keystroke... + * so if some input is invalid or incomplete... + * the input gets reset or otherwise effed... + * + * This is the solution. + * + * Enough melodrama. Its an input that only sends changes + * to its parent on blur. + */ +var BlurInput = React.createClass({displayName: 'BlurInput', + propTypes: { + value: React.PropTypes.string.isRequired, + onChange: React.PropTypes.func.isRequired + }, + getInitialState: function() { + return { value: this.props.value }; + }, + render: function() { + return this.transferPropsTo(React.DOM.input( + {type:"text", + value:this.state.value, + onChange:this.handleChange, + onBlur:this.handleBlur} )); + }, + componentWillReceiveProps: function(nextProps) { + this.setState({ value: nextProps.value }); + }, + handleChange: function(e) { + this.setState({ value: e.target.value }); + }, + handleBlur: function(e) { + this.props.onChange(e.target.value); + } +}); + +module.exports = BlurInput; + +},{"react":45}],37:[function(require,module,exports){ +/** @jsx React.DOM */ + +var React = require('react'); +var RCSS = require('rcss'); +var _ = require('underscore'); + +var outerStyle = { + display: 'inline-block', }; -module.exports = htmlEscapes; +var buttonStyle = { + backgroundColor: 'white', + border: '1px solid #ccc', + borderBottom: '1px solid #ccc', + borderLeft: '0', + cursor: 'pointer', + margin: '0', + padding: '5px 10px', + position: 'relative', // for hover -},{}],37:[function(require,module,exports){ -/** - * Lo-Dash 2.4.1 (Custom Build) - * Build: `lodash modularize modern exports="npm" -o ./npm/` - * Copyright 2012-2013 The Dojo Foundation - * Based on Underscore.js 1.5.2 - * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license + ':first-child': { + borderLeft: '1px solid #ccc', + borderTopLeftRadius: '3px', + borderBottomLeftRadius: '3px' + }, + + ':last-child': { + borderRight: '1px solid #ccc', + borderTopRightRadius: '3px', + borderBottomRightRadius: '3px' + }, + + ':hover': { + backgroundColor: '#ccc' + }, + + ':focus': { + zIndex: '2' + } +}; + +var selectedStyle = { + backgroundColor: '#ddd' +}; + +RCSS.createClass(outerStyle); +RCSS.createClass(buttonStyle); +RCSS.createClass(selectedStyle); + +/* ButtonGroup is an aesthetically pleasing group of buttons. + * + * The class requires these properties: + * buttons - an array of objects with keys: + * "value": this is the value returned when the button is selected + * "text": this is the text shown on the button + * "title": this is the title-text shown on hover + * onChange - a function that is provided with the updated value + * (which it then is responsible for updating) + * + * The class has these optional properties: + * value - the initial value of the button selected, defaults to null. + * allowEmpty - if false, exactly one button _must_ be selected; otherwise + * it defaults to true and _at most_ one button (0 or 1) may be selected. + * + * Requires stylesheets/perseus-admin-package/editor.less to look nice. */ -var htmlEscapes = require('lodash._htmlescapes'), - keys = require('lodash.keys'); -/** Used to match HTML entities and HTML characters */ -var reUnescapedHtml = RegExp('[' + keys(htmlEscapes).join('') + ']', 'g'); +var ButtonGroup = React.createClass({displayName: 'ButtonGroup', + propTypes: { + value: React.PropTypes.any, + buttons: React.PropTypes.arrayOf(React.PropTypes.shape({ + value: React.PropTypes.any.isRequired, + text: React.PropTypes.renderable, + title: React.PropTypes.string + })).isRequired, + onChange: React.PropTypes.func.isRequired, + allowEmpty: React.PropTypes.bool + }, -module.exports = reUnescapedHtml; + getDefaultProps: function() { + return { + value: null, + allowEmpty: true + }; + }, -},{"lodash._htmlescapes":38,"lodash.keys":39}],38:[function(require,module,exports){ -module.exports=require(36) -},{}],39:[function(require,module,exports){ -arguments[4][30][0].apply(exports,arguments) -},{"lodash._isnative":40,"lodash._shimkeys":41,"lodash.isobject":43}],40:[function(require,module,exports){ -module.exports=require(10) -},{}],41:[function(require,module,exports){ -module.exports=require(32) -},{"lodash._objecttypes":42}],42:[function(require,module,exports){ -module.exports=require(29) -},{}],43:[function(require,module,exports){ -module.exports=require(18) -},{"lodash._objecttypes":44}],44:[function(require,module,exports){ -module.exports=require(29) -},{}],45:[function(require,module,exports){ -var _validCSSProps = { - 'alignment-adjust': true, - 'alignment-baseline': true, - 'animation': true, - 'animation-delay': true, - 'animation-direction': true, - 'animation-duration': true, - 'animation-iteration-count': true, - 'animation-name': true, - 'animation-play-state': true, - 'animation-timing-function': true, - 'appearance': true, - 'backface-visibility': true, - 'background': true, - 'background-attachment': true, - 'background-clip': true, - 'background-color': true, - 'background-image': true, - 'background-origin': true, - 'background-position': true, - 'background-repeat': true, - 'background-size': true, - 'baseline-shift': true, - 'bookmark-label': true, - 'bookmark-level': true, - 'bookmark-target': true, - 'border': true, - 'border-bottom': true, - 'border-bottom-color': true, - 'border-bottom-left-radius': true, - 'border-bottom-right-radius': true, - 'border-bottom-style': true, - 'border-bottom-width': true, - 'border-collapse': true, - 'border-color': true, - 'border-image': true, - 'border-image-outset': true, - 'border-image-repeat': true, - 'border-image-slice': true, - 'border-image-source': true, - 'border-image-width': true, - 'border-left': true, - 'border-left-color': true, - 'border-left-style': true, - 'border-left-width': true, - 'border-radius': true, - 'border-right': true, - 'border-right-color': true, - 'border-right-style': true, - 'border-right-width': true, - 'border-spacing': true, - 'border-style': true, - 'border-top': true, - 'border-top-color': true, - 'border-top-left-radius': true, - 'border-top-right-radius': true, - 'border-top-style': true, - 'border-top-width': true, - 'border-width': true, - 'bottom': true, - 'box-align': true, - 'box-decoration-break': true, - 'box-direction': true, - 'box-flex': true, - 'box-flex-group': true, - 'box-lines': true, - 'box-ordinal-group': true, - 'box-orient': true, - 'box-pack': true, - 'box-shadow': true, - 'box-sizing': true, - 'caption-side': true, - 'clear': true, - 'clip': true, - 'color': true, - 'color-profile': true, - 'column-count': true, - 'column-fill': true, - 'column-gap': true, - 'column-rule': true, - 'column-rule-color': true, - 'column-rule-style': true, - 'column-rule-width': true, - 'column-span': true, - 'column-width': true, - 'columns': true, - 'content': true, - 'counter-increment': true, - 'counter-reset': true, - 'crop': true, - 'cursor': true, - 'direction': true, - 'display': true, - 'dominant-baseline': true, - 'drop-initial-after-adjust': true, - 'drop-initial-after-align': true, - 'drop-initial-before-adjust': true, - 'drop-initial-before-align': true, - 'drop-initial-size': true, - 'drop-initial-value': true, - 'empty-cells': true, - 'fit': true, - 'fit-position': true, - 'float': true, - 'float-offset': true, - 'font': true, - 'font-family': true, - 'font-size': true, - 'font-size-adjust': true, - 'font-stretch': true, - 'font-style': true, - 'font-variant': true, - 'font-weight': true, - 'grid-columns': true, - 'grid-rows': true, - 'hanging-punctuation': true, - 'height': true, - 'hyphenate-after': true, - 'hyphenate-before': true, - 'hyphenate-character': true, - 'hyphenate-lines': true, - 'hyphenate-resource': true, - 'hyphens': true, - 'icon': true, - 'image-orientation': true, - 'image-resolution': true, - 'inline-box-align': true, - 'left': true, - 'letter-spacing': true, - 'line-height': true, - 'line-stacking': true, - 'line-stacking-ruby': true, - 'line-stacking-shift': true, - 'line-stacking-strategy': true, - 'list-style': true, - 'list-style-image': true, - 'list-style-position': true, - 'list-style-type': true, - 'margin': true, - 'margin-bottom': true, - 'margin-left': true, - 'margin-right': true, - 'margin-top': true, - 'mark': true, - 'mark-after': true, - 'mark-before': true, - 'marks': true, - 'marquee-direction': true, - 'marquee-play-count': true, - 'marquee-speed': true, - 'marquee-style': true, - 'max-height': true, - 'max-width': true, - 'min-height': true, - 'min-width': true, - 'move-to': true, - 'nav-down': true, - 'nav-index': true, - 'nav-left': true, - 'nav-right': true, - 'nav-up': true, - 'opacity': true, - 'orphans': true, - 'outline': true, - 'outline-color': true, - 'outline-offset': true, - 'outline-style': true, - 'outline-width': true, - 'overflow': true, - 'overflow-style': true, - 'overflow-x': true, - 'overflow-y': true, - 'padding': true, - 'padding-bottom': true, - 'padding-left': true, - 'padding-right': true, - 'padding-top': true, - 'page': true, - 'page-break-after': true, - 'page-break-before': true, - 'page-break-inside': true, - 'page-policy': true, - 'perspective': true, - 'perspective-origin': true, - 'phonemes': true, - 'position': true, - 'punctuation-trim': true, - 'quotes': true, - 'rendering-intent': true, - 'resize': true, - 'rest': true, - 'rest-after': true, - 'rest-before': true, - 'right': true, - 'rotation': true, - 'rotation-point': true, - 'ruby-align': true, - 'ruby-overhang': true, - 'ruby-position': true, - 'ruby-span': true, - 'size': true, - 'string-set': true, - 'table-layout': true, - 'target': true, - 'target-name': true, - 'target-new': true, - 'target-position': true, - 'text-align': true, - 'text-align-last': true, - 'text-decoration': true, - 'text-height': true, - 'text-indent': true, - 'text-justify': true, - 'text-outline': true, - 'text-overflow': true, - 'text-shadow': true, - 'text-transform': true, - 'text-wrap': true, - 'top': true, - 'transform': true, - 'transform-origin': true, - 'transform-style': true, - 'transition': true, - 'transition-delay': true, - 'transition-duration': true, - 'transition-property': true, - 'transition-timing-function': true, - 'unicode-bidi': true, - 'user-select': true, - 'vertical-align': true, - 'visibility': true, - 'voice-balance': true, - 'voice-duration': true, - 'voice-pitch': true, - 'voice-pitch-range': true, - 'voice-rate': true, - 'voice-stress': true, - 'voice-volume': true, - 'white-space': true, - 'widows': true, - 'width': true, - 'word-break': true, - 'word-spacing': true, - 'word-wrap': true, - 'z-index': true -} - -var vendorPrefixRegEx = /^-.+-/; - -module.exports = function(prop) { - if (prop[0] === '-') { - return !!_validCSSProps[prop.replace(vendorPrefixRegEx, '')]; - } - return !!_validCSSProps[prop]; -}; - -},{}],46:[function(require,module,exports){ -var every = require('lodash.every'); + render: function() { + var value = this.props.value; + var buttons = _(this.props.buttons).map(function(button, i) { + var maybeSelected = button.value === value ? + selectedStyle.className : + ""; + return React.DOM.button( {title:button.title, + id:"" + i, + ref:"button" + i, + key:"" + i, + className:(buttonStyle.className + " " + maybeSelected), + onClick:this.toggleSelect.bind(this, button.value)}, + button.text || "" + button.value + ); + }.bind(this)); -function isValidRatio(ratio) { - var re = /\d+\/\d+/; - return !!ratio.match(re); -} + return React.DOM.div( {className:outerStyle.className}, + buttons + ); + }, -function isValidInteger(integer) { - var re = /\d+/; - return !!integer.match(re); -} + focus: function() { + this.getDOMNode().focus(); + return true; + }, -function isValidLength(length) { - var re = /\d+(?:ex|em|ch|rem|vh|vw|vmin|vmax|px|mm|cm|in|pt|pc)?$/; - return !!length.match(re); -} + toggleSelect: function(newValue) { + var value = this.props.value; -function isValidOrientation(orientation) { - return orientation === 'landscape' || orientation === 'portrait'; -} + if (this.props.allowEmpty) { + // Select the new button or unselect if it's already selected + this.props.onChange(value !== newValue ? newValue : null); + } else { + this.props.onChange(newValue); + } + } +}); -function isValidScan(scan) { - return scan === 'progressive' || scan === 'interlace'; -} +module.exports = ButtonGroup; -function isValidResolution(resolution) { - var re = /(?:\+|-)?(?:\d+|\d*\.\d+)(?:e\d+)?(?:dpi|dpcm|dppx)$/; - return !!resolution.match(re); -} +},{"rcss":40,"react":45,"underscore":46}],38:[function(require,module,exports){ +/** @jsx React.DOM */ -function isValidValue(value) { - return value != null && typeof value !== 'boolean' && value !== ''; -} +var React = require('react'); -var _mediaFeatureValidator = { - 'width': isValidLength, - 'min-width': isValidLength, - 'max-width': isValidLength, - 'height': isValidLength, - 'min-height': isValidLength, - 'max-height': isValidLength, - 'device-width': isValidLength, - 'min-device-width': isValidLength, - 'max-device-width': isValidLength, - 'device-height': isValidLength, - 'min-device-height': isValidLength, - 'max-device-height': isValidLength, - 'aspect-ratio': isValidRatio, - 'min-aspect-ratio': isValidRatio, - 'max-aspect-ratio': isValidRatio, - 'device-aspect-ratio': isValidRatio, - 'min-device-aspect-ratio': isValidRatio, - 'max-device-aspect-ratio': isValidRatio, - 'color': isValidValue, - 'min-color': isValidValue, - 'max-color': isValidValue, - 'color-index': isValidInteger, - 'min-color-index': isValidInteger, - 'max-color-index': isValidInteger, - 'monochrome': isValidInteger, - 'min-monochrome': isValidInteger, - 'max-monochrome': isValidInteger, - 'resolution': isValidResolution, - 'min-resolution': isValidResolution, - 'max-resolution': isValidResolution, - 'scan': isValidScan, - 'grid': isValidInteger, - 'orientation': isValidOrientation -}; +/* This component makes its children a drag target. Example: + * + * Drag to me + * + * ... + * + * handleDrop: function(e) { + * this.addImages(e.nativeEvent.dataTransfer.files); + * } + * + * Now "Drag to me" will be a drag target - when something is dragged over it, + * the element will become partially transparent as a visual indicator that + * it's a target. + */ +// TODO(joel) - indicate before the hover is over the target that it's possible +// to drag into the target. This would (I think) require a high level handler - +// like on Perseus itself, waiting for onDragEnter, then passing down the +// event. Sounds like a pain. Possible workaround - create a div covering the +// entire page... +// +// Other extensions: +// * custom styles for global drag and dragOver +// * only respond to certain types of drags (only images for instance)! +var DragTarget = React.createClass({displayName: 'DragTarget', + propTypes: { + onDrop: React.PropTypes.func.isRequired, + component: React.PropTypes.component, + shouldDragHighlight: React.PropTypes.func + }, + render: function() { + var opacity = this.state.dragHover ? { "opacity": 0.3 } : {}; + var component = this.props.component; -var _validMediaFeatures = { - 'width': true, - 'min-width': true, - 'max-width': true, - 'height': true, - 'min-height': true, - 'max-height': true, - 'device-width': true, - 'min-device-width': true, - 'max-device-width': true, - 'device-height': true, - 'min-device-height': true, - 'max-device-height': true, - 'aspect-ratio': true, - 'min-aspect-ratio': true, - 'max-aspect-ratio': true, - 'device-aspect-ratio': true, - 'min-device-aspect-ratio': true, - 'max-device-aspect-ratio': true, - 'color': true, - 'min-color': true, - 'max-color': true, - 'color-index': true, - 'min-color-index': true, - 'max-color-index': true, - 'monochrome': true, - 'min-monochrome': true, - 'max-monochrome': true, - 'resolution': true, - 'min-resolution': true, - 'max-resolution': true, - 'scan': true, - 'grid': true, - 'orientation': true -}; + return this.transferPropsTo( + component( {style:opacity, + onDrop:this.handleDrop, + onDragEnd:this.handleDragEnd, + onDragOver:this.handleDragOver, + onDragEnter:this.handleDragEnter, + onDragLeave:this.handleDragLeave}, + this.props.children + ) + ); + }, + getInitialState: function() { + return { dragHover: false }; + }, + getDefaultProps: function() { + return { + component: React.DOM.div, + shouldDragHighlight: function() {return true;} + }; + }, + handleDrop: function(e) { + e.stopPropagation(); + e.preventDefault(); + this.setState({ dragHover: false }); + this.props.onDrop(e); + }, + handleDragEnd: function() { + this.setState({ dragHover: false }); + }, + handleDragOver: function(e) { + e.preventDefault(); + }, + handleDragLeave: function() { + this.setState({ dragHover: false }); + }, + handleDragEnter: function(e) { + this.setState({ dragHover: this.props.shouldDragHighlight(e) }); + } +}); -var _validMediaTypes = { - 'all': true, - 'aural': true, - 'braille': true, - 'handheld': true, - 'print': true, - 'projection': true, - 'screen': true, - 'tty': true, - 'tv': true, - 'embossed': true +module.exports = DragTarget; + +},{"react":45}],39:[function(require,module,exports){ +/** @jsx React.DOM */ + +var React = require('react'); +var RCSS = require('rcss'); +var _ = require('underscore'); + +var colors = { + grayLight: '#aaa', + basicBorderColor: '#ccc', + white: '#fff' }; -var _validQualifiers = { - 'not': true, - 'only': true +var infoTip = { + display: 'inline-block', + marginLeft: '5px', + position: 'relative' }; -function isValidFeature(feature) { - return !!_validMediaFeatures[feature]; -} +var infoTipI = { + cursor: 'pointer' +}; -function isValidQualifier(qualifier) { - return !!_validQualifiers[qualifier]; -} +var infoTipContainer = { + position: 'absolute', + 'top': '-12px', + left: '22px', + zIndex: '1000' +}; -function isValidMediaType(mediaType) { - return !!_validMediaTypes[mediaType]; -} +var triangleBeforeAfter = { + borderBottom: '9px solid transparent', + borderTop: '9px solid transparent', + content: ' ', + height: '0', + position: 'absolute', + 'top': '0', + width: '0' +}; -function isValidQualifiedMediaType(mediaType) { - var terms = mediaType.trim().split(/\s+/); - switch (terms.length) { - case 1: - return isValidMediaType(terms[0]); - case 2: - return isValidQualifier(terms[0]) && isValidMediaType(terms[1]); - default: - return false; - } -} +var infoTipTriangle = { + height: '10px', + left: '0', + position: 'absolute', + 'top': '8px', + width: '0', + zIndex: '1', -function isValidExpression(expression) { - if (expression.length < 2) { - return false; - } + ':before': _.extend({}, triangleBeforeAfter, { + borderRight: '9px solid #bbb', + right: '0', + }), - // Parentheses are required around expressions - if (expression[0] !== '(' || expression[expression.length - 1] !== ')') { - return false; - } + ':after': _.extend({}, triangleBeforeAfter, { + borderRight: ("9px solid " + colors.white), + right: '-1px' + }) +}; - // Remove parentheses and spacess - expression = expression.substring(1, expression.length - 1); +var basicBorder = { + border: ("1px solid " + colors.basicBorderColor) +}; - // Is there a value to accompany the media feature? - var featureAndValue = expression.split(/\s*:\s*/); - switch (featureAndValue.length) { - case 1: - var feature = featureAndValue[0].trim(); - return isValidFeature(feature); - case 2: - var feature = featureAndValue[0].trim(); - var value = featureAndValue[1].trim(); - return isValidFeature(feature) && - _mediaFeatureValidator[feature](value); - default: - return false; - } -} +var boxShadow = function(str) { return { boxShadow: str }; }; -function isValidMediaQuery(query) { - var andSplitter = /\s+and\s+/; - var queryTerms = query.split(andSplitter); - return (isValidQualifiedMediaType(queryTerms[0]) || - isValidExpression(queryTerms[0])) && - every(queryTerms.slice(1), isValidExpression); -} +var verticalShadow = RCSS.merge( + basicBorder, + boxShadow(("0 1px 3px " + colors.basicBorderColor)), + { borderBottom: ("1px solid " + colors.grayLight) } +); -function isValidMediaQueryList(mediaQuery) { - mediaQuery = mediaQuery.toLowerCase(); +var infoTipContentContainer = RCSS.merge(verticalShadow, { + background: colors.white, + padding: '5px 10px', + width: '240px' +}); - if (mediaQuery.substring(0, 6) !== '@media') { - return false; +RCSS.createClass(infoTip); +RCSS.createClass(infoTipI); +RCSS.createClass(infoTipTriangle); +RCSS.createClass(verticalShadow); +RCSS.createClass(infoTipContainer); +RCSS.createClass(infoTipContentContainer); + +var questionMark = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3NpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NDkxMSwgMjAxMy8xMC8yOS0xMTo0NzoxNiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo2N2M3NTAxYS04YmVlLTQ0M2MtYmRiNS04OGM2N2IxN2NhYzEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUJCRTk4Qjc4NjAwMTFFMzg3QUJDNEI4Mzk2QTRGQkQiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUJCRTk4QjY4NjAwMTFFMzg3QUJDNEI4Mzk2QTRGQkQiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NGE5ZDI0OTMtODk1NC00OGFkLTlhMTgtZDAwM2MwYWNjNDJlIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjY3Yzc1MDFhLThiZWUtNDQzYy1iZGI1LTg4YzY3YjE3Y2FjMSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pqm89uYAAADMSURBVHjaXJA9DoJAEIUH1M4TUHIFsCMGen9OwCGw1YRGW2ntKel0exsojHIBC0ouQAyUviFDstmXfNmZeS+zm7XSNCXRFiRgJf0bXIHixpbhGdxBBJYC1w/xaA424MhNEATkui71fU9KqfEU78UbD9PdbJRlOdae55GmhIP+1NV1TcMwkOM41DSNHvRtMhTHMRVFQW3b6mOLgx99kue5GRp/gIOZuZGvNpTNwjD8oliANU+qqqKu6/TQBdymN57AHjzBT+B6Jx79BRgAvc49kQA4yxgAAAAASUVORK5CYII='; // @NoLint + +var InfoTip = React.createClass({displayName: 'InfoTip', + getInitialState: function() { + return { + hover: false + }; + }, + + render: function() { + return React.DOM.div( {className:infoTip.className}, + React.DOM.img( {width:10, + height:10, + src:questionMark, + onMouseEnter:this.handleMouseEnter, + onMouseLeave:this.handleMouseLeave} ), + React.DOM.div( {className:infoTipContainer.className, + style:{display: this.state.hover ? 'block' : 'none'}}, + React.DOM.div( {className:infoTipTriangle.className} ), + /* keep the classes here - used for selectors on KA */ + React.DOM.div( {className:infoTipContentContainer.className}, + this.props.children + ) + ) + ); + }, + + handleMouseEnter: function() { + this.setState({hover: true}); + }, + + handleMouseLeave: function() { + this.setState({hover: false}); } +}); - var commaSplitter = /\s*,\s*/; - var queryList = mediaQuery.substring(7, mediaQuery.length) - .split(commaSplitter); - return every(queryList, isValidMediaQuery); -} +module.exports = InfoTip; -module.exports = isValidMediaQueryList +},{"rcss":40,"react":45,"underscore":46}],40:[function(require,module,exports){ +var assign = require('lodash.assign'); -},{"lodash.every":47}],47:[function(require,module,exports){ -/** - * Lo-Dash 2.4.1 (Custom Build) - * Build: `lodash modularize modern exports="npm" -o ./npm/` - * Copyright 2012-2013 The Dojo Foundation - * Based on Underscore.js 1.5.2 - * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license - */ -var createCallback = require('lodash.createcallback'), - forOwn = require('lodash.forown'); +var styleRuleValidator = require('./styleRuleValidator'); +var styleRuleConverter = require('./styleRuleConverter'); +var mediaQueryValidator = require('valid-media-queries'); -/** - * Checks if the given callback returns truey value for **all** elements of - * a collection. The callback is bound to `thisArg` and invoked with three - * arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias all - * @category Collections - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|Object|string} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {boolean} Returns `true` if all elements passed the callback check, - * else `false`. - * @example - * - * _.every([true, 1, null, 'yes']); - * // => false - * - * var characters = [ - * { 'name': 'barney', 'age': 36 }, - * { 'name': 'fred', 'age': 40 } - * ]; - * - * // using "_.pluck" callback shorthand - * _.every(characters, 'age'); - * // => true - * - * // using "_.where" callback shorthand - * _.every(characters, { 'age': 36 }); - * // => false - */ -function every(collection, callback, thisArg) { - var result = true; - callback = createCallback(callback, thisArg, 3); +var existingClasses = {}; +var styleTag = createStyleTag(); - var index = -1, - length = collection ? collection.length : 0; +var classNameId = 0; +var randomSuffix = Math.random().toString(36).slice(-5); - if (typeof length == 'number') { - while (++index < length) { - if (!(result = !!callback(collection[index], index, collection))) { - break; - } - } - } else { - forOwn(collection, function(value, index, collection) { - return (result = !!callback(value, index, collection)); - }); - } - return result; +function generateValidCSSClassName() { + // CSS classNames can't start with a number. + return 'c' + (classNameId++) + '-' + randomSuffix; } -module.exports = every; +function objToCSS(style) { + var serialized = ''; + for (var propName in style) { + // we put that ourselves + if (propName == 'className') continue; -},{"lodash.createcallback":48,"lodash.forown":84}],48:[function(require,module,exports){ -/** - * Lo-Dash 2.4.3 (Custom Build) - * Build: `lodash modularize modern exports="npm" -o ./npm/` - * Copyright 2012-2013 The Dojo Foundation - * Based on Underscore.js 1.5.2 - * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license - */ -var baseCreateCallback = require('lodash._basecreatecallback'), - baseIsEqual = require('lodash._baseisequal'), - isObject = require('lodash.isobject'), - keys = require('lodash.keys'), - property = require('lodash.property'); + var cssPropName = styleRuleConverter.hyphenateProp(propName); + if (!styleRuleValidator.isValidProp(cssPropName)) { + console.warn( + '%s (transformed into %s) is not a valid CSS property name.', propName, cssPropName + ); + continue; + } -/** - * Produces a callback bound to an optional `thisArg`. If `func` is a property - * name the created callback will return the property value for a given element. - * If `func` is an object the created callback will return `true` for elements - * that contain the equivalent object properties, otherwise it will return `false`. - * - * @static - * @memberOf _ - * @category Utilities - * @param {*} [func=identity] The value to convert to a callback. - * @param {*} [thisArg] The `this` binding of the created callback. - * @param {number} [argCount] The number of arguments the callback accepts. - * @returns {Function} Returns a callback function. - * @example - * - * var characters = [ - * { 'name': 'barney', 'age': 36 }, - * { 'name': 'fred', 'age': 40 } - * ]; - * - * // wrap to create custom callback shorthands - * _.createCallback = _.wrap(_.createCallback, function(func, callback, thisArg) { - * var match = /^(.+?)__([gl]t)(.+)$/.exec(callback); - * return !match ? func(callback, thisArg) : function(object) { - * return match[2] == 'gt' ? object[match[1]] > match[3] : object[match[1]] < match[3]; - * }; - * }); - * - * _.filter(characters, 'age__gt38'); - * // => [{ 'name': 'fred', 'age': 40 }] - */ -function createCallback(func, thisArg, argCount) { - var type = typeof func; - if (func == null || type == 'function') { - return baseCreateCallback(func, thisArg, argCount); - } - // handle "_.pluck" style callback shorthands - if (type != 'object') { - return property(func); - } - var props = keys(func), - key = props[0], - a = func[key]; + var styleValue = style[propName]; + if (!styleRuleValidator.isValidValue(styleValue)) continue; - // handle "_.where" style callback shorthands - if (props.length == 1 && a === a && !isObject(a)) { - // fast path the common case of providing an object with a single - // property containing a primitive value - return function(object) { - var b = object[key]; - return a === b && (a !== 0 || (1 / a == 1 / b)); - }; + if (styleValue !== null) { + serialized += cssPropName + ':'; + serialized += styleRuleConverter.escapeValueForProp(styleValue, + cssPropName) + ';'; + } } - return function(object) { - var length = props.length, - result = false; + return serialized || null; +} - while (length--) { - if (!(result = baseIsEqual(object[props[length]], func[props[length]], null, true))) { - break; - } - } - return result; - }; +function createStyleTag() { + var tag = document.createElement('style'); + document.getElementsByTagName('head')[0].appendChild(tag); + return tag; } -module.exports = createCallback; +function styleToCSS(style) { + var styleStr = '.' + style.className + '{'; + styleStr += objToCSS(style.value); + styleStr += '}'; -},{"lodash._basecreatecallback":49,"lodash._baseisequal":68,"lodash.isobject":77,"lodash.keys":79,"lodash.property":83}],49:[function(require,module,exports){ -arguments[4][8][0].apply(exports,arguments) -},{"lodash._setbinddata":50,"lodash.bind":53,"lodash.identity":65,"lodash.support":66}],50:[function(require,module,exports){ -module.exports=require(9) -},{"lodash._isnative":51,"lodash.noop":52}],51:[function(require,module,exports){ -module.exports=require(10) -},{}],52:[function(require,module,exports){ -module.exports=require(11) -},{}],53:[function(require,module,exports){ -arguments[4][12][0].apply(exports,arguments) -},{"lodash._createwrapper":54,"lodash._slice":64}],54:[function(require,module,exports){ -arguments[4][13][0].apply(exports,arguments) -},{"lodash._basebind":55,"lodash._basecreatewrapper":59,"lodash._slice":64,"lodash.isfunction":63}],55:[function(require,module,exports){ -arguments[4][14][0].apply(exports,arguments) -},{"lodash._basecreate":56,"lodash._setbinddata":50,"lodash._slice":64,"lodash.isobject":77}],56:[function(require,module,exports){ -arguments[4][15][0].apply(exports,arguments) -},{"lodash._isnative":57,"lodash.isobject":77,"lodash.noop":58}],57:[function(require,module,exports){ -module.exports=require(10) -},{}],58:[function(require,module,exports){ -module.exports=require(11) -},{}],59:[function(require,module,exports){ -arguments[4][19][0].apply(exports,arguments) -},{"lodash._basecreate":60,"lodash._setbinddata":50,"lodash._slice":64,"lodash.isobject":77}],60:[function(require,module,exports){ -arguments[4][15][0].apply(exports,arguments) -},{"lodash._isnative":61,"lodash.isobject":77,"lodash.noop":62}],61:[function(require,module,exports){ -module.exports=require(10) -},{}],62:[function(require,module,exports){ -module.exports=require(11) -},{}],63:[function(require,module,exports){ -module.exports=require(24) -},{}],64:[function(require,module,exports){ -module.exports=require(25) -},{}],65:[function(require,module,exports){ -module.exports=require(26) -},{}],66:[function(require,module,exports){ -module.exports=require(27) -},{"lodash._isnative":67}],67:[function(require,module,exports){ -module.exports=require(10) -},{}],68:[function(require,module,exports){ -/** - * Lo-Dash 2.4.1 (Custom Build) - * Build: `lodash modularize modern exports="npm" -o ./npm/` - * Copyright 2012-2013 The Dojo Foundation - * Based on Underscore.js 1.5.2 - * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license - */ -var forIn = require('lodash.forin'), - getArray = require('lodash._getarray'), - isFunction = require('lodash.isfunction'), - objectTypes = require('lodash._objecttypes'), - releaseArray = require('lodash._releasearray'); + if (style.media) { + if (!mediaQueryValidator(style.media)) { + console.log('%s is not a valid media query.', style.media); + } + styleStr = style.media + '{' + styleStr + '}'; + } -/** `Object#toString` result shortcuts */ -var argsClass = '[object Arguments]', - arrayClass = '[object Array]', - boolClass = '[object Boolean]', - dateClass = '[object Date]', - numberClass = '[object Number]', - objectClass = '[object Object]', - regexpClass = '[object RegExp]', - stringClass = '[object String]'; - -/** Used for native method references */ -var objectProto = Object.prototype; - -/** Used to resolve the internal [[Class]] of values */ -var toString = objectProto.toString; + return styleStr; +} -/** Native method shortcuts */ -var hasOwnProperty = objectProto.hasOwnProperty; +// TODO: support media queries +function parseStyles(className, styleObj) { + var mainStyle = { + className: className, + value: {} + }; + var styles = [mainStyle]; -/** - * The base implementation of `_.isEqual`, without support for `thisArg` binding, - * that allows partial "_.where" style comparisons. - * - * @private - * @param {*} a The value to compare. - * @param {*} b The other value to compare. - * @param {Function} [callback] The function to customize comparing values. - * @param {Function} [isWhere=false] A flag to indicate performing partial comparisons. - * @param {Array} [stackA=[]] Tracks traversed `a` objects. - * @param {Array} [stackB=[]] Tracks traversed `b` objects. - * @returns {boolean} Returns `true` if the values are equivalent, else `false`. - */ -function baseIsEqual(a, b, callback, isWhere, stackA, stackB) { - // used to indicate that when comparing objects, `a` has at least the properties of `b` - if (callback) { - var result = callback(a, b); - if (typeof result != 'undefined') { - return !!result; + Object.keys(styleObj).forEach(function(k){ + // pseudo-selector, insert a new rule + if (k[0] === ':') { + styles.push({ + className: className+k, + value: styleObj[k] + }); + return; + } else if (k.substring(0, 6) === '@media') { + styles.push({ + className: className, + value: styleObj[k], + media: k + }); + return; } - } - // exit early for identical values - if (a === b) { - // treat `+0` vs. `-0` as not equal - return a !== 0 || (1 / a == 1 / b); - } - var type = typeof a, - otherType = typeof b; - // exit early for unlike primitive values - if (a === a && - !(a && objectTypes[type]) && - !(b && objectTypes[otherType])) { - return false; - } - // exit early for `null` and `undefined` avoiding ES3's Function#call behavior - // http://es5.github.io/#x15.3.4.4 - if (a == null || b == null) { - return a === b; - } - // compare [[Class]] names - var className = toString.call(a), - otherClass = toString.call(b); + // normal rule, insert into main one + mainStyle.value[k] = styleObj[k]; + }); - if (className == argsClass) { - className = objectClass; - } - if (otherClass == argsClass) { - otherClass = objectClass; - } - if (className != otherClass) { - return false; - } - switch (className) { - case boolClass: - case dateClass: - // coerce dates and booleans to numbers, dates to milliseconds and booleans - // to `1` or `0` treating invalid dates coerced to `NaN` as not equal - return +a == +b; + return styles; +} - case numberClass: - // treat `NaN` vs. `NaN` as equal - return (a != +a) - ? b != +b - // but treat `+0` vs. `-0` as not equal - : (a == 0 ? (1 / a == 1 / b) : a == +b); +function insertStyle(className, styleObj) { + var styles = parseStyles(className, styleObj); + var styleStr = styles.map(styleToCSS).join(''); + styleTag.innerHTML += styleStr; +} - case regexpClass: - case stringClass: - // coerce regexes to strings (http://es5.github.io/#x15.10.6.4) - // treat string primitives and their corresponding object instances as equal - return a == String(b); - } - var isArr = className == arrayClass; - if (!isArr) { - // unwrap any `lodash` wrapped values - var aWrapped = hasOwnProperty.call(a, '__wrapped__'), - bWrapped = hasOwnProperty.call(b, '__wrapped__'); +var RCSS = { + merge: function(a, b, c, d, e) { + return assign({}, a, b, c, d, e); + }, - if (aWrapped || bWrapped) { - return baseIsEqual(aWrapped ? a.__wrapped__ : a, bWrapped ? b.__wrapped__ : b, callback, isWhere, stackA, stackB); - } - // exit for functions and DOM nodes - if (className != objectClass) { - return false; - } - // in older versions of Opera, `arguments` objects have `Array` constructors - var ctorA = a.constructor, - ctorB = b.constructor; + createClass: function(styleObj) { + var styleId = JSON.stringify(styleObj); + var className; - // non `Object` object instances with different constructors are not equal - if (ctorA != ctorB && - !(isFunction(ctorA) && ctorA instanceof ctorA && isFunction(ctorB) && ctorB instanceof ctorB) && - ('constructor' in a && 'constructor' in b) - ) { - return false; + if (existingClasses[styleId]) { + // already exists, use the existing className + className = existingClasses[styleId]; + } else { + // generate a new class and insert it + className = generateValidCSSClassName(); + existingClasses[styleId] = className; + insertStyle(className, styleObj); } - } - // assume cyclic structures are equal - // the algorithm for detecting cyclic structures is adapted from ES 5.1 - // section 15.12.3, abstract operation `JO` (http://es5.github.io/#x15.12.3) - var initedStack = !stackA; - stackA || (stackA = getArray()); - stackB || (stackB = getArray()); - var length = stackA.length; - while (length--) { - if (stackA[length] == a) { - return stackB[length] == b; - } + styleObj.className = className; + return styleObj; } - var size = 0; - result = true; +}; - // add `a` and `b` to the stack of traversed objects - stackA.push(a); - stackB.push(b); +module.exports = RCSS; - // recursively compare objects and arrays (susceptible to call stack limits) - if (isArr) { - // compare lengths to determine if a deep comparison is necessary - length = a.length; - size = b.length; - result = size == length; +},{"./styleRuleConverter":41,"./styleRuleValidator":42,"lodash.assign":22,"valid-media-queries":48}],41:[function(require,module,exports){ +var escape = require('lodash.escape'); - if (result || isWhere) { - // deep compare the contents, ignoring non-numeric properties - while (size--) { - var index = length, - value = b[size]; +var _uppercasePattern = /([A-Z])/g; - if (isWhere) { - while (index--) { - if ((result = baseIsEqual(a[index], value, callback, isWhere, stackA, stackB))) { - break; - } - } - } else if (!(result = baseIsEqual(a[size], value, callback, isWhere, stackA, stackB))) { - break; - } - } - } - } - else { - // deep compare objects using `forIn`, instead of `forOwn`, to avoid `Object.keys` - // which, in this case, is more costly - forIn(b, function(value, key, b) { - if (hasOwnProperty.call(b, key)) { - // count the number of properties. - size++; - // deep compare each property value. - return (result = hasOwnProperty.call(a, key) && baseIsEqual(a[key], value, callback, isWhere, stackA, stackB)); - } - }); +function hyphenateProp(string) { + return string.replace(_uppercasePattern, '-$1').toLowerCase(); +} - if (result && !isWhere) { - // ensure both objects have the same number of properties - forIn(a, function(value, key, a) { - if (hasOwnProperty.call(a, key)) { - // `size` will be `-1` if `a` has more properties than `b` - return (result = --size > -1); - } - }); - } +function escapeValueForProp(value, prop) { + // 'content' is a special property that must be quoted + if (prop === 'content') { + return '"' + value + '"'; } - stackA.pop(); - stackB.pop(); + return escape(value); +} - if (initedStack) { - releaseArray(stackA); - releaseArray(stackB); - } - return result; +module.exports = { + hyphenateProp: hyphenateProp, + escapeValueForProp: escapeValueForProp +}; + +},{"lodash.escape":25}],42:[function(require,module,exports){ +var isValidCSSProps = require('valid-css-props'); + +function isValidProp(prop) { + return isValidCSSProps(prop); } -module.exports = baseIsEqual; +function isValidValue(value) { + return value != null && typeof value !== 'boolean' && value !== ''; +} + +module.exports = { + isValidProp: isValidProp, + isValidValue: isValidValue +}; -},{"lodash._getarray":69,"lodash._objecttypes":71,"lodash._releasearray":72,"lodash.forin":75,"lodash.isfunction":76}],69:[function(require,module,exports){ +},{"valid-css-props":47}],43:[function(require,module,exports){ +/** @jsx React.DOM */ /** - * Lo-Dash 2.4.1 (Custom Build) - * Build: `lodash modularize modern exports="npm" -o ./npm/` - * Copyright 2012-2013 The Dojo Foundation - * Based on Underscore.js 1.5.2 - * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license + * For math rendered using KaTex and/or MathJax. Use me like 2x + 3. */ -var arrayPool = require('lodash._arraypool'); +// TODO(joel) - require MathJax / katex so they don't have to be global -/** - * Gets an array from the array pool or creates a new one if the pool is empty. - * - * @private - * @returns {Array} The array from the pool. - */ -function getArray() { - return arrayPool.pop() || []; +var React = require('react'); + +var pendingScripts = []; +var needsProcess = false; +var timeout = null; + +function process(script, callback) { + pendingScripts.push(script); + if (!needsProcess) { + needsProcess = true; + timeout = setTimeout(doProcess, 0, callback); + } } -module.exports = getArray; +function doProcess(callback) { + MathJax.Hub.Queue(function() { + var oldElementScripts = MathJax.Hub.elementScripts; + MathJax.Hub.elementScripts = function(element) { + var scripts = pendingScripts; + pendingScripts = []; + needsProcess = false; + return scripts; + }; -},{"lodash._arraypool":70}],70:[function(require,module,exports){ -/** - * Lo-Dash 2.4.1 (Custom Build) - * Build: `lodash modularize modern exports="npm" -o ./npm/` - * Copyright 2012-2013 The Dojo Foundation - * Based on Underscore.js 1.5.2 - * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license - */ + try { + return MathJax.Hub.Process(null, callback); + } catch (e) { + // IE8 requires `catch` in order to use `finally` + throw e; + } finally { + MathJax.Hub.elementScripts = oldElementScripts; + } + }); +} -/** Used to pool arrays and objects used internally */ -var arrayPool = []; +var TeX = React.createClass({displayName: 'TeX', + getDefaultProps: function() { + return { + // Called after math is rendered or re-rendered + onRender: function() {} + }; + }, -module.exports = arrayPool; + render: function() { + return React.DOM.span( {style:this.props.style}, + React.DOM.span( {ref:"mathjax"} ), + React.DOM.span( {ref:"katex"} ) + ); + }, -},{}],71:[function(require,module,exports){ -module.exports=require(29) -},{}],72:[function(require,module,exports){ -/** - * Lo-Dash 2.4.1 (Custom Build) - * Build: `lodash modularize modern exports="npm" -o ./npm/` - * Copyright 2012-2013 The Dojo Foundation - * Based on Underscore.js 1.5.2 - * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license - */ -var arrayPool = require('lodash._arraypool'), - maxPoolSize = require('lodash._maxpoolsize'); + componentDidMount: function() { + var text = this.props.children; + var onRender = this.props.onRender; -/** - * Releases the given array back to the array pool. - * - * @private - * @param {Array} [array] The array to release. - */ -function releaseArray(array) { - array.length = 0; - if (arrayPool.length < maxPoolSize) { - arrayPool.push(array); - } -} + try { + var katexHolder = this.refs.katex.getDOMNode(); + katex.process(text, katexHolder); + onRender(); + return; + } catch (e) { + /* jshint -W103 */ + if (e.__proto__ !== katex.ParseError.prototype) { + /* jshint +W103 */ + throw e; + } + } -module.exports = releaseArray; + this.setScriptText(text); + process(this.script, onRender); + }, -},{"lodash._arraypool":73,"lodash._maxpoolsize":74}],73:[function(require,module,exports){ -module.exports=require(70) -},{}],74:[function(require,module,exports){ -/** - * Lo-Dash 2.4.1 (Custom Build) - * Build: `lodash modularize modern exports="npm" -o ./npm/` - * Copyright 2012-2013 The Dojo Foundation - * Based on Underscore.js 1.5.2 - * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license - */ + componentDidUpdate: function(prevProps, prevState) { + var oldText = prevProps.children; + var newText = this.props.children; + var onRender = this.props.onRender; -/** Used as the max size of the `arrayPool` and `objectPool` */ -var maxPoolSize = 40; + if (oldText !== newText) { + try { + var katexHolder = this.refs.katex.getDOMNode(); + katex.process(newText, katexHolder); + if (this.script) { + var jax = MathJax.Hub.getJaxFor(this.script); + if (jax) { + jax.Remove(); + } + } + onRender(); + return; + } catch (e) { + /* jshint -W103 */ + if (e.__proto__ !== katex.ParseError.prototype) { + /* jshint +W103 */ + throw e; + } + } -module.exports = maxPoolSize; + $(this.refs.katex.getDOMNode()).empty(); -},{}],75:[function(require,module,exports){ -/** - * Lo-Dash 2.4.1 (Custom Build) - * Build: `lodash modularize modern exports="npm" -o ./npm/` - * Copyright 2012-2013 The Dojo Foundation - * Based on Underscore.js 1.5.2 - * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license - */ -var baseCreateCallback = require('lodash._basecreatecallback'), - objectTypes = require('lodash._objecttypes'); + if (this.script) { + var component = this; + MathJax.Hub.Queue(function() { + var jax = MathJax.Hub.getJaxFor(component.script); + if (jax) { + return jax.Text(newText, onRender); + } else { + component.setScriptText(newText); + process(component.script, onRender); + } + }); + } else { + this.setScriptText(newText); + process(this.script, onRender); + } + } + }, -/** - * Iterates over own and inherited enumerable properties of an object, - * executing the callback for each property. The callback is bound to `thisArg` - * and invoked with three arguments; (value, key, object). Callbacks may exit - * iteration early by explicitly returning `false`. - * - * @static - * @memberOf _ - * @type Function - * @category Objects - * @param {Object} object The object to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns `object`. - * @example - * - * function Shape() { - * this.x = 0; - * this.y = 0; - * } - * - * Shape.prototype.move = function(x, y) { - * this.x += x; - * this.y += y; - * }; - * - * _.forIn(new Shape, function(value, key) { - * console.log(key); - * }); - * // => logs 'x', 'y', and 'move' (property order is not guaranteed across environments) - */ -var forIn = function(collection, callback, thisArg) { - var index, iterable = collection, result = iterable; - if (!iterable) return result; - if (!objectTypes[typeof iterable]) return result; - callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3); - for (index in iterable) { - if (callback(iterable[index], index, collection) === false) return result; + setScriptText: function(text) { + if (!this.script) { + this.script = document.createElement("script"); + this.script.type = "math/tex"; + this.refs.mathjax.getDOMNode().appendChild(this.script); + } + if ("text" in this.script) { + // IE8, etc + this.script.text = text; + } else { + this.script.textContent = text; + } + }, + + componentWillUnmount: function() { + if (this.script) { + var jax = MathJax.Hub.getJaxFor(this.script); + if (jax) { + jax.Remove(); + } + } } - return result -}; +}); -module.exports = forIn; +module.exports = TeX; -},{"lodash._basecreatecallback":49,"lodash._objecttypes":71}],76:[function(require,module,exports){ -module.exports=require(24) -},{}],77:[function(require,module,exports){ -module.exports=require(18) -},{"lodash._objecttypes":78}],78:[function(require,module,exports){ -module.exports=require(29) -},{}],79:[function(require,module,exports){ -arguments[4][30][0].apply(exports,arguments) -},{"lodash._isnative":80,"lodash._shimkeys":81,"lodash.isobject":77}],80:[function(require,module,exports){ -module.exports=require(10) -},{}],81:[function(require,module,exports){ -module.exports=require(32) -},{"lodash._objecttypes":82}],82:[function(require,module,exports){ -module.exports=require(29) -},{}],83:[function(require,module,exports){ -/** - * Lo-Dash 2.4.1 (Custom Build) - * Build: `lodash modularize modern exports="npm" -o ./npm/` - * Copyright 2012-2013 The Dojo Foundation - * Based on Underscore.js 1.5.2 - * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license - */ +},{"react":45}],44:[function(require,module,exports){ +/** @jsx React.DOM */ + +var React = require("react"); +var _ = require("underscore"); + +// TODO(joel/jack) fix z-index issues https://s3.amazonaws.com/uploads.hipchat.com/6574/29028/yOApjwmgiMhEZYJ/Screen%20Shot%202014-05-30%20at%203.34.18%20PM.png +// z-index: 3 on perseus-formats-tooltip seemed to work /** - * Creates a "_.pluck" style function, which returns the `key` value of a - * given object. + * A generic tooltip library for React.js * - * @static - * @memberOf _ - * @category Utilities - * @param {string} key The name of the property to retrieve. - * @returns {Function} Returns the new function. - * @example - * - * var characters = [ - * { 'name': 'fred', 'age': 40 }, - * { 'name': 'barney', 'age': 36 } - * ]; - * - * var getName = _.property('name'); - * - * _.map(characters, getName); - * // => ['barney', 'fred'] + * This should eventually end up in react-components * - * _.sortBy(characters, getName); - * // => [{ 'name': 'barney', 'age': 36 }, { 'name': 'fred', 'age': 40 }] - */ -function property(key) { - return function(object) { - return object[key]; - }; -} - -module.exports = property; - -},{}],84:[function(require,module,exports){ -/** - * Lo-Dash 2.4.1 (Custom Build) - * Build: `lodash modularize modern exports="npm" -o ./npm/` - * Copyright 2012-2013 The Dojo Foundation - * Based on Underscore.js 1.5.2 - * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license - */ -var baseCreateCallback = require('lodash._basecreatecallback'), - keys = require('lodash.keys'), - objectTypes = require('lodash._objecttypes'); - -/** - * Iterates over own enumerable properties of an object, executing the callback - * for each property. The callback is bound to `thisArg` and invoked with three - * arguments; (value, key, object). Callbacks may exit iteration early by - * explicitly returning `false`. + * Interface: ({a, b} means one of a or b) + * var Tooltip = require("./tooltip.jsx"); + * + * + * + * + * * - * @static - * @memberOf _ - * @type Function - * @category Objects - * @param {Object} object The object to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {*} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns `object`. - * @example + * To show/hide the tooltip, the parent component should call the + * .show() and .hide() methods of the tooltip when appropriate. + * (These are usually set up as handlers of events on the target element.) * - * _.forOwn({ '0': 'zero', '1': 'one', 'length': 2 }, function(num, key) { - * console.log(key); - * }); - * // => logs '0', '1', and 'length' (property order is not guaranteed across environments) + * Notes: + * className should not specify a border; that is handled by borderColor + * so that the arrow and tooltip match */ -var forOwn = function(collection, callback, thisArg) { - var index, iterable = collection, result = iterable; - if (!iterable) return result; - if (!objectTypes[typeof iterable]) return result; - callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3); - var ownIndex = -1, - ownProps = objectTypes[typeof iterable] && keys(iterable), - length = ownProps ? ownProps.length : 0; - - while (++ownIndex < length) { - index = ownProps[ownIndex]; - if (callback(iterable[index], index, collection) === false) return result; - } - return result -}; - -module.exports = forOwn; - -},{"lodash._basecreatecallback":85,"lodash._objecttypes":106,"lodash.keys":107}],85:[function(require,module,exports){ -arguments[4][8][0].apply(exports,arguments) -},{"lodash._setbinddata":86,"lodash.bind":89,"lodash.identity":103,"lodash.support":104}],86:[function(require,module,exports){ -module.exports=require(9) -},{"lodash._isnative":87,"lodash.noop":88}],87:[function(require,module,exports){ -module.exports=require(10) -},{}],88:[function(require,module,exports){ -module.exports=require(11) -},{}],89:[function(require,module,exports){ -arguments[4][12][0].apply(exports,arguments) -},{"lodash._createwrapper":90,"lodash._slice":102}],90:[function(require,module,exports){ -arguments[4][13][0].apply(exports,arguments) -},{"lodash._basebind":91,"lodash._basecreatewrapper":96,"lodash._slice":102,"lodash.isfunction":101}],91:[function(require,module,exports){ -arguments[4][14][0].apply(exports,arguments) -},{"lodash._basecreate":92,"lodash._setbinddata":86,"lodash._slice":102,"lodash.isobject":95}],92:[function(require,module,exports){ -arguments[4][15][0].apply(exports,arguments) -},{"lodash._isnative":93,"lodash.isobject":95,"lodash.noop":94}],93:[function(require,module,exports){ -module.exports=require(10) -},{}],94:[function(require,module,exports){ -module.exports=require(11) -},{}],95:[function(require,module,exports){ -module.exports=require(18) -},{"lodash._objecttypes":106}],96:[function(require,module,exports){ -arguments[4][19][0].apply(exports,arguments) -},{"lodash._basecreate":97,"lodash._setbinddata":86,"lodash._slice":102,"lodash.isobject":100}],97:[function(require,module,exports){ -arguments[4][15][0].apply(exports,arguments) -},{"lodash._isnative":98,"lodash.isobject":100,"lodash.noop":99}],98:[function(require,module,exports){ -module.exports=require(10) -},{}],99:[function(require,module,exports){ -module.exports=require(11) -},{}],100:[function(require,module,exports){ -module.exports=require(18) -},{"lodash._objecttypes":106}],101:[function(require,module,exports){ -module.exports=require(24) -},{}],102:[function(require,module,exports){ -module.exports=require(25) -},{}],103:[function(require,module,exports){ -module.exports=require(26) -},{}],104:[function(require,module,exports){ -module.exports=require(27) -},{"lodash._isnative":105}],105:[function(require,module,exports){ -module.exports=require(10) -},{}],106:[function(require,module,exports){ -module.exports=require(29) -},{}],107:[function(require,module,exports){ -arguments[4][30][0].apply(exports,arguments) -},{"lodash._isnative":108,"lodash._shimkeys":109,"lodash.isobject":110}],108:[function(require,module,exports){ -module.exports=require(10) -},{}],109:[function(require,module,exports){ -module.exports=require(32) -},{"lodash._objecttypes":106}],110:[function(require,module,exports){ -module.exports=require(18) -},{"lodash._objecttypes":106}],111:[function(require,module,exports){ -var escape = require('lodash.escape'); - -var _uppercasePattern = /([A-Z])/g; - -function hyphenateProp(string) { - return string.replace(_uppercasePattern, '-$1').toLowerCase(); -} - -function escapeValueForProp(value, prop) { - // 'content' is a special property that must be quoted - if (prop === 'content') { - return '"' + value + '"'; - } - return escape(value); -} - -module.exports = { - hyphenateProp: hyphenateProp, - escapeValueForProp: escapeValueForProp -}; - -},{"lodash.escape":34}],112:[function(require,module,exports){ -var isValidCSSProps = require('valid-css-props'); -function isValidProp(prop) { - return isValidCSSProps(prop); -} +// __,,--``\\ +// _,,-''`` \\ , +// '----------_.------'-.___|\__ +// _.--''`` `)__ )__ @\__ +// ( .. ''---/___,,E/__,E'------` +// `-''`'' +// Here be dragons. -function isValidValue(value) { - return value != null && typeof value !== 'boolean' && value !== ''; -} +var zIndex = 10; -module.exports = { - isValidProp: isValidProp, - isValidValue: isValidValue -}; +var Triangle = React.createClass({displayName: 'Triangle', + propTypes: { + color: React.PropTypes.string.isRequired, + left: React.PropTypes.number.isRequired, + "top": React.PropTypes.number.isRequired, + width: React.PropTypes.number.isRequired, + height: React.PropTypes.number.isRequired, + horizontalDirection: React.PropTypes.oneOf( + ["left", "right"] + ).isRequired, + verticalDirection: React.PropTypes.oneOf( + ["top", "bottom"] + ).isRequired, + }, -},{"valid-css-props":45}],113:[function(require,module,exports){ -/** @jsx React.DOM */ -/** - * For math rendered using KaTex and/or MathJax. Use me like 2x + 3. - */ -// TODO(joel) - require MathJax / katex so they don't have to be global + render: function() { + var borderLeft, borderRight, borderTop, borderBottom; -var React = require('react'); + var hBorder = (this.props.width + "px solid transparent"); + if (this.props.horizontalDirection === "right") { + borderLeft = hBorder; + } else { + borderRight = hBorder; + } -var pendingScripts = []; -var needsProcess = false; -var timeout = null; + var vBorder = (this.props.height + "px solid " + this.props.color); + if (this.props.verticalDirection === "top") { + borderTop = vBorder; + } else { + borderBottom = vBorder; + } -function process(script, callback) { - pendingScripts.push(script); - if (!needsProcess) { - needsProcess = true; - timeout = setTimeout(doProcess, 0, callback); + return React.DOM.div( {style:{ + display: "block", + height: 0, + width: 0, + position: "absolute", + left: this.props.left, + "top": this.props["top"], + borderLeft: borderLeft, + borderRight: borderRight, + borderTop: borderTop, + borderBottom: borderBottom + }} ); } -} - -function doProcess(callback) { - MathJax.Hub.Queue(function() { - var oldElementScripts = MathJax.Hub.elementScripts; - MathJax.Hub.elementScripts = function(element) { - var scripts = pendingScripts; - pendingScripts = []; - needsProcess = false; - return scripts; - }; +}); - try { - return MathJax.Hub.Process(null, callback); - } catch (e) { - // IE8 requires `catch` in order to use `finally` - throw e; - } finally { - MathJax.Hub.elementScripts = oldElementScripts; - } - }); -} +var TooltipArrow = React.createClass({displayName: 'TooltipArrow', + propTypes: { + position: React.PropTypes.string, + visibility: React.PropTypes.string, + left: React.PropTypes.number, + "top": React.PropTypes.number, + color: React.PropTypes.string.isRequired, // a css color + border: React.PropTypes.string.isRequired, // a css color + width: React.PropTypes.number.isRequired, + height: React.PropTypes.number.isRequired, + horizontalDirection: React.PropTypes.oneOf( + ["left", "right"] + ).isRequired, + verticalDirection: React.PropTypes.oneOf( + ["top", "bottom"] + ).isRequired + }, -var TeX = React.createClass({displayName: 'TeX', getDefaultProps: function() { return { - // Called after math is rendered or re-rendered - onRender: function() {} + position: "relative", + visibility: "visible", + left: 0, + "top": 0 }; }, + // TODO(jack): Think about adding a box-shadow to the triangle here + // See http://css-tricks.com/triangle-with-shadow/ render: function() { - return React.DOM.span( {style:this.props.style}, - React.DOM.span( {ref:"mathjax"} ), - React.DOM.span( {ref:"katex"} ) - ); - }, - - componentDidMount: function() { - var text = this.props.children; - var onRender = this.props.onRender; - - try { - var katexHolder = this.refs.katex.getDOMNode(); - katex.process(text, katexHolder); - onRender(); - return; - } catch (e) { - /* jshint -W103 */ - if (e.__proto__ !== katex.ParseError.prototype) { - /* jshint +W103 */ - throw e; - } - } - - this.setScriptText(text); - process(this.script, onRender); - }, - - componentDidUpdate: function(prevProps, prevState) { - var oldText = prevProps.children; - var newText = this.props.children; - var onRender = this.props.onRender; - - if (oldText !== newText) { - try { - var katexHolder = this.refs.katex.getDOMNode(); - katex.process(newText, katexHolder); - if (this.script) { - var jax = MathJax.Hub.getJaxFor(this.script); - if (jax) { - jax.Remove(); - } - } - onRender(); - return; - } catch (e) { - /* jshint -W103 */ - if (e.__proto__ !== katex.ParseError.prototype) { - /* jshint +W103 */ - throw e; - } - } - - $(this.refs.katex.getDOMNode()).empty(); - - if (this.script) { - var component = this; - MathJax.Hub.Queue(function() { - var jax = MathJax.Hub.getJaxFor(component.script); - if (jax) { - return jax.Text(newText, onRender); - } else { - component.setScriptText(newText); - process(component.script, onRender); - } - }); - } else { - this.setScriptText(newText); - process(this.script, onRender); - } - } - }, - - setScriptText: function(text) { - if (!this.script) { - this.script = document.createElement("script"); - this.script.type = "math/tex"; - this.refs.mathjax.getDOMNode().appendChild(this.script); - } - if ("text" in this.script) { - // IE8, etc - this.script.text = text; - } else { - this.script.textContent = text; - } - }, - - componentWillUnmount: function() { - if (this.script) { - var jax = MathJax.Hub.getJaxFor(this.script); - if (jax) { - jax.Remove(); - } - } - } -}); - -module.exports = TeX; - -},{"react":115}],114:[function(require,module,exports){ -/** @jsx React.DOM */ - -var React = require("react"); -var _ = require("underscore"); - -// TODO(joel/jack) fix z-index issues https://s3.amazonaws.com/uploads.hipchat.com/6574/29028/yOApjwmgiMhEZYJ/Screen%20Shot%202014-05-30%20at%203.34.18%20PM.png -// z-index: 3 on perseus-formats-tooltip seemed to work - -/** - * A generic tooltip library for React.js - * - * This should eventually end up in react-components - * - * Interface: ({a, b} means one of a or b) - * var Tooltip = require("./tooltip.jsx"); - * - * - * - * - * - * - * To show/hide the tooltip, the parent component should call the - * .show() and .hide() methods of the tooltip when appropriate. - * (These are usually set up as handlers of events on the target element.) - * - * Notes: - * className should not specify a border; that is handled by borderColor - * so that the arrow and tooltip match - */ - -// __,,--``\\ -// _,,-''`` \\ , -// '----------_.------'-.___|\__ -// _.--''`` `)__ )__ @\__ -// ( .. ''---/___,,E/__,E'------` -// `-''`'' -// Here be dragons. - -var zIndex = 10; - -var Triangle = React.createClass({displayName: 'Triangle', - propTypes: { - color: React.PropTypes.string.isRequired, - left: React.PropTypes.number.isRequired, - "top": React.PropTypes.number.isRequired, - width: React.PropTypes.number.isRequired, - height: React.PropTypes.number.isRequired, - horizontalDirection: React.PropTypes.oneOf( - ["left", "right"] - ).isRequired, - verticalDirection: React.PropTypes.oneOf( - ["top", "bottom"] - ).isRequired, - }, - - render: function() { - var borderLeft, borderRight, borderTop, borderBottom; - - var hBorder = (this.props.width + "px solid transparent"); - if (this.props.horizontalDirection === "right") { - borderLeft = hBorder; - } else { - borderRight = hBorder; - } - - var vBorder = (this.props.height + "px solid " + this.props.color); - if (this.props.verticalDirection === "top") { - borderTop = vBorder; - } else { - borderBottom = vBorder; - } - - return React.DOM.div( {style:{ - display: "block", - height: 0, - width: 0, - position: "absolute", - left: this.props.left, - "top": this.props["top"], - borderLeft: borderLeft, - borderRight: borderRight, - borderTop: borderTop, - borderBottom: borderBottom - }} ); - } -}); - -var TooltipArrow = React.createClass({displayName: 'TooltipArrow', - propTypes: { - position: React.PropTypes.string, - visibility: React.PropTypes.string, - left: React.PropTypes.number, - "top": React.PropTypes.number, - color: React.PropTypes.string.isRequired, // a css color - border: React.PropTypes.string.isRequired, // a css color - width: React.PropTypes.number.isRequired, - height: React.PropTypes.number.isRequired, - horizontalDirection: React.PropTypes.oneOf( - ["left", "right"] - ).isRequired, - verticalDirection: React.PropTypes.oneOf( - ["top", "bottom"] - ).isRequired - }, - - getDefaultProps: function() { - return { - position: "relative", - visibility: "visible", - left: 0, - "top": 0 - }; - }, - - // TODO(jack): Think about adding a box-shadow to the triangle here - // See http://css-tricks.com/triangle-with-shadow/ - render: function() { - var isRight = (this.props.horizontalDirection === "right"); - var isTop = (this.props.verticalDirection === "top"); + var isRight = (this.props.horizontalDirection === "right"); + var isTop = (this.props.verticalDirection === "top"); var frontTopOffset = isTop ? 0 : 1; var borderTopOffset = isTop ? 0 : -1; @@ -3459,244 +2984,702 @@ var HORIZONTAL_CORNERS = { targetLeft: 0, }, - right: { - targetLeft: "100%", - } -}; + right: { + targetLeft: "100%", + } +}; + +var HORIZONTAL_ALIGNMNENTS = { + left: { + tooltipLeft: 0, + arrowLeft: function(arrowSize) {return 0;} + }, + right: { + tooltipLeft: "-100%", + arrowLeft: function(arrowSize) {return -arrowSize - 2;} + } +}; + + +var Tooltip = React.createClass({displayName: 'Tooltip', + propTypes: { + show: React.PropTypes.bool.isRequired, + className: React.PropTypes.string, + arrowSize: React.PropTypes.number, + borderColor: React.PropTypes.string, + verticalPosition: React.PropTypes.oneOf( + _.keys(VERTICAL_CORNERS) + ), + horizontalPosition: React.PropTypes.oneOf( + _.keys(HORIZONTAL_CORNERS) + ), + horizontalAlign: React.PropTypes.oneOf( + _.keys(HORIZONTAL_ALIGNMNENTS) + ), + children: React.PropTypes.arrayOf( + React.PropTypes.component + ).isRequired + }, + + getDefaultProps: function() { + return { + className: "", + arrowSize: 10, + borderColor: "#ccc", + verticalPosition: "bottom", + horizontalPosition: "left", + horizontalAlign: "left" + }; + }, + + getInitialState: function() { + return { + height: null // used for offsetting "top" positioned tooltips + }; + }, + + componentWillReceiveProps: function() { + // If the contents have changed, reset our measure of the height + this.setState({height: null}); + }, + + render: function() { + var isTooltipAbove = this.props.verticalPosition === "top"; + + /* We wrap the entire output in a span so that it displays inline */ + return React.DOM.span(null, + isTooltipAbove && this._renderToolTipDiv(isTooltipAbove), + + /* We wrap our input in a div so that we can put the tooltip in a + div above/below it */ + React.DOM.div(null, + _.first(this.props.children) + ), + + !isTooltipAbove && this._renderToolTipDiv() + ); + }, + + _renderToolTipDiv: function(isTooltipAbove) { + var settings = _.extend({}, + HORIZONTAL_CORNERS[this.props.horizontalPosition], + HORIZONTAL_ALIGNMNENTS[this.props.horizontalAlign], + VERTICAL_CORNERS[this.props.verticalPosition] + ); + + var arrowAbove; + var arrowBelow; + + if (isTooltipAbove) { + // We put an absolutely positioned arrow in the correct place + arrowAbove = TooltipArrow( + {verticalDirection:"top", + horizontalDirection:this.props.horizontalAlign, + position:"absolute", + color:"white", + border:this.props.borderColor, + left:settings.arrowLeft(this.props.arrowSize), + top:-this.props.arrowSize + 2, + width:this.props.arrowSize, + height:this.props.arrowSize, + zIndex:zIndex} ); + + // And we use a visibility: hidden arrow below to shift up the + // content by the correct amount + arrowBelow = TooltipArrow( + {verticalDirection:"top", + horizontalDirection:this.props.horizontalAlign, + visibility:"hidden", + color:"white", + border:this.props.borderColor, + left:settings.arrowLeft(this.props.arrowSize), + top:-1, + width:this.props.arrowSize, + height:this.props.arrowSize, + zIndex:zIndex} ); + } else { + arrowAbove = TooltipArrow( + {verticalDirection:"bottom", + horizontalDirection:this.props.horizontalAlign, + color:"white", + border:this.props.borderColor, + left:settings.arrowLeft(this.props.arrowSize), + top:-1, + width:this.props.arrowSize, + height:this.props.arrowSize, + zIndex:zIndex} ); + + arrowBelow = null; + } + + /* A positioned div below the input to be the parent for our + tooltip */ + return React.DOM.div( {style:{ + position: "relative", + height: 0, + display: this.props.show ? "block" : "none", + }}, + React.DOM.div( {ref:"tooltipContainer", className:"tooltipContainer", style:{ + position: "absolute", + // height must start out undefined, not null, so that + // we can measure the actual height with jquery. + // This is used to position the tooltip with top: -100% + // when in verticalPosition: "top" mode + height: this.state.height || undefined, + left: settings.targetLeft + }}, + arrowAbove, + + /* The contents of the tooltip */ + React.DOM.div( {className:this.props.className, + ref:"tooltipContent", + style:{ + position: "relative", + "top": settings["top"], + "left": settings.tooltipLeft, + border: "1px solid " + this.props.borderColor, + "-webkit-box-shadow": "0 1px 3px " + + this.props.borderColor, + "-moz-box-shadow": "0 1px 3px " + + this.props.borderColor, + boxShadow: "0 1px 3px " + + this.props.borderColor, + zIndex: zIndex - 1 + }}, + _.rest(this.props.children) + ), + + arrowBelow + ) + ); + }, + + componentDidMount: function() { + this._updateHeight(); + }, + + componentDidUpdate: function() { + this._updateHeight(); + }, + + _updateHeight: function() { + var height = this.refs.tooltipContainer.getDOMNode().offsetHeight; + if (height !== this.state.height) { + this.setState({height:height}); + } + } +}); + +// Sorry. // Apology-Oriented-Programming +module.exports = Tooltip; + +},{"react":45,"underscore":46}],45:[function(require,module,exports){ +/* This note applies to rcss, react, and underscore. + * + * We're faking a node module for this package by just exporting the global. + * There are a few complications which led us to this solution as a temporary + * fix. + * + * - Browserify can slow down a lot when you include the other packages (and + * their dependency graphs). We were also battling general browserify + * slowness at this time - browserify 3.4.0 is "good" but later versions + * (3.53 if I remember correctly) are terribly slow (on the order of 20x + * slower). + * + * - I'm not clear on the details of packaging this so we don't duplicate + * dependencies anywhere. For instance when packaging perseus for webapp we + * need to be careful not to include packages like underscore from our + * dependencies or from the packages we depend on. (note: this is a very good + * opportunity to either explain how existing tools solve the problem or + * create a new tool to solve it) + * + * - Joel (and Jack) + */ +module.exports = window.React; + +},{}],46:[function(require,module,exports){ +/* This note applies to rcss, react, and underscore. + * + * We're faking a node module for this package by just exporting the global. + * There are a few complications which led us to this solution as a temporary + * fix. + * + * - Browserify can slow down a lot when you include the other packages (and + * their dependency graphs). We were also battling general browserify + * slowness at this time - browserify 3.4.0 is "good" but later versions + * (3.53 if I remember correctly) are terribly slow (on the order of 20x + * slower). + * + * - I'm not clear on the details of packaging this so we don't duplicate + * dependencies anywhere. For instance when packaging perseus for webapp we + * need to be careful not to include packages like underscore from our + * dependencies or from the packages we depend on. (note: this is a very good + * opportunity to either explain how existing tools solve the problem or + * create a new tool to solve it) + * + * - Joel (and Jack) + */ +module.exports = window._; + +},{}],47:[function(require,module,exports){ +var _validCSSProps = { + 'alignment-adjust': true, + 'alignment-baseline': true, + 'animation': true, + 'animation-delay': true, + 'animation-direction': true, + 'animation-duration': true, + 'animation-iteration-count': true, + 'animation-name': true, + 'animation-play-state': true, + 'animation-timing-function': true, + 'appearance': true, + 'backface-visibility': true, + 'background': true, + 'background-attachment': true, + 'background-clip': true, + 'background-color': true, + 'background-image': true, + 'background-origin': true, + 'background-position': true, + 'background-repeat': true, + 'background-size': true, + 'baseline-shift': true, + 'bookmark-label': true, + 'bookmark-level': true, + 'bookmark-target': true, + 'border': true, + 'border-bottom': true, + 'border-bottom-color': true, + 'border-bottom-left-radius': true, + 'border-bottom-right-radius': true, + 'border-bottom-style': true, + 'border-bottom-width': true, + 'border-collapse': true, + 'border-color': true, + 'border-image': true, + 'border-image-outset': true, + 'border-image-repeat': true, + 'border-image-slice': true, + 'border-image-source': true, + 'border-image-width': true, + 'border-left': true, + 'border-left-color': true, + 'border-left-style': true, + 'border-left-width': true, + 'border-radius': true, + 'border-right': true, + 'border-right-color': true, + 'border-right-style': true, + 'border-right-width': true, + 'border-spacing': true, + 'border-style': true, + 'border-top': true, + 'border-top-color': true, + 'border-top-left-radius': true, + 'border-top-right-radius': true, + 'border-top-style': true, + 'border-top-width': true, + 'border-width': true, + 'bottom': true, + 'box-align': true, + 'box-decoration-break': true, + 'box-direction': true, + 'box-flex': true, + 'box-flex-group': true, + 'box-lines': true, + 'box-ordinal-group': true, + 'box-orient': true, + 'box-pack': true, + 'box-shadow': true, + 'box-sizing': true, + 'caption-side': true, + 'clear': true, + 'clip': true, + 'color': true, + 'color-profile': true, + 'column-count': true, + 'column-fill': true, + 'column-gap': true, + 'column-rule': true, + 'column-rule-color': true, + 'column-rule-style': true, + 'column-rule-width': true, + 'column-span': true, + 'column-width': true, + 'columns': true, + 'content': true, + 'counter-increment': true, + 'counter-reset': true, + 'crop': true, + 'cursor': true, + 'direction': true, + 'display': true, + 'dominant-baseline': true, + 'drop-initial-after-adjust': true, + 'drop-initial-after-align': true, + 'drop-initial-before-adjust': true, + 'drop-initial-before-align': true, + 'drop-initial-size': true, + 'drop-initial-value': true, + 'empty-cells': true, + 'fit': true, + 'fit-position': true, + 'float': true, + 'float-offset': true, + 'font': true, + 'font-family': true, + 'font-size': true, + 'font-size-adjust': true, + 'font-stretch': true, + 'font-style': true, + 'font-variant': true, + 'font-weight': true, + 'grid-columns': true, + 'grid-rows': true, + 'hanging-punctuation': true, + 'height': true, + 'hyphenate-after': true, + 'hyphenate-before': true, + 'hyphenate-character': true, + 'hyphenate-lines': true, + 'hyphenate-resource': true, + 'hyphens': true, + 'icon': true, + 'image-orientation': true, + 'image-resolution': true, + 'inline-box-align': true, + 'left': true, + 'letter-spacing': true, + 'line-height': true, + 'line-stacking': true, + 'line-stacking-ruby': true, + 'line-stacking-shift': true, + 'line-stacking-strategy': true, + 'list-style': true, + 'list-style-image': true, + 'list-style-position': true, + 'list-style-type': true, + 'margin': true, + 'margin-bottom': true, + 'margin-left': true, + 'margin-right': true, + 'margin-top': true, + 'mark': true, + 'mark-after': true, + 'mark-before': true, + 'marks': true, + 'marquee-direction': true, + 'marquee-play-count': true, + 'marquee-speed': true, + 'marquee-style': true, + 'max-height': true, + 'max-width': true, + 'min-height': true, + 'min-width': true, + 'move-to': true, + 'nav-down': true, + 'nav-index': true, + 'nav-left': true, + 'nav-right': true, + 'nav-up': true, + 'opacity': true, + 'orphans': true, + 'outline': true, + 'outline-color': true, + 'outline-offset': true, + 'outline-style': true, + 'outline-width': true, + 'overflow': true, + 'overflow-style': true, + 'overflow-x': true, + 'overflow-y': true, + 'padding': true, + 'padding-bottom': true, + 'padding-left': true, + 'padding-right': true, + 'padding-top': true, + 'page': true, + 'page-break-after': true, + 'page-break-before': true, + 'page-break-inside': true, + 'page-policy': true, + 'perspective': true, + 'perspective-origin': true, + 'phonemes': true, + 'position': true, + 'punctuation-trim': true, + 'quotes': true, + 'rendering-intent': true, + 'resize': true, + 'rest': true, + 'rest-after': true, + 'rest-before': true, + 'right': true, + 'rotation': true, + 'rotation-point': true, + 'ruby-align': true, + 'ruby-overhang': true, + 'ruby-position': true, + 'ruby-span': true, + 'size': true, + 'string-set': true, + 'table-layout': true, + 'target': true, + 'target-name': true, + 'target-new': true, + 'target-position': true, + 'text-align': true, + 'text-align-last': true, + 'text-decoration': true, + 'text-height': true, + 'text-indent': true, + 'text-justify': true, + 'text-outline': true, + 'text-overflow': true, + 'text-shadow': true, + 'text-transform': true, + 'text-wrap': true, + 'top': true, + 'transform': true, + 'transform-origin': true, + 'transform-style': true, + 'transition': true, + 'transition-delay': true, + 'transition-duration': true, + 'transition-property': true, + 'transition-timing-function': true, + 'unicode-bidi': true, + 'user-select': true, + 'vertical-align': true, + 'visibility': true, + 'voice-balance': true, + 'voice-duration': true, + 'voice-pitch': true, + 'voice-pitch-range': true, + 'voice-rate': true, + 'voice-stress': true, + 'voice-volume': true, + 'white-space': true, + 'widows': true, + 'width': true, + 'word-break': true, + 'word-spacing': true, + 'word-wrap': true, + 'z-index': true +} + +var vendorPrefixRegEx = /^-.+-/; -var HORIZONTAL_ALIGNMNENTS = { - left: { - tooltipLeft: 0, - arrowLeft: function(arrowSize) {return 0;} - }, - right: { - tooltipLeft: "-100%", - arrowLeft: function(arrowSize) {return -arrowSize - 2;} - } +module.exports = function(prop) { + if (prop[0] === '-') { + return !!_validCSSProps[prop.replace(vendorPrefixRegEx, '')]; + } + return !!_validCSSProps[prop]; }; +},{}],48:[function(require,module,exports){ +var every = require('lodash.every'); -var Tooltip = React.createClass({displayName: 'Tooltip', - propTypes: { - show: React.PropTypes.bool.isRequired, - className: React.PropTypes.string, - arrowSize: React.PropTypes.number, - borderColor: React.PropTypes.string, - verticalPosition: React.PropTypes.oneOf( - _.keys(VERTICAL_CORNERS) - ), - horizontalPosition: React.PropTypes.oneOf( - _.keys(HORIZONTAL_CORNERS) - ), - horizontalAlign: React.PropTypes.oneOf( - _.keys(HORIZONTAL_ALIGNMNENTS) - ), - children: React.PropTypes.arrayOf( - React.PropTypes.component - ).isRequired - }, - - getDefaultProps: function() { - return { - className: "", - arrowSize: 10, - borderColor: "#ccc", - verticalPosition: "bottom", - horizontalPosition: "left", - horizontalAlign: "left" - }; - }, - - getInitialState: function() { - return { - height: null // used for offsetting "top" positioned tooltips - }; - }, - - componentWillReceiveProps: function() { - // If the contents have changed, reset our measure of the height - this.setState({height: null}); - }, +function isValidRatio(ratio) { + var re = /\d+\/\d+/; + return !!ratio.match(re); +} - render: function() { - var isTooltipAbove = this.props.verticalPosition === "top"; +function isValidInteger(integer) { + var re = /\d+/; + return !!integer.match(re); +} - /* We wrap the entire output in a span so that it displays inline */ - return React.DOM.span(null, - isTooltipAbove && this._renderToolTipDiv(isTooltipAbove), +function isValidLength(length) { + var re = /\d+(?:ex|em|ch|rem|vh|vw|vmin|vmax|px|mm|cm|in|pt|pc)?$/; + return !!length.match(re); +} - /* We wrap our input in a div so that we can put the tooltip in a - div above/below it */ - React.DOM.div(null, - _.first(this.props.children) - ), +function isValidOrientation(orientation) { + return orientation === 'landscape' || orientation === 'portrait'; +} - !isTooltipAbove && this._renderToolTipDiv() - ); - }, +function isValidScan(scan) { + return scan === 'progressive' || scan === 'interlace'; +} - _renderToolTipDiv: function(isTooltipAbove) { - var settings = _.extend({}, - HORIZONTAL_CORNERS[this.props.horizontalPosition], - HORIZONTAL_ALIGNMNENTS[this.props.horizontalAlign], - VERTICAL_CORNERS[this.props.verticalPosition] - ); +function isValidResolution(resolution) { + var re = /(?:\+|-)?(?:\d+|\d*\.\d+)(?:e\d+)?(?:dpi|dpcm|dppx)$/; + return !!resolution.match(re); +} - var arrowAbove; - var arrowBelow; +function isValidValue(value) { + return value != null && typeof value !== 'boolean' && value !== ''; +} - if (isTooltipAbove) { - // We put an absolutely positioned arrow in the correct place - arrowAbove = TooltipArrow( - {verticalDirection:"top", - horizontalDirection:this.props.horizontalAlign, - position:"absolute", - color:"white", - border:this.props.borderColor, - left:settings.arrowLeft(this.props.arrowSize), - top:-this.props.arrowSize + 2, - width:this.props.arrowSize, - height:this.props.arrowSize, - zIndex:zIndex} ); +var _mediaFeatureValidator = { + 'width': isValidLength, + 'min-width': isValidLength, + 'max-width': isValidLength, + 'height': isValidLength, + 'min-height': isValidLength, + 'max-height': isValidLength, + 'device-width': isValidLength, + 'min-device-width': isValidLength, + 'max-device-width': isValidLength, + 'device-height': isValidLength, + 'min-device-height': isValidLength, + 'max-device-height': isValidLength, + 'aspect-ratio': isValidRatio, + 'min-aspect-ratio': isValidRatio, + 'max-aspect-ratio': isValidRatio, + 'device-aspect-ratio': isValidRatio, + 'min-device-aspect-ratio': isValidRatio, + 'max-device-aspect-ratio': isValidRatio, + 'color': isValidValue, + 'min-color': isValidValue, + 'max-color': isValidValue, + 'color-index': isValidInteger, + 'min-color-index': isValidInteger, + 'max-color-index': isValidInteger, + 'monochrome': isValidInteger, + 'min-monochrome': isValidInteger, + 'max-monochrome': isValidInteger, + 'resolution': isValidResolution, + 'min-resolution': isValidResolution, + 'max-resolution': isValidResolution, + 'scan': isValidScan, + 'grid': isValidInteger, + 'orientation': isValidOrientation +}; - // And we use a visibility: hidden arrow below to shift up the - // content by the correct amount - arrowBelow = TooltipArrow( - {verticalDirection:"top", - horizontalDirection:this.props.horizontalAlign, - visibility:"hidden", - color:"white", - border:this.props.borderColor, - left:settings.arrowLeft(this.props.arrowSize), - top:-1, - width:this.props.arrowSize, - height:this.props.arrowSize, - zIndex:zIndex} ); - } else { - arrowAbove = TooltipArrow( - {verticalDirection:"bottom", - horizontalDirection:this.props.horizontalAlign, - color:"white", - border:this.props.borderColor, - left:settings.arrowLeft(this.props.arrowSize), - top:-1, - width:this.props.arrowSize, - height:this.props.arrowSize, - zIndex:zIndex} ); +var _validMediaFeatures = { + 'width': true, + 'min-width': true, + 'max-width': true, + 'height': true, + 'min-height': true, + 'max-height': true, + 'device-width': true, + 'min-device-width': true, + 'max-device-width': true, + 'device-height': true, + 'min-device-height': true, + 'max-device-height': true, + 'aspect-ratio': true, + 'min-aspect-ratio': true, + 'max-aspect-ratio': true, + 'device-aspect-ratio': true, + 'min-device-aspect-ratio': true, + 'max-device-aspect-ratio': true, + 'color': true, + 'min-color': true, + 'max-color': true, + 'color-index': true, + 'min-color-index': true, + 'max-color-index': true, + 'monochrome': true, + 'min-monochrome': true, + 'max-monochrome': true, + 'resolution': true, + 'min-resolution': true, + 'max-resolution': true, + 'scan': true, + 'grid': true, + 'orientation': true +}; - arrowBelow = null; - } +var _validMediaTypes = { + 'all': true, + 'aural': true, + 'braille': true, + 'handheld': true, + 'print': true, + 'projection': true, + 'screen': true, + 'tty': true, + 'tv': true, + 'embossed': true +}; - /* A positioned div below the input to be the parent for our - tooltip */ - return React.DOM.div( {style:{ - position: "relative", - height: 0, - display: this.props.show ? "block" : "none", - }}, - React.DOM.div( {ref:"tooltipContainer", className:"tooltipContainer", style:{ - position: "absolute", - // height must start out undefined, not null, so that - // we can measure the actual height with jquery. - // This is used to position the tooltip with top: -100% - // when in verticalPosition: "top" mode - height: this.state.height || undefined, - left: settings.targetLeft - }}, - arrowAbove, +var _validQualifiers = { + 'not': true, + 'only': true +}; - /* The contents of the tooltip */ - React.DOM.div( {className:this.props.className, - ref:"tooltipContent", - style:{ - position: "relative", - "top": settings["top"], - "left": settings.tooltipLeft, - border: "1px solid " + this.props.borderColor, - "-webkit-box-shadow": "0 1px 3px " + - this.props.borderColor, - "-moz-box-shadow": "0 1px 3px " + - this.props.borderColor, - boxShadow: "0 1px 3px " + - this.props.borderColor, - zIndex: zIndex - 1 - }}, - _.rest(this.props.children) - ), +function isValidFeature(feature) { + return !!_validMediaFeatures[feature]; +} - arrowBelow - ) - ); - }, +function isValidQualifier(qualifier) { + return !!_validQualifiers[qualifier]; +} - componentDidMount: function() { - this._updateHeight(); - }, +function isValidMediaType(mediaType) { + return !!_validMediaTypes[mediaType]; +} - componentDidUpdate: function() { - this._updateHeight(); - }, +function isValidQualifiedMediaType(mediaType) { + var terms = mediaType.trim().split(/\s+/); + switch (terms.length) { + case 1: + return isValidMediaType(terms[0]); + case 2: + return isValidQualifier(terms[0]) && isValidMediaType(terms[1]); + default: + return false; + } +} - _updateHeight: function() { - var height = this.refs.tooltipContainer.getDOMNode().offsetHeight; - if (height !== this.state.height) { - this.setState({height:height}); - } +function isValidExpression(expression) { + if (expression.length < 2) { + return false; } -}); -// Sorry. // Apology-Oriented-Programming -module.exports = Tooltip; + // Parentheses are required around expressions + if (expression[0] !== '(' || expression[expression.length - 1] !== ')') { + return false; + } -},{"react":115,"underscore":116}],115:[function(require,module,exports){ -/* This note applies to rcss, react, and underscore. - * - * We're faking a node module for this package by just exporting the global. - * There are a few complications which led us to this solution as a temporary - * fix. - * - * - Browserify can slow down a lot when you include the other packages (and - * their dependency graphs). We were also battling general browserify - * slowness at this time - browserify 3.4.0 is "good" but later versions - * (3.53 if I remember correctly) are terribly slow (on the order of 20x - * slower). - * - * - I'm not clear on the details of packaging this so we don't duplicate - * dependencies anywhere. For instance when packaging perseus for webapp we - * need to be careful not to include packages like underscore from our - * dependencies or from the packages we depend on. (note: this is a very good - * opportunity to either explain how existing tools solve the problem or - * create a new tool to solve it) - * - * - Joel (and Jack) - */ -module.exports = window.React; + // Remove parentheses and spacess + expression = expression.substring(1, expression.length - 1); -},{}],116:[function(require,module,exports){ -/* This note applies to rcss, react, and underscore. - * - * We're faking a node module for this package by just exporting the global. - * There are a few complications which led us to this solution as a temporary - * fix. - * - * - Browserify can slow down a lot when you include the other packages (and - * their dependency graphs). We were also battling general browserify - * slowness at this time - browserify 3.4.0 is "good" but later versions - * (3.53 if I remember correctly) are terribly slow (on the order of 20x - * slower). - * - * - I'm not clear on the details of packaging this so we don't duplicate - * dependencies anywhere. For instance when packaging perseus for webapp we - * need to be careful not to include packages like underscore from our - * dependencies or from the packages we depend on. (note: this is a very good - * opportunity to either explain how existing tools solve the problem or - * create a new tool to solve it) - * - * - Joel (and Jack) - */ -module.exports = window._; + // Is there a value to accompany the media feature? + var featureAndValue = expression.split(/\s*:\s*/); + switch (featureAndValue.length) { + case 1: + var feature = featureAndValue[0].trim(); + return isValidFeature(feature); + case 2: + var feature = featureAndValue[0].trim(); + var value = featureAndValue[1].trim(); + return isValidFeature(feature) && + _mediaFeatureValidator[feature](value); + default: + return false; + } +} + +function isValidMediaQuery(query) { + var andSplitter = /\s+and\s+/; + var queryTerms = query.split(andSplitter); + return (isValidQualifiedMediaType(queryTerms[0]) || + isValidExpression(queryTerms[0])) && + every(queryTerms.slice(1), isValidExpression); +} + +function isValidMediaQueryList(mediaQuery) { + mediaQuery = mediaQuery.toLowerCase(); + + if (mediaQuery.substring(0, 6) !== '@media') { + return false; + } + + var commaSplitter = /\s*,\s*/; + var queryList = mediaQuery.substring(7, mediaQuery.length) + .split(commaSplitter); + return every(queryList, isValidMediaQuery); +} -},{}],117:[function(require,module,exports){ +module.exports = isValidMediaQueryList + +},{"lodash.every":26}],49:[function(require,module,exports){ var Widgets = require("./widgets.js"); _.each([ @@ -3720,12 +3703,14 @@ _.each([ require("./widgets/sorter.jsx"), require("./widgets/table.jsx"), require("./widgets/transformer.jsx"), - require("./widgets/image.jsx") + require("./widgets/image.jsx"), + require("./widgets/speaking-text-input.jsx"), + require("./widgets/speaking-voice.jsx") ], function(widget) { Widgets.register(widget.name, _.omit(widget, "name")); }); -},{"./widgets.js":171,"./widgets/categorizer.jsx":172,"./widgets/dropdown.jsx":173,"./widgets/example-graphie-widget.jsx":174,"./widgets/example-widget.jsx":175,"./widgets/expression.jsx":176,"./widgets/iframe.jsx":177,"./widgets/image.jsx":178,"./widgets/input-number.jsx":179,"./widgets/interactive-graph.jsx":180,"./widgets/interactive-number-line.jsx":181,"./widgets/lights-puzzle.jsx":182,"./widgets/matcher.jsx":183,"./widgets/measurer.jsx":184,"./widgets/number-line.jsx":185,"./widgets/numeric-input.jsx":186,"./widgets/orderer.jsx":187,"./widgets/plotter.jsx":188,"./widgets/radio.jsx":189,"./widgets/sorter.jsx":190,"./widgets/table.jsx":191,"./widgets/transformer.jsx":192}],118:[function(require,module,exports){ +},{"./widgets.js":103,"./widgets/categorizer.jsx":104,"./widgets/dropdown.jsx":105,"./widgets/example-graphie-widget.jsx":106,"./widgets/example-widget.jsx":107,"./widgets/expression.jsx":108,"./widgets/iframe.jsx":109,"./widgets/image.jsx":110,"./widgets/input-number.jsx":111,"./widgets/interactive-graph.jsx":112,"./widgets/interactive-number-line.jsx":113,"./widgets/lights-puzzle.jsx":114,"./widgets/matcher.jsx":115,"./widgets/measurer.jsx":116,"./widgets/number-line.jsx":117,"./widgets/numeric-input.jsx":118,"./widgets/orderer.jsx":119,"./widgets/plotter.jsx":120,"./widgets/radio.jsx":121,"./widgets/sorter.jsx":122,"./widgets/speaking-text-input.jsx":123,"./widgets/speaking-voice.jsx":124,"./widgets/table.jsx":125,"./widgets/transformer.jsx":126}],50:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -3738,12 +3723,16 @@ var WidgetsInAnswerAreaEditor = ['Image']; var AnswerAreaEditor = React.createClass({displayName: 'AnswerAreaEditor', getDefaultProps: function() { return { - type: "input-number", + type: "multiple", options: {}, calculator: false }; }, + getInitialState: function() { + return {showSolutionArea: this.props.type === "radio" || this.props.type === "input-number"} + }, + render: function() { var cls; if (this.props.type === "multiple") { @@ -3764,36 +3753,27 @@ var AnswerAreaEditor = React.createClass({displayName: 'AnswerAreaEditor', return React.DOM.div( {className:"perseus-answer-editor"}, React.DOM.div( {className:"perseus-answer-options"}, - React.DOM.div(null - ), - React.DOM.div(null, React.DOM.label(null, - ' ',"Answer type:",' ', - React.DOM.select( {value:this.props.type, - onChange:function(e) { - this.props.onChange({ - type: e.target.value, - options: {} - }, function() { - this.refs.editor.focus(); - }.bind(this)); - }.bind(this)}, - React.DOM.option( {value:"radio"}, "Multiple choice"), - React.DOM.option( {value:"input-number"}, "Text input (number)") + this.state.showSolutionArea && React.DOM.div( {className:cls !== Editor ? "perseus-answer-widget" : ""}, + editor ) ) - ) - ), - React.DOM.div( {className:cls !== Editor ? "perseus-answer-widget" : ""}, - editor - ) ); }, + getEditorInAnswerArea: function() { + if (this.refs !== undefined) { + return this.refs.editor; + } else { + return undefined; + } + }, + toJSON: function(skipValidation) { // Could be just _.pick(this.props, "type", "options"); but validation! + var editor = this.getEditorInAnswerArea(); return { type: this.props.type, - options: this.refs.editor.toJSON(skipValidation), + options: editor !== undefined ? this.refs.editor.toJSON(skipValidation) : {}, calculator: this.props.calculator }; } @@ -3801,7 +3781,7 @@ var AnswerAreaEditor = React.createClass({displayName: 'AnswerAreaEditor', module.exports = AnswerAreaEditor; -},{"./editor.jsx":143,"./widgets.js":171,"react":115,"react-components/info-tip":5}],119:[function(require,module,exports){ +},{"./editor.jsx":75,"./widgets.js":103,"react":45,"react-components/info-tip":39}],51:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -4133,6 +4113,32 @@ var AnswerAreaRenderer = React.createClass({displayName: 'AnswerAreaRenderer', this.refs.widget.focus(); }, + showGuess: function(answerData) { + if( !answerData ) + return; + if (answerData instanceof Array) { + // Answer area contains no widgets. + } else if (this.refs.widget.setAnswerFromJSON === undefined) { + // Target widget cannot show answer. + console.log("Target widget cannot show in answerarea",answerData); + return 'no setAnswerFromJSON implemented for widgets in answer area.'; + } else { + console.log("Target widget show in answerarea") + // Just show the given answer. + this.refs.widget.setAnswerFromJSON(answerData); + } + }, + + canShowAllHistoryWidgets: function(answerData) { + if(!answerData) + return true; + if (this.refs.widget.setAnswerFromJSON === undefined) { + console.log('no setAnswerFromJSON implemented for widgets in answer area.'); + return false; + } + return true; + }, + guessAndScore: function() { // TODO(alpert): These should probably have the same signature... if (this.props.type === "multiple") { @@ -4156,7 +4162,7 @@ var AnswerAreaRenderer = React.createClass({displayName: 'AnswerAreaRenderer', module.exports = AnswerAreaRenderer; -},{"./enabled-features.jsx":144,"./perseus-api.jsx":162,"./question-paragraph.jsx":164,"./renderer.jsx":165,"./util.js":168,"./widget-container.jsx":170,"./widgets.js":171,"react":115}],120:[function(require,module,exports){ +},{"./enabled-features.jsx":76,"./perseus-api.jsx":94,"./question-paragraph.jsx":96,"./renderer.jsx":97,"./util.js":100,"./widget-container.jsx":102,"./widgets.js":103,"react":45}],52:[function(require,module,exports){ /** @jsx React.DOM */ /** @@ -4344,7 +4350,7 @@ FancySelect.Option = FancyOption; module.exports = FancySelect; -},{"react":115}],121:[function(require,module,exports){ +},{"react":45}],53:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -4356,6 +4362,7 @@ var NumberInput = require("../components/number-input.jsx"); var PropCheckBox = require("../components/prop-check-box.jsx"); var RangeInput = require("../components/range-input.jsx"); var Util = require("../util.js"); +var BlurInput = require("react-components/blur-input"); var defaultBoxSize = 400; var defaultBackgroundImage = { @@ -4391,7 +4398,7 @@ var GraphSettings = React.createClass({displayName: 'GraphSettings', step: [1, 1], gridStep: [1, 1], snapStep: Util.snapStepFromGridStep( - this.props.gridStep || [1, 1]), + this.gridStep || [1, 1]), valid: true, backgroundImage: defaultBackgroundImage, markings: "graph", @@ -4406,14 +4413,14 @@ var GraphSettings = React.createClass({displayName: 'GraphSettings', return React.DOM.div(null, React.DOM.div( {className:"graph-settings"}, React.DOM.div( {className:"perseus-widget-row"}, - React.DOM.div( {className:"perseus-widget-left-col"}, " x Label", + React.DOM.div( {className:"perseus-widget-left-col"}, " x軸標籤", React.DOM.input( {type:"text", className:"graph-settings-axis-label", ref:"labels-0", onChange:this.changeLabel.bind(this, 0), value:this.state.labelsTextbox[0]} ) ), - React.DOM.div( {className:"perseus-widget-right-col"}, "y Label", + React.DOM.div( {className:"perseus-widget-right-col"}, "y軸標籤", React.DOM.input( {type:"text", className:"graph-settings-axis-label", ref:"labels-1", @@ -4424,58 +4431,53 @@ var GraphSettings = React.createClass({displayName: 'GraphSettings', React.DOM.div( {className:"perseus-widget-row"}, React.DOM.div( {className:"perseus-widget-left-col"}, - "x Range", + "x軸範圍", RangeInput( {value: this.state.rangeTextbox[0], onChange: this.changeRange.bind(this, 0)} ) ), React.DOM.div( {className:"perseus-widget-right-col"}, - "y Range", + "y軸範圍", RangeInput( {value: this.state.rangeTextbox[1], onChange: this.changeRange.bind(this, 1)} ) ) ), React.DOM.div( {className:"perseus-widget-row"}, React.DOM.div( {className:"perseus-widget-left-col"}, - "Tick Step", + "座標間距", RangeInput( {value: this.state.stepTextbox, onChange: this.changeStep} ) ), React.DOM.div( {className:"perseus-widget-right-col"}, - "Grid Step", + "網格間距", RangeInput( {value: this.state.gridStepTextbox, onChange: this.changeGridStep} ) ) ), React.DOM.div( {className:"perseus-widget-row"}, React.DOM.div( {className:"perseus-widget-left-col"}, - "Snap Step", + "答案拖拉間距", RangeInput( {value: this.state.snapStepTextbox, onChange: this.changeSnapStep} ) ) ), React.DOM.div( {className:"perseus-widget-row"}, - React.DOM.label(null, "Markings:",' ', " " ), + React.DOM.label(null, "標記:",' ', " " ), ButtonGroup( {value:this.props.markings, allowEmpty:false, buttons:[ - {value: "graph", text: "Graph"}, - {value: "grid", text: "Grid"}, - {value: "none", text: "None"}], + {value: "graph", text: "座標圖"}, + {value: "grid", text: "僅網格"}, + {value: "none", text: "無"}], onChange:this.change("markings")} ) ) ), React.DOM.div( {className:"image-settings"}, - React.DOM.div(null, "Background image:"), + React.DOM.div(null, "背景圖:"), React.DOM.div(null, "Url:",' ', - React.DOM.input( {type:"text", - className:"graph-settings-background-url", - ref:"bg-url", - defaultValue:this.props.backgroundImage.url, - onKeyPress:this.changeBackgroundUrl, - onBlur:this.changeBackgroundUrl} ), + BlurInput( {value:this.props.backgroundImage.url, + onChange:this.changeBackgroundUrl}), InfoTip(null, - React.DOM.p(null, "Create an image in graphie, or use the \"Add image\""+' '+ - "function to create a background.") + React.DOM.p(null, "請在圖形中增加圖片,或於欄中輸入圖片連結。") ) ), this.props.backgroundImage.url && React.DOM.div(null, @@ -4518,25 +4520,25 @@ var GraphSettings = React.createClass({displayName: 'GraphSettings', this.props.showRuler && React.DOM.div(null, React.DOM.div(null, React.DOM.label(null, - ' ',"Ruler label:",' ', + ' ',"直尺單位:",' ', React.DOM.select( {onChange:this.changeRulerLabel, value:this.props.rulerLabel} , - React.DOM.option( {value:""}, "None"), - React.DOM.optgroup( {label:"Metric"}, + React.DOM.option( {value:""}, "無"), + React.DOM.optgroup( {label:"公制"}, this.renderLabelChoices([ - ["milimeters", "mm"], - ["centimeters", "cm"], - ["meters", "m"], - ["kilometers", "km"] + ["公厘", "mm"], + ["公分", "cm"], + ["公尺", "m"], + ["公里", "km"] ]) ), - React.DOM.optgroup( {label:"Imperial"}, + React.DOM.optgroup( {label:"英制"}, this.renderLabelChoices([ - ["inches", "in"], - ["feet", "ft"], - ["yards", "yd"], - ["miles", "mi"] + ["英吋", "in"], + ["英呎", "ft"], + ["碼", "yd"], + ["英里", "mi"] ]) ) ) @@ -4544,7 +4546,7 @@ var GraphSettings = React.createClass({displayName: 'GraphSettings', ), React.DOM.div(null, React.DOM.label(null, - ' ',"Ruler ticks:",' ', + ' ',"直尺間隔:",' ', React.DOM.select( {onChange:this.changeRulerTicks, value:this.props.rulerTicks} , @@ -4749,36 +4751,26 @@ var GraphSettings = React.createClass({displayName: 'GraphSettings', } }, - changeBackgroundUrl: function(e) { - var self = this; - - // Only continue on blur or "enter" - if (e.type === "keypress" && e.keyCode !== 13) { - return; - } + setUrl: function(url, width, height) { + var image = _.clone(this.props.backgroundImage); + image.url = url; + image.width = width; + image.height = height; + this.props.onChange({ + backgroundImage: image, + markings: url ? "none" : "graph" + }); + }, - var url = self.refs["bg-url"].getDOMNode().value; - var setUrl = function() { - var image = _.clone(self.props.backgroundImage); - image.url = url; - image.width = img.width; - image.height = img.height; - self.props.onChange({ - backgroundImage: image, - markings: url ? "none" : "graph" - }); - }; + changeBackgroundUrl: function(url) { if (url) { - var img = new Image(); - img.onload = setUrl; - img.src = url; + if(this.props.backgroundImage.url != url){ + var img = new Image(); + img.onload = function() {return this.setUrl(url, img.width, img.height);}.bind(this); + img.src = url; + } } else { - var img = { - url: url, - width: 0, - height: 0 - }; - setUrl(); + this.setUrl(url,0,0); } }, @@ -4801,7 +4793,7 @@ var GraphSettings = React.createClass({displayName: 'GraphSettings', module.exports = GraphSettings; -},{"../components/number-input.jsx":129,"../components/prop-check-box.jsx":130,"../components/range-input.jsx":131,"../mixins/changeable.jsx":159,"../util.js":168,"react":115,"react-components/button-group":3,"react-components/info-tip":5}],122:[function(require,module,exports){ +},{"../components/number-input.jsx":61,"../components/prop-check-box.jsx":62,"../components/range-input.jsx":63,"../mixins/changeable.jsx":91,"../util.js":100,"react":45,"react-components/blur-input":36,"react-components/button-group":37,"react-components/info-tip":39}],54:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -4990,7 +4982,7 @@ var Graph = React.createClass({displayName: 'Graph', $(graphieDiv).empty(); var labels = this.props.labels; var range = this.props.range; - var graphie = this._graphie = KhanUtil.createGraphie(graphieDiv); + var graphie = this._graphie = KhanUtil.currentGraph = KhanUtil.createGraphie(graphieDiv); var gridConfig = this._getGridConfig(); graphie.snap = this.props.snapStep; @@ -5109,7 +5101,7 @@ var Graph = React.createClass({displayName: 'Graph', if (this.props.showProtractor) { var coord = this.pointsFromNormalized([[0.50, 0.05]])[0]; - this.protractor = this._graphie.protractor(coord); + this.protractor = this._graphie.Protractor(coord); } }, @@ -5121,7 +5113,7 @@ var Graph = React.createClass({displayName: 'Graph', if (this.props.showRuler) { var coord = this.pointsFromNormalized([[0.50, 0.25]])[0]; var extent = this._graphie.range[0][1] - this._graphie.range[0][0]; - this.ruler = this._graphie.ruler({ + this.ruler = this._graphie.Ruler({ center: coord, label: this.props.rulerLabel, pixelsPerUnit: this._graphie.scale[0], @@ -5140,7 +5132,7 @@ var Graph = React.createClass({displayName: 'Graph', module.exports = Graph; -},{"../util.js":168,"react":115}],123:[function(require,module,exports){ +},{"../util.js":100,"react":45}],55:[function(require,module,exports){ /** @jsx React.DOM */var Util = require("../util.js"); var nestedMap = Util.nestedMap; var deepEq = Util.deepEq; @@ -5271,7 +5263,7 @@ module.exports = { createSimpleClass: createSimpleClass }; -},{"../util.js":168}],124:[function(require,module,exports){ +},{"../util.js":100}],56:[function(require,module,exports){ /** @jsx React.DOM */var GraphieClasses = require("./graphie-classes.jsx"); var Interactive2 = require("../interactive2.js"); var InteractiveUtil = require("../interactive2/interactive-util.js"); @@ -5322,7 +5314,7 @@ module.exports = { MovablePoint: MovablePoint }; -},{"../interactive2.js":148,"../interactive2/interactive-util.js":149,"./graphie-classes.jsx":123}],125:[function(require,module,exports){ +},{"../interactive2.js":80,"../interactive2/interactive-util.js":81,"./graphie-classes.jsx":55}],57:[function(require,module,exports){ /** @jsx React.DOM */ var GraphieClasses = require("./graphie-classes.jsx"); @@ -5450,7 +5442,8 @@ var Graphie = React.createClass({displayName: 'Graphie', onClick: this.props.onClick, onMouseDown: this.props.onMouseDown, onMouseUp: this.props.onMouseUp, - onMouseMove: this.props.onMouseMove + onMouseMove: this.props.onMouseMove, + allowScratchpad: this.props.allowScratchpad }); graphie.snap = this.props.options.snapStep || [1, 1]; this.props.setup(graphie, this.props.options); @@ -5601,7 +5594,7 @@ _.extend(Graphie, Movables); module.exports = Graphie; -},{"../interactive2/interactive-util.js":149,"../util.js":168,"./graphie-classes.jsx":123,"./graphie-movables.jsx":124}],126:[function(require,module,exports){ +},{"../interactive2/interactive-util.js":81,"../util.js":100,"./graphie-classes.jsx":55,"./graphie-movables.jsx":56}],58:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -5761,7 +5754,7 @@ var InputWithExamples = React.createClass({displayName: 'InputWithExamples', module.exports = InputWithExamples; -},{"../perseus-api.jsx":162,"../renderer.jsx":165,"../util.js":168,"./math-input.jsx":127,"./text-input.jsx":134,"react":115,"react-components/tooltip":114}],127:[function(require,module,exports){ +},{"../perseus-api.jsx":94,"../renderer.jsx":97,"../util.js":100,"./math-input.jsx":59,"./text-input.jsx":66,"react":45,"react-components/tooltip":44}],59:[function(require,module,exports){ /** @jsx React.DOM */ // TODO(alex): Package MathQuill @@ -5780,7 +5773,9 @@ var MathInput = React.createClass({displayName: 'MathInput', convertDotToTimes: PT.bool, buttonsVisible: PT.oneOf(['always', 'never', 'focused']), onFocus: PT.func, - onBlur: PT.func + onBlur: PT.func, + buttonSets: TexButtons.buttonSetsType.isRequired, + offsetLeft: PT.number }, render: function() { @@ -5794,11 +5789,22 @@ var MathInput = React.createClass({displayName: 'MathInput', }); var buttons = null; + var button_height = "0px"; if (this._shouldShowButtons()) { buttons = TexButtons( {className:"math-input-buttons absolute", convertDotToTimes:this.props.convertDotToTimes, - onInsert:this.insert} ); + onInsert:this.insert, + sets:this.props.buttonSets} ); + button_height = (6 + 58 * this.props.buttonSets.length).toString() + "px"; + } + var button_left = "0px"; + if(!this.props.inEditor){ + if (this.props.offsetLeft >= 260){ + button_left = "-240px"; + } else if (this.props.offsetLeft > 130 && this.props.offsetLeft < 260){ + button_left = "-120px"; + } } return React.DOM.div( {style:{display: "inline-block"}}, @@ -5808,7 +5814,7 @@ var MathInput = React.createClass({displayName: 'MathInput', onFocus:this.handleFocus, onBlur:this.handleBlur} ) ), - React.DOM.div( {style:{position: "relative"}}, + React.DOM.div( {style:{position: "relative", height: button_height, left: button_left}}, buttons ) ); @@ -5998,7 +6004,7 @@ var MathInput = React.createClass({displayName: 'MathInput', module.exports = MathInput; -},{"./tex-buttons.jsx":133,"react":115,"underscore":116}],128:[function(require,module,exports){ +},{"./tex-buttons.jsx":65,"react":45,"underscore":46}],60:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -6070,7 +6076,7 @@ var MultiButtonGroup = React.createClass({displayName: 'MultiButtonGroup', module.exports = MultiButtonGroup; -},{"react":115}],129:[function(require,module,exports){ +},{"react":45}],61:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -6249,7 +6255,7 @@ var NumberInput = React.createClass({displayName: 'NumberInput', module.exports = NumberInput; -},{"../util.js":168,"react":115}],130:[function(require,module,exports){ +},{"../util.js":100,"react":45}],62:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -6311,7 +6317,7 @@ var PropCheckBox = React.createClass({displayName: 'PropCheckBox', module.exports = PropCheckBox; -},{"react":115}],131:[function(require,module,exports){ +},{"react":45}],63:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -6370,7 +6376,7 @@ var RangeInput = React.createClass({displayName: 'RangeInput', module.exports = RangeInput; -},{"../components/number-input.jsx":129,"react":115}],132:[function(require,module,exports){ +},{"../components/number-input.jsx":61,"react":45}],64:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -6887,7 +6893,7 @@ var Sortable = React.createClass({displayName: 'Sortable', module.exports = Sortable; -},{"../renderer.jsx":165,"../util.js":168,"react":115}],133:[function(require,module,exports){ +},{"../renderer.jsx":97,"../util.js":100,"react":45}],65:[function(require,module,exports){ /** @jsx React.DOM */ var React = require("react"); @@ -6903,38 +6909,42 @@ var symbStyle = { fontSize: "130%" }; // on the page rather than reusing an instance (which will cause an error). // Also, it's useful for things which might look different depending on the // props. -var buttons = [ - // basics - [ - function() {return [React.DOM.span( {style:slightlyBig}, "+"), "+"];}, - function() {return [React.DOM.span( {style:prettyBig}, "-"), "-"];}, +var basic = [ - // TODO(joel) - display as \cdot when appropriate - function(props) { - if (props.convertDotToTimes) { - return [TeX( {style:prettyBig}, "\\times"), "\\times"]; - } else { - return [TeX( {style:prettyBig}, "\\cdot"), "\\cdot"]; - } - }, - function() {return [ - TeX( {style:prettyBig}, "\\frac{□}{□}"), + function() {return [React.DOM.span( {style:slightlyBig}, "+"), "+"];}, + function() {return [React.DOM.span( {style:prettyBig}, "-"), "-"];}, - // If there's something in the input that can become part of a - // fraction, typing "/" puts it in the numerator. If not, typing - // "/" does nothing. In that case, enter a \frac. - function(input) { - var contents = input.latex(); - input.typedText("/"); - if (input.latex() === contents) { - input.cmd("\\frac"); - } + // TODO(joel) - display as \cdot when appropriate + function(props) { + if (props.convertDotToTimes) { + return [TeX( {style:prettyBig}, "\\times"), "\\times"]; + } else { + return [TeX( {style:prettyBig}, "\\cdot"), "\\cdot"]; + } + }, + function() {return [ + TeX( {style:prettyBig}, "\\frac{□}{□}"), + + // If there's something in the input that can become part of a + // fraction, typing "/" puts it in the numerator. If not, typing + // "/" does nothing. In that case, enter a \frac. + function(input) { + var contents = input.latex(); + input.cmd("\\frac"); } - ];} - ], + + ];}, + function() {return [TeX( {style:slightlyBig}, "\\div"), "\\div"];}, + function() {return [TeX( {style:slightlyBig, key:"eq"}, "="), "="];}, + +]; - // relations +var buttonSets = { + + basic:basic, + + relations: [ // [{"="}, "\\eq"], function() {return [TeX(null, "\\neq"), "\\neq"];}, @@ -6944,8 +6954,7 @@ var buttons = [ function() {return [TeX(null, "\\gt"), "\\gt"];}, ], - // trig - [ + trig:[ function() {return [TeX(null, "\\sin"), "\\sin"];}, function() {return [TeX(null, "\\cos"), "\\cos"];}, function() {return [TeX(null, "\\tan"), "\\tan"];}, @@ -6953,13 +6962,10 @@ var buttons = [ function() {return [TeX( {style:symbStyle}, "\\phi"), "\\phi"];} ], - // prealgebra - [ + prealgebra:[ function() {return [TeX(null, "\\sqrt{x}"), "\\sqrt"];}, - // TODO(joel) - how does desmos do this? - // ["\\sqrt[3]{x}", "\\sqrt[3]{x}"], function() {return [ - TeX( {style:slightlyBig}, "□^a"), + TeX( {style:slightlyBig}, "a^□"), function(input) { var contents = input.latex(); input.keystroke("Up"); @@ -6971,14 +6977,28 @@ var buttons = [ function() {return [TeX( {style:slightlyBig}, "\\pi"), "\\pi"];} ] -]; +}; + +//declare buttonSetsType type from buttonSets +var buttonSetsType = React.PropTypes.arrayOf( + React.PropTypes.oneOf(_(buttonSets).keys()) + ); var TexButtons = React.createClass({displayName: 'TexButtons', propTypes: { - onInsert: React.PropTypes.func.isRequired + onInsert: React.PropTypes.func.isRequired, + sets: buttonSetsType.isRequired, }, + render: function() { - var buttonRows = _(buttons).map(function(row) {return row.map(function(symbGen) { + // sort this.props.sets component by key_index of buttonSets + // var sortedButtonSets = _.sortBy(this.props.sets, + // (setName) => _.keys(buttonSets).indexOf(setName)); + + // make buttonSet(checked) by this.props.sets from buttonSets(template) + var buttonSet = _(this.props.sets).map(function(setName) {return buttonSets[setName];}); + + var buttonRows = _(buttonSet).map(function(row) {return row.map(function(symbGen) { // create a (component, thing we should send to mathquill) pair var symbol = symbGen(this.props); return React.DOM.button( {onClick:function() {return this.props.onInsert(symbol[1]);}.bind(this), @@ -6990,18 +7010,25 @@ var TexButtons = React.createClass({displayName: 'TexButtons', }.bind(this));}.bind(this)); var buttonPopup = _(buttonRows).map(function(row, i) { - return React.DOM.div( {className:"clearfix tex-button-row"}, row); - }); + return React.DOM.div( {className:"clearfix tex-button-row", + key:this.props.sets[i]}, + row + ); + }.bind(this)); return React.DOM.div( {className:this.props.className}, buttonPopup ); + }, + statics: { + buttonSets:buttonSets, + buttonSetsType:buttonSetsType } }); module.exports = TexButtons; -},{"react":115,"react-components/tex":113}],134:[function(require,module,exports){ +},{"react":45,"react-components/tex":43}],66:[function(require,module,exports){ /** @jsx React.DOM */ var TextInput = React.createClass({displayName: 'TextInput', @@ -7033,7 +7060,7 @@ var TextInput = React.createClass({displayName: 'TextInput', module.exports = TextInput; -},{}],135:[function(require,module,exports){ +},{}],67:[function(require,module,exports){ /** @jsx React.DOM */ var textWidthCache = {}; @@ -7163,7 +7190,7 @@ var TextListEditor = React.createClass({displayName: 'TextListEditor', module.exports = TextListEditor; -},{}],136:[function(require,module,exports){ +},{}],68:[function(require,module,exports){ /** @jsx React.DOM */ // Responsible for combining the text diffs from text-diff and the widget @@ -7278,7 +7305,7 @@ var RevisionDiff = React.createClass({displayName: 'RevisionDiff', module.exports = RevisionDiff; -},{"./text-diff.jsx":139,"./widget-diff.jsx":141}],137:[function(require,module,exports){ +},{"./text-diff.jsx":71,"./widget-diff.jsx":73}],69:[function(require,module,exports){ /** @jsx React.DOM */// Split a word-wise diff generated by jsdiff into multiple lines, for the // purpose of breaking up the diffs into lines, so that modified lines can be // faintly highlighted @@ -7308,7 +7335,7 @@ var splitDiff = function(diffEntries) { module.exports = splitDiff; -},{}],138:[function(require,module,exports){ +},{}],70:[function(require,module,exports){ /** @jsx React.DOM */var jsdiff = require("../../lib/jsdiff"); var statusFor = function(chunk) { @@ -7378,7 +7405,7 @@ var stringArrayDiff = function(a, b) { module.exports = stringArrayDiff; -},{"../../lib/jsdiff":1}],139:[function(require,module,exports){ +},{"../../lib/jsdiff":1}],71:[function(require,module,exports){ /** @jsx React.DOM */ var diff = require("../../lib/jsdiff"); @@ -7543,7 +7570,7 @@ var TextDiff = React.createClass({displayName: 'TextDiff', module.exports = TextDiff; -},{"../../lib/jsdiff":1,"./split-diff.jsx":137,"./string-array-diff.jsx":138}],140:[function(require,module,exports){ +},{"../../lib/jsdiff":1,"./split-diff.jsx":69,"./string-array-diff.jsx":70}],72:[function(require,module,exports){ /** @jsx React.DOM */var UNCHANGED = "unchanged"; var CHANGED = "changed"; var ADDED = "added"; @@ -7613,7 +7640,7 @@ var performDiff = function(before, after, /* optional */ key) { module.exports = performDiff; -},{}],141:[function(require,module,exports){ +},{}],73:[function(require,module,exports){ /** @jsx React.DOM */ var cx = React.addons.classSet; @@ -7809,7 +7836,7 @@ var WidgetDiff = React.createClass({displayName: 'WidgetDiff', module.exports = WidgetDiff; -},{"./widget-diff-performer.jsx":140}],142:[function(require,module,exports){ +},{"./widget-diff-performer.jsx":72}],74:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -8093,7 +8120,7 @@ var EditorPage = React.createClass({displayName: 'EditorPage', module.exports = EditorPage; -},{"./components/prop-check-box.jsx":130,"./enabled-features.jsx":144,"./hint-editor.jsx":145,"./item-editor.jsx":157,"./item-renderer.jsx":158,"./perseus-api.jsx":162,"react":115}],143:[function(require,module,exports){ +},{"./components/prop-check-box.jsx":62,"./enabled-features.jsx":76,"./hint-editor.jsx":77,"./item-editor.jsx":89,"./item-renderer.jsx":90,"./perseus-api.jsx":94,"react":45}],75:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -8106,7 +8133,12 @@ var DragTarget = require("react-components/drag-target"); var rWidgetSplit = /(\[\[\u2603 [a-z-]+ [0-9]+\]\])/g; // widgets junyi can use now: -var widgetsInEditor = ['image']; +var widgetsInEditor = ['image', 'categorizer', 'dropdown', 'expression', + 'input-number', 'interactive-graph', 'interactive-number-line', + 'lights-puzzle', 'measurer', 'number-line', + 'iframe', 'numeric-input', 'plotter', + 'radio', 'sorter', 'table', 'transformer', 'matcher', + 'speaking-text-input', 'speaking-voice']; var WidgetSelect = React.createClass({displayName: 'WidgetSelect', handleChange: function(e) { @@ -8126,13 +8158,13 @@ var WidgetSelect = React.createClass({displayName: 'WidgetSelect', }, render: function() { var widgets = Widgets.getPublicWidgets(); - var junyiValidWidgets = _.pick(widgets, widgetsInEditor[0]); + var junyiValidWidgets = _.pick(widgets, widgetsInEditor); var orderedWidgetNames = _.sortBy(_.keys(junyiValidWidgets), function(name) { return junyiValidWidgets[name].displayName; }); return React.DOM.select( {onChange:this.handleChange}, - React.DOM.option( {value:""}, "Add a widget","\u2026"), + React.DOM.option( {value:""}, "新增一個 widget","\u2026"), React.DOM.option( {disabled:true}, "--"), _.map(orderedWidgetNames, function(name) { return React.DOM.option( {value:name, key:name}, @@ -8182,7 +8214,7 @@ var WidgetEditor = React.createClass({displayName: 'WidgetEditor', var isUngradedEnabled = (type === "transformer"); var direction = this.state.showWidget ? "down" : "right"; - var gradedPropBox = PropCheckBox( {label:"Graded:", + var gradedPropBox = PropCheckBox( {label:"評分:", graded:upgradedWidgetInfo.graded, onChange:this.props.onChange} ); @@ -8690,7 +8722,7 @@ var Editor = React.createClass({displayName: 'Editor', module.exports = Editor; -},{"./components/prop-check-box.jsx":130,"./util.js":168,"./widgets.js":171,"react":115,"react-components/drag-target":4}],144:[function(require,module,exports){ +},{"./components/prop-check-box.jsx":62,"./util.js":100,"./widgets.js":103,"react":45,"react-components/drag-target":38}],76:[function(require,module,exports){ /** @jsx React.DOM */var React = require('react'); module.exports = { @@ -8706,7 +8738,7 @@ module.exports = { } }; -},{"react":115}],145:[function(require,module,exports){ +},{"react":45}],77:[function(require,module,exports){ /** @jsx React.DOM */ /* Collection of classes for rendering the hint editor area, @@ -8922,7 +8954,7 @@ var CombinedHintsEditor = React.createClass({displayName: 'CombinedHintsEditor', module.exports = CombinedHintsEditor; -},{"./editor.jsx":143,"./hint-renderer.jsx":146,"react":115,"react-components/info-tip":5}],146:[function(require,module,exports){ +},{"./editor.jsx":75,"./hint-renderer.jsx":78,"react":45,"react-components/info-tip":39}],78:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -8950,7 +8982,7 @@ var HintRenderer = React.createClass({displayName: 'HintRenderer', module.exports = HintRenderer; -},{"./renderer.jsx":165,"react":115}],147:[function(require,module,exports){ +},{"./renderer.jsx":97,"react":45}],79:[function(require,module,exports){ var React = require('react'); var init = function(options) { @@ -8991,7 +9023,7 @@ var init = function(options) { module.exports = init; -},{"react":115}],148:[function(require,module,exports){ +},{"react":45}],80:[function(require,module,exports){ var Movable = require("./interactive2/movable.js"); var MovablePoint = require("./interactive2/movable-point.js"); var MovableLine = require("./interactive2/movable-line.js"); @@ -9011,7 +9043,7 @@ var Interactive2 = { module.exports = Interactive2; -},{"./interactive2/movable-line.js":152,"./interactive2/movable-point.js":154,"./interactive2/movable.js":155}],149:[function(require,module,exports){ +},{"./interactive2/movable-line.js":84,"./interactive2/movable-point.js":86,"./interactive2/movable.js":87}],81:[function(require,module,exports){ /** * Utility functions for writing Interactive2 movablethings */ @@ -9089,7 +9121,7 @@ var InteractiveUtil = { module.exports = InteractiveUtil; -},{"./movable-helper-methods.js":150}],150:[function(require,module,exports){ +},{"./movable-helper-methods.js":82}],82:[function(require,module,exports){ /** * MovableThing convenience methods * @@ -9192,7 +9224,7 @@ var MovableHelperMethods = { module.exports = MovableHelperMethods; -},{}],151:[function(require,module,exports){ +},{}],83:[function(require,module,exports){ /** * A library of options to pass to add/draw/remove/constraints */ @@ -9475,7 +9507,7 @@ module.exports = { onMoveEnd: {standard: null}, }; -},{}],152:[function(require,module,exports){ +},{}],84:[function(require,module,exports){ /** * MovableLine */ @@ -9759,7 +9791,7 @@ _.extend(MovableLine.prototype, { module.exports = MovableLine; -},{"./interactive-util.js":149,"./movable-line-options.js":151,"./objective_.js":156}],153:[function(require,module,exports){ +},{"./interactive-util.js":81,"./movable-line-options.js":83,"./objective_.js":88}],85:[function(require,module,exports){ /** * A library of options to pass to add/draw/remove/constraints */ @@ -9904,7 +9936,7 @@ module.exports = { onClick: {standard: null} }; -},{}],154:[function(require,module,exports){ +},{}],86:[function(require,module,exports){ /** * Creates and adds a point to the graph that can be dragged around. * It allows constraints on its movement and draws when moves happen. @@ -10237,7 +10269,7 @@ _.extend(MovablePoint.prototype, { module.exports = MovablePoint; -},{"./interactive-util.js":149,"./movable-point-options.js":153,"./objective_.js":156}],155:[function(require,module,exports){ +},{"./interactive-util.js":81,"./movable-point-options.js":85,"./objective_.js":88}],87:[function(require,module,exports){ /** * Movable * @@ -10502,7 +10534,7 @@ _.extend(Movable.prototype, { module.exports = Movable; -},{"./interactive-util.js":149}],156:[function(require,module,exports){ +},{"./interactive-util.js":81}],88:[function(require,module,exports){ /** * A work-in-progress of _ methods for objects. * That is, they take an object as a parameter, @@ -10531,7 +10563,7 @@ var pluck = exports.pluck = function(table, subKey) { })); }; -},{}],157:[function(require,module,exports){ +},{}],89:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -10561,6 +10593,7 @@ var ItemEditor = React.createClass({displayName: 'ItemEditor', }, render: function() { + var img_src = Math.random() > 0.05 ? "/images/face-smiley01.png" : "/images/face-smiley02.png"; return React.DOM.div( {className:"perseus-editor-table"}, React.DOM.div( {className:"perseus-editor-row perseus-question-container"}, React.DOM.div( {className:"perseus-editor-left-cell"}, @@ -10614,7 +10647,7 @@ var ItemEditor = React.createClass({displayName: 'ItemEditor', onClick:this.props.onCheckAnswer, value:"Check Answer"} ), this.props.wasAnswered && - React.DOM.img( {src:"/images/face-smiley.png", + React.DOM.img( {src:img_src, className:"smiley"} ), this.props.gradeMessage && React.DOM.span(null, this.props.gradeMessage) @@ -10640,7 +10673,7 @@ var ItemEditor = React.createClass({displayName: 'ItemEditor', module.exports = ItemEditor; -},{"./answer-area-editor.jsx":118,"./editor.jsx":143,"./version.json":169,"react":115}],158:[function(require,module,exports){ +},{"./answer-area-editor.jsx":50,"./editor.jsx":75,"./version.json":101,"react":45}],90:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -10900,8 +10933,13 @@ var ItemRenderer = React.createClass({displayName: 'ItemRenderer', document.querySelector(this.props.hintsAreaSelector)); }, - showHint: function() { - if (this.state.hintsVisible < this.getNumHints()) { + showHint: function(hintNum) { + if( hintNum ){ + this.setState({ + hintsVisible: ( hintNum + 1 ) + }); + } + else if (this.state.hintsVisible < this.getNumHints()) { this.setState({ hintsVisible: this.state.hintsVisible + 1 }); @@ -10912,6 +10950,22 @@ var ItemRenderer = React.createClass({displayName: 'ItemRenderer', return this.props.item.hints.length; }, + showGuess: function(answerData) { + this.questionRenderer.showGuess(answerData) + if (answerData !== undefined && this.questionRenderer.widgetIds.length > 0) { + // Left answers for answer widgets only. + answerData = answerData[1]; + } + this.answerAreaRenderer.showGuess(answerData); + return ; + }, + canShowAllHistoryWidgets: function() { + var canShowAllHistoryWidgetsInAnswer = this.answerAreaRenderer.canShowAllHistoryWidgets(); + var canShowAllHistoryWidgetsInQuestion = this.questionRenderer.canShowAllHistoryWidgets(); + if (canShowAllHistoryWidgetsInAnswer && canShowAllHistoryWidgetsInQuestion) + return true; + return false; + }, scoreInput: function() { var qGuessAndScore = this.questionRenderer.guessAndScore(); var aGuessAndScore = this.answerAreaRenderer.guessAndScore(); @@ -10960,7 +11014,7 @@ var ItemRenderer = React.createClass({displayName: 'ItemRenderer', module.exports = ItemRenderer; -},{"./answer-area-renderer.jsx":119,"./enabled-features.jsx":144,"./hint-renderer.jsx":146,"./perseus-api.jsx":162,"./renderer.jsx":165,"./util.js":168,"react":115}],159:[function(require,module,exports){ +},{"./answer-area-renderer.jsx":51,"./enabled-features.jsx":76,"./hint-renderer.jsx":78,"./perseus-api.jsx":94,"./renderer.jsx":97,"./util.js":100,"react":45}],91:[function(require,module,exports){ /** @jsx React.DOM */ /** * Changeable @@ -11061,7 +11115,7 @@ var Changeable = { module.exports = Changeable; -},{"./widget-prop-blacklist.jsx":161}],160:[function(require,module,exports){ +},{"./widget-prop-blacklist.jsx":93}],92:[function(require,module,exports){ /** @jsx React.DOM */var WIDGET_PROP_BLACKLIST = require("./widget-prop-blacklist.jsx"); var JsonifyProps = { @@ -11073,7 +11127,7 @@ var JsonifyProps = { module.exports = JsonifyProps; -},{"./widget-prop-blacklist.jsx":161}],161:[function(require,module,exports){ +},{"./widget-prop-blacklist.jsx":93}],93:[function(require,module,exports){ /** @jsx React.DOM */module.exports = [ // standard props "added" by react // (technically the renderer still adds them) @@ -11087,7 +11141,7 @@ module.exports = JsonifyProps; "apiOptions" ]; -},{}],162:[function(require,module,exports){ +},{}],94:[function(require,module,exports){ /** @jsx React.DOM *//** * [Most of] the Perseus client API. * @@ -11146,7 +11200,7 @@ module.exports = { }; -},{}],163:[function(require,module,exports){ +},{}],95:[function(require,module,exports){ require("./all-widgets.js"); var version = require("./version.json"); @@ -11166,7 +11220,7 @@ module.exports = { Util: require("./util.js") }; -},{"./all-widgets.js":117,"./answer-area-renderer.jsx":119,"./diffs/revision-diff.jsx":136,"./editor-page.jsx":142,"./editor.jsx":143,"./init.js":147,"./item-renderer.jsx":158,"./perseus-api.jsx":162,"./renderer.jsx":165,"./stateful-editor-page.jsx":166,"./util.js":168,"./version.json":169}],164:[function(require,module,exports){ +},{"./all-widgets.js":49,"./answer-area-renderer.jsx":51,"./diffs/revision-diff.jsx":68,"./editor-page.jsx":74,"./editor.jsx":75,"./init.js":79,"./item-renderer.jsx":90,"./perseus-api.jsx":94,"./renderer.jsx":97,"./stateful-editor-page.jsx":98,"./util.js":100,"./version.json":101}],96:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -11182,7 +11236,7 @@ var QuestionParagraph = React.createClass({displayName: 'QuestionParagraph', module.exports = QuestionParagraph; -},{"react":115}],165:[function(require,module,exports){ +},{"react":45}],97:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -11699,12 +11753,47 @@ var Renderer = React.createClass({displayName: 'Renderer', }, function() {return focus;}); }, + showGuess: function(answerData) { + if( !answerData ) + return {}; + return _.map(this.widgetIds, function(id, index) { + if (this.refs[id].setAnswerFromJSON === undefined) { + // Target widget cannot show answer. + return {showSuccess:false,err:'no setAnswerFromJSON implemented for ' + id + ' widget'}; + } else { + // Just show the given answer. + if(answerData[0].length<=index) { + console.log("showGuess err"); + return {}; + } + widgetAnswerData = answerData[0][index]; + this.refs[id].setAnswerFromJSON(widgetAnswerData); + return {showSuccess:true}; + } + }, this); + }, + + canShowAllHistoryWidgets: function(answerData) { + var r = true; + _.map(this.widgetIds, function(id, index) { + if (this.refs[id].setAnswerFromJSON === undefined) { + if ( id !== 'image 1') { + r = false; + } + } + }, this); + return r; + }, + guessAndScore: function() { var widgetProps = this.props.widgets; var onInputError = this.props.apiOptions.onInputError || function() { }; var totalGuess = _.map(this.widgetIds, function(id) { + if (id.indexOf('lights-puzzle') > -1 || id.indexOf('transformer') > -1 || id.indexOf('image') > -1) { + return 'no save ' + id +' widget' + } return this.refs[id].toJSON(); }, this); @@ -11814,7 +11903,7 @@ function extractMathAndWidgets(text) { module.exports = Renderer; -},{"./enabled-features.jsx":144,"./perseus-api.jsx":162,"./question-paragraph.jsx":164,"./tex.jsx":167,"./util.js":168,"./widget-container.jsx":170,"./widgets.js":171,"react":115}],166:[function(require,module,exports){ +},{"./enabled-features.jsx":76,"./perseus-api.jsx":94,"./question-paragraph.jsx":96,"./tex.jsx":99,"./util.js":100,"./widget-container.jsx":102,"./widgets.js":103,"react":45}],98:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -11857,7 +11946,7 @@ var StatefulEditorPage = React.createClass({displayName: 'StatefulEditorPage', module.exports = StatefulEditorPage; -},{"./editor-page.jsx":142,"react":115}],167:[function(require,module,exports){ +},{"./editor-page.jsx":74,"react":45}],99:[function(require,module,exports){ /** @jsx React.DOM */ /** * For math rendered using MathJax. Use me like 2x + 3. @@ -12010,7 +12099,7 @@ var TeX = React.createClass({displayName: 'TeX', module.exports = TeX; -},{"react":115}],168:[function(require,module,exports){ +},{"react":45}],100:[function(require,module,exports){ var nestedMap = function(children, func, context) { if (_.isArray(children)) { return _.map(children, function(child) { @@ -12033,6 +12122,22 @@ var Util = { message: null }, + asc: function(text) { // 全型轉半型的 function + if (typeof text != "string") { + return text + } + var asciiTable = "!\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; + var big5Table = "%uFF01%u201D%uFF03%uFF04%uFF05%uFF06%u2019%uFF08%uFF09%uFF0A%uFF0B%uFF0C%uFF0D%uFF0E%uFF0F%uFF10%uFF11%uFF12%uFF13%uFF14%uFF15%uFF16%uFF17%uFF18%uFF19%uFF1A%uFF1B%uFF1C%uFF1D%uFF1E%uFF1F%uFF20%uFF21%uFF22%uFF23%uFF24%uFF25%uFF26%uFF27%uFF28%uFF29%uFF2A%uFF2B%uFF2C%uFF2D%uFF2E%uFF2F%uFF30%uFF31%uFF32%uFF33%uFF34%uFF35%uFF36%uFF37%uFF38%uFF39%uFF3A%uFF3B%uFF3C%uFF3D%uFF3E%uFF3F%u2018%uFF41%uFF42%uFF43%uFF44%uFF45%uFF46%uFF47%uFF48%uFF49%uFF4A%uFF4B%uFF4C%uFF4D%uFF4E%uFF4F%uFF50%uFF51%uFF52%uFF53%uFF54%uFF55%uFF56%uFF57%uFF58%uFF59%uFF5A%uFF5B%uFF5C%uFF5D%uFF5E"; + + var result = ""; + for (var i = 0; i < text.length; i++) { + var val = escape(text.charAt(i)); + var j = big5Table.indexOf(val); + result += (((j > -1) && (val.length == 6)) ? asciiTable.charAt(j / 6) : text.charAt(i)); + } + return result; + }, + seededRNG: function(seed) { var randomSeed = seed; @@ -12550,7 +12655,7 @@ Util.random = Util.seededRNG(new Date().getTime() & 0xffffffff); module.exports = Util; -},{}],169:[function(require,module,exports){ +},{}],101:[function(require,module,exports){ module.exports={ "apiVersion": { "major": 1, @@ -12562,7 +12667,7 @@ module.exports={ } } -},{}],170:[function(require,module,exports){ +},{}],102:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -12601,7 +12706,7 @@ var WidgetContainer = React.createClass({displayName: 'WidgetContainer', module.exports = WidgetContainer; -},{"react":115}],171:[function(require,module,exports){ +},{"react":45}],103:[function(require,module,exports){ var widgets = {}; var Widgets = { @@ -12741,7 +12846,7 @@ var Widgets = { module.exports = Widgets; -},{}],172:[function(require,module,exports){ +},{}],104:[function(require,module,exports){ /** @jsx React.DOM */ var Changeable = require("../mixins/changeable.jsx"); @@ -12773,7 +12878,9 @@ var Categorizer = React.createClass({displayName: 'Categorizer', values: [] }; }, - + setAnswerFromJSON: function(answerData) { + this.props.onChange(answerData); + }, getInitialState: function() { return { uniqueId: _.uniqueId("perseus_radio_") @@ -12883,13 +12990,13 @@ var CategorizerEditor = React.createClass({displayName: 'CategorizerEditor', render: function() { return React.DOM.div(null, - "Categories:", + "類別:", TextListEditor( {options:this.props.categories, onChange:function(cat) {this.change("categories", cat);}.bind(this), layout:"horizontal"} ), - "Items:", + "項目:", TextListEditor( {options:this.props.items, onChange:function(items) {this.change({ @@ -12917,17 +13024,17 @@ var CategorizerEditor = React.createClass({displayName: 'CategorizerEditor', module.exports = { name: "categorizer", - displayName: "Categorizer", + displayName: "Categorizer/分類器", widget: Categorizer, editor: CategorizerEditor, transform: function(editorProps) { return _.pick(editorProps, "items", "categories"); }, - hidden: true + hidden: false }; -},{"../components/text-list-editor.jsx":135,"../mixins/changeable.jsx":159,"../mixins/jsonify-props.jsx":160,"../renderer.jsx":165,"../util.js":168}],173:[function(require,module,exports){ +},{"../components/text-list-editor.jsx":67,"../mixins/changeable.jsx":91,"../mixins/jsonify-props.jsx":92,"../renderer.jsx":97,"../util.js":100}],105:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -12957,7 +13064,9 @@ var Dropdown = React.createClass({displayName: 'Dropdown', apiOptions: ApiOptions.defaults }; }, - + setAnswerFromJSON: function(answerData) { + this.props.onChange({selected:answerData.value}); + }, render: function() { var choices = this.props.choices.slice(); @@ -12980,11 +13089,15 @@ var Dropdown = React.createClass({displayName: 'Dropdown', ); } else { + var style = { + fontSize: "120%" + }; return React.DOM.select( {onChange:this._handleChangeEvent, onTouchStart:captureScratchpadTouchStart, className:"perseus-widget-dropdown", - value:this.props.selected}, + value:this.props.selected, + style:style}, React.DOM.option( {value:0, disabled:true}, this.props.placeholder ), @@ -13069,24 +13182,19 @@ var DropdownEditor = React.createClass({displayName: 'DropdownEditor', render: function() { var dropdownGroupName = _.uniqueId("perseus_dropdown_"); return React.DOM.div( {className:"perseus-widget-dropdown"}, - React.DOM.div(null, "Dropdown", + React.DOM.div(null, "下拉式選單", InfoTip(null, - React.DOM.p(null, "The drop down is useful for making inequalities in a"+' '+ - "custom format. We normally use the symbols ", "<",", ", ">",","+' '+ - "≤, ≥ (in that order) which you can copy into the"+' '+ - "choices. When possible, use the \"multiple choice\" answer"+' '+ - "type instead.") + React.DOM.p(null, "短敘述的單選題。例如: ", "<",", ", ">",","+' '+ + "≤, ≥ " ) ) ), React.DOM.input( {type:"text", - placeholder:"Placeholder value", + placeholder:"預設值", value:this.props.placeholder, onChange:this.onPlaceholderChange} ), InfoTip(null, - React.DOM.p(null, "This value will appear as the drop down default. It should"+' '+ - "give the user some indication of the values available in the"+' '+ - "drop down itself, e.g., Yes/No/Maybe.") + React.DOM.p(null, "這會顯示為下拉式選單的預設值,可以給使用者一些下拉式選單可能答案的指示。例如:是/不是/可能是。") ), React.DOM.ul(null, this.props.choices.map(function(choice, i) { @@ -13117,7 +13225,7 @@ var DropdownEditor = React.createClass({displayName: 'DropdownEditor', React.DOM.a( {href:"#", className:"simple-button orange", onClick:this.addChoice}, React.DOM.span( {className:"icon-plus"} ), - ' ',"Add a choice",' ' + ' ',"增加選項",' ' ) ) ); @@ -13179,14 +13287,14 @@ var propTransform = function(editorProps) { module.exports = { name: "dropdown", - displayName: "Drop down", + displayName: "Drop down/下拉式選單", widget: Dropdown, editor: DropdownEditor, transform: propTransform, - hidden: true + hidden: false }; -},{"../components/fancy-select.jsx":120,"../mixins/jsonify-props.jsx":160,"../perseus-api.jsx":162,"../util.js":168,"react":115,"react-components/info-tip":5}],174:[function(require,module,exports){ +},{"../components/fancy-select.jsx":52,"../mixins/jsonify-props.jsx":92,"../perseus-api.jsx":94,"../util.js":100,"react":45,"react-components/info-tip":39}],106:[function(require,module,exports){ /** @jsx React.DOM */ /** @@ -13376,12 +13484,12 @@ var ExampleGraphieWidgetEditor = React.createClass({displayName: 'ExampleGraphie module.exports = { name: "example-graphie-widget", displayName: "Example Graphie Widget", - hidden: true, // Hides this widget from the Perseus.Editor widget select + hidden: false, // Hides this widget from the Perseus.Editor widget select widget: ExampleGraphieWidget, editor: ExampleGraphieWidgetEditor }; -},{"../components/graphie.jsx":125,"../mixins/changeable.jsx":159,"../mixins/jsonify-props.jsx":160,"../util.js":168,"react":115}],175:[function(require,module,exports){ +},{"../components/graphie.jsx":57,"../mixins/changeable.jsx":91,"../mixins/jsonify-props.jsx":92,"../util.js":100,"react":45}],107:[function(require,module,exports){ /** @jsx React.DOM */ /** @@ -13560,12 +13668,12 @@ var ExampleWidgetEditor = React.createClass({displayName: 'ExampleWidgetEditor', module.exports = { name: "example-widget", displayName: "Example Widget", - hidden: true, // Hides this widget from the Perseus.Editor widget select + hidden: false, // Hides this widget from the Perseus.Editor widget select widget: ExampleWidget, editor: ExampleWidgetEditor }; -},{"../mixins/changeable.jsx":159,"../mixins/jsonify-props.jsx":160,"react":115}],176:[function(require,module,exports){ +},{"../mixins/changeable.jsx":91,"../mixins/jsonify-props.jsx":92,"react":45}],108:[function(require,module,exports){ /** @jsx React.DOM */ var React = require("react"); @@ -13586,12 +13694,13 @@ var TexButtons = require("../components/tex-buttons.jsx"); var cx = React.addons.classSet; var EnabledFeatures = require("../enabled-features.jsx"); +var Util = require("../util.js"); var ERROR_MESSAGE = $._("Sorry, I don't understand that!"); // The new, MathQuill input expression widget var Expression = React.createClass({displayName: 'Expression', - mixins: [Changeable, JsonifyProps], + mixins: [Changeable], propTypes: { value: React.PropTypes.string, @@ -13599,7 +13708,9 @@ var Expression = React.createClass({displayName: 'Expression', functions: React.PropTypes.arrayOf(React.PropTypes.string), buttonsVisible: React.PropTypes.oneOf(['always', 'never', 'focused']), enabledFeatures: EnabledFeatures.propTypes, - apiOptions: ApiOptions.propTypes + apiOptions: ApiOptions.propTypes, + buttonSets: TexButtons.buttonSetsType, + easybuttons: React.PropTypes.bool, }, getDefaultProps: function() { @@ -13610,14 +13721,15 @@ var Expression = React.createClass({displayName: 'Expression', onFocus: function() { }, onBlur: function() { }, enabledFeatures: EnabledFeatures.defaults, - apiOptions: ApiOptions.defaults + apiOptions: ApiOptions.defaults, }; }, getInitialState: function() { return { showErrorTooltip: false, - showErrorText: false + showErrorText: false, + offsetLeft: 0 }; }, @@ -13631,7 +13743,24 @@ var Expression = React.createClass({displayName: 'Expression', return KAS.parse(value, options); }, + componentDidMount: function() { + var expression = this.getDOMNode(); + this.setState({offsetLeft: expression.offsetLeft}); + }, + render: function() { + // for old questions without buttonSets, make buttonSets by easybuttons + if (!this.props.buttonSets) + { + if(!this.props.easybuttons) { + this.props.buttonSets = ["basic", "relations", "trig", "prealgebra"]; + } + else { + this.props.buttonSets = ["basic"]; + } + this.props.onChange; + } + if (this.props.apiOptions.staticRender) { var style = { borderRadius: "5px", @@ -13681,19 +13810,28 @@ var Expression = React.createClass({displayName: 'Expression', "show-error-tooltip": this.state.showErrorTooltip }); + var inEditor = window.location.pathname.indexOf("/questionpanel/perseus_editor/") >= 0; + return React.DOM.span( {className:className}, MathInput( {ref:"input", value:this.props.value, - onChange:this.change("value"), + onChange:this.handleChange, convertDotToTimes:this.props.times, buttonsVisible:this.props.buttonsVisible || "focused", + buttonSets:this.props.buttonSets, onFocus:this._handleFocus, - onBlur:this._handleBlur} ), + onBlur:this._handleBlur, + offsetLeft:this.state.offsetLeft, + inEditor:inEditor} ), this.state.showErrorTooltip && errorTooltip ); } }, + + handleChange: function(newValue) { + this.props.onChange({ value: Util.asc(newValue) }); + }, _handleFocus: function() { if (this.props.apiOptions.staticRender) { @@ -13756,326 +13894,62 @@ var Expression = React.createClass({displayName: 'Expression', return Expression.validate(this.toJSON(), rubric, onInputError); }, - statics: { - displayMode: "inline-block" - } -}); - -_.extend(Expression, { - validate: function(state, rubric, onInputError) { - var options = _.clone(rubric); - if (icu && icu.getDecimalFormatSymbols) { - _.extend(options, icu.getDecimalFormatSymbols()); - } - // We don't give options to KAS.parse here because that is parsing - // the solution answer, not the student answer, and we don't - // want a solution to work if the student is using a different - // language but not in english. - var val = Khan.answerTypes.expression.createValidatorFunctional( - KAS.parse(rubric.value, rubric).expr, options); - - var result = val(state.value); - - // TODO(eater): Seems silly to translate result to this invalid/points - // thing and immediately translate it back in ItemRenderer.scoreInput() - if (result.empty) { - var apiResult = onInputError( - null, // reserved for some widget identifier - state.value, - result.message - ); - return { - type: "invalid", - message: (apiResult === false) ? null : result.message - }; - } else { - return { - type: "points", - earned: result.correct ? 1 : 0, - total: 1, - message: result.message - }; - } - } -}); - -// The old, plain-text input expression widget -var OldExpression = React.createClass({displayName: 'OldExpression', - propTypes: { - value: React.PropTypes.string, - times: React.PropTypes.bool, - functions: React.PropTypes.arrayOf(React.PropTypes.string), - enabledFeatures: EnabledFeatures.propTypes - }, - - getDefaultProps: function() { - return { - value: "", - times: false, - functions: [], - onFocus: function() { }, - onBlur: function() { }, - enabledFeatures: EnabledFeatures.defaults, - apiOptions: ApiOptions.defaults - }; - }, - - getInitialState: function() { - return { - lastParsedTex: "" - }; - }, - - parse: function(value, props) { - // TODO(jack): Disable icu for content creators here, or - // make it so that solution answers with ','s or '.'s work - var options = _.pick(props || this.props, "functions"); - if (icu && icu.getDecimalFormatSymbols) { - _.extend(options, icu.getDecimalFormatSymbols()); - } - return KAS.parse(value, options); - }, - - componentWillMount: function() { - this.updateParsedTex(this.props.value); - }, - - componentWillReceiveProps: function(nextProps) { - this.updateParsedTex(nextProps.value, nextProps); - }, - - render: function() { - var result = this.parse(this.props.value); - var shouldShowExamples = this.props.enabledFeatures.toolTipFormats; - - return React.DOM.span( {className:"perseus-widget-expression-old"}, - InputWithExamples( - {ref:"input", - value:this.props.value, - onKeyDown:this.handleKeyDown, - onKeyPress:this.handleKeyPress, - onChange:this.handleChange, - examples:this.examples(), - shouldShowExamples:shouldShowExamples, - interceptFocus:this._getInterceptFocus(), - onFocus:this._handleFocus, - onBlur:this._handleBlur} ), - React.DOM.span( {className:"output"}, - React.DOM.span( {className:"tex", - style:{opacity: result.parsed ? 1.0 : 0.5}}, - TeX(null, this.state.lastParsedTex) - ), - React.DOM.span( {className:"placeholder"}, - React.DOM.span( {ref:"error", className:"error", - style:{display: "none"}}, - React.DOM.span( {className:"buddy"} ), - React.DOM.span( {className:"message"}, React.DOM.span(null, - ERROR_MESSAGE - )) - ) - ) - ) - ); - }, - - _handleFocus: function() { - this.props.onFocus([], this.refs.input.getInputDOMNode()); - }, - - _handleBlur: function() { - this.props.onBlur([], this.refs.input.getInputDOMNode()); - }, - - _getInterceptFocus: function() { - return this.props.apiOptions.interceptInputFocus ? - this._interceptFocus : null; - }, - - _interceptFocus: function() { - var interceptProp = this.props.apiOptions.interceptInputFocus; - if (interceptProp) { - return interceptProp( - this.props.widgetId, - this.refs.input.getInputDOMNode() - ); - } - }, - - errorTimeout: null, - - componentDidMount: function() { - this.componentDidUpdate(); - }, - - componentDidUpdate: function() { - clearTimeout(this.errorTimeout); - if (this.parse(this.props.value).parsed) { - this.hideError(); - } else { - this.errorTimeout = setTimeout(this.showError, 2000); - } - }, - - componentWillUnmount: function() { - clearTimeout(this.errorTimeout); - }, - - showError: function() { - var apiResult = this.props.apiOptions.onInputError( - null, // reserved for some widget identifier - this.props.value, - ERROR_MESSAGE - ); - if (apiResult !== false) { - var $error = $(this.refs.error.getDOMNode()); - if (!$error.is(":visible")) { - $error.css({ top: 50, opacity: 0.1 }).show() - .animate({ top: 0, opacity: 1.0 }, 300); - } - } else { - this.hideError(); - } - }, - - hideError: function() { - var $error = $(this.refs.error.getDOMNode()); - if ($error.is(":visible")) { - $error.animate({ top: 50, opacity: 0.1 }, 300, function() { - $(this).hide(); - }); - } - }, - - /** - * The keydown handler handles clearing the error timeout, telling - * props.value to update, and intercepting the backspace key when - * appropriate... - */ - handleKeyDown: function(event) { - var input = this.refs.input.getDOMNode(); - var text = input.value; - - var start = input.selectionStart; - var end = input.selectionEnd; - var supported = start !== undefined; - - var which = event.nativeEvent.keyCode; - - if (supported && which === 8 /* backspace */) { - if (start === end && text.slice(start - 1, start + 1) === "()") { - event.preventDefault(); - var val = text.slice(0, start - 1) + text.slice(start + 1); - - // this.props.onChange will update the value for us, but - // asynchronously, making it harder to set the selection - // usefully, so we just set .value directly here as well. - input.value = val; - input.selectionStart = start - 1; - input.selectionEnd = end - 1; - this.props.onChange({value: val}); - } - } - }, - - /** - * ...whereas the keypress handler handles the parentheses because keyCode - * is more useful for actual character insertions (keypress gives 40 for an - * open paren '(' instead of keydown which gives 57, the code for '9'). - */ - handleKeyPress: function(event) { - var input = this.refs.input.getDOMNode(); - var text = input.value; - - var start = input.selectionStart; - var end = input.selectionEnd; - var supported = start !== undefined; - - var which = event.nativeEvent.charCode; - - if (supported && which === 40 /* left paren */) { - event.preventDefault(); - - var val; - if (start === end) { - var insertMatched = _.any([" ", ")", ""], function(val) { - return text.charAt(start) === val; - }); - - val = text.slice(0, start) + - (insertMatched ? "()" : "(") + text.slice(end); - } else { - val = text.slice(0, start) + - "(" + text.slice(start, end) + ")" + text.slice(end); - } - - input.value = val; - input.selectionStart = start + 1; - input.selectionEnd = end + 1; - this.props.onChange({value: val}); - - } else if (supported && which === 41 /* right paren */) { - if (start === end && text.charAt(start) === ")") { - event.preventDefault(); - input.selectionStart = start + 1; - input.selectionEnd = end + 1; - } - } - }, - - handleChange: function(newValue) { - this.props.onChange({value: newValue}); - }, - - focus: function() { - this.refs.input.focus(); - return true; - }, - - toJSON: function(skipValidation) { - return {value: this.props.value}; - }, - - updateParsedTex: function(value, props) { - var result = this.parse(value, props); - var options = _.pick(this.props, "times"); - if (result.parsed) { - this.setState({lastParsedTex: result.expr.asTex(options)}); - } - }, - - simpleValidate: function(rubric, onInputError) { - onInputError = onInputError || function() { }; - return Expression.validate(this.toJSON(), rubric, onInputError); - }, - - examples: function() { - var mult = $._("For $2\\cdot2$, enter **2*2**"); - if (this.props.times) { - mult = mult.replace(/\\cdot/g, "\\times"); + setAnswerFromJSON: function(answerData) { + if (answerData === undefined) { + answerData = {value: ""}; } + this.props.onChange(answerData); + }, - return [ - $._("**Acceptable Formats**"), - mult, - $._("For $3y$, enter **3y** or **3*y**"), - $._("For $\\dfrac{1}{x}$, enter **1/x**"), - $._("For $\\dfrac{1}{xy}$, enter **1/(xy)**"), - $._("For $\\dfrac{2}{x + 3}$, enter **2/(x + 3)**"), - $._("For $x^{y}$, enter **x^y**"), - $._("For $x^{2/3}$, enter **x^(2/3)**"), - $._("For $\\sqrt{x}$, enter **sqrt(x)**"), - $._("For $\\pi$, enter **pi**"), - $._("For $\\sin \\theta$, enter **sin(theta)**"), - $._("For $\\le$ or $\\ge$, enter **<=** or **>=**"), - $._("For $\\neq$, enter **=/=**") - ]; + toJSON: function(skipValidation) { + return {value: this.props.value}; }, statics: { - displayMode: "block" + displayMode: "inline-block" + } +}); + +_.extend(Expression, { + validate: function(state, rubric, onInputError) { + var options = _.clone(rubric); + if (icu && icu.getDecimalFormatSymbols) { + _.extend(options, icu.getDecimalFormatSymbols()); + } + // We don't give options to KAS.parse here because that is parsing + // the solution answer, not the student answer, and we don't + // want a solution to work if the student is using a different + // language but not in english. + var val = Khan.answerTypes.expression.createValidatorFunctional( + KAS.parse(rubric.value, rubric).expr, options); + + var result = val(state.value); + + // TODO(eater): Seems silly to translate result to this invalid/points + // thing and immediately translate it back in ItemRenderer.scoreInput() + if (result.empty) { + var apiResult = onInputError( + null, // reserved for some widget identifier + state.value, + result.message + ); + return { + type: "invalid", + message: (apiResult === false) ? null : result.message + }; + } else { + return { + type: "points", + earned: result.correct ? 1 : 0, + total: 1, + message: result.message + }; + } } }); + + var ExpressionEditor = React.createClass({displayName: 'ExpressionEditor', mixins: [Changeable, JsonifyProps], @@ -14084,16 +13958,19 @@ var ExpressionEditor = React.createClass({displayName: 'ExpressionEditor', form: React.PropTypes.bool, simplify: React.PropTypes.bool, times: React.PropTypes.bool, - functions: React.PropTypes.arrayOf(React.PropTypes.string) + functions: React.PropTypes.arrayOf(React.PropTypes.string), + buttonSets: TexButtons.buttonSetsType, + easybuttons: React.PropTypes.bool }, getDefaultProps: function() { return { value: "", - form: false, + form: true, simplify: false, - times: false, - functions: ["f", "g", "h"] + times: true, + functions: ["f", "g", "h"], + easybuttons: true }; }, @@ -14101,15 +13978,25 @@ var ExpressionEditor = React.createClass({displayName: 'ExpressionEditor', var value = this.props.value; return { - // Is the format of `value` TeX or plain text? - // TODO(alex): Remove after backfilling everything to TeX - isTex: value === "" || // default to TeX if new; - _.indexOf(value, "\\") !== -1 || // only TeX has backslashes - _.indexOf(value, "{") !== -1 // and curly braces + // In Junyi, all expressions are new expression widget, not oldExpression widget. + // So isTeX default is true. + isTex: true }; }, render: function() { + // for editing old questions, make buttonSets by easybuttons + if (!this.props.buttonSets) + { + if(!this.props.easybuttons) { + this.props.buttonSets = ["basic", "relations", "trig", "prealgebra"]; + } + else { + this.props.buttonSets = ["basic"]; + } + this.props.onChange; + } + var simplifyWarning = null; var shouldTryToParse = this.props.simplify && this.props.value !== ""; if (shouldTryToParse) { @@ -14130,32 +14017,73 @@ var ExpressionEditor = React.createClass({displayName: 'ExpressionEditor', times: this.props.times, functions: this.props.functions, onChange: function(newProps) {return this.change(newProps);}.bind(this), - buttonsVisible: "never" + buttonsVisible: "never", + buttonSets: this.props.buttonSets, }; var expression = this.state.isTex ? Expression : OldExpression; + // checkboxes to choose which sets of input buttons are shown + var buttonSetChoices = _(TexButtons.buttonSets).map(function(set, name) { + // The first one gets special cased to always be checked, disabled, + // and float left. + var isFirst = name === "basic"; + var checked = _.contains(this.props.buttonSets, name) || isFirst; + var className = isFirst ? + "button-set-label-float" : + "button-set-label"; + + var chineseName = ""; + switch (name){ + case "basic": + chineseName = "基本運算"; + break; + case "relations": + chineseName = "不等式"; + break; + case "trig": + chineseName = "三角函數"; + break; + case "prealgebra": + chineseName = "初階代數"; + break; + default: + chineseName = "其他"; + }; + + return React.DOM.div(null, + React.DOM.label( {className:className, key:name}, + React.DOM.input( {type:"checkbox", + checked:checked, + disabled:isFirst, + onChange:function() {return this.handleButtonSet(name);}.bind(this)} ), + chineseName + ) + ); + }.bind(this)); + // TODO(joel) - move buttons outside of the label so they don't weirdly // focus return React.DOM.div(null, React.DOM.div(null, React.DOM.label(null, - "Correct answer:",' ', + "正確答案:",' ', expression(expressionProps) )), this.state.isTex && TexButtons( {className:"math-input-buttons", convertDotToTimes:this.props.times, - onInsert:this.handleTexInsert} ), + onInsert:this.handleTexInsert, + sets:this.props.buttonSets} ), React.DOM.div(null, PropCheckBox( {form:this.props.form, onChange:this.props.onChange, labelAlignment:"right", - label:"Answer expression must have the same form."} ), + label:"答案一定要與格式相符。"} ), InfoTip(null, - React.DOM.p(null, "The student's answer must be in the same form."+' '+ - "Commutativity and excess negative signs are ignored.") + React.DOM.p(null, "學生必須輸入相同的算式。"+' '+ + "但容許交換律與負號,例如:1+3,可接受3+1或1-(-3),但不能接受4或2+2。") ) ), @@ -14164,13 +14092,12 @@ var ExpressionEditor = React.createClass({displayName: 'ExpressionEditor', {simplify:this.props.simplify, onChange:this.props.onChange, labelAlignment:"right", - label:"Answer expression must be fully expanded and"+' '+ - "simplified."} ), + label:"答案一定要化簡、展開。"} ), InfoTip(null, - React.DOM.p(null, "The student's answer must be fully expanded and"+' '+ - "simplified. Answering this equation (x^2+2x+1) with this"+' '+ + React.DOM.p(null, "答案一定要化簡或展開,例如方程式 (x^2+2x+1) ,如果輸入"+' '+ + "(x+1)^2 就會算不對,並且提示學生:"+' '+ "factored equation (x+1)^2 will render this response"+' '+ - "\"Your answer is not fully expanded and simplified.\"") + "\"你的答案還沒化簡或展開\"。") ) ), @@ -14181,26 +14108,26 @@ var ExpressionEditor = React.createClass({displayName: 'ExpressionEditor', {times:this.props.times, onChange:this.props.onChange, labelAlignment:"right", - label:"Use × for rendering multiplication instead of a"+' '+ - "center dot."} ), + label:"用 × 表示乘號。"} ), InfoTip(null, - React.DOM.p(null, "For pre-algebra problems this option displays"+' '+ - "multiplication as \\times instead of \\cdot in both the"+' '+ - "rendered output and the acceptable formats examples.") + React.DOM.p(null, "算術問題使用 × 表示乘法,代數問題用・表示乘法。") ) ), + React.DOM.div(null, + React.DOM.div(null, "運算符號選擇:"), + buttonSetChoices + ), + React.DOM.div(null, React.DOM.label(null, - "Function variables: ", + "函數名稱: ", React.DOM.input( {type:"text", defaultValue:this.props.functions.join(" "), onChange:this.handleFunctions} ) ), InfoTip(null, React.DOM.p(null, - "Single-letter variables listed here will be"+' '+ - "interpreted as functions. This let us know that f(x) means"+' '+ - "\"f of x\" and not \"f times x\"." + "列在此處的變數為函數名稱,當我們使用 f(x),會把它解讀成函數,而不是解釋成 f 乘以 x 。" )) ) @@ -14211,6 +14138,25 @@ var ExpressionEditor = React.createClass({displayName: 'ExpressionEditor', this.refs.expression.insert(str); }, + // called when the selected buttonset changes + handleButtonSet: function(changingName) { + var buttonSetNames = _(TexButtons.buttonSets).keys(); + + // Filter to preserve order - using .union and .difference would always + // move the last added button set to the end. + // Because filter by buttonSetNames, the order can be keep + var buttonSets = _(buttonSetNames).filter(function(set) { + // if set in original buttonSets & set is changingName => false + // if set in original buttonSets & set is not changingName => true + // if set not in original buttonSets & set is changingName => true + // if set not in original buttonSets & set is not changingName => false + return _(this.props.buttonSets).contains(set) !== + (set === changingName); + }.bind(this)); + + this.props.onChange({ buttonSets:buttonSets }); + }, + handleFunctions: function(e) { var newProps = {}; newProps.functions = _.compact(e.target.value.split(/[ ,]+/)); @@ -14225,19 +14171,19 @@ var ExpressionEditor = React.createClass({displayName: 'ExpressionEditor', module.exports = { name: "expression", - displayName: "Expression / Equation", + displayName: "Expression/數學式", getWidget: function(enabledFeatures) { // Allow toggling between the two versions of the widget return enabledFeatures.useMathQuill ? Expression : OldExpression; }, editor: ExpressionEditor, transform: function(editorProps) { - return _.pick(editorProps, "times", "functions"); + return _.pick(editorProps, "times", "functions", "buttonSets", "easybuttons"); }, - hidden: true + hidden: false }; -},{"../components/input-with-examples.jsx":126,"../components/math-input.jsx":127,"../components/prop-check-box.jsx":130,"../components/tex-buttons.jsx":133,"../enabled-features.jsx":144,"../mixins/changeable.jsx":159,"../mixins/jsonify-props.jsx":160,"../perseus-api.jsx":162,"../tex.jsx":167,"react":115,"react-components/info-tip":5,"react-components/tooltip":114}],177:[function(require,module,exports){ +},{"../components/input-with-examples.jsx":58,"../components/math-input.jsx":59,"../components/prop-check-box.jsx":62,"../components/tex-buttons.jsx":65,"../enabled-features.jsx":76,"../mixins/changeable.jsx":91,"../mixins/jsonify-props.jsx":92,"../perseus-api.jsx":94,"../tex.jsx":99,"../util.js":100,"react":45,"react-components/info-tip":39,"react-components/tooltip":44}],109:[function(require,module,exports){ /** @jsx React.DOM */ /** @@ -14270,9 +14216,10 @@ var Iframe = React.createClass({displayName: 'Iframe', getDefaultProps: function() { return { // options: incomplete, incorrect, correct - status: "incomplete", + status: "correct", // optional message - message: null + message: null, + allowFullScreen: true }; }, @@ -14309,9 +14256,8 @@ var Iframe = React.createClass({displayName: 'Iframe', var url = this.props.url; // If the URL doesnt start with http, it must be a program ID - if (url.length && url.indexOf("http") !== 0) { - url = "http://khanacademy.org/cs/program/" + url + - "/embedded?buttons=no&embed=yes&editor=no&author=no"; + if (url && url.length && url.indexOf("http") !== 0) { + url = "https://www.youtube.com/embed/" + url; // Origin is used by output.js in deciding to send messages url = updateQueryString(url, "origin", window.location.origin); } @@ -14333,7 +14279,8 @@ var Iframe = React.createClass({displayName: 'Iframe', // creator "went wild". // http://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/ return React.DOM.iframe( {sandbox:"allow-same-origin allow-scripts", - style:style, src:url} ); + style:style, src:url, + allowFullScreen:this.props.allowFullScreen}); }, simpleValidate: function(rubric) { @@ -14398,11 +14345,11 @@ var PairEditor = React.createClass({displayName: 'PairEditor', render: function() { return React.DOM.fieldset(null, - React.DOM.label(null, "Name:", + React.DOM.label(null, "名稱:", BlurInput( {value:this.props.name, onChange:this.change("name")} ) ), - React.DOM.label(null, "Value:", + React.DOM.label(null, "設定值:", BlurInput( {value:this.props.value, onChange:this.change("value")} ) ) @@ -14458,34 +14405,17 @@ var IframeEditor = React.createClass({displayName: 'IframeEditor', return { url: "", settings: [{name: "", value: ""}], - width: 400, - height: 400 + width: 560, + height: 320 }; }, render: function() { return React.DOM.div(null, - React.DOM.label(null, "Url or Program ID:", + React.DOM.label(null, "網址 Url:", BlurInput( {name:"url", value:this.props.url, onChange:this.change("url")} ) - ), - React.DOM.br(null), - React.DOM.label(null, "Settings:", - PairsEditor( {name:"settings", - pairs:this.props.settings, - onChange:this.handleSettingsChange} ) - ), - React.DOM.br(null), - React.DOM.label(null, "Width:", - BlurInput( {name:"width", - value:this.props.width, - onChange:this.change("width")} ) - ), - React.DOM.label(null, "Height:", - BlurInput( {name:"height", - value:this.props.height, - onChange:this.change("height")} ) ) ); }, @@ -14498,14 +14428,14 @@ var IframeEditor = React.createClass({displayName: 'IframeEditor', module.exports = { name: "iframe", - displayName: "Iframe", + displayName: "Iframe/外掛套件", widget: Iframe, // Let's not expose it to all content creators yet - hidden: true, + hidden: false, editor: IframeEditor }; -},{"../mixins/changeable.jsx":159,"../mixins/jsonify-props.jsx":160,"../util.js":168,"react":115,"react-components/blur-input":2}],178:[function(require,module,exports){ +},{"../mixins/changeable.jsx":91,"../mixins/jsonify-props.jsx":92,"../util.js":100,"react":45,"react-components/blur-input":36}],110:[function(require,module,exports){ /** @jsx React.DOM */ var BlurInput = require("react-components/blur-input"); @@ -14524,6 +14454,7 @@ var defaultBackgroundImage = { width: 0, height: 0 }; +var maxImageSize = 480; /** * Alignment option for captions, relative to specified coordinates. @@ -14569,7 +14500,8 @@ var ImageWidget = React.createClass({displayName: 'ImageWidget', coordinates: React.PropTypes.arrayOf(React.PropTypes.number), alignment: React.PropTypes.string }) - ) + ), + allowScratchpad: React.PropTypes.bool }, getDefaultProps: function() { @@ -14577,7 +14509,8 @@ var ImageWidget = React.createClass({displayName: 'ImageWidget', range: [defaultRange, defaultRange], box: [defaultBoxSize, defaultBoxSize], backgroundImage: defaultBackgroundImage, - labels: [] + labels: [], + allowScratchpad: true }; }, @@ -14606,7 +14539,8 @@ var ImageWidget = React.createClass({displayName: 'ImageWidget', box:this.props.box, range:this.props.range, options:_.pick(this.props, "box", "range", "labels"), - setup:this.setupGraphie} + setup:this.setupGraphie, + allowScratchpad:this.props.allowScratchpad} ) ); }, @@ -14660,19 +14594,19 @@ var ImageEditor = React.createClass({displayName: 'ImageEditor', render: function() { var imageSettings = React.DOM.div( {className:"image-settings"}, - React.DOM.div(null, "Url:",' ', + React.DOM.div(null, "圖片網址:",' ', BlurInput( {value:this.props.backgroundImage.url, onChange:this.onUrlChange} ), InfoTip(null, - React.DOM.p(null, "填入圖片的網址") + React.DOM.p(null, "填入圖片的網址。例如,先上傳至 http://imgur.com ,貼上圖片網址 (Direct link)。") ) ), React.DOM.label(null, React.DOM.input( {type:"checkbox", checked:this.props.useBoxSize, - onChange:this.toggleUseBoxSize} ),"手動調整寬度" + onChange:this.toggleUseBoxSize} ),"手動調整寬度,寬度上限480" ), - React.DOM.div(null, "Width:",' ', + React.DOM.div(null, "寬度:",' ', BlurInput( {value:parseInt(this.props.box[0]), onChange:this.onWidthChange} ), InfoTip(null, @@ -14772,7 +14706,7 @@ var ImageEditor = React.createClass({displayName: 'ImageEditor', var image = _.clone(this.props.backgroundImage); if (this.props.useBoxSize) { var w_h_ratio = image.height / image.width; - image.width = parseInt(newAlignment); + image.width = parseInt(newAlignment) > maxImageSize ? maxImageSize:parseInt(newAlignment); image.height = Math.round(image.width * w_h_ratio); } var box = [image.width, image.height]; @@ -14830,13 +14764,13 @@ var ImageEditor = React.createClass({displayName: 'ImageEditor', module.exports = { name: "image", - displayName: "Image", + displayName: "Image/圖片", widget: ImageWidget, editor: ImageEditor }; -},{"../components/graphie.jsx":125,"../components/range-input.jsx":131,"../mixins/changeable.jsx":159,"../mixins/jsonify-props.jsx":160,"react-components/blur-input":2,"react-components/info-tip":5}],179:[function(require,module,exports){ +},{"../components/graphie.jsx":57,"../components/range-input.jsx":63,"../mixins/changeable.jsx":91,"../mixins/jsonify-props.jsx":92,"react-components/blur-input":36,"react-components/info-tip":39}],111:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -14854,35 +14788,35 @@ var toNumericString = KhanUtil.toNumericString; var answerTypes = { number: { - name: "Numbers", + name: "數字", forms: "integer, decimal, proper, improper, mixed" }, decimal: { - name: "Decimals", + name: "小數", forms: "decimal" }, integer: { - name: "Integers", + name: "整數", forms: "integer" }, rational: { - name: "Fractions and mixed numbers", + name: "分數與帶分數", forms: "integer, proper, improper, mixed" }, improper: { - name: "Improper numbers (no mixed)", + name: "假分數 (不接受帶分數)", forms: "integer, proper, improper" }, mixed: { - name: "Mixed numbers (no improper)", + name: "帶分數 (不接受假分數)", forms: "integer, proper, mixed" }, percent: { - name: "Numbers or percents", + name: "數字或百分數", forms: "integer, decimal, proper, improper, mixed, percent" }, pi: { - name: "Numbers with pi", forms: "pi" + name: "有 \u03C0 的數", forms: "pi" } }; @@ -14890,29 +14824,29 @@ var formExamples = { "integer": function(options) { return $._("an integer, like $6$"); }, "proper": function(options) { if (options.simplify === "optional") { - return $._("a *proper* fraction, like $1/2$ or $6/10$"); + return $._("真分數, 例 $1/2$ or $6/10$"); } else { - return $._("a *simplified proper* fraction, like $3/5$"); + return $._("最簡真分數, 例 $3/5$"); } }, "improper": function(options) { if (options.simplify === "optional") { - return $._("an *improper* fraction, like $10/7$ or $14/8$"); + return $._("假分數, 例 $10/7$ or $14/8$"); } else { - return $._("a *simplified improper* fraction, like $7/4$"); + return $._("最簡假分數, 例 $7/4$"); } }, "mixed": function(options) { - return $._("a mixed number, like $1\\ 3/4$"); + return $._("帶分數, 例 $1\\ 3/4$"); }, "decimal": function(options) { - return $._("an *exact* decimal, like $0.75$"); + return $._("精確的小數, 例 $0.75$"); }, "percent": function(options) { return $._("a percent, like $12.34\\%$"); }, "pi": function(options) { - return $._("a multiple of pi, like $12\\ \\text{pi}$ or " + + return $._("pi 的倍數, 例 $12\\ \\text{pi}$ or " + "$2/3\\ \\text{pi}$"); } }; @@ -14994,7 +14928,7 @@ var InputNumber = React.createClass({displayName: 'InputNumber', }, handleChange: function(newValue) { - this.props.onChange({ currentValue: newValue }); + this.props.onChange({ currentValue: Util.asc(newValue) }); }, focus: function() { @@ -15002,6 +14936,13 @@ var InputNumber = React.createClass({displayName: 'InputNumber', return true; }, + setAnswerFromJSON: function(answerData) { + if (answerData === undefined) { + answerData = {currentValue: ""}; + } + this.props.onChange(answerData); + }, + toJSON: function(skipValidation) { return { currentValue: this.props.currentValue @@ -15095,14 +15036,14 @@ var InputNumberEditor = React.createClass({displayName: 'InputNumberEditor', return React.DOM.div(null, React.DOM.div(null, React.DOM.label(null, - ' ',"Correct answer:",' ', + ' ',"正確答案:",' ', BlurInput( {value:"" + this.props.value, onChange:this.handleAnswerChange, ref:"input"} ) )), React.DOM.div(null, - ' ',"Answer type:",' ', + ' ',"答案類型:",' ', React.DOM.select( {value:this.props.answerType, onChange:function(e) { @@ -15111,27 +15052,23 @@ var InputNumberEditor = React.createClass({displayName: 'InputNumberEditor', answerTypeOptions ), InfoTip(null, - React.DOM.p(null, "Use the default \"Numbers\" unless the answer must be in a"+' '+ - "specific form (e.g., question is about converting decimals to"+' '+ - "fractions).") + React.DOM.p(null, "預設使用「數字」,除非答案需要是一個特定的格式。(例如:將小數轉換成分數的問題)") ) ), React.DOM.div(null, React.DOM.label(null, - ' ',"Width",' ', + ' ',"寬度",' ', React.DOM.select( {value:this.props.size, onChange:function(e) { this.props.onChange({size: e.target.value}); }.bind(this)}, - React.DOM.option( {value:"normal"}, "Normal (80px)"), - React.DOM.option( {value:"small"}, "Small (40px)") + React.DOM.option( {value:"normal"}, "一般 (80px)"), + React.DOM.option( {value:"small"}, "較小 (40px)") ) ), InfoTip(null, - React.DOM.p(null, "Use size \"Normal\" for all text boxes, unless there are"+' '+ - "multiple text boxes in one line and the answer area is too"+' '+ - "narrow to fit them.") + React.DOM.p(null, "預設使用一般大小,除非需要很多個答案格在同一行,會出現放不下的情況。") ) ) ); @@ -15161,7 +15098,7 @@ module.exports = { transform: propTransform }; -},{"../components/input-with-examples.jsx":126,"../enabled-features.jsx":144,"../perseus-api.jsx":162,"../renderer.jsx":165,"../tex.jsx":167,"../util.js":168,"react":115,"react-components/blur-input":2,"react-components/info-tip":5}],180:[function(require,module,exports){ +},{"../components/input-with-examples.jsx":58,"../enabled-features.jsx":76,"../perseus-api.jsx":94,"../renderer.jsx":97,"../tex.jsx":99,"../util.js":100,"react":45,"react-components/blur-input":36,"react-components/info-tip":39}],112:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -15462,12 +15399,10 @@ var InteractiveGraph = React.createClass({displayName: 'InteractiveGraph', }, getDefaultProps: function() { - var range = this.props.range || [[-10, 10], [-10, 10]]; - var step = this.props.step || [1, 1]; - var gridStep = this.props.gridStep || - Util.getGridStep(range, step, defaultBoxSize); - var snapStep = this.props.snapStep || - Util.snapStepFromGridStep(gridStep); + var range = [[-10, 10], [-10, 10]]; + var step = [1, 1]; + var gridStep = Util.getGridStep(range, step, defaultBoxSize); + var snapStep = Util.snapStepFromGridStep(gridStep); return { labels: ["x", "y"], range: range, @@ -15532,16 +15467,16 @@ var InteractiveGraph = React.createClass({displayName: 'InteractiveGraph', graph: {type: type} }); }.bind(this)}, - React.DOM.option( {value:"linear"}, "Linear function"), - React.DOM.option( {value:"quadratic"}, "Quadratic function"), - React.DOM.option( {value:"sinusoid"}, "Sinusoid function"), - React.DOM.option( {value:"circle"}, "Circle"), - React.DOM.option( {value:"point"}, "Point(s)"), - React.DOM.option( {value:"linear-system"}, "Linear System"), - React.DOM.option( {value:"polygon"}, "Polygon"), - React.DOM.option( {value:"segment"}, "Line Segment(s)"), - React.DOM.option( {value:"ray"}, "Ray"), - React.DOM.option( {value:"angle"}, "Angle") + React.DOM.option( {value:"linear"}, "線性函數"), + React.DOM.option( {value:"quadratic"}, "二次函數"), + React.DOM.option( {value:"sinusoid"}, "正餘弦函數"), + React.DOM.option( {value:"circle"}, "圓形"), + React.DOM.option( {value:"point"}, "點"), + React.DOM.option( {value:"linear-system"}, "聯立方程組"), + React.DOM.option( {value:"polygon"}, "多邊形"), + React.DOM.option( {value:"segment"}, "線段"), + React.DOM.option( {value:"ray"}, "射線"), + React.DOM.option( {value:"angle"}, "角度") ); if (this.props.graph.type === "point") { @@ -15561,10 +15496,10 @@ var InteractiveGraph = React.createClass({displayName: 'InteractiveGraph', }.bind(this)}, _.map(_.range(1, 7), function(n) { return React.DOM.option( {value:n}, - n, " point",n > 1 && "s" + n, " 點" ); }), - React.DOM.option( {value:UNLIMITED}, "unlimited") + React.DOM.option( {value:UNLIMITED}, "無限制") ); } else if (this.props.graph.type === "polygon") { extraOptions = React.DOM.div(null, @@ -15585,13 +15520,13 @@ var InteractiveGraph = React.createClass({displayName: 'InteractiveGraph', this.props.onChange({graph: graph}); }.bind(this)}, _.map(_.range(3, 13), function(n) { - return React.DOM.option( {value:n}, n, " sides"); + return React.DOM.option( {value:n}, n, " 邊"); }), - React.DOM.option( {value:UNLIMITED}, "unlimited sides") + React.DOM.option( {value:UNLIMITED}, "無限制") ) ), React.DOM.div(null, - React.DOM.label(null, " Snap to",' ', + React.DOM.label(null, " 對齊",' ', React.DOM.select( {key:"polygon-snap", value:this.props.graph.snapTo, @@ -15604,45 +15539,40 @@ var InteractiveGraph = React.createClass({displayName: 'InteractiveGraph', }); this.props.onChange({graph: graph}); }.bind(this)}, - React.DOM.option( {value:"grid"}, "grid"), + React.DOM.option( {value:"grid"}, "網格"), (this.props.graph.numSides !== UNLIMITED) && [ - React.DOM.option( {value:"angles"}, - ' ',"interior angles",' ' + React.DOM.option( {value:"interior angles"}, + ' ',"內角",' ' ), - React.DOM.option( {value:"sides"}, - ' ',"side measures",' ' + React.DOM.option( {value:"side measures"}, + ' ',"邊長",' ' ) ] ) ), InfoTip(null, - React.DOM.p(null, "These options affect the movement of the vertex"+' '+ - "points. The grid option will guide the points to"+' '+ - "the nearest half step along the grid."), - - React.DOM.p(null, "The interior angle and side measure options"+' '+ - "guide the points to the nearest whole angle or"+' '+ - "side"), " measure respectively.",' ' + React.DOM.p(null, "此選項是用來決定答案的符合情況,\"對齊網格\"為頂點位置需符合答案要求,"+' '+ + "\"對齊內角\"為內角需符合答案要求,\"對齊邊長\"為各邊需符合答案要求。") ) ), React.DOM.div(null, - React.DOM.label(null, "Show angle measures:",' ', + React.DOM.label(null, "顯示角度度數:",' ', React.DOM.input( {type:"checkbox", checked:this.props.graph.showAngles, onChange:this.toggleShowAngles} ) ), InfoTip(null, - React.DOM.p(null, "Displays the interior angle measures.") + React.DOM.p(null, "顯示出各內角的角度") ) ), React.DOM.div(null, - React.DOM.label(null, "Show side measures:",' ', + React.DOM.label(null, "顯示邊長長度:",' ', React.DOM.input( {type:"checkbox", checked:this.props.graph.showSides, onChange:this.toggleShowSides} ) ), InfoTip(null, - React.DOM.p(null, "Displays the side lengths.") + React.DOM.p(null, "顯示出各邊的長度") ) ) ); @@ -15662,7 +15592,7 @@ var InteractiveGraph = React.createClass({displayName: 'InteractiveGraph', }.bind(this)}, _.map(_.range(1, 7), function(n) { return React.DOM.option( {value:n}, - n, " segment",n > 1 && "s" + n, " 線段" ); }) ); @@ -15673,14 +15603,14 @@ var InteractiveGraph = React.createClass({displayName: 'InteractiveGraph', ); extraOptions = React.DOM.div(null, React.DOM.div(null, - React.DOM.label(null, "Show angle measure:",' ', + React.DOM.label(null, "顯示角度值:",' ', React.DOM.input( {type:"checkbox", checked:this.props.graph.showAngles, onChange:this.toggleShowAngles} ) ) ), React.DOM.div(null, - React.DOM.label(null, "Allow reflex angles:",' ', + React.DOM.label(null, "允許反角:",' ', React.DOM.input( {type:"checkbox", checked:allowReflexAngles, onChange:function(newVal) { @@ -15695,16 +15625,15 @@ var InteractiveGraph = React.createClass({displayName: 'InteractiveGraph', ), InfoTip(null, React.DOM.p(null, - "Reflex angles are angles with a measure"+' '+ - "greater than 180 degrees." + "反角是指大於 180 度的角度。" ), React.DOM.p(null, - "By default, these should remain enabled." + "(預設為允許)" ) ) ), React.DOM.div(null, - React.DOM.label(null, "Snap to increments of",' ', + React.DOM.label(null, "符合",' ', NumberInput( {key:"degree-snap", placeholder:1, @@ -15713,2286 +15642,1748 @@ var InteractiveGraph = React.createClass({displayName: 'InteractiveGraph', this.props.onChange({ graph: _.extend({}, this.props.graph, { snapDegrees: Math.abs(newVal), - coords: null - }) - }); - }.bind(this)} ), - ' ',"degrees",' ' - ) - ), - React.DOM.div(null, - React.DOM.label(null, - ' ',"With an offset of",' ', - NumberInput( - {key:"angle-offset", - placeholder:0, - value:this.props.graph.angleOffsetDeg, - onChange:function(newVal) { - this.props.onChange({ - graph: _.extend({}, this.props.graph, { - angleOffsetDeg: newVal, - coords: null - }) - }); - }.bind(this)} ), - ' ',"degrees",' ' - ) - ) - ); - } - } - - var box = this.props.box; - - var image = this.props.backgroundImage; - if (image.url) { - var preScale = box[0] / defaultBoxSize; - var scale = image.scale * preScale; - var style = { - bottom: (preScale * image.bottom) + "px", - left: (preScale * image.left) + "px", - width: (scale * image.width) + "px", - height: (scale * image.height) + "px" - }; - image = React.DOM.img( {style:style, src:image.url} ); - } else { - image = null; - } - - var instructions; - if (this.isClickToAddPoints() && this.state.shouldShowInstructions) { - if (this.props.graph.type === "point") { - instructions = $._("Click to add points"); - } else if (this.props.graph.type === "polygon") { - instructions = $._("Click to add vertices"); - } - } else { - instructions = undefined; - } - - var onMouseDown = this.isClickToAddPoints() ? - this.handleAddPointsMouseDown : - null; - - return React.DOM.div( {className:"perseus-widget " + - "perseus-widget-interactive-graph", - style:{ - width: box[0], - height: this.props.flexibleType ? "auto" : box[1] - }}, - Graph( - {instructions:instructions, - ref:"graph", - box:this.props.box, - labels:this.props.labels, - range:this.props.range, - step:this.props.step, - gridStep:this.props.gridStep, - snapStep:this.props.snapStep, - markings:this.props.markings, - backgroundImage:this.props.backgroundImage, - showProtractor:this.props.showProtractor, - showRuler:this.props.showRuler, - rulerLabel:this.props.rulerLabel, - rulerTicks:this.props.rulerTicks, - onMouseDown:onMouseDown, - onNewGraphie:this.setGraphie} ), - typeSelect,extraOptions - ); - }, - - setGraphie: function(newGraphie) { - this.graphie = newGraphie; - this.setupGraphie(); - }, - - handleAddPointsMouseDown: function(coord) { - // This function should only be called when this.isClickToAddPoints() - // is true - if (!this.isClickToAddPoints()) { - throw new Error("handleAddPointsClick should not be registered" + - "when isClickToAddPoints() is false"); - } - if (!this.isCoordInTrash(coord)) { - var point; - if (this.props.graph.type === "point") { - point = this.createPointForPointsType( - coord, - this.points.length - ); - if (!point.constrain()) { - point.remove(); - return; - } - this.points.push(point); - - // interactive2 allows us to grab the point - var idx = this.points.length - 1; - this.points[idx].grab(coord); - - this.updateCoordsFromPoints(); - } else if (this.props.graph.type === "polygon") { - if (this.polygon.closed) { - return; - } - point = this.createPointForPolygonType( - coord, - this.points.length - ); - this.points.push(point); - - var idx = this.points.length - 1; - this.points[idx].grab(); - - // We don't call updateCoordsFromPoints for - // polygons, since the polygon won't be - // closed yet. - this.updatePolygon(); - } - - this.setState({ - shouldShowInstructions: false - }); - } - }, - - resetGraphie: function() { - this.shouldResetGraphie = false; - this.refs.graph.reset(); - }, - - setupGraphie: function() { - this.setTrashCanVisibility(0); - if (this.isClickToAddPoints()) { - this.setTrashCanVisibility(0.5); - } - - var type = this.props.graph.type; - this["add" + capitalize(type) + "Controls"](); - }, - - setTrashCanVisibility: function(opacity) { - var graphie = this.graphie; - - if (knumber.equal(opacity, 0)) { - if (this.trashCan) { - this.trashCan.remove(); - this.trashCan = null; - } - } else { - if (!this.trashCan) { - this.trashCan = graphie.raphael.image(TRASH_ICON_URI, - graphie.xpixels - 40, - graphie.ypixels - 40, - 40, - 40 + coords: null + }) + }); + }.bind(this)} ), + ' ',"度",' ' + ) + ), + React.DOM.div(null, + React.DOM.label(null, + ' ',"偏移",' ', + NumberInput( + {key:"angle-offset", + placeholder:0, + value:this.props.graph.angleOffsetDeg, + onChange:function(newVal) { + this.props.onChange({ + graph: _.extend({}, this.props.graph, { + angleOffsetDeg: newVal, + coords: null + }) + }); + }.bind(this)} ), + ' ',"度",' ' + ) + ) ); } - // TODO(jack): svg opacity is broken in chrome 34. - // Uncomment this once chrome 35 is out. - //this.trashCan.attr({ - // opacity: opacity - //}); - } - }, - - componentWillReceiveProps: function(nextProps) { - if (this.isClickToAddPoints() !== this.isClickToAddPoints(nextProps)) { - this.shouldResetGraphie = true; - this.setState({ - shouldShowInstructions: - this._getShouldShowInstructions(nextProps) - }); - } - }, - - isClickToAddPoints: function(props) { - props = props || this.props; - return (props.graph.type === "point" && - props.graph.numPoints === UNLIMITED) || - (props.graph.type === "polygon" && - props.graph.numSides === UNLIMITED); - }, - - addLine: function(type) { - var self = this; - var graphie = self.graphie; - var coords = InteractiveGraph.getLineCoords( - self.props.graph, - self.props - ); - - var points = self.points = _.map(coords, function(coord) { - return Interactive2.addMovablePoint(graphie, { - coord: coord, - constraints: [ - Interactive2.MovablePoint.constraints.bound(), - Interactive2.MovablePoint.constraints.snap() - ], - onMove: function() { - var graph = _.extend({}, self.props.graph, { - coords: _.invoke(points, "coord") - }); - self.props.onChange({graph: graph}); - }, - normalStyle: { - stroke: KhanUtil.INTERACTIVE, - fill: KhanUtil.INTERACTIVE - } - }); - }); - - var lineConfig = { - points: points, - static: true - }; - - if (type === "line") { - lineConfig.extendLine = true; - } else if (type === "ray") { - lineConfig.extendRay = true; } - var line = self.line = Interactive2.addMovableLine( - graphie, - lineConfig - ); - - // A and B can't be in the same place - points[0].listen("constraints", "isLine", function(coord) { - return !kpoint.equal(coord, points[1].coord()); - }); - points[1].listen("constraints", "isLine", function(coord) { - return !kpoint.equal(coord, points[0].coord()); - }); - }, - - removeLine: function() { - _.invoke(this.points, "remove"); - this.line.remove(); - }, - - addLinearControls: function() { - this.addLine("line"); - }, - - removeLinearControls: function() { - this.removeLine(); - }, + var box = this.props.box; - addQuadraticControls: function() { - var graphie = this.graphie; - var coords = this.props.graph.coords; - if (!coords) { - coords = InteractiveGraph.defaultQuadraticCoords(this.props); + var image = this.props.backgroundImage; + if (image.url) { + var preScale = box[0] / defaultBoxSize; + var scale = image.scale * preScale; + var style = { + bottom: (preScale * image.bottom) + "px", + left: (preScale * image.left) + "px", + width: (scale * image.width) + "px", + height: (scale * image.height) + "px" + }; + image = React.DOM.img( {style:style, src:image.url} ); + } else { + image = null; } - var pointA = this.pointA = graphie.addMovablePoint({ - coord: coords[0], - snapX: graphie.snap[0], - snapY: graphie.snap[1], - normalStyle: { - stroke: KhanUtil.INTERACTIVE, - fill: KhanUtil.INTERACTIVE - } - }); - - var pointB = this.pointB = graphie.addMovablePoint({ - coord: coords[1], - snapX: graphie.snap[0], - snapY: graphie.snap[1], - normalStyle: { - stroke: KhanUtil.INTERACTIVE, - fill: KhanUtil.INTERACTIVE - } - }); - - var pointC = this.pointC = graphie.addMovablePoint({ - coord: coords[2], - snapX: graphie.snap[0], - snapY: graphie.snap[1], - normalStyle: { - stroke: KhanUtil.INTERACTIVE, - fill: KhanUtil.INTERACTIVE + var instructions; + if (this.isClickToAddPoints() && this.state.shouldShowInstructions) { + if (this.props.graph.type === "point") { + instructions = $._("Click to add points"); + } else if (this.props.graph.type === "polygon") { + instructions = $._("Click to add vertices"); } - }); - - // A, B, and C can't be in the same place - pointA.onMove = function(x, y) { - return x !== pointB.coord[0] && x !== pointC.coord[0]; - }; - pointB.onMove = function(x, y) { - return x !== pointA.coord[0] && x !== pointC.coord[0]; - }; - pointC.onMove = function(x, y) { - return x !== pointA.coord[0] && x !== pointB.coord[0]; - }; - - this.updateQuadratic(); - - $([pointA, pointB, pointC]).on("move", function() { - var graph = _.extend({}, this.props.graph, { - coords: [pointA.coord, pointB.coord, pointC.coord] - }); - this.props.onChange({graph: graph}); - this.updateQuadratic(); - }.bind(this)); - }, - - updateQuadratic: function() { - if (this.parabola) { - this.parabola.remove(); + } else { + instructions = undefined; } - var coeffs = InteractiveGraph.getCurrentQuadraticCoefficients( - this.props); - if (!coeffs) { - return; - } + var onMouseDown = this.isClickToAddPoints() ? + this.handleAddPointsMouseDown : + null; - var a = coeffs[0], b = coeffs[1], c = coeffs[2]; - this.parabola = this.graphie.plot(function(x) { - return (a * x + b) * x + c; - }, this.props.range[0]).attr({ - stroke: KhanUtil.INTERACTIVE - }); - this.parabola.toBack(); + return React.DOM.div( {className:"perseus-widget " + + "perseus-widget-interactive-graph", + style:{ + width: box[0], + height: this.props.flexibleType ? "auto" : box[1] + }}, + Graph( + {instructions:instructions, + ref:"graph", + box:this.props.box, + labels:this.props.labels, + range:this.props.range, + step:this.props.step, + gridStep:this.props.gridStep, + snapStep:this.props.snapStep, + markings:this.props.markings, + backgroundImage:this.props.backgroundImage, + showProtractor:this.props.showProtractor, + showRuler:this.props.showRuler, + rulerLabel:this.props.rulerLabel, + rulerTicks:this.props.rulerTicks, + onMouseDown:onMouseDown, + onNewGraphie:this.setGraphie} ), + typeSelect,extraOptions + ); }, - removeQuadraticControls: function() { - this.pointA.remove(); - this.pointB.remove(); - this.pointC.remove(); - if (this.parabola) { - this.parabola.remove(); - } + setGraphie: function(newGraphie) { + this.graphie = newGraphie; + this.setupGraphie(); }, - addSinusoidControls: function() { - var graphie = this.graphie; - var coords = this.props.graph.coords; - if (!coords) { - coords = InteractiveGraph.defaultSinusoidCoords(this.props); - } - - var pointA = this.pointA = Interactive2.addMovablePoint(graphie, { - coord: coords[0], - constraints: [ - Interactive2.MovablePoint.constraints.bound(), - Interactive2.MovablePoint.constraints.snap(), - function(coord) { - return !pointA || coord[0] !== pointB.coord()[0]; + handleAddPointsMouseDown: function(coord) { + // This function should only be called when this.isClickToAddPoints() + // is true + if (!this.isClickToAddPoints()) { + throw new Error("handleAddPointsClick should not be registered" + + "when isClickToAddPoints() is false"); + } + if (!this.isCoordInTrash(coord)) { + var point; + if (this.props.graph.type === "point") { + point = this.createPointForPointsType( + coord, + this.points.length + ); + if (!point.constrain()) { + point.remove(); + return; } - ], - onMove: function() { - var graph = _.extend({}, this.props.graph, { - coords: [pointA.coord(), pointB.coord()] - }); - this.props.onChange({graph: graph}); - this.updateSinusoid(); - }.bind(this), - normalStyle: { - stroke: KhanUtil.BLUE, - fill: KhanUtil.BLUE - } - }); + this.points.push(point); - var pointB = this.pointB = Interactive2.addMovablePoint(graphie, { - coord: coords[1], - constraints: [ - Interactive2.MovablePoint.constraints.bound(), - Interactive2.MovablePoint.constraints.snap(), - function(coord) { - return !pointA || coord[0] !== pointA.coord()[0]; + // interactive2 allows us to grab the point + var idx = this.points.length - 1; + this.points[idx].grab(coord); + + this.updateCoordsFromPoints(); + } else if (this.props.graph.type === "polygon") { + if (this.polygon.closed) { + return; } - ], - onMove: function() { - var graph = _.extend({}, this.props.graph, { - coords: [pointA.coord(), pointB.coord()] - }); - this.props.onChange({graph: graph}); - this.updateSinusoid(); - }.bind(this), - normalStyle: { - stroke: KhanUtil.BLUE, - fill: KhanUtil.BLUE - } - }); + point = this.createPointForPolygonType( + coord, + this.points.length + ); + this.points.push(point); - this.updateSinusoid(); - }, + var idx = this.points.length - 1; + this.points[idx].grab(); - updateSinusoid: function() { - if (this.sinusoid) { - this.sinusoid.remove(); - } + // We don't call updateCoordsFromPoints for + // polygons, since the polygon won't be + // closed yet. + this.updatePolygon(); + } - var coeffs = InteractiveGraph.getCurrentSinusoidCoefficients( - this.props); - if (!coeffs) { - return; + this.setState({ + shouldShowInstructions: false + }); } + }, - var a = coeffs[0], b = coeffs[1], c = coeffs[2], d = coeffs[3]; - this.sinusoid = this.graphie.plot(function(x) { - return a * Math.sin(b * x - c) + d; - }, this.props.range[0]).attr({ - stroke: KhanUtil.BLUE - }); - this.sinusoid.toBack(); + resetGraphie: function() { + this.shouldResetGraphie = false; + this.refs.graph.reset(); }, - removeSinusoidControls: function() { - this.pointA.remove(); - this.pointB.remove(); - if (this.sinusoid) { - this.sinusoid.remove(); + setupGraphie: function() { + this.setTrashCanVisibility(0); + if (this.isClickToAddPoints()) { + this.setTrashCanVisibility(0.5); } + + var type = this.props.graph.type; + this["add" + capitalize(type) + "Controls"](); }, - addCircleControls: function() { + setTrashCanVisibility: function(opacity) { var graphie = this.graphie; - var minSnap = _.min(graphie.snap); - var circle = this.circle = graphie.addCircleGraph({ - center: this.props.graph.center || [0, 0], - radius: this.props.graph.radius || _.min(this.props.step), - snapX: graphie.snap[0], - snapY: graphie.snap[1], - minRadius: minSnap * 2, - snapRadius: minSnap - }); + if (knumber.equal(opacity, 0)) { + if (this.trashCan) { + this.trashCan.remove(); + this.trashCan = null; + } + } else { + if (!this.trashCan) { + this.trashCan = graphie.raphael.image(TRASH_ICON_URI, + graphie.xpixels - 40, + graphie.ypixels - 40, + 40, + 40 + ); + } + // TODO(jack): svg opacity is broken in chrome 34. + // Uncomment this once chrome 35 is out. + //this.trashCan.attr({ + // opacity: opacity + //}); + } + }, - $(circle).on("move", function() { - var graph = _.extend({}, this.props.graph, { - center: circle.center, - radius: circle.radius + componentWillReceiveProps: function(nextProps) { + if (this.isClickToAddPoints() !== this.isClickToAddPoints(nextProps)) { + this.shouldResetGraphie = true; + this.setState({ + shouldShowInstructions: + this._getShouldShowInstructions(nextProps) }); - this.props.onChange({graph: graph}); - }.bind(this)); + } }, - removeCircleControls: function() { - this.circle.remove(); + isClickToAddPoints: function(props) { + props = props || this.props; + return (props.graph.type === "point" && + props.graph.numPoints === UNLIMITED) || + (props.graph.type === "polygon" && + props.graph.numSides === UNLIMITED); }, - addLinearSystemControls: function() { - var graphie = this.graphie; - var coords = InteractiveGraph.getLinearSystemCoords(this.props.graph, - this.props); - - var segmentColors = [KhanUtil.INTERACTIVE, KhanUtil.GREEN]; - var points = this.points = _.map(coords, - function(segmentCoords, segmentIndex) { - var segmentPoints = _.map(segmentCoords, function(coord, i) { - return Interactive2.addMovablePoint(graphie, { - coord: coord, - constraints: [ - Interactive2.MovablePoint.constraints.bound(), - Interactive2.MovablePoint.constraints.snap(), - function(coord) { - if (!segmentPoints) { - // points hasn't been defined yet because - // we're still creating them - return; - } - return !kpoint.equal( - coord, - segmentPoints[1 - i].coord() - ); - } - ], - onMove: function() { - var graph = _.extend({}, this.props.graph, { - coords: _.map( - this.points, - function(segment) {return _.invoke(segment, "coord");} - ) - }); - this.props.onChange({graph: graph}); - }.bind(this), - normalStyle: { - stroke: segmentColors[segmentIndex], - fill: segmentColors[segmentIndex] - } - }); - }.bind(this)); - return segmentPoints; - }.bind(this)); + addLine: function(type) { + var self = this; + var graphie = self.graphie; + var coords = InteractiveGraph.getLineCoords( + self.props.graph, + self.props + ); - var lines = this.lines = _.map(points, - function(segmentPoints, segmentIndex) { - return Interactive2.addMovableLine(graphie, { - points: segmentPoints, - static: true, - extendLine: true, + var points = self.points = _.map(coords, function(coord) { + return Interactive2.addMovablePoint(graphie, { + coord: coord, + constraints: [ + Interactive2.MovablePoint.constraints.bound(), + Interactive2.MovablePoint.constraints.snap() + ], + onMove: function() { + var graph = _.extend({}, self.props.graph, { + coords: _.invoke(points, "coord") + }); + self.props.onChange({graph: graph}); + }, normalStyle: { - stroke: segmentColors[segmentIndex] + stroke: KhanUtil.INTERACTIVE, + fill: KhanUtil.INTERACTIVE } }); }); + + var lineConfig = { + points: points, + static: true + }; + + if (type === "line") { + lineConfig.extendLine = true; + } else if (type === "ray") { + lineConfig.extendRay = true; + } + + var line = self.line = Interactive2.addMovableLine( + graphie, + lineConfig + ); + + // A and B can't be in the same place + points[0].listen("constraints", "isLine", function(coord) { + return !kpoint.equal(coord, points[1].coord()); + }); + points[1].listen("constraints", "isLine", function(coord) { + return !kpoint.equal(coord, points[0].coord()); + }); }, - removeLinearSystemControls: function() { - _.invoke(this.lines, "remove"); - _.map(this.points, function(segment) {return _.invoke(segment, "remove");}); + removeLine: function() { + _.invoke(this.points, "remove"); + this.line.remove(); }, - isCoordInTrash: function(coord) { - var graphie = this.graphie; - var screenPoint = graphie.scalePoint(coord); - return screenPoint[0] >= graphie.xpixels - 40 && - screenPoint[1] >= graphie.ypixels - 40; + addLinearControls: function() { + this.addLine("line"); }, - createPointForPointsType: function(coord, i) { - var self = this; - var graphie = self.graphie; - var point = Interactive2.addMovablePoint(graphie, { - coord: coord, + removeLinearControls: function() { + this.removeLine(); + }, + + addQuadraticControls: function() { + var graphie = this.graphie; + var coords = this.props.graph.coords; + if (!coords) { + coords = InteractiveGraph.defaultQuadraticCoords(this.props); + } + + var pointA = this.pointA = graphie.addMovablePoint({ + coord: coords[0], snapX: graphie.snap[0], snapY: graphie.snap[1], - constraints: [ - Interactive2.MovablePoint.constraints.bound(), - Interactive2.MovablePoint.constraints.snap(), - function(coord) { - // TODO(jack): There ought to be a - // MovablePoint.constraints.avoid - // default that lets you do things like this - return _.all(self.points, function(pt) { - return point === pt || - !kpoint.equal(coord, pt.coord()); - }); - } - ], - onMoveStart: function() { - if (self.isClickToAddPoints()) { - self.setTrashCanVisibility(1); - } - }, - onMove: self.updateCoordsFromPoints, - onMoveEnd: function(coord) { - if (self.isClickToAddPoints()) { - if (self.isCoordInTrash(coord)) { - // remove this point from points - self.points = _.filter(self.points, function(pt) { - return pt !== point; - }); - // update the correct answer box - self.updateCoordsFromPoints(); - - // remove this movablePoint from graphie. - // we wait to do this until we're not inside of - // said point's onMoveEnd method so its state is - // consistent throughout this method call - setTimeout(point.remove.bind(point), 0); - } - // In case we mouseup'd off the graphie and that - // stopped the move (in which case, we might not - // be in isCoordInTrash() - self.setTrashCanVisibility(0.5); - } - }, normalStyle: { stroke: KhanUtil.INTERACTIVE, fill: KhanUtil.INTERACTIVE } }); - return point; - }, - - removePoint: function(point) { - var index = null; - this.points = _.filter(this.points, function(pt, i) { - if (pt === point) { - index = i; - return false; - } else { - return true; + var pointB = this.pointB = graphie.addMovablePoint({ + coord: coords[1], + snapX: graphie.snap[0], + snapY: graphie.snap[1], + normalStyle: { + stroke: KhanUtil.INTERACTIVE, + fill: KhanUtil.INTERACTIVE } }); - return index; - }, - - createPointForPolygonType: function(coord, i) { - var self = this; - var graphie = this.graphie; - // TODO(alex): check against "grid" instead, use constants - var snapToGrid = !_.contains(["angles", "sides"], - this.props.graph.snapTo); - - var point = graphie.addMovablePoint(_.extend({ - coord: coord, + var pointC = this.pointC = graphie.addMovablePoint({ + coord: coords[2], + snapX: graphie.snap[0], + snapY: graphie.snap[1], normalStyle: { stroke: KhanUtil.INTERACTIVE, fill: KhanUtil.INTERACTIVE } - }, snapToGrid ? { - snapX: graphie.snap[0], - snapY: graphie.snap[1] - } : {} - )); + }); - // Index relative to current point -> absolute index - // NOTE: This does not work when isClickToAddPoints() == true, - // as `i` can be changed by dragging a point to the trash - // Currently this function is only called when !isClickToAddPoints() - function rel(j) { - return (i + j + self.points.length) % self.points.length; - } + // A, B, and C can't be in the same place + pointA.onMove = function(x, y) { + return x !== pointB.coord[0] && x !== pointC.coord[0]; + }; + pointB.onMove = function(x, y) { + return x !== pointA.coord[0] && x !== pointC.coord[0]; + }; + pointC.onMove = function(x, y) { + return x !== pointA.coord[0] && x !== pointB.coord[0]; + }; - point.hasMoved = false; + this.updateQuadratic(); - point.onMove = function(x, y) { - var coords = _.pluck(this.points, "coord"); - coords[i] = [x, y]; - if (!kpoint.equal([x, y], point.coord)) { - point.hasMoved = true; - } + $([pointA, pointB, pointC]).on("move", function() { + var graph = _.extend({}, this.props.graph, { + coords: [pointA.coord, pointB.coord, pointC.coord] + }); + this.props.onChange({graph: graph}); + this.updateQuadratic(); + }.bind(this)); + }, - // Check for invalid positioning, but only if we aren't adding - // points one click at a time, since those added points could - // have already violated these constraints - if (!self.isClickToAddPoints()) { - // Polygons can't have consecutive collinear points - if (collinear(coords[rel(-2)], coords[rel(-1)], coords[i]) || - collinear(coords[rel(-1)], coords[i], coords[rel(1)]) || - collinear(coords[i], coords[rel(1)], coords[rel(2)])) { - return false; - } + updateQuadratic: function() { + if (this.parabola) { + this.parabola.remove(); + } - var segments = _.zip(coords, rotate(coords)); + var coeffs = InteractiveGraph.getCurrentQuadraticCoefficients( + this.props); + if (!coeffs) { + return; + } - if (self.points.length > 3) { - // Constrain to simple (non self-intersecting) polygon by - // testing whether adjacent segments intersect any others - for (var j = -1; j <= 0; j++) { - var segment = segments[rel(j)]; - var others = _.without(segments, - segment, segments[rel(j-1)], segments[rel(j+1)]); + var a = coeffs[0], b = coeffs[1], c = coeffs[2]; + this.parabola = this.graphie.plot(function(x) { + return (a * x + b) * x + c; + }, this.props.range[0]).attr({ + stroke: KhanUtil.INTERACTIVE + }); + this.parabola.toBack(); + }, - for (var k = 0; k < others.length; k++) { - var other = others[k]; - if (intersects(segment, other)) { - return false; - } - } - } - } - } + removeQuadraticControls: function() { + this.pointA.remove(); + this.pointB.remove(); + this.pointC.remove(); + if (this.parabola) { + this.parabola.remove(); + } + }, - if (this.props.graph.snapTo === "angles" && - self.points.length > 2) { - // Snap to whole degree interior angles + addSinusoidControls: function() { + var graphie = this.graphie; + var coords = this.props.graph.coords; + if (!coords) { + coords = InteractiveGraph.defaultSinusoidCoords(this.props); + } - var angles = _.map(angleMeasures(coords), function(rad) { - return rad * 180 / Math.PI; + var pointA = this.pointA = Interactive2.addMovablePoint(graphie, { + coord: coords[0], + constraints: [ + Interactive2.MovablePoint.constraints.bound(), + Interactive2.MovablePoint.constraints.snap(), + function(coord) { + return !pointA || coord[0] !== pointB.coord()[0]; + } + ], + onMove: function() { + var graph = _.extend({}, this.props.graph, { + coords: [pointA.coord(), pointB.coord()] }); + this.props.onChange({graph: graph}); + this.updateSinusoid(); + }.bind(this), + normalStyle: { + stroke: KhanUtil.BLUE, + fill: KhanUtil.BLUE + } + }); - _.each([-1, 1], function(j) { - angles[rel(j)] = Math.round(angles[rel(j)]); + var pointB = this.pointB = Interactive2.addMovablePoint(graphie, { + coord: coords[1], + constraints: [ + Interactive2.MovablePoint.constraints.bound(), + Interactive2.MovablePoint.constraints.snap(), + function(coord) { + return !pointA || coord[0] !== pointA.coord()[0]; + } + ], + onMove: function() { + var graph = _.extend({}, this.props.graph, { + coords: [pointA.coord(), pointB.coord()] }); + this.props.onChange({graph: graph}); + this.updateSinusoid(); + }.bind(this), + normalStyle: { + stroke: KhanUtil.BLUE, + fill: KhanUtil.BLUE + } + }); - var getAngle = function(a, vertex, b) { - var angle = KhanUtil.findAngle( - coords[rel(a)], coords[rel(b)], coords[rel(vertex)] - ); - return (angle + 360) % 360; - }; - - var innerAngles = [ - angles[rel(-1)] - getAngle(-2, -1, 1), - angles[rel(1)] - getAngle(-1, 1, 2) - ]; - innerAngles[2] = 180 - (innerAngles[0] + innerAngles[1]); - - // Avoid degenerate triangles - if (_.any(innerAngles, function(angle) { - return leq(angle, 1); - })) { - return false; - } + this.updateSinusoid(); + }, - var knownSide = magnitude(vector(coords[rel(-1)], - coords[rel(1)])); + updateSinusoid: function() { + if (this.sinusoid) { + this.sinusoid.remove(); + } - var onLeft = sign(ccw( - coords[rel(-1)], coords[rel(1)], coords[i] - )) === 1; + var coeffs = InteractiveGraph.getCurrentSinusoidCoefficients( + this.props); + if (!coeffs) { + return; + } - // Solve for side by using the law of sines - var side = Math.sin(innerAngles[1] * Math.PI / 180) / - Math.sin(innerAngles[2] * Math.PI / 180) * knownSide; + var a = coeffs[0], b = coeffs[1], c = coeffs[2], d = coeffs[3]; + this.sinusoid = this.graphie.plot(function(x) { + return a * Math.sin(b * x - c) + d; + }, this.props.range[0]).attr({ + stroke: KhanUtil.BLUE + }); + this.sinusoid.toBack(); + }, - var outerAngle = KhanUtil.findAngle(coords[rel(1)], - coords[rel(-1)]); + removeSinusoidControls: function() { + this.pointA.remove(); + this.pointB.remove(); + if (this.sinusoid) { + this.sinusoid.remove(); + } + }, - var offset = this.graphie.polar( - side, - outerAngle + (onLeft? 1 : -1) * innerAngles[0] - ); + addCircleControls: function() { + var graphie = this.graphie; + var minSnap = _.min(graphie.snap); - return this.graphie.addPoints(coords[rel(-1)], offset); + var circle = this.circle = graphie.addCircleGraph({ + center: this.props.graph.center || [0, 0], + radius: this.props.graph.radius || _.min(this.props.step), + snapX: graphie.snap[0], + snapY: graphie.snap[1], + minRadius: minSnap * 2, + snapRadius: minSnap + }); + $(circle).on("move", function() { + var graph = _.extend({}, this.props.graph, { + center: circle.center, + radius: circle.radius + }); + this.props.onChange({graph: graph}); + }.bind(this)); + }, - } else if (this.props.graph.snapTo === "sides" && - self.points.length > 1) { - // Snap to whole unit side measures + removeCircleControls: function() { + this.circle.remove(); + }, - var sides = _.map([ - [coords[rel(-1)], coords[i]], - [coords[i], coords[rel(1)]], - [coords[rel(-1)], coords[rel(1)]] - ], function(coords) { - return magnitude(vector.apply(null, coords)); - }); + addLinearSystemControls: function() { + var graphie = this.graphie; + var coords = InteractiveGraph.getLinearSystemCoords(this.props.graph, + this.props); - _.each([0, 1], function(j) { - sides[j] = Math.round(sides[j]); + var segmentColors = [KhanUtil.INTERACTIVE, KhanUtil.GREEN]; + var points = this.points = _.map(coords, + function(segmentCoords, segmentIndex) { + var segmentPoints = _.map(segmentCoords, function(coord, i) { + return Interactive2.addMovablePoint(graphie, { + coord: coord, + constraints: [ + Interactive2.MovablePoint.constraints.bound(), + Interactive2.MovablePoint.constraints.snap(), + function(coord) { + if (!segmentPoints) { + // points hasn't been defined yet because + // we're still creating them + return; + } + return !kpoint.equal( + coord, + segmentPoints[1 - i].coord() + ); + } + ], + onMove: function() { + var graph = _.extend({}, this.props.graph, { + coords: _.map( + this.points, + function(segment) {return _.invoke(segment, "coord");} + ) + }); + this.props.onChange({graph: graph}); + }.bind(this), + normalStyle: { + stroke: segmentColors[segmentIndex], + fill: segmentColors[segmentIndex] + } }); + }.bind(this)); + return segmentPoints; + }.bind(this)); - // Avoid degenerate triangles - if (leq(sides[1] + sides[2], sides[0]) || - leq(sides[0] + sides[2], sides[1]) || - leq(sides[0] + sides[1], sides[2])) { - return false; + var lines = this.lines = _.map(points, + function(segmentPoints, segmentIndex) { + return Interactive2.addMovableLine(graphie, { + points: segmentPoints, + static: true, + extendLine: true, + normalStyle: { + stroke: segmentColors[segmentIndex] } + }); + }); + }, - // Solve for angle by using the law of cosines - var innerAngle = lawOfCosines(sides[0], - sides[2], sides[1]); - - var outerAngle = KhanUtil.findAngle(coords[rel(1)], - coords[rel(-1)]); - - var onLeft = sign(ccw( - coords[rel(-1)], coords[rel(1)], coords[i] - )) === 1; - - var offset = this.graphie.polar( - sides[0], - outerAngle + (onLeft ? 1 : -1) * innerAngle - ); - - return this.graphie.addPoints(coords[rel(-1)], offset); - - } else { - // Snap to grid (already done) - return true; - } - - }.bind(this); + removeLinearSystemControls: function() { + _.invoke(this.lines, "remove"); + _.map(this.points, function(segment) {return _.invoke(segment, "remove");}); + }, - if (self.isClickToAddPoints()) { - point.onMoveEnd = function(x, y) { - if (self.isCoordInTrash([x, y])) { - // remove this point from points - var index = self.removePoint(point); - if (self.polygon.closed) { - self.points = rotate(self.points, index); - self.polygon.closed = false; - } - self.polygon.points = self.points; - self.updatePolygon(); - // the polygon is now unclosed, so we need to - // remove any points props - self.clearCoords(); + isCoordInTrash: function(coord) { + var graphie = this.graphie; + var screenPoint = graphie.scalePoint(coord); + return screenPoint[0] >= graphie.xpixels - 40 && + screenPoint[1] >= graphie.ypixels - 40; + }, - // remove this movablePoint from graphie. - // we wait to do this until we're not inside of - // said point's onMoveEnd method so its state is - // consistent throughout this method call - setTimeout(point.remove.bind(point), 0); - } else if (self.points.length > 1 && (( - point === self.points[0] && - kpoint.equal([x, y], _.last(self.points).coord) - ) || ( - point === _.last(self.points) && - kpoint.equal([x, y], self.points[0].coord) - ))) { - // Join endpoints - var pointToRemove = self.points.pop(); - if (self.points.length > 2) { - self.polygon.closed = true; - self.updateCoordsFromPoints(); - } else { - self.polygon.closed = false; - self.clearCoords(); - } - self.updatePolygon(); - // remove this movablePoint from graphie. - // we wait to do this until we're not inside of - // said point's onMoveEnd method so its state is - // consistent throughout this method call - setTimeout(pointToRemove.remove.bind(pointToRemove), 0); - } else { - var shouldRemove = _.any(self.points, function(pt) { - return pt !== point && kpoint.equal(pt.coord, [x, y]); + createPointForPointsType: function(coord, i) { + var self = this; + var graphie = self.graphie; + var point = Interactive2.addMovablePoint(graphie, { + coord: coord, + snapX: graphie.snap[0], + snapY: graphie.snap[1], + constraints: [ + Interactive2.MovablePoint.constraints.bound(), + Interactive2.MovablePoint.constraints.snap(), + function(coord) { + // TODO(jack): There ought to be a + // MovablePoint.constraints.avoid + // default that lets you do things like this + return _.all(self.points, function(pt) { + return point === pt || + !kpoint.equal(coord, pt.coord()); }); - if (shouldRemove) { - self.removePoint(point); - self.polygon.points = self.points; - if (self.points.length < 3) { - self.polygon.closed = false; - self.clearCoords(); - } else if (self.polygon.closed) { - self.updateCoordsFromPoints(); - } - self.updatePolygon(); + } + ], + onMoveStart: function() { + if (self.isClickToAddPoints()) { + self.setTrashCanVisibility(1); + } + }, + onMove: self.updateCoordsFromPoints, + onMoveEnd: function(coord) { + if (self.isClickToAddPoints()) { + if (self.isCoordInTrash(coord)) { + // remove this point from points + self.points = _.filter(self.points, function(pt) { + return pt !== point; + }); + // update the correct answer box + self.updateCoordsFromPoints(); + // remove this movablePoint from graphie. // we wait to do this until we're not inside of // said point's onMoveEnd method so its state is // consistent throughout this method call setTimeout(point.remove.bind(point), 0); } + // In case we mouseup'd off the graphie and that + // stopped the move (in which case, we might not + // be in isCoordInTrash() + self.setTrashCanVisibility(0.5); } - // In case we mouseup'd off the graphie and that - // stopped the move - self.setTrashCanVisibility(0.5); - return true; - }; - } - - point.isTouched = false; - $(point.mouseTarget[0]).on("vmousedown", function() { - if (self.isClickToAddPoints()) { - self.setTrashCanVisibility(1); + }, + normalStyle: { + stroke: KhanUtil.INTERACTIVE, + fill: KhanUtil.INTERACTIVE } - point.isTouched = true; }); - $(point.mouseTarget[0]).on("vmouseup", function() { - if (self.isClickToAddPoints()) { - self.setTrashCanVisibility(0.5); - } - // If this was - // * a click on the first or last point - // * and not a drag, - // * and our polygon is not closed, - // * and we can close it (we need at least 3 points), - // then close it - if ((point === this.points[0] || point === _.last(this.points)) && - point.isTouched && - !point.hasMoved && - !this.polygon.closed && - this.points.length > 2) { - this.polygon.closed = true; - this.updatePolygon(); - // We finally have a closed polygon, so save our - // points to props - this.updateCoordsFromPoints(); - } - point.isTouched = false; - point.hasMoved = false; - }.bind(this)); - - $(point).on("move", function() { - this.polygon.transform(); - if (this.polygon.closed) { - this.updateCoordsFromPoints(); - } - }.bind(this)); - return point; }, - updateCoordsFromPoints: function() { - var graph = _.extend({}, this.props.graph, { - // Handle old movable points with .coord, or - // Interactive2.MovablePoint's with .coord() - coords: _.map(this.points, function(point) { - return _.result(point, "coord"); - }) + removePoint: function(point) { + var index = null; + this.points = _.filter(this.points, function(pt, i) { + if (pt === point) { + index = i; + return false; + } else { + return true; + } }); - this.props.onChange({graph: graph}); + return index; }, - clearCoords: function() { - var graph = _.extend({}, this.props.graph, { - coords: null - }); - this.props.onChange({graph: graph}); - }, + createPointForPolygonType: function(coord, i) { + var self = this; + var graphie = this.graphie; - addPointControls: function() { - var coords = InteractiveGraph.getPointCoords( - this.props.graph, - this.props - ); - // Clear out our old points so that newly added points don't - // "collide" with them and reposition when being added - // Without this, when added, each point checks whether it is on top - // of a point in this.points, which (a) shouldn't matter since - // we're clearing out this.points anyways, and (b) can cause problems - // if each of this.points is a MovablePoint instead of an - // Interactive2.MovablePoint, since one has a .coord and the other - // has .coord() - // TODO(jack): Figure out a better way to do this - this.points = []; - this.points = _.map(coords, this.createPointForPointsType, this); - }, + // TODO(alex): check against "grid" instead, use constants + var snapToGrid = !_.contains(["angles", "sides"], + this.props.graph.snapTo); - removePointControls: function() { - _.invoke(this.points, "remove"); - }, + var point = graphie.addMovablePoint(_.extend({ + coord: coord, + normalStyle: { + stroke: KhanUtil.INTERACTIVE, + fill: KhanUtil.INTERACTIVE + } + }, snapToGrid ? { + snapX: graphie.snap[0], + snapY: graphie.snap[1] + } : {} + )); - addSegmentControls: function() { - var self = this; - var graphie = this.graphie; + // Index relative to current point -> absolute index + // NOTE: This does not work when isClickToAddPoints() == true, + // as `i` can be changed by dragging a point to the trash + // Currently this function is only called when !isClickToAddPoints() + function rel(j) { + return (i + j + self.points.length) % self.points.length; + } - var coords = InteractiveGraph.getSegmentCoords( - this.props.graph, - this.props - ); + point.hasMoved = false; - this.points = []; - this.lines = _.map(coords, function(segment, i) { - var updateCoordProps = function() { - var graph = _.extend({}, self.props.graph, { - coords: _.invoke(self.lines, "coords") - }); - self.props.onChange({graph: graph}); - }; + point.onMove = function(x, y) { + var coords = _.pluck(this.points, "coord"); + coords[i] = [x, y]; + if (!kpoint.equal([x, y], point.coord)) { + point.hasMoved = true; + } - var points = _.map(segment, function(coord, i) { - return Interactive2.addMovablePoint(graphie, { - coord: coord, - normalStyle: { - stroke: KhanUtil.INTERACTIVE, - fill: KhanUtil.INTERACTIVE - }, - constraints: [ - Interactive2.MovablePoint.constraints.bound(), - Interactive2.MovablePoint.constraints.snap(), - function(coord) { - if (!points) { - // points hasn't been defined yet because - // we're still creating them - return; + // Check for invalid positioning, but only if we aren't adding + // points one click at a time, since those added points could + // have already violated these constraints + if (!self.isClickToAddPoints()) { + // Polygons can't have consecutive collinear points + if (collinear(coords[rel(-2)], coords[rel(-1)], coords[i]) || + collinear(coords[rel(-1)], coords[i], coords[rel(1)]) || + collinear(coords[i], coords[rel(1)], coords[rel(2)])) { + return false; + } + + var segments = _.zip(coords, rotate(coords)); + + if (self.points.length > 3) { + // Constrain to simple (non self-intersecting) polygon by + // testing whether adjacent segments intersect any others + for (var j = -1; j <= 0; j++) { + var segment = segments[rel(j)]; + var others = _.without(segments, + segment, segments[rel(j-1)], segments[rel(j+1)]); + + for (var k = 0; k < others.length; k++) { + var other = others[k]; + if (intersects(segment, other)) { + return false; } - return !kpoint.equal(coord, points[1 - i].coord()); } - ], - onMove: updateCoordProps + } + } + } + + if (this.props.graph.snapTo === "angles" && + self.points.length > 2) { + // Snap to whole degree interior angles + + var angles = _.map(angleMeasures(coords), function(rad) { + return rad * 180 / Math.PI; }); - }); - self.points = self.points.concat(points); - var line = Interactive2.addMovableLine(graphie, { - points: points, - static: false, - updatePoints: true, - constraints: [ - Interactive2.MovableLine.constraints.bound(), - Interactive2.MovableLine.constraints.snap() - ], - onMove: updateCoordProps, - normalStyle: { - stroke: KhanUtil.INTERACTIVE - }, - highlightStyle: { - stroke: KhanUtil.INTERACTING + _.each([-1, 1], function(j) { + angles[rel(j)] = Math.round(angles[rel(j)]); + }); + + var getAngle = function(a, vertex, b) { + var angle = KhanUtil.findAngle( + coords[rel(a)], coords[rel(b)], coords[rel(vertex)] + ); + return (angle + 360) % 360; + }; + + var innerAngles = [ + angles[rel(-1)] - getAngle(-2, -1, 1), + angles[rel(1)] - getAngle(-1, 1, 2) + ]; + innerAngles[2] = 180 - (innerAngles[0] + innerAngles[1]); + + // Avoid degenerate triangles + if (_.any(innerAngles, function(angle) { + return leq(angle, 1); + })) { + return false; } - }); - _.invoke(points, "toFront"); - return line; - }, this); - }, + var knownSide = magnitude(vector(coords[rel(-1)], + coords[rel(1)])); + + var onLeft = sign(ccw( + coords[rel(-1)], coords[rel(1)], coords[i] + )) === 1; - removeSegmentControls: function() { - _.invoke(this.points, "remove"); - _.invoke(this.lines, "remove"); - }, + // Solve for side by using the law of sines + var side = Math.sin(innerAngles[1] * Math.PI / 180) / + Math.sin(innerAngles[2] * Math.PI / 180) * knownSide; - addRayControls: function() { - this.addLine("ray"); - }, + var outerAngle = KhanUtil.findAngle(coords[rel(1)], + coords[rel(-1)]); - removeRayControls: function() { - this.removeLine(); - }, + var offset = this.graphie.polar( + side, + outerAngle + (onLeft? 1 : -1) * innerAngles[0] + ); - addPolygonControls: function() { - this.polygon = null; - var coords = InteractiveGraph.getPolygonCoords( - this.props.graph, - this.props - ); - this.points = _.map(coords, this.createPointForPolygonType); - this.updatePolygon(); - }, + return this.graphie.addPoints(coords[rel(-1)], offset); - updatePolygon: function() { - var closed; - if (this.polygon) { - closed = this.polygon.closed; - } else if (this.points.length >= 3) { - closed = true; - } else { - // There will only be fewer than 3 points in click-to-add-vertices - // mode, so we don't need to explicitly check for that here. - closed = false; - } - if (this.polygon) { - this.polygon.remove(); - } + } else if (this.props.graph.snapTo === "sides" && + self.points.length > 1) { + // Snap to whole unit side measures - var graphie = this.graphie; - var n = this.points.length; + var sides = _.map([ + [coords[rel(-1)], coords[i]], + [coords[i], coords[rel(1)]], + [coords[rel(-1)], coords[rel(1)]] + ], function(coords) { + return magnitude(vector.apply(null, coords)); + }); - // TODO(alex): check against "grid" instead, use constants - var snapToGrid = !_.contains(["angles", "sides"], - this.props.graph.snapTo); + _.each([0, 1], function(j) { + sides[j] = Math.round(sides[j]); + }); - var angleLabels = _.times(n, function(i) { - if (!this.props.graph.showAngles || - (!closed && (i === 0 || i === n - 1))) { - return ""; - } else if (this.props.graph.snapTo === "angles") { - return "$deg0"; - } else { - return "$deg1"; - } - }, this); + // Avoid degenerate triangles + if (leq(sides[1] + sides[2], sides[0]) || + leq(sides[0] + sides[2], sides[1]) || + leq(sides[0] + sides[1], sides[2])) { + return false; + } - var showRightAngleMarkers = _.times(n, function(i) { - return closed || (i !== 0 && i !== n - 1); - }, this); + // Solve for angle by using the law of cosines + var innerAngle = lawOfCosines(sides[0], + sides[2], sides[1]); - var numArcs = _.times(n, function(i) { - if (this.props.graph.showAngles && - (closed || (i !== 0 && i !== n - 1))) { - return 1; - } else { - return 0; - } - }, this); + var outerAngle = KhanUtil.findAngle(coords[rel(1)], + coords[rel(-1)]); - var sideLabels = _.times(n, function(i) { - if (!this.props.graph.showSides || - (!closed && i === n - 1)) { - return ""; - } else if (this.props.graph.snapTo === "sides") { - return "$len0"; - } else { - return "$len1"; - } - }, this); + var onLeft = sign(ccw( + coords[rel(-1)], coords[rel(1)], coords[i] + )) === 1; - this.polygon = graphie.addMovablePolygon(_.extend({ - closed: closed, - points: this.points, - angleLabels: angleLabels, - showRightAngleMarkers: showRightAngleMarkers, - numArcs: numArcs, - sideLabels: sideLabels, - updateOnPointMove: false - }, snapToGrid ? { - snapX: graphie.snap[0], - snapY: graphie.snap[1] - } : {} - )); + var offset = this.graphie.polar( + sides[0], + outerAngle + (onLeft ? 1 : -1) * innerAngle + ); - $(this.polygon).on("move", function() { - if (this.polygon.closed) { - this.updateCoordsFromPoints(); - } - }.bind(this)); - }, + return this.graphie.addPoints(coords[rel(-1)], offset); - removePolygonControls: function() { - _.invoke(this.points, "remove"); - this.polygon.remove(); - }, + } else { + // Snap to grid (already done) + return true; + } - addAngleControls: function() { - var graphie = this.graphie; + }.bind(this); - var coords = InteractiveGraph.getAngleCoords( - this.props.graph, - this.props - ); + if (self.isClickToAddPoints()) { + point.onMoveEnd = function(x, y) { + if (self.isCoordInTrash([x, y])) { + // remove this point from points + var index = self.removePoint(point); + if (self.polygon.closed) { + self.points = rotate(self.points, index); + self.polygon.closed = false; + } + self.polygon.points = self.points; + self.updatePolygon(); + // the polygon is now unclosed, so we need to + // remove any points props + self.clearCoords(); - // The vertex snaps to the grid, but the rays don't... - this.points = _.map(coords, function(coord, i) { - return graphie.addMovablePoint(_.extend({ - coord: coord, - normalStyle: { - stroke: KhanUtil.INTERACTIVE, - fill: KhanUtil.INTERACTIVE + // remove this movablePoint from graphie. + // we wait to do this until we're not inside of + // said point's onMoveEnd method so its state is + // consistent throughout this method call + setTimeout(point.remove.bind(point), 0); + } else if (self.points.length > 1 && (( + point === self.points[0] && + kpoint.equal([x, y], _.last(self.points).coord) + ) || ( + point === _.last(self.points) && + kpoint.equal([x, y], self.points[0].coord) + ))) { + // Join endpoints + var pointToRemove = self.points.pop(); + if (self.points.length > 2) { + self.polygon.closed = true; + self.updateCoordsFromPoints(); + } else { + self.polygon.closed = false; + self.clearCoords(); + } + self.updatePolygon(); + // remove this movablePoint from graphie. + // we wait to do this until we're not inside of + // said point's onMoveEnd method so its state is + // consistent throughout this method call + setTimeout(pointToRemove.remove.bind(pointToRemove), 0); + } else { + var shouldRemove = _.any(self.points, function(pt) { + return pt !== point && kpoint.equal(pt.coord, [x, y]); + }); + if (shouldRemove) { + self.removePoint(point); + self.polygon.points = self.points; + if (self.points.length < 3) { + self.polygon.closed = false; + self.clearCoords(); + } else if (self.polygon.closed) { + self.updateCoordsFromPoints(); + } + self.updatePolygon(); + // remove this movablePoint from graphie. + // we wait to do this until we're not inside of + // said point's onMoveEnd method so its state is + // consistent throughout this method call + setTimeout(point.remove.bind(point), 0); + } } - }, i === 1 ? { - snapX: graphie.snap[0], - snapY: graphie.snap[1] - } : {})); - }); + // In case we mouseup'd off the graphie and that + // stopped the move + self.setTrashCanVisibility(0.5); + return true; + }; + } - // ...they snap to whole-degree angles from the vertex. - this.angle = graphie.addMovableAngle({ - points: this.points, - snapDegrees: this.props.graph.snapDegrees || 1, - snapOffsetDeg: this.props.graph.angleOffsetDeg || 0, - angleLabel: this.props.graph.showAngles ? "$deg0" : "", - pushOut: 2, - allowReflex: defaultVal(this.props.graph.allowReflexAngles, true) + point.isTouched = false; + $(point.mouseTarget[0]).on("vmousedown", function() { + if (self.isClickToAddPoints()) { + self.setTrashCanVisibility(1); + } + point.isTouched = true; }); - $(this.angle).on("move", function() { - var graph = _.extend({}, this.props.graph, { - coords: this.angle.getClockwiseCoords() - }); - this.props.onChange({graph: graph}); + $(point.mouseTarget[0]).on("vmouseup", function() { + if (self.isClickToAddPoints()) { + self.setTrashCanVisibility(0.5); + } + // If this was + // * a click on the first or last point + // * and not a drag, + // * and our polygon is not closed, + // * and we can close it (we need at least 3 points), + // then close it + if ((point === this.points[0] || point === _.last(this.points)) && + point.isTouched && + !point.hasMoved && + !this.polygon.closed && + this.points.length > 2) { + this.polygon.closed = true; + this.updatePolygon(); + // We finally have a closed polygon, so save our + // points to props + this.updateCoordsFromPoints(); + } + point.isTouched = false; + point.hasMoved = false; + }.bind(this)); + + $(point).on("move", function() { + this.polygon.transform(); + if (this.polygon.closed) { + this.updateCoordsFromPoints(); + } }.bind(this)); - }, - removeAngleControls: function() { - _.invoke(this.points, "remove"); - this.angle.remove(); + return point; }, - toggleShowAngles: function() { + updateCoordsFromPoints: function() { var graph = _.extend({}, this.props.graph, { - showAngles: !this.props.graph.showAngles + // Handle old movable points with .coord, or + // Interactive2.MovablePoint's with .coord() + coords: _.map(this.points, function(point) { + return _.result(point, "coord"); + }) }); this.props.onChange({graph: graph}); }, - toggleShowSides: function() { + clearCoords: function() { var graph = _.extend({}, this.props.graph, { - showSides: !this.props.graph.showSides + coords: null }); this.props.onChange({graph: graph}); }, - toJSON: function() { - return this.props.graph; - }, - - simpleValidate: function(rubric) { - return InteractiveGraph.validate(this.toJSON(), rubric, this); - }, - - focus: $.noop, - - statics: { - displayMode: "block" - } -}); - - -_.extend(InteractiveGraph, { - getQuadraticCoefficients: function(coords) { - var p1 = coords[0]; - var p2 = coords[1]; - var p3 = coords[2]; - - var denom = (p1[0] - p2[0]) * (p1[0] - p3[0]) * (p2[0] - p3[0]); - if (denom === 0) { - return; - } - var a = (p3[0] * (p2[1] - p1[1]) + - p2[0] * (p1[1] - p3[1]) + - p1[0] * (p3[1] - p2[1])) / denom; - var b = ((p3[0] * p3[0]) * (p1[1] - p2[1]) + - (p2[0] * p2[0]) * (p3[1] - p1[1]) + - (p1[0] * p1[0]) * (p2[1] - p3[1])) / denom; - var c = (p2[0] * p3[0] * (p2[0] - p3[0]) * p1[1] + - p3[0] * p1[0] * (p3[0] - p1[0]) * p2[1] + - p1[0] * p2[0] * (p1[0] - p2[0]) * p3[1]) / denom; - return [a, b, c]; + addPointControls: function() { + var coords = InteractiveGraph.getPointCoords( + this.props.graph, + this.props + ); + // Clear out our old points so that newly added points don't + // "collide" with them and reposition when being added + // Without this, when added, each point checks whether it is on top + // of a point in this.points, which (a) shouldn't matter since + // we're clearing out this.points anyways, and (b) can cause problems + // if each of this.points is a MovablePoint instead of an + // Interactive2.MovablePoint, since one has a .coord and the other + // has .coord() + // TODO(jack): Figure out a better way to do this + this.points = []; + this.points = _.map(coords, this.createPointForPointsType, this); }, - getSinusoidCoefficients: function(coords) { - // It's assumed that p1 is the root and p2 is the first peak - var p1 = coords[0]; - var p2 = coords[1]; - - // Resulting coefficients are canonical for this sine curve - var amplitude = (p2[1] - p1[1]); - var angularFrequency = Math.PI / (2 * (p2[0] - p1[0])); - var phase = p1[0] * angularFrequency; - var verticalOffset = p1[1]; - - return [amplitude, angularFrequency, phase, verticalOffset]; + removePointControls: function() { + _.invoke(this.points, "remove"); }, + addSegmentControls: function() { + var self = this; + var graphie = this.graphie; - /** - * @param {object} graph Like props.graph or props.correct - * @param {object} props of an InteractiveGraph instance - */ - getLineCoords: function(graph, props) { - return graph.coords || InteractiveGraph.pointsFromNormalized( - props, - [ - [0.25, 0.75], - [0.75, 0.75] - ] + var coords = InteractiveGraph.getSegmentCoords( + this.props.graph, + this.props ); - }, - - /** - * @param {object} graph Like props.graph or props.correct - * @param {object} props of an InteractiveGraph instance - */ - getPointCoords: function(graph, props) { - var numPoints = graph.numPoints || 1; - var coords = graph.coords; - - if (coords) { - return coords; - } else { - switch (numPoints) { - case 1: - // Back in the day, one point's coords were in graph.coord - coords = [graph.coord || [0, 0]]; - break; - case 2: - coords = [[-5, 0], [5, 0]]; - break; - case 3: - coords = [[-5, 0], [0, 0], [5, 0]]; - break; - case 4: - coords = [[-6, 0], [-2, 0], [2, 0], [6, 0]]; - break; - case 5: - coords = [[-6, 0], [-3, 0], [0, 0], [3, 0], [6, 0]]; - break; - case 6: - coords = [[-5, 0], [-3, 0], [-1, 0], [1, 0], [3, 0], - [5, 0]]; - break; - case UNLIMITED: - coords = []; - break; - } - // Transform coords from their -10 to 10 space to 0 to 1 - // because of the old graph.coord, and also it's easier. - var range = [[-10, 10], [-10, 10]]; - coords = InteractiveGraph.normalizeCoords(coords, range); - var coords = InteractiveGraph.pointsFromNormalized(props, coords); - return coords; - } - }, + this.points = []; + this.lines = _.map(coords, function(segment, i) { + var updateCoordProps = function() { + var graph = _.extend({}, self.props.graph, { + coords: _.invoke(self.lines, "coords") + }); + self.props.onChange({graph: graph}); + }; - /** - * @param {object} graph Like props.graph or props.correct - * @param {object} props of an InteractiveGraph instance - */ - getLinearSystemCoords: function(graph, props) { - return graph.coords || - _.map([ - [[0.25, 0.75], [0.75, 0.75]], - [[0.25, 0.25], [0.75, 0.25]] - ], function(coords) { - return InteractiveGraph.pointsFromNormalized(props, coords); + var points = _.map(segment, function(coord, i) { + return Interactive2.addMovablePoint(graphie, { + coord: coord, + normalStyle: { + stroke: KhanUtil.INTERACTIVE, + fill: KhanUtil.INTERACTIVE + }, + constraints: [ + Interactive2.MovablePoint.constraints.bound(), + Interactive2.MovablePoint.constraints.snap(), + function(coord) { + if (!points) { + // points hasn't been defined yet because + // we're still creating them + return; + } + return !kpoint.equal(coord, points[1 - i].coord()); + } + ], + onMove: updateCoordProps + }); }); - }, - - /** - * @param {object} graph Like props.graph or props.correct - * @param {object} props of an InteractiveGraph instance - */ - getPolygonCoords: function(graph, props) { - var coords = graph.coords; - if (coords) { - return coords; - } - - var n = graph.numSides || 3; - - if (n === UNLIMITED) { - coords = []; - } else { - var angle = 2 * Math.PI / n; - var offset = (1 / n - 1 / 2) * Math.PI; - - // TODO(alex): Generalize this to more than just triangles so that - // all polygons have whole number side lengths if snapping to sides - var radius = graph.snapTo === "sides" ? Math.sqrt(3) / 3 * 7: 4; - // Generate coords of a regular polygon with n sides - coords = _.times(n, function(i) { - return [ - radius * Math.cos(i * angle + offset), - radius * Math.sin(i * angle + offset) - ]; + self.points = self.points.concat(points); + var line = Interactive2.addMovableLine(graphie, { + points: points, + static: false, + updatePoints: true, + constraints: [ + Interactive2.MovableLine.constraints.bound(), + Interactive2.MovableLine.constraints.snap() + ], + onMove: updateCoordProps, + normalStyle: { + stroke: KhanUtil.INTERACTIVE + }, + highlightStyle: { + stroke: KhanUtil.INTERACTING + } }); - } - - var range = [[-10, 10], [-10, 10]]; - coords = InteractiveGraph.normalizeCoords(coords, range); + _.invoke(points, "toFront"); - var snapToGrid = !_.contains(["angles", "sides"], graph.snapTo); - coords = InteractiveGraph.pointsFromNormalized(props, coords, - /* noSnap */ !snapToGrid); + return line; + }, this); + }, - return coords; + removeSegmentControls: function() { + _.invoke(this.points, "remove"); + _.invoke(this.lines, "remove"); }, - /** - * @param {object} graph Like props.graph or props.correct - * @param {object} props of an InteractiveGraph instance - */ - getSegmentCoords: function(graph, props) { - var coords = graph.coords; - if (coords) { - return coords; - } + addRayControls: function() { + this.addLine("ray"); + }, - var n = graph.numSegments || 1; - var ys = { - 1: [5], - 2: [5, -5], - 3: [5, 0, -5], - 4: [6, 2, -2, -6], - 5: [6, 3, 0, -3, -6], - 6: [5, 3, 1, -1, -3, -5] - }[n]; - var range = [[-10, 10], [-10, 10]]; + removeRayControls: function() { + this.removeLine(); + }, - return _.map(ys, function(y) { - var segment = [[-5, y], [5, y]]; - segment = InteractiveGraph.normalizeCoords(segment, range); - segment = InteractiveGraph.pointsFromNormalized(props, segment); - return segment; - }); + addPolygonControls: function() { + this.polygon = null; + var coords = InteractiveGraph.getPolygonCoords( + this.props.graph, + this.props + ); + this.points = _.map(coords, this.createPointForPolygonType); + this.updatePolygon(); }, - /** - * @param {object} graph Like props.graph or props.correct - * @param {object} props of an InteractiveGraph instance - */ - getAngleCoords: function(graph, props) { - var coords = graph.coords; - if (coords) { - return coords; + updatePolygon: function() { + var closed; + if (this.polygon) { + closed = this.polygon.closed; + } else if (this.points.length >= 3) { + closed = true; + } else { + // There will only be fewer than 3 points in click-to-add-vertices + // mode, so we don't need to explicitly check for that here. + closed = false; } - var snap = graph.snapDegrees || 1; - var angle = snap; - while (angle < 20) { - angle += snap; + if (this.polygon) { + this.polygon.remove(); } - angle = angle * Math.PI / 180; - var offset = (graph.angleOffsetDeg || 0) * Math.PI / 180; - coords = InteractiveGraph.pointsFromNormalized(props, [ - [0.85, 0.50], - [0.5, 0.50] - ]); - - var radius = magnitude(vector.apply(null, coords)); - - // Adjust the lower point by angleOffsetDeg degrees - coords[0] = [ - coords[1][0] + radius * Math.cos(offset), - coords[1][1] + radius * Math.sin(offset) - ]; - // Position the upper point angle radians from the - // lower point - coords[2] = [ - coords[1][0] + radius * Math.cos(angle + offset), - coords[1][1] + radius * Math.sin(angle + offset) - ]; + var graphie = this.graphie; + var n = this.points.length; - return coords; - }, + // TODO(alex): check against "grid" instead, use constants + var snapToGrid = !_.contains(["angles", "sides"], + this.props.graph.snapTo); - normalizeCoords: function(coordsList, range) { - return _.map(coordsList, function(coords) { - return _.map(coords, function(coord, i) { - var extent = range[i][1] - range[i][0]; - return ((coord + range[i][1]) / extent); - }); - }); - }, + var angleLabels = _.times(n, function(i) { + if (!this.props.graph.showAngles || + (!closed && (i === 0 || i === n - 1))) { + return ""; + } else if (this.props.graph.snapTo === "angles") { + return "$deg0"; + } else { + return "$deg1"; + } + }, this); - getEquationString: function(props) { - var type = props.graph.type; - var funcName = "get" + capitalize(type) + "EquationString"; - return InteractiveGraph[funcName](props); - }, + var showRightAngleMarkers = _.times(n, function(i) { + return closed || (i !== 0 && i !== n - 1); + }, this); - pointsFromNormalized: function(props, coordsList, noSnap) { - return _.map(coordsList, function(coords) { - return _.map(coords, function(coord, i) { - var range = props.range[i]; - if (noSnap) { - return range[0] + (range[1] - range[0]) * coord; - } else { - var step = props.step[i]; - var nSteps = numSteps(range, step); - var tick = Math.round(coord * nSteps); - return range[0] + step * tick; - } - }); - }); - }, + var numArcs = _.times(n, function(i) { + if (this.props.graph.showAngles && + (closed || (i !== 0 && i !== n - 1))) { + return 1; + } else { + return 0; + } + }, this); - getLinearEquationString: function(props) { - var coords = InteractiveGraph.getLineCoords(props.graph, props); - if (eq(coords[0][0], coords[1][0])) { - return "x = " + coords[0][0].toFixed(3); - } else { - var m = (coords[1][1] - coords[0][1]) / - (coords[1][0] - coords[0][0]); - var b = coords[0][1] - m * coords[0][0]; - if (eq(m, 0)) { - return "y = " + b.toFixed(3); + var sideLabels = _.times(n, function(i) { + if (!this.props.graph.showSides || + (!closed && i === n - 1)) { + return ""; + } else if (this.props.graph.snapTo === "sides") { + return "$len0"; } else { - return "y = " + m.toFixed(3) + "x + " + b.toFixed(3); + return "$len1"; } - } - }, + }, this); - getCurrentQuadraticCoefficients: function(props) { - // TODO(alpert): Don't duplicate - var coords = props.graph.coords || - InteractiveGraph.defaultQuadraticCoords(props); - return InteractiveGraph.getQuadraticCoefficients(coords); - }, + this.polygon = graphie.addMovablePolygon(_.extend({ + closed: closed, + points: this.points, + angleLabels: angleLabels, + showRightAngleMarkers: showRightAngleMarkers, + numArcs: numArcs, + sideLabels: sideLabels, + updateOnPointMove: false + }, snapToGrid ? { + snapX: graphie.snap[0], + snapY: graphie.snap[1] + } : {} + )); - defaultQuadraticCoords: function(props) { - var coords = [[0.25, 0.75], [0.5, 0.25], [0.75, 0.75]]; - return InteractiveGraph.pointsFromNormalized(props, coords); + $(this.polygon).on("move", function() { + if (this.polygon.closed) { + this.updateCoordsFromPoints(); + } + }.bind(this)); }, - getQuadraticEquationString: function(props) { - var coeffs = InteractiveGraph.getCurrentQuadraticCoefficients( - props); - return "y = " + coeffs[0].toFixed(3) + "x^2 + " + - coeffs[1].toFixed(3) + "x + " + - coeffs[2].toFixed(3); + removePolygonControls: function() { + _.invoke(this.points, "remove"); + this.polygon.remove(); }, - getCurrentSinusoidCoefficients: function(props) { - var coords = props.graph.coords || - InteractiveGraph.defaultSinusoidCoords(props); - return InteractiveGraph.getSinusoidCoefficients(coords); - }, + addAngleControls: function() { + var graphie = this.graphie; - defaultSinusoidCoords: function(props) { - var coords = [[0.5, 0.5], [0.65, 0.60]]; - return InteractiveGraph.pointsFromNormalized(props, coords); - }, + var coords = InteractiveGraph.getAngleCoords( + this.props.graph, + this.props + ); - getSinusoidEquationString: function(props) { - var coeffs = InteractiveGraph.getCurrentSinusoidCoefficients( - props); - return "y = " + coeffs[0].toFixed(3) + "sin(" + - coeffs[1].toFixed(3) + "x - " + - coeffs[2].toFixed(3) + ") + " + - coeffs[3].toFixed(3); - }, + // The vertex snaps to the grid, but the rays don't... + this.points = _.map(coords, function(coord, i) { + return graphie.addMovablePoint(_.extend({ + coord: coord, + normalStyle: { + stroke: KhanUtil.INTERACTIVE, + fill: KhanUtil.INTERACTIVE + } + }, i === 1 ? { + snapX: graphie.snap[0], + snapY: graphie.snap[1] + } : {})); + }); - getCircleEquationString: function(props) { - var graph = props.graph; - // TODO(alpert): Don't duplicate - var center = graph.center || [0, 0]; - var radius = graph.radius || 2; - return "center (" + center[0] + ", " + center[1] + "), radius " + - radius; - }, + // ...they snap to whole-degree angles from the vertex. + this.angle = graphie.addMovableAngle({ + points: this.points, + snapDegrees: this.props.graph.snapDegrees || 1, + snapOffsetDeg: this.props.graph.angleOffsetDeg || 0, + angleLabel: this.props.graph.showAngles ? "$deg0" : "", + pushOut: 2, + allowReflex: defaultVal(this.props.graph.allowReflexAngles, true) + }); - getLinearSystemEquationString: function(props) { - var coords = InteractiveGraph.getLinearSystemCoords( - props.graph, - props - ); - return "\n" + - getLineEquation(coords[0][0], coords[0][1]) + - "\n" + - getLineEquation(coords[1][0], coords[1][1]) + - "\n" + - getLineIntersection(coords[0], coords[1]); + $(this.angle).on("move", function() { + var graph = _.extend({}, this.props.graph, { + coords: this.angle.getClockwiseCoords() + }); + this.props.onChange({graph: graph}); + }.bind(this)); }, - getPointEquationString: function(props) { - var coords = InteractiveGraph.getPointCoords(props.graph, props); - return coords.map(function(coord) { - return "(" + coord[0] + ", " + coord[1] + ")"; - }).join(", "); + removeAngleControls: function() { + _.invoke(this.points, "remove"); + this.angle.remove(); }, - getSegmentEquationString: function(props) { - var segments = InteractiveGraph.getSegmentCoords(props.graph, - props); - return _.map(segments, function(segment) { - return "[" + - _.map(segment, function(coord) { - return "(" + coord.join(", ") + ")"; - }).join(" ") + - "]"; - }).join(" "); + toggleShowAngles: function() { + var graph = _.extend({}, this.props.graph, { + showAngles: !this.props.graph.showAngles + }); + this.props.onChange({graph: graph}); }, - getRayEquationString: function(props) { - var coords = InteractiveGraph.getLineCoords(props.graph, props); - var a = coords[0]; - var b = coords[1]; - var eq = InteractiveGraph.getLinearEquationString(props); - - if (a[0] > b[0]) { - eq += " (for x <= " + a[0].toFixed(3) + ")"; - } else if (a[0] < b[0]) { - eq += " (for x >= " + a[0].toFixed(3) + ")"; - } else if (a[1] > b[1]) { - eq += " (for y <= " + a[1].toFixed(3) + ")"; - } else { - eq += " (for y >= " + a[1].toFixed(3) + ")"; - } - - return eq; + toggleShowSides: function() { + var graph = _.extend({}, this.props.graph, { + showSides: !this.props.graph.showSides + }); + this.props.onChange({graph: graph}); }, - getPolygonEquationString: function(props) { - var coords = InteractiveGraph.getPolygonCoords( - props.graph, - props - ); - return _.map(coords, function(coord) { - return "(" + coord.join(", ") + ")"; - }).join(" "); + toJSON: function() { + return this.props.graph; }, - getAngleEquationString: function(props) { - var coords = InteractiveGraph.getAngleCoords(props.graph, props); - var angle = KhanUtil.findAngle(coords[2], coords[0], coords[1]); - return angle.toFixed(0) + "\u00B0 angle" + - " at (" + coords[1].join(", ") + ")"; + simpleValidate: function(rubric) { + return InteractiveGraph.validate(this.toJSON(), rubric, this); }, - validate: function(state, rubric, component) { - // TODO(alpert): Because this.props.graph doesn't always have coords, - // check that .coords exists here, which is always true when something - // has moved - if (state.type === rubric.correct.type && state.coords) { - if (state.type === "linear") { - var guess = state.coords; - var correct = rubric.correct.coords; - // If both of the guess points are on the correct line, it's - // correct. - if (collinear(correct[0], correct[1], guess[0]) && - collinear(correct[0], correct[1], guess[1])) { - return { - type: "points", - earned: 1, - total: 1, - message: null - }; - } - } else if (state.type === "linear-system") { - var guess = state.coords; - var correct = rubric.correct.coords; + focus: $.noop, - if (( - collinear(correct[0][0], correct[0][1], guess[0][0]) && - collinear(correct[0][0], correct[0][1], guess[0][1]) && - collinear(correct[1][0], correct[1][1], guess[1][0]) && - collinear(correct[1][0], correct[1][1], guess[1][1]) - ) || ( - collinear(correct[0][0], correct[0][1], guess[1][0]) && - collinear(correct[0][0], correct[0][1], guess[1][1]) && - collinear(correct[1][0], correct[1][1], guess[0][0]) && - collinear(correct[1][0], correct[1][1], guess[0][1]) - )) { - return { - type: "points", - earned: 1, - total: 1, - message: null - }; - } + statics: { + displayMode: "block" + } +}); - } else if (state.type === "quadratic") { - // If the parabola coefficients match, it's correct. - var guessCoeffs = this.getQuadraticCoefficients(state.coords); - var correctCoeffs = this.getQuadraticCoefficients( - rubric.correct.coords); - if (deepEq(guessCoeffs, correctCoeffs)) { - return { - type: "points", - earned: 1, - total: 1, - message: null - }; - } - } else if (state.type === "sinusoid") { - var guessCoeffs = this.getSinusoidCoefficients( - state.coords); - var correctCoeffs = this.getSinusoidCoefficients( - rubric.correct.coords); - var canonicalGuessCoeffs = canonicalSineCoefficients( - guessCoeffs); - var canonicalCorrectCoeffs = canonicalSineCoefficients( - correctCoeffs); - // If the canonical coefficients match, it's correct. - if (deepEq(canonicalGuessCoeffs, canonicalCorrectCoeffs)) { - return { - type: "points", - earned: 1, - total: 1, - message: null - }; - } - } else if (state.type === "circle") { - if (deepEq(state.center, rubric.correct.center) && - eq(state.radius, rubric.correct.radius)) { - return { - type: "points", - earned: 1, - total: 1, - message: null - }; - } - } else if (state.type === "point") { - var guess = state.coords; - var correct = InteractiveGraph.getPointCoords( - rubric.correct, component); - guess = guess.slice(); - correct = correct.slice(); - // Everything's already rounded so we shouldn't need to do an - // eq() comparison but _.isEqual(0, -0) is false, so we'll use - // eq() anyway. The sort should be fine because it'll stringify - // it and -0 converted to a string is "0" - guess.sort(); - correct.sort(); - if (deepEq(guess, correct)) { - return { - type: "points", - earned: 1, - total: 1, - message: null - }; - } - } else if (state.type === "polygon") { - var guess = state.coords.slice(); - var correct = rubric.correct.coords.slice(); +_.extend(InteractiveGraph, { + getQuadraticCoefficients: function(coords) { + var p1 = coords[0]; + var p2 = coords[1]; + var p3 = coords[2]; - var match; - if (rubric.correct.match === "similar") { - match = similar(guess, correct, Number.POSITIVE_INFINITY); - } else if (rubric.correct.match === "congruent") { - match = similar(guess, correct, knumber.DEFAULT_TOLERANCE); - } else if (rubric.correct.match === "approx") { - match = similar(guess, correct, 0.1); - } else { /* exact */ - guess.sort(); - correct.sort(); - match = deepEq(guess, correct); - } + var denom = (p1[0] - p2[0]) * (p1[0] - p3[0]) * (p2[0] - p3[0]); + if (denom === 0) { + return; + } + var a = (p3[0] * (p2[1] - p1[1]) + + p2[0] * (p1[1] - p3[1]) + + p1[0] * (p3[1] - p2[1])) / denom; + var b = ((p3[0] * p3[0]) * (p1[1] - p2[1]) + + (p2[0] * p2[0]) * (p3[1] - p1[1]) + + (p1[0] * p1[0]) * (p2[1] - p3[1])) / denom; + var c = (p2[0] * p3[0] * (p2[0] - p3[0]) * p1[1] + + p3[0] * p1[0] * (p3[0] - p1[0]) * p2[1] + + p1[0] * p2[0] * (p1[0] - p2[0]) * p3[1]) / denom; + return [a, b, c]; + }, - if (match) { - return { - type: "points", - earned: 1, - total: 1, - message: null - }; - } - } else if (state.type === "segment") { - var guess = state.coords.slice(); - var correct = rubric.correct.coords.slice(); - guess = _.invoke(guess, "sort").sort(); - correct = _.invoke(correct, "sort").sort(); - if (deepEq(guess, correct)) { - return { - type: "points", - earned: 1, - total: 1, - message: null - }; - } - } else if (state.type === "ray") { - var guess = state.coords; - var correct = rubric.correct.coords; - if (deepEq(guess[0], correct[0]) && - collinear(correct[0], correct[1], guess[1])) { - return { - type: "points", - earned: 1, - total: 1, - message: null - }; - } - } else if (state.type === "angle") { - var guess = state.coords; - var correct = rubric.correct.coords; + getSinusoidCoefficients: function(coords) { + // It's assumed that p1 is the root and p2 is the first peak + var p1 = coords[0]; + var p2 = coords[1]; - var match; - if (rubric.correct.match === "congruent") { - var angles = _.map([guess, correct], function(coords) { - var angle = KhanUtil.findAngle( - coords[2], coords[0], coords[1]); - return (angle + 360) % 360; - }); - match = eq.apply(null, angles); - } else { /* exact */ - match = deepEq(guess[1], correct[1]) && - collinear(correct[1], correct[0], guess[0]) && - collinear(correct[1], correct[2], guess[2]); - } + // Resulting coefficients are canonical for this sine curve + var amplitude = (p2[1] - p1[1]); + var angularFrequency = Math.PI / (2 * (p2[0] - p1[0])); + var phase = p1[0] * angularFrequency; + var verticalOffset = p1[1]; - if (match) { - return { - type: "points", - earned: 1, - total: 1, - message: null - }; - } + return [amplitude, angularFrequency, phase, verticalOffset]; + }, + + + /** + * @param {object} graph Like props.graph or props.correct + * @param {object} props of an InteractiveGraph instance + */ + getLineCoords: function(graph, props) { + return graph.coords || InteractiveGraph.pointsFromNormalized( + props, + [ + [0.25, 0.75], + [0.75, 0.75] + ] + ); + }, + + /** + * @param {object} graph Like props.graph or props.correct + * @param {object} props of an InteractiveGraph instance + */ + getPointCoords: function(graph, props) { + var numPoints = graph.numPoints || 1; + var coords = graph.coords; + + if (coords) { + return coords; + } else { + switch (numPoints) { + case 1: + // Back in the day, one point's coords were in graph.coord + coords = [graph.coord || [0, 0]]; + break; + case 2: + coords = [[-5, 0], [5, 0]]; + break; + case 3: + coords = [[-5, 0], [0, 0], [5, 0]]; + break; + case 4: + coords = [[-6, 0], [-2, 0], [2, 0], [6, 0]]; + break; + case 5: + coords = [[-6, 0], [-3, 0], [0, 0], [3, 0], [6, 0]]; + break; + case 6: + coords = [[-5, 0], [-3, 0], [-1, 0], [1, 0], [3, 0], + [5, 0]]; + break; + case UNLIMITED: + coords = []; + break; } + // Transform coords from their -10 to 10 space to 0 to 1 + // because of the old graph.coord, and also it's easier. + var range = [[-10, 10], [-10, 10]]; + coords = InteractiveGraph.normalizeCoords(coords, range); + + var coords = InteractiveGraph.pointsFromNormalized(props, coords); + return coords; } + }, - // The input wasn't correct, so check if it's a blank input or if it's - // actually just wrong - if (!state.coords || _.isEqual(state, rubric.graph)) { - // We're where we started. - return { - type: "invalid", - message: null - }; + /** + * @param {object} graph Like props.graph or props.correct + * @param {object} props of an InteractiveGraph instance + */ + getLinearSystemCoords: function(graph, props) { + return graph.coords || + _.map([ + [[0.25, 0.75], [0.75, 0.75]], + [[0.25, 0.25], [0.75, 0.25]] + ], function(coords) { + return InteractiveGraph.pointsFromNormalized(props, coords); + }); + }, + + /** + * @param {object} graph Like props.graph or props.correct + * @param {object} props of an InteractiveGraph instance + */ + getPolygonCoords: function(graph, props) { + var coords = graph.coords; + if (coords) { + return coords; + } + + var n = graph.numSides || 3; + + if (n === UNLIMITED) { + coords = []; } else { - return { - type: "points", - earned: 0, - total: 1, - message: null - }; + var angle = 2 * Math.PI / n; + var offset = (1 / n - 1 / 2) * Math.PI; + + // TODO(alex): Generalize this to more than just triangles so that + // all polygons have whole number side lengths if snapping to sides + var radius = graph.snapTo === "sides" ? Math.sqrt(3) / 3 * 7: 4; + + // Generate coords of a regular polygon with n sides + coords = _.times(n, function(i) { + return [ + radius * Math.cos(i * angle + offset), + radius * Math.sin(i * angle + offset) + ]; + }); } - } -}); -var InteractiveGraphEditor = React.createClass({displayName: 'InteractiveGraphEditor', - className: "perseus-widget-interactive-graph", + var range = [[-10, 10], [-10, 10]]; + coords = InteractiveGraph.normalizeCoords(coords, range); - getDefaultProps: function() { - var range = this.props.range || [[-10, 10], [-10, 10]]; - var step = this.props.step || [1, 1]; - var gridStep = this.props.gridStep || - Util.getGridStep(range, step, defaultEditorBoxSize); - var snapStep = this.props.snapStep || - Util.snapStepFromGridStep(gridStep); - return { - box: [defaultEditorBoxSize, defaultEditorBoxSize], - labels: ["x", "y"], - range: range, - step: step, - gridStep: gridStep, - snapStep: snapStep, - valid: true, - backgroundImage: defaultBackgroundImage, - markings: "graph", - showProtractor: false, - showRuler: false, - rulerLabel: "", - rulerTicks: 10, - correct: { - type: "linear", - coords: null - } - }; + var snapToGrid = !_.contains(["angles", "sides"], graph.snapTo); + coords = InteractiveGraph.pointsFromNormalized(props, coords, + /* noSnap */ !snapToGrid); + + return coords; }, - mixins: [DeprecationMixin], - deprecatedProps: deprecatedProps, + /** + * @param {object} graph Like props.graph or props.correct + * @param {object} props of an InteractiveGraph instance + */ + getSegmentCoords: function(graph, props) { + var coords = graph.coords; + if (coords) { + return coords; + } - render: function() { - var graph; - var equationString; + var n = graph.numSegments || 1; + var ys = { + 1: [5], + 2: [5, -5], + 3: [5, 0, -5], + 4: [6, 2, -2, -6], + 5: [6, 3, 0, -3, -6], + 6: [5, 3, 1, -1, -3, -5] + }[n]; + var range = [[-10, 10], [-10, 10]]; - if (this.props.valid === true) { - // TODO(jack): send these down all at once - var graphProps = { - ref: "graph", - box: this.props.box, - range: this.props.range, - labels: this.props.labels, - step: this.props.step, - gridStep: this.props.gridStep, - snapStep: this.props.snapStep, - graph: this.props.correct, - backgroundImage: this.props.backgroundImage, - markings: this.props.markings, - showProtractor: this.props.showProtractor, - showRuler: this.props.showRuler, - rulerLabel: this.props.rulerLabel, - rulerTicks: this.props.rulerTicks, - flexibleType: true, - onChange: function(newProps) { - var correct = this.props.correct; - if (correct.type === newProps.graph.type) { - correct = _.extend({}, correct, newProps.graph); - } else { - // Clear options from previous graph - correct = newProps.graph; - } - this.props.onChange({correct: correct}); - }.bind(this) - }; - graph = InteractiveGraph(graphProps); - equationString = InteractiveGraph.getEquationString(graphProps); - } else { - graph = React.DOM.div(null, this.props.valid); + return _.map(ys, function(y) { + var segment = [[-5, y], [5, y]]; + segment = InteractiveGraph.normalizeCoords(segment, range); + segment = InteractiveGraph.pointsFromNormalized(props, segment); + return segment; + }); + }, + + /** + * @param {object} graph Like props.graph or props.correct + * @param {object} props of an InteractiveGraph instance + */ + getAngleCoords: function(graph, props) { + var coords = graph.coords; + if (coords) { + return coords; + } + + var snap = graph.snapDegrees || 1; + var angle = snap; + while (angle < 20) { + angle += snap; } + angle = angle * Math.PI / 180; + var offset = (graph.angleOffsetDeg || 0) * Math.PI / 180; - return React.DOM.div( {className:"perseus-widget-interactive-graph"}, - React.DOM.div(null, "Correct answer",' ', - InfoTip(null, - React.DOM.p(null, "Graph the correct answer in the graph below and ensure"+' '+ - "the equation or point coordinates displayed represent the"+' '+ - "correct answer.") - ), - ' ',": ", equationString), - + coords = InteractiveGraph.pointsFromNormalized(props, [ + [0.85, 0.50], + [0.5, 0.50] + ]); - GraphSettings( - {box:this.props.box, - range:this.props.range, - labels:this.props.labels, - step:this.props.step, - gridStep:this.props.gridStep, - snapStep:this.props.snapStep, - valid:this.props.valid, - backgroundImage:this.props.backgroundImage, - markings:this.props.markings, - showProtractor:this.props.showProtractor, - showRuler:this.props.showRuler, - rulerLabel:this.props.rulerLabel, - rulerTicks:this.props.rulerTicks, - onChange:this.props.onChange} ), + var radius = magnitude(vector.apply(null, coords)); + // Adjust the lower point by angleOffsetDeg degrees + coords[0] = [ + coords[1][0] + radius * Math.cos(offset), + coords[1][1] + radius * Math.sin(offset) + ]; + // Position the upper point angle radians from the + // lower point + coords[2] = [ + coords[1][0] + radius * Math.cos(angle + offset), + coords[1][1] + radius * Math.sin(angle + offset) + ]; - this.props.correct.type === "polygon" && - React.DOM.div( {className:"type-settings"}, - React.DOM.label(null, - ' ',"Student answer must",' ', - React.DOM.select( - {value:this.props.correct.match, - onChange:this.changeMatchType}, - React.DOM.option( {value:"exact"}, "match exactly"), - React.DOM.option( {value:"congruent"}, "be congruent"), - React.DOM.option( {value:"approx"}, - "be approximately congruent"), - React.DOM.option( {value:"similar"}, "be similar") - ) - ), - InfoTip(null, - React.DOM.ul(null, - React.DOM.li(null, - React.DOM.p(null, React.DOM.b(null, "Match Exactly:"), " Match exactly in size,"+' '+ - "orientation, and location on the grid even if it is"+' '+ - "not shown in the background.") - ), - React.DOM.li(null, - React.DOM.p(null, React.DOM.b(null, "Be Congruent:"), " Be congruent in size and"+' '+ - "shape, but can be located anywhere on the grid.") - ), - React.DOM.li(null, - React.DOM.p(null, - React.DOM.b(null, "Be Approximately Congruent:"), " Be exactly"+' '+ - "similar, and congruent in size and shape to"+' '+ - "within 0.1 units, but can be located anywhere"+' '+ - "on the grid. ", React.DOM.em(null, "Use this with snapping to"+' '+ - "angle measure.") - ) - ), - React.DOM.li(null, - React.DOM.p(null, React.DOM.b(null, "Be Similar:"), " Be similar with matching"+' '+ - "interior angles, and side measures that are"+' '+ - "matching or a multiple of the correct side"+' '+ - "measures. The figure can be located anywhere on the"+' '+ - "grid.") - ) - ) - ) - ), - this.props.correct.type === "angle" && - React.DOM.div( {className:"type-settings"}, - React.DOM.div(null, - React.DOM.label(null, - ' ',"Student answer must",' ', - React.DOM.select( - {value:this.props.correct.match, - onChange:this.changeMatchType}, - React.DOM.option( {value:"exact"}, "match exactly"), - React.DOM.option( {value:"congruent"}, "be congruent") - ) - ), - InfoTip(null, - React.DOM.p(null, "Congruency requires only that the angle measures are"+' '+ - "the same. An exact match implies congruency, but also"+' '+ - "requires that the angles have the same orientation and"+' '+ - "that the vertices are in the same position.") - ) - ) - ), - graph - ); + return coords; }, - changeMatchType: function(e) { - var correct = _.extend({}, this.props.correct, { - match: e.target.value + normalizeCoords: function(coordsList, range) { + return _.map(coordsList, function(coords) { + return _.map(coords, function(coord, i) { + var extent = range[i][1] - range[i][0]; + return ((coord + range[i][1]) / extent); + }); }); - this.props.onChange({correct: correct}); }, - toJSON: function() { - var json = _.pick(this.props, "step", "backgroundImage", "markings", - "labels", "showProtractor", "showRuler", "rulerLabel", - "rulerTicks", "range", "gridStep", "snapStep"); + getEquationString: function(props) { + var type = props.graph.type; + var funcName = "get" + capitalize(type) + "EquationString"; + return InteractiveGraph[funcName](props); + }, - var graph = this.refs.graph; - if (graph) { - var correct = graph && graph.toJSON(); - _.extend(json, { - // TODO(alpert): Allow specifying flexibleType (whether the - // graph type should be a choice or not) - graph: {type: correct.type}, - correct: correct + pointsFromNormalized: function(props, coordsList, noSnap) { + return _.map(coordsList, function(coords) { + return _.map(coords, function(coord, i) { + var range = props.range[i]; + if (noSnap) { + return range[0] + (range[1] - range[0]) * coord; + } else { + var step = props.step[i]; + var nSteps = numSteps(range, step); + var tick = Math.round(coord * nSteps); + return range[0] + step * tick; + } }); + }); + }, - _.each(["allowReflexAngles", "angleOffsetDeg", "numPoints", - "numSides", "numSegments", "showAngles", "showSides", - "snapTo", "snapDegrees"], - function(key) { - if (_.has(correct, key)) { - json.graph[key] = correct[key]; - } - }); + getLinearEquationString: function(props) { + var coords = InteractiveGraph.getLineCoords(props.graph, props); + if (eq(coords[0][0], coords[1][0])) { + return "x = " + coords[0][0].toFixed(3); + } else { + var m = (coords[1][1] - coords[0][1]) / + (coords[1][0] - coords[0][0]); + var b = coords[0][1] - m * coords[0][0]; + if (eq(m, 0)) { + return "y = " + b.toFixed(3); + } else { + return "y = " + m.toFixed(3) + "x + " + b.toFixed(3); + } } - return json; - } -}); - -module.exports = { - name: "interactive-graph", - displayName: "Interactive graph", - widget: InteractiveGraph, - editor: InteractiveGraphEditor, - hidden: true -}; - -},{"../components/graph-settings.jsx":121,"../components/graph.jsx":122,"../components/number-input.jsx":129,"../interactive2.js":148,"../util.js":168,"react":115,"react-components/info-tip":5}],181:[function(require,module,exports){ -/** @jsx React.DOM */ - -var InfoTip = require("react-components/info-tip"); -var PropCheckBox = require("../components/prop-check-box.jsx"); -var Util = require("../util.js"); - -function eq(x, y) { - return Math.abs(x - y) < 1e-9; -} - -var reverseRel = { - ge: "le", - gt: "lt", - le: "ge", - lt: "gt" -}; - -var toggleStrictRel = { - ge: "gt", - gt: "ge", - le: "lt", - lt: "le" -}; + }, -function formatImproper(n, d) { - if (d === 1) { - return "" + n; - } else { - return n + "/" + d; - } -} + getCurrentQuadraticCoefficients: function(props) { + // TODO(alpert): Don't duplicate + var coords = props.graph.coords || + InteractiveGraph.defaultQuadraticCoords(props); + return InteractiveGraph.getQuadraticCoefficients(coords); + }, -function formatMixed(n, d) { - if (n < 0) { - return "-" + formatMixed(-n, d); - } - var w = Math.floor(n / d); - if (w === 0) { - return formatImproper(n, d); - } else if (n - w * d === 0) { - return "" + w; - } else { - return w + "\\:" + formatImproper(n - w * d, d); - } -} + defaultQuadraticCoords: function(props) { + var coords = [[0.25, 0.75], [0.5, 0.25], [0.75, 0.75]]; + return InteractiveGraph.pointsFromNormalized(props, coords); + }, -var InteractiveNumberLine = React.createClass({displayName: 'InteractiveNumberLine', - getDefaultProps: function() { - return { - labelStyle: "decimal", - labelTicks: false, - isInequality: false, - pointX: 0, - rel: "ge" - }; + getQuadraticEquationString: function(props) { + var coeffs = InteractiveGraph.getCurrentQuadraticCoefficients( + props); + return "y = " + coeffs[0].toFixed(3) + "x^2 + " + + coeffs[1].toFixed(3) + "x + " + + coeffs[2].toFixed(3); }, - isValid: function() { - return this.props.range[0] < this.props.range[1] && - 0 < this.props.tickStep && - 0 < this.props.snapDivisions; + getCurrentSinusoidCoefficients: function(props) { + var coords = props.graph.coords || + InteractiveGraph.defaultSinusoidCoords(props); + return InteractiveGraph.getSinusoidCoefficients(coords); }, - render: function() { - var inequalityControls; - if (this.props.isInequality) { - inequalityControls = React.DOM.div(null, - React.DOM.input( {type:"button", value:"Switch direction", - onClick:this.handleReverse} ), - React.DOM.input( {type:"button", - value: - this.props.rel === "le" || this.props.rel === "ge" ? - "Make circle open" : - "Make circle filled", - - onClick:this.handleToggleStrict} ) - ); - } + defaultSinusoidCoords: function(props) { + var coords = [[0.5, 0.5], [0.65, 0.60]]; + return InteractiveGraph.pointsFromNormalized(props, coords); + }, - var valid = this.isValid(); - return React.DOM.div( {className:"perseus-widget " + - "perseus-widget-interactive-number-line"}, - React.DOM.div( {style:{display: valid ? "" : "none"}, - className:"graphie above-scratchpad", ref:"graphieDiv"} ), - React.DOM.div( {style:{display: valid ? "none" : ""}}, - ' ',"invalid number line configuration",' ' - ), - inequalityControls - ); + getSinusoidEquationString: function(props) { + var coeffs = InteractiveGraph.getCurrentSinusoidCoefficients( + props); + return "y = " + coeffs[0].toFixed(3) + "sin(" + + coeffs[1].toFixed(3) + "x - " + + coeffs[2].toFixed(3) + ") + " + + coeffs[3].toFixed(3); }, - handleReverse: function() { - this.props.onChange({rel: reverseRel[this.props.rel]}); + getCircleEquationString: function(props) { + var graph = props.graph; + // TODO(alpert): Don't duplicate + var center = graph.center || [0, 0]; + var radius = graph.radius || 2; + return "center (" + center[0] + ", " + center[1] + "), radius " + + radius; }, - handleToggleStrict: function() { - this.props.onChange({rel: toggleStrictRel[this.props.rel]}); + getLinearSystemEquationString: function(props) { + var coords = InteractiveGraph.getLinearSystemCoords( + props.graph, + props + ); + return "\n" + + getLineEquation(coords[0][0], coords[0][1]) + + "\n" + + getLineEquation(coords[1][0], coords[1][1]) + + "\n" + + getLineIntersection(coords[0], coords[1]); }, - componentDidMount: function() { - this.addGraphie(); + getPointEquationString: function(props) { + var coords = InteractiveGraph.getPointCoords(props.graph, props); + return coords.map(function(coord) { + return "(" + coord[0] + ", " + coord[1] + ")"; + }).join(", "); }, - componentDidUpdate: function() { - // Use jQuery to remove so event handlers don't leak - var node = this.refs.graphieDiv.getDOMNode(); - $(node).children().remove(); - - this.addGraphie(); + getSegmentEquationString: function(props) { + var segments = InteractiveGraph.getSegmentCoords(props.graph, + props); + return _.map(segments, function(segment) { + return "[" + + _.map(segment, function(coord) { + return "(" + coord.join(", ") + ")"; + }).join(" ") + + "]"; + }).join(" "); }, - _label: function(value) { - var graphie = this.graphie; - var labelStyle = this.props.labelStyle; - - // TODO(jack): Find out if any exercises have "decimal ticks" set, - // and if so, re-save them and remove this check. - if (labelStyle === "decimal" || labelStyle === "decimal ticks") { - graphie.label([value, -0.53], value, "center"); - } else if (labelStyle === "improper") { - var frac = KhanUtil.toFraction(value); - graphie.label([value, -0.53], - formatImproper(frac[0], frac[1]), "center"); - } else if (labelStyle === "mixed") { - var frac = KhanUtil.toFraction(value); - graphie.label([value, -0.53], - formatMixed(frac[0], frac[1]), "center"); - } - }, + getRayEquationString: function(props) { + var coords = InteractiveGraph.getLineCoords(props.graph, props); + var a = coords[0]; + var b = coords[1]; + var eq = InteractiveGraph.getLinearEquationString(props); - addGraphie: function() { - var self = this; - var graphie = this.graphie = KhanUtil.createGraphie( - this.refs.graphieDiv.getDOMNode()); - // Ensure a sane configuration to avoid infinite loops - if (!this.isValid()) { - return; + if (a[0] > b[0]) { + eq += " (for x <= " + a[0].toFixed(3) + ")"; + } else if (a[0] < b[0]) { + eq += " (for x >= " + a[0].toFixed(3) + ")"; + } else if (a[1] > b[1]) { + eq += " (for y <= " + a[1].toFixed(3) + ")"; + } else { + eq += " (for y >= " + a[1].toFixed(3) + ")"; } - var range = this.props.range; - var tickStep = this.props.tickStep; - var scale = 400 / (range[1] - range[0]); - - graphie.init({ - range: [[range[0] - 30 / scale, - range[1] + 30 / scale], - [-1, 1]], - scale: [scale, 40] - }); - graphie.addMouseLayer({ - allowScratchpad: true - }); - - // Line - - graphie.line([range[0] - (25 / scale), 0], - [range[1] + (25 / scale), 0], { - arrows: "->" - }); - graphie.line([range[1] + (25 / scale), 0], - [range[0] - (25 / scale), 0], { - arrows: "->" - }); - - // Ticks - var labelStyle = this.props.labelStyle; - for (var x = Math.ceil(range[0] / tickStep) * tickStep; x <= range[1]; - x += tickStep) { - graphie.line([x, -0.2], [x, 0.2]); + return eq; + }, - // TODO(jack): Find out if any exercises have "decimal ticks" set, - // and if so, re-save them and remove this check. - if (this.props.labelTicks || labelStyle === "decimal ticks") { - this._label(x); - } - } + getPolygonEquationString: function(props) { + var coords = InteractiveGraph.getPolygonCoords( + props.graph, + props + ); + return _.map(coords, function(coord) { + return "(" + coord.join(", ") + ")"; + }).join(" "); + }, - graphie.style({ - stroke: KhanUtil.INTERACTIVE, - strokeWidth: 3.5 - }, function() { - graphie.line([range[0], -0.2], [range[0], 0.2]); - graphie.line([range[1], -0.2], [range[1], 0.2]); - if (range[0] < 0 && 0 < range[1]) { - graphie.line([0, -0.2], [0, 0.2]); - } - }); + getAngleEquationString: function(props) { + var coords = InteractiveGraph.getAngleCoords(props.graph, props); + var angle = KhanUtil.findAngle(coords[2], coords[0], coords[1]); + return angle.toFixed(0) + "\u00B0 angle" + + " at (" + coords[1].join(", ") + ")"; + }, - graphie.style({color: KhanUtil.INTERACTIVE}, function() { - self._label(range[0]); - self._label(range[1]); - if (range[0] < 0 && 0 < range[1] && !self.props.labelTicks) { - graphie.label([0, -0.53], "0", "center"); - } - }); + validate: function(state, rubric, component) { + // TODO(alpert): Because this.props.graph doesn't always have coords, + // check that .coords exists here, which is always true when something + // has moved + if (state.type === rubric.correct.type && state.coords) { + if (state.type === "linear") { + var guess = state.coords; + var correct = rubric.correct.coords; + // If both of the guess points are on the correct line, it's + // correct. + if (collinear(correct[0], correct[1], guess[0]) && + collinear(correct[0], correct[1], guess[1])) { + return { + type: "points", + earned: 1, + total: 1, + message: null + }; + } + } else if (state.type === "linear-system") { + var guess = state.coords; + var correct = rubric.correct.coords; - // Point + if (( + collinear(correct[0][0], correct[0][1], guess[0][0]) && + collinear(correct[0][0], correct[0][1], guess[0][1]) && + collinear(correct[1][0], correct[1][1], guess[1][0]) && + collinear(correct[1][0], correct[1][1], guess[1][1]) + ) || ( + collinear(correct[0][0], correct[0][1], guess[1][0]) && + collinear(correct[0][0], correct[0][1], guess[1][1]) && + collinear(correct[1][0], correct[1][1], guess[0][0]) && + collinear(correct[1][0], correct[1][1], guess[0][1]) + )) { + return { + type: "points", + earned: 1, + total: 1, + message: null + }; + } - var isInequality = this.props.isInequality; - var rel = this.props.rel; + } else if (state.type === "quadratic") { + // If the parabola coefficients match, it's correct. + var guessCoeffs = this.getQuadraticCoefficients(state.coords); + var correctCoeffs = this.getQuadraticCoefficients( + rubric.correct.coords); + if (deepEq(guessCoeffs, correctCoeffs)) { + return { + type: "points", + earned: 1, + total: 1, + message: null + }; + } + } else if (state.type === "sinusoid") { + var guessCoeffs = this.getSinusoidCoefficients( + state.coords); + var correctCoeffs = this.getSinusoidCoefficients( + rubric.correct.coords); - var pointSize; - var pointStyle; - var highlightStyle; - if (isInequality && (rel === "lt" || rel === "gt")) { - pointSize = 5; - pointStyle = { - stroke: KhanUtil.INTERACTING, - fill: KhanUtil._BACKGROUND, - "stroke-width": 3 - }; - highlightStyle = { - stroke: KhanUtil.INTERACTING, - fill: KhanUtil._BACKGROUND, - "stroke-width": 4 - }; - } else { - pointSize = 4; - pointStyle = highlightStyle = { - stroke: KhanUtil.INTERACTING, - fill: KhanUtil.INTERACTING - }; - } + var canonicalGuessCoeffs = canonicalSineCoefficients( + guessCoeffs); + var canonicalCorrectCoeffs = canonicalSineCoefficients( + correctCoeffs); + // If the canonical coefficients match, it's correct. + if (deepEq(canonicalGuessCoeffs, canonicalCorrectCoeffs)) { + return { + type: "points", + earned: 1, + total: 1, + message: null + }; + } + } else if (state.type === "circle") { + if (deepEq(state.center, rubric.correct.center) && + eq(state.radius, rubric.correct.radius)) { + return { + type: "points", + earned: 1, + total: 1, + message: null + }; + } + } else if (state.type === "point") { + var guess = state.coords; + var correct = InteractiveGraph.getPointCoords( + rubric.correct, component); + guess = guess.slice(); + correct = correct.slice(); + // Everything's already rounded so we shouldn't need to do an + // eq() comparison but _.isEqual(0, -0) is false, so we'll use + // eq() anyway. The sort should be fine because it'll stringify + // it and -0 converted to a string is "0" + guess.sort(); + correct.sort(); + if (deepEq(guess, correct)) { + return { + type: "points", + earned: 1, + total: 1, + message: null + }; + } + } else if (state.type === "polygon") { + var guess = state.coords.slice(); + var correct = rubric.correct.coords.slice(); - var x = Math.min(Math.max(range[0], this.props.pointX), range[1]); - var point = this.point = graphie.addMovablePoint({ - pointSize: pointSize, - coord: [x, 0], - snapX: this.props.tickStep / this.props.snapDivisions, - constraints: { - constrainY: true - }, - normalStyle: pointStyle, - highlightStyle: highlightStyle - }); - point.onMove = function(x, y) { - x = Math.min(Math.max(range[0], x), range[1]); - updateInequality(x, y); - return [x, y]; - }; - point.onMoveEnd = function(x, y) { - this.props.onChange({pointX: x}); - }.bind(this); + var match; + if (rubric.correct.match === "similar") { + match = similar(guess, correct, Number.POSITIVE_INFINITY); + } else if (rubric.correct.match === "congruent") { + match = similar(guess, correct, knumber.DEFAULT_TOLERANCE); + } else if (rubric.correct.match === "approx") { + match = similar(guess, correct, 0.1); + } else { /* exact */ + guess.sort(); + correct.sort(); + match = deepEq(guess, correct); + } - // Inequality line + if (match) { + return { + type: "points", + earned: 1, + total: 1, + message: null + }; + } + } else if (state.type === "segment") { + var guess = state.coords.slice(); + var correct = rubric.correct.coords.slice(); + guess = _.invoke(guess, "sort").sort(); + correct = _.invoke(correct, "sort").sort(); + if (deepEq(guess, correct)) { + return { + type: "points", + earned: 1, + total: 1, + message: null + }; + } + } else if (state.type === "ray") { + var guess = state.coords; + var correct = rubric.correct.coords; + if (deepEq(guess[0], correct[0]) && + collinear(correct[0], correct[1], guess[1])) { + return { + type: "points", + earned: 1, + total: 1, + message: null + }; + } + } else if (state.type === "angle") { + var guess = state.coords; + var correct = rubric.correct.coords; - var inequalityLine; - updateInequality(x, 0); + var match; + if (rubric.correct.match === "congruent") { + var angles = _.map([guess, correct], function(coords) { + var angle = KhanUtil.findAngle( + coords[2], coords[0], coords[1]); + return (angle + 360) % 360; + }); + match = eq.apply(null, angles); + } else { /* exact */ + match = deepEq(guess[1], correct[1]) && + collinear(correct[1], correct[0], guess[0]) && + collinear(correct[1], correct[2], guess[2]); + } - function updateInequality(px, py) { - if (inequalityLine) { - inequalityLine.remove(); - inequalityLine = null; - } - if (isInequality) { - var end; - if (rel === "ge" || rel === "gt") { - end = [range[1] + (26 / scale), 0]; - } else { - end = [range[0] - (26 / scale), 0]; + if (match) { + return { + type: "points", + earned: 1, + total: 1, + message: null + }; } - inequalityLine = graphie.line( - [px, py], - end, - { - arrows: "->", - stroke: KhanUtil.INTERACTIVE, - strokeWidth: 3.5 - } - ); - point.toFront(); } } - }, - - toJSON: function() { - return { - pointX: this.props.pointX, - rel: this.props.isInequality ? this.props.rel : "eq" - }; - }, - - simpleValidate: function(rubric) { - return InteractiveNumberLine.validate(this.toJSON(), rubric); - }, - - focus: $.noop, - - statics: { - displayMode: "block" - } -}); - -_.extend(InteractiveNumberLine, { - validate: function(state, rubric) { - var range = rubric.range; - var start = Math.min(Math.max(range[0], 0), range[1]); - var startRel = rubric.isInequality ? "ge" : "eq"; - var correctRel = rubric.correctRel || "eq"; - - if (eq(state.pointX, rubric.correctX || 0) && - correctRel === state.rel) { - return { - type: "points", - earned: 1, - total: 1, - message: null - }; - } else if (state.pointX === start && state.rel === startRel) { + // The input wasn't correct, so check if it's a blank input or if it's + // actually just wrong + if (!state.coords || _.isEqual(state, rubric.graph)) { // We're where we started. return { type: "invalid", @@ -18009,131 +17400,662 @@ _.extend(InteractiveNumberLine, { } }); +var InteractiveGraphEditor = React.createClass({displayName: 'InteractiveGraphEditor', + className: "perseus-widget-interactive-graph", -var InteractiveNumberLineEditor = React.createClass({displayName: 'InteractiveNumberLineEditor', getDefaultProps: function() { + var range = [[-10, 10], [-10, 10]]; + var step = [1, 1]; + var gridStep = Util.getGridStep(range, step, defaultEditorBoxSize); + var snapStep = Util.snapStepFromGridStep(gridStep); return { - range: [0, 10], - labelStyle: "decimal", - labelTicks: false, - tickStep: 1, - snapDivisions: 4, - correctRel: "eq", - correctX: 0 + box: [defaultEditorBoxSize, defaultEditorBoxSize], + labels: ["x", "y"], + range: range, + step: step, + gridStep: gridStep, + snapStep: snapStep, + valid: true, + backgroundImage: defaultBackgroundImage, + markings: "graph", + showProtractor: false, + showRuler: false, + rulerLabel: "", + rulerTicks: 10, + correct: { + type: "linear", + coords: null + } }; }, + mixins: [DeprecationMixin], + deprecatedProps: deprecatedProps, + render: function() { - return React.DOM.div(null, - React.DOM.label(null, - ' ',"min x: ", React.DOM.input( {defaultValue:'' + this.props.range[0], - onBlur:this.onRangeBlur.bind(this, 0)} ) - ),React.DOM.br(null ), - React.DOM.label(null, - ' ',"max x: ", React.DOM.input( {defaultValue:'' + this.props.range[1], - onBlur:this.onRangeBlur.bind(this, 1)} ) - ), - InfoTip(null, - React.DOM.p(null, "Change \"label styles\" below to display the max and min x in"+' '+ - "different number formats.") - ),React.DOM.br(null ), - React.DOM.span(null, - ' ',"correct:",' ', - React.DOM.select( {value:this.props.correctRel, - onChange:this.onChange.bind(this, "correctRel")}, - React.DOM.optgroup( {label:"Equality"}, - React.DOM.option( {value:"eq"}, "x =") - ), - React.DOM.optgroup( {label:"Inequality"}, - React.DOM.option( {value:"lt"}, "x <"), - React.DOM.option( {value:"gt"}, "x >"), - React.DOM.option( {value:"le"}, "x ≤"), - React.DOM.option( {value:"ge"}, "x ≥") - ) + var graph; + var equationString; + + if (this.props.valid === true) { + // TODO(jack): send these down all at once + var graphProps = { + ref: "graph", + box: this.props.box, + range: this.props.range, + labels: this.props.labels, + step: this.props.step, + gridStep: this.props.gridStep, + snapStep: this.props.snapStep, + graph: this.props.correct, + backgroundImage: this.props.backgroundImage, + markings: this.props.markings, + showProtractor: this.props.showProtractor, + showRuler: this.props.showRuler, + rulerLabel: this.props.rulerLabel, + rulerTicks: this.props.rulerTicks, + flexibleType: true, + onChange: function(newProps) { + var correct = this.props.correct; + if (correct.type === newProps.graph.type) { + correct = _.extend({}, correct, newProps.graph); + } else { + // Clear options from previous graph + correct = newProps.graph; + } + this.props.onChange({correct: correct}); + }.bind(this) + }; + graph = InteractiveGraph(graphProps); + equationString = InteractiveGraph.getEquationString(graphProps); + } else { + graph = React.DOM.div(null, this.props.valid); + } + + return React.DOM.div( {className:"perseus-widget-interactive-graph"}, + React.DOM.div(null, "正確答案",' ', + InfoTip(null, + React.DOM.p(null, "將正確答案於下圖中繪製出來,請注意所有的函數圖或資料點需表示出正確的答案。") ), - React.DOM.input( {defaultValue:'' + this.props.correctX, - onBlur:this.onNumBlur.bind(this, "correctX")} ) - ),React.DOM.br(null ),React.DOM.br(null ), - React.DOM.label(null, - ' ',"label style:",' ', - React.DOM.select( {value:this.props.labelStyle, - onChange:this.onChange.bind(this, "labelStyle")}, - React.DOM.option( {value:"decimal"}, "Decimals"), - React.DOM.option( {value:"improper"}, "Improper fractions"), - React.DOM.option( {value:"mixed"}, "Mixed numbers") + ' ',": ", equationString), + + + GraphSettings( + {box:this.props.box, + range:this.props.range, + labels:this.props.labels, + step:this.props.step, + gridStep:this.props.gridStep, + snapStep:this.props.snapStep, + valid:this.props.valid, + backgroundImage:this.props.backgroundImage, + markings:this.props.markings, + showProtractor:this.props.showProtractor, + showRuler:this.props.showRuler, + rulerLabel:this.props.rulerLabel, + rulerTicks:this.props.rulerTicks, + onChange:this.props.onChange} ), + + + this.props.correct.type === "polygon" && + React.DOM.div( {className:"type-settings"}, + React.DOM.label(null, + ' ',"學生的答案必須要",' ', + React.DOM.select( + {value:this.props.correct.match, + onChange:this.changeMatchType}, + React.DOM.option( {value:"exact"}, "完全符合"), + React.DOM.option( {value:"congruent"}, "全等"), + React.DOM.option( {value:"approx"}, "大致上全等"), + React.DOM.option( {value:"similar"}, "相似") + ) ), - PropCheckBox( - {label:"label ticks", - labelTicks:this.props.labelTicks, - onChange:this.props.onChange} ) - ),React.DOM.br(null ), - React.DOM.label(null, - ' ',"tick step: ", React.DOM.input( {defaultValue:'' + this.props.tickStep, - onBlur:this.onNumBlur.bind(this, "tickStep")} ) + InfoTip(null, + React.DOM.ul(null, + React.DOM.li(null, + React.DOM.p(null, React.DOM.b(null, "完全符合:"), " 圖形於網格上的大小、方向、位置皆需完全符合答案。") + ), + React.DOM.li(null, + React.DOM.p(null, React.DOM.b(null, "全等:"), " 圖形的大小和形狀需與答案符合,但圖形於網格上的位置並無限制。") + ), + React.DOM.li(null, + React.DOM.p(null, + React.DOM.b(null, "大致上全等:"), " 圖形需與答案非常相似,圖形的大小和形狀與答案可誤差於 0.1 個"+' '+ + "網格單位,且圖形於網格上的位置並無限制。", + React.DOM.em(null, "(可使用此答案於對齊角度的選項)") + ) + ), + React.DOM.li(null, + React.DOM.p(null, React.DOM.b(null, "相似:"), " 圖形的內角、邊長與答案大致相似,或是圖形大部份邊的性質符合答案,且"+' '+ + "圖形於網格上的位置並無限制。") + ) + ) + ) ), - InfoTip(null, - React.DOM.p(null, "A tick mark is placed at every number of steps"+' '+ - "indicated.") - ),React.DOM.br(null ), - React.DOM.label(null, - ' ',"snap increments per tick:",' ', - React.DOM.input( {defaultValue:'' + this.props.snapDivisions, - onBlur:this.onNumBlur.bind(this, "snapDivisions")} ) + this.props.correct.type === "angle" && + React.DOM.div( {className:"type-settings"}, + React.DOM.div(null, + React.DOM.label(null, + ' ',"學生的答案必須要",' ', + React.DOM.select( + {value:this.props.correct.match, + onChange:this.changeMatchType}, + React.DOM.option( {value:"exact"}, "完全符合"), + React.DOM.option( {value:"congruent"}, "全等") + ) + ), + InfoTip(null, + React.DOM.p(null, "\"完全符合\"是指圖形於網格上的方向、位置皆需完全符合答案;"+' '+ + "\"全等\"僅要求角度部份相同即可。") + ) + ) ), - InfoTip(null, - React.DOM.p(null, "Ensure the required number of snap increments is provided to"+' '+ - "answer the question.") - ) + graph ); }, - onRangeBlur: function(i, e) { - var x = Util.firstNumericalParse(e.target.value) || 0; - e.target.value = x; - - var range = this.props.range.slice(); - range[i] = x; - this.props.onChange({range: range}); - }, - - onChange: function(key, e) { - var opts = {}; - opts[key] = e.target.value; - this.props.onChange(opts); + changeMatchType: function(e) { + var correct = _.extend({}, this.props.correct, { + match: e.target.value + }); + this.props.onChange({correct: correct}); }, - onNumBlur: function(key, e) { - var x = Util.firstNumericalParse(e.target.value) || 0; - e.target.value = x; + toJSON: function() { + var json = _.pick(this.props, "step", "backgroundImage", "markings", + "labels", "showProtractor", "showRuler", "rulerLabel", + "rulerTicks", "range", "gridStep", "snapStep"); - var opts = {}; - opts[key] = x; - this.props.onChange(opts); - }, + var graph = this.refs.graph; + if (graph) { + var correct = graph && graph.toJSON(); + _.extend(json, { + // TODO(alpert): Allow specifying flexibleType (whether the + // graph type should be a choice or not) + graph: {type: correct.type}, + correct: correct + }); - toJSON: function() { - return { - range: this.props.range, - labelStyle: this.props.labelStyle, - labelTicks: this.props.labelTicks, - tickStep: this.props.tickStep, - snapDivisions: this.props.snapDivisions, - correctRel: this.props.correctRel, - isInequality: this.props.correctRel !== "eq", - correctX: this.props.correctX - }; + _.each(["allowReflexAngles", "angleOffsetDeg", "numPoints", + "numSides", "numSegments", "showAngles", "showSides", + "snapTo", "snapDegrees"], + function(key) { + if (_.has(correct, key)) { + json.graph[key] = correct[key]; + } + }); + } + return json; } }); module.exports = { - name: "interactive-number-line", - displayName: "Number line 2", - hidden: true, - widget: InteractiveNumberLine, - editor: InteractiveNumberLineEditor + name: "interactive-graph", + displayName: "Interactive graph/互動式座標圖", + widget: InteractiveGraph, + editor: InteractiveGraphEditor, + hidden: false }; -},{"../components/prop-check-box.jsx":130,"../util.js":168,"react-components/info-tip":5}],182:[function(require,module,exports){ +},{"../components/graph-settings.jsx":53,"../components/graph.jsx":54,"../components/number-input.jsx":61,"../interactive2.js":80,"../util.js":100,"react":45,"react-components/info-tip":39}],113:[function(require,module,exports){ +/** @jsx React.DOM */ + +var InfoTip = require("react-components/info-tip"); +var PropCheckBox = require("../components/prop-check-box.jsx"); +var Util = require("../util.js"); + +function eq(x, y) { + return Math.abs(x - y) < 1e-9; +} + +var reverseRel = { + ge: "le", + gt: "lt", + le: "ge", + lt: "gt" +}; + +var toggleStrictRel = { + ge: "gt", + gt: "ge", + le: "lt", + lt: "le" +}; + +function formatImproper(n, d) { + if (d === 1) { + return "" + n; + } else { + return n + "/" + d; + } +} + +function formatMixed(n, d) { + if (n < 0) { + return "-" + formatMixed(-n, d); + } + var w = Math.floor(n / d); + if (w === 0) { + return formatImproper(n, d); + } else if (n - w * d === 0) { + return "" + w; + } else { + return w + "\\:" + formatImproper(n - w * d, d); + } +} + +var InteractiveNumberLine = React.createClass({displayName: 'InteractiveNumberLine', + getDefaultProps: function() { + return { + labelStyle: "decimal", + labelTicks: false, + isInequality: false, + pointX: 0, + rel: "ge" + }; + }, + + isValid: function() { + return this.props.range[0] < this.props.range[1] && + 0 < this.props.tickStep && + 0 < this.props.snapDivisions; + }, + + render: function() { + var inequalityControls; + if (this.props.isInequality) { + inequalityControls = React.DOM.div(null, + React.DOM.input( {type:"button", value:"換方向", + onClick:this.handleReverse} ), + React.DOM.input( {type:"button", + value: + this.props.rel === "le" || this.props.rel === "ge" ? + "改為空心圓" : + "改為實心圓", + + onClick:this.handleToggleStrict} ) + ); + } + + var valid = this.isValid(); + return React.DOM.div( {className:"perseus-widget " + + "perseus-widget-interactive-number-line"}, + React.DOM.div( {style:{display: valid ? "" : "none"}, + className:"graphie above-scratchpad", ref:"graphieDiv"} ), + React.DOM.div( {style:{display: valid ? "none" : ""}}, + ' ',"invalid number line configuration",' ' + ), + inequalityControls + ); + }, + + handleReverse: function() { + this.props.onChange({rel: reverseRel[this.props.rel]}); + }, + + handleToggleStrict: function() { + this.props.onChange({rel: toggleStrictRel[this.props.rel]}); + }, + + componentDidMount: function() { + this.addGraphie(); + }, + + componentDidUpdate: function() { + // Use jQuery to remove so event handlers don't leak + var node = this.refs.graphieDiv.getDOMNode(); + $(node).children().remove(); + + this.addGraphie(); + }, + + _label: function(value) { + var graphie = this.graphie; + var labelStyle = this.props.labelStyle; + + // TODO(jack): Find out if any exercises have "decimal ticks" set, + // and if so, re-save them and remove this check. + if (labelStyle === "decimal" || labelStyle === "decimal ticks") { + graphie.label([value, -0.53], value, "center"); + } else if (labelStyle === "improper") { + var frac = KhanUtil.toFraction(value); + graphie.label([value, -0.53], + formatImproper(frac[0], frac[1]), "center"); + } else if (labelStyle === "mixed") { + var frac = KhanUtil.toFraction(value); + graphie.label([value, -0.53], + formatMixed(frac[0], frac[1]), "center"); + } + }, + + addGraphie: function() { + var self = this; + var graphie = this.graphie = KhanUtil.createGraphie( + this.refs.graphieDiv.getDOMNode()); + // Ensure a sane configuration to avoid infinite loops + if (!this.isValid()) { + return; + } + + var range = this.props.range; + var tickStep = this.props.tickStep; + var scale = 400 / (range[1] - range[0]); + + graphie.init({ + range: [[range[0] - 30 / scale, + range[1] + 30 / scale], + [-1, 1]], + scale: [scale, 40] + }); + graphie.addMouseLayer({ + allowScratchpad: true + }); + + // Line + + graphie.line([range[0] - (25 / scale), 0], + [range[1] + (25 / scale), 0], { + arrows: "->" + }); + graphie.line([range[1] + (25 / scale), 0], + [range[0] - (25 / scale), 0], { + arrows: "->" + }); + + // Ticks + var labelStyle = this.props.labelStyle; + for (var x = Math.ceil(range[0] / tickStep) * tickStep; x <= range[1]; + x += tickStep) { + graphie.line([x, -0.2], [x, 0.2]); + + // TODO(jack): Find out if any exercises have "decimal ticks" set, + // and if so, re-save them and remove this check. + if (this.props.labelTicks || labelStyle === "decimal ticks") { + this._label(x); + } + } + + graphie.style({ + stroke: KhanUtil.INTERACTIVE, + strokeWidth: 3.5 + }, function() { + graphie.line([range[0], -0.2], [range[0], 0.2]); + graphie.line([range[1], -0.2], [range[1], 0.2]); + if (range[0] < 0 && 0 < range[1]) { + graphie.line([0, -0.2], [0, 0.2]); + } + }); + + graphie.style({color: KhanUtil.INTERACTIVE}, function() { + self._label(range[0]); + self._label(range[1]); + if (range[0] < 0 && 0 < range[1] && !self.props.labelTicks) { + graphie.label([0, -0.53], "0", "center"); + } + }); + + // Point + + var isInequality = this.props.isInequality; + var rel = this.props.rel; + + var pointSize; + var pointStyle; + var highlightStyle; + if (isInequality && (rel === "lt" || rel === "gt")) { + pointSize = 5; + pointStyle = { + stroke: KhanUtil.INTERACTING, + fill: KhanUtil._BACKGROUND, + "stroke-width": 3 + }; + highlightStyle = { + stroke: KhanUtil.INTERACTING, + fill: KhanUtil._BACKGROUND, + "stroke-width": 4 + }; + } else { + pointSize = 4; + pointStyle = highlightStyle = { + stroke: KhanUtil.INTERACTING, + fill: KhanUtil.INTERACTING + }; + } + + var x = Math.min(Math.max(range[0], this.props.pointX), range[1]); + var point = this.point = graphie.addMovablePoint({ + pointSize: pointSize, + coord: [x, 0], + snapX: this.props.tickStep / this.props.snapDivisions, + constraints: { + constrainY: true + }, + normalStyle: pointStyle, + highlightStyle: highlightStyle + }); + point.onMove = function(x, y) { + x = Math.min(Math.max(range[0], x), range[1]); + updateInequality(x, y); + return [x, y]; + }; + point.onMoveEnd = function(x, y) { + this.props.onChange({pointX: x}); + }.bind(this); + + // Inequality line + + var inequalityLine; + updateInequality(x, 0); + + function updateInequality(px, py) { + if (inequalityLine) { + inequalityLine.remove(); + inequalityLine = null; + } + if (isInequality) { + var end; + if (rel === "ge" || rel === "gt") { + end = [range[1] + (26 / scale), 0]; + } else { + end = [range[0] - (26 / scale), 0]; + } + inequalityLine = graphie.line( + [px, py], + end, + { + arrows: "->", + stroke: KhanUtil.INTERACTIVE, + strokeWidth: 3.5 + } + ); + point.toFront(); + } + } + }, + + setAnswerFromJSON: function(answerData) { + if (answerData === undefined) { + answerData = this.getDefaultProps(); + } + if (answerData.rel === "eq") { + answerData.rel = "ge"; + answerData.isInequality = false; + } + this.props.onChange(answerData); + }, + + toJSON: function() { + return { + pointX: this.props.pointX, + rel: this.props.isInequality ? this.props.rel : "eq" + }; + }, + + simpleValidate: function(rubric) { + return InteractiveNumberLine.validate(this.toJSON(), rubric); + }, + + focus: $.noop, + + statics: { + displayMode: "block" + } +}); + + +_.extend(InteractiveNumberLine, { + validate: function(state, rubric) { + var range = rubric.range; + var start = Math.min(Math.max(range[0], 0), range[1]); + var startRel = rubric.isInequality ? "ge" : "eq"; + var correctRel = rubric.correctRel || "eq"; + + if (eq(state.pointX, rubric.correctX || 0) && + correctRel === state.rel) { + return { + type: "points", + earned: 1, + total: 1, + message: null + }; + } else if (state.pointX === start && state.rel === startRel) { + // We're where we started. + return { + type: "invalid", + message: null + }; + } else { + return { + type: "points", + earned: 0, + total: 1, + message: null + }; + } + } +}); + + +var InteractiveNumberLineEditor = React.createClass({displayName: 'InteractiveNumberLineEditor', + getDefaultProps: function() { + return { + range: [0, 10], + labelStyle: "decimal", + labelTicks: false, + tickStep: 1, + snapDivisions: 4, + correctRel: "eq", + correctX: 0 + }; + }, + + render: function() { + return React.DOM.div(null, + React.DOM.label(null, + ' ',"最小 x: ", React.DOM.input( {defaultValue:'' + this.props.range[0], + onBlur:this.onRangeBlur.bind(this, 0)} ) + ),React.DOM.br(null ), + React.DOM.label(null, + ' ',"最大 x: ", React.DOM.input( {defaultValue:'' + this.props.range[1], + onBlur:this.onRangeBlur.bind(this, 1)} ) + ), + InfoTip(null, + React.DOM.p(null, "利用下方的「標籤格式」來改變最大與最小 x 的標籤顯示格式。") + ),React.DOM.br(null ), + React.DOM.span(null, + ' ',"正確答案:",' ', + React.DOM.select( {value:this.props.correctRel, + onChange:this.onChange.bind(this, "correctRel")}, + React.DOM.optgroup( {label:"等式"}, + React.DOM.option( {value:"eq"}, "x =") + ), + React.DOM.optgroup( {label:"不等式"}, + React.DOM.option( {value:"lt"}, "x <"), + React.DOM.option( {value:"gt"}, "x >"), + React.DOM.option( {value:"le"}, "x ≤"), + React.DOM.option( {value:"ge"}, "x ≥") + ) + ), + React.DOM.input( {defaultValue:'' + this.props.correctX, + onBlur:this.onNumBlur.bind(this, "correctX")} ) + ),React.DOM.br(null ),React.DOM.br(null ), + React.DOM.label(null, + ' ',"標籤格式:",' ', + React.DOM.select( {value:this.props.labelStyle, + onChange:this.onChange.bind(this, "labelStyle")}, + React.DOM.option( {value:"decimal"}, "小數"), + React.DOM.option( {value:"improper"}, "假分數"), + React.DOM.option( {value:"mixed"}, "帶分數") + ), + PropCheckBox( + {label:"顯示刻度代表的數字", + labelTicks:this.props.labelTicks, + onChange:this.props.onChange} ) + ),React.DOM.br(null ), + React.DOM.label(null, + ' ',"每一刻度之間距離: ", React.DOM.input( {defaultValue:'' + this.props.tickStep, + onBlur:this.onNumBlur.bind(this, "tickStep")} ) + ), + InfoTip(null, + React.DOM.p(null, "每一個刻度都會標上刻度線。") + ),React.DOM.br(null ), + React.DOM.label(null, + ' ',"刻度之間的分割數量:",' ', + React.DOM.input( {defaultValue:'' + this.props.snapDivisions, + onBlur:this.onNumBlur.bind(this, "snapDivisions")} ) + ), + InfoTip(null, + React.DOM.p(null, "確保分割數量足夠讓使用者回答問題,即答案會落在某分割的位置。") + ) + ); + }, + + onRangeBlur: function(i, e) { + var x = Util.firstNumericalParse(e.target.value) || 0; + e.target.value = x; + + var range = this.props.range.slice(); + range[i] = x; + this.props.onChange({range: range}); + }, + + onChange: function(key, e) { + var opts = {}; + opts[key] = e.target.value; + this.props.onChange(opts); + }, + + onNumBlur: function(key, e) { + var x = Util.firstNumericalParse(e.target.value) || 0; + e.target.value = x; + + var opts = {}; + opts[key] = x; + this.props.onChange(opts); + }, + + toJSON: function() { + return { + range: this.props.range, + labelStyle: this.props.labelStyle, + labelTicks: this.props.labelTicks, + tickStep: this.props.tickStep, + snapDivisions: this.props.snapDivisions, + correctRel: this.props.correctRel, + isInequality: this.props.correctRel !== "eq", + correctX: this.props.correctX + }; + } +}); + +module.exports = { + name: "interactive-number-line", + displayName: "Interactive-number-line/互動式數線", + hidden: false, + widget: InteractiveNumberLine, + editor: InteractiveNumberLineEditor +}; + +},{"../components/prop-check-box.jsx":62,"../util.js":100,"react-components/info-tip":39}],114:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -18340,12 +18262,12 @@ var LightsPuzzle = React.createClass({displayName: 'LightsPuzzle', onChange:this._flipTile} ), React.DOM.div( {style:{width: pxWidth}}, React.DOM.div( {style:MOVE_COUNT_STYLE}, - "Moves: ", this.props.moveCount + "移動次數: ", this.props.moveCount ), React.DOM.div( {style:RESET_BUTTON_STYLE}, React.DOM.input( {type:"button", - value:"Reset", + value:"重置", onClick:this._reset, className:"simple-button"} ) ) @@ -18456,20 +18378,20 @@ var LightsPuzzleEditor = React.createClass({displayName: 'LightsPuzzleEditor', render: function() { return React.DOM.div(null, React.DOM.div(null, - "Width:", + "寬度:", NumberInput( {value:this._width(), placeholder:5, onChange:this._changeWidth} ), ", ", - "Height:", + "高度:", NumberInput( {value:this._height(), placeholder:5, onChange:this._changeHeight} ) ), React.DOM.div(null, - "Flip pattern:", + "翻轉圖樣:", React.DOM.select( {value:this.props.flipPattern, onChange:this._handlePatternChange}, @@ -18479,17 +18401,17 @@ var LightsPuzzleEditor = React.createClass({displayName: 'LightsPuzzleEditor', ) ), React.DOM.div(null, - "Grade incomplete puzzles as wrong:", + "將未完成的謎題視為錯誤:", " ", PropCheckBox( {gradeIncompleteAsWrong:this.props.gradeIncompleteAsWrong, onChange:this.props.onChange} ), InfoTip(null, - "By default, incomplete puzzles are graded as empty." + "預設未完成的謎題會被當成空白來處理。" ) ), React.DOM.div(null, - "Starting configuration:" + "起始圖案設定:" ), React.DOM.div( {style:{overflowX: "auto"}}, TileGrid( @@ -18588,14 +18510,14 @@ var transformProps = function(editorProps) { module.exports = { name: "lights-puzzle", - displayName: "Lights Puzzle", - hidden: true, + displayName: "Lights Puzzle/點燈謎題", + hidden: false, widget: LightsPuzzle, editor: LightsPuzzleEditor, transform: transformProps }; -},{"../components/number-input.jsx":129,"../components/prop-check-box.jsx":130,"../mixins/changeable.jsx":159,"../mixins/jsonify-props.jsx":160,"react":115,"react-components/info-tip":5}],183:[function(require,module,exports){ +},{"../components/number-input.jsx":61,"../components/prop-check-box.jsx":62,"../mixins/changeable.jsx":91,"../mixins/jsonify-props.jsx":92,"react":45,"react-components/info-tip":39}],115:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -18751,11 +18673,9 @@ var MatcherEditor = React.createClass({displayName: 'MatcherEditor', render: function() { return React.DOM.div( {className:"perseus-matcher-editor"}, React.DOM.div(null, - ' ',"Correct answer:",' ', + ' ',"正確答案:",' ', InfoTip(null, - React.DOM.p(null, "Enter the correct answers here. The preview on the right"+' '+ - "will show the cards in a randomized order, which is how the"+' '+ - "student will see them.") + React.DOM.p(null, "在此輸入配對題組的正確答案。當題目顯示時,會隨機排序卡片的順序。") ) ), React.DOM.div( {className:"ui-helper-clearfix"}, @@ -18773,9 +18693,9 @@ var MatcherEditor = React.createClass({displayName: 'MatcherEditor', layout:"vertical"} ) ), React.DOM.span(null, - ' ',"Labels:",' ', + ' ',"標籤:",' ', InfoTip(null, - React.DOM.p(null, "These are entirely optional.") + React.DOM.p(null, "此欄位非必填。") ) ), React.DOM.div(null, @@ -18788,26 +18708,22 @@ var MatcherEditor = React.createClass({displayName: 'MatcherEditor', ), React.DOM.div(null, PropCheckBox( - {label:"Order of the matched pairs matters:", + {label:"第一欄的欄位順序可重新調整:", orderMatters:this.props.orderMatters, onChange:this.props.onChange} ), InfoTip(null, - React.DOM.p(null, "With this option enabled, only the order provided above"+' '+ - "will be treated as correct. This is useful when ordering is"+' '+ - "significant, such as in the context of a proof."), - React.DOM.p(null, "If disabled, pairwise matching is sufficient. To make"+' '+ - "this clear, the left column becomes fixed in the provided"+' '+ - "order and only the cards in the right column can be"+' '+ - "moved.") + React.DOM.p(null, "當此功能開啟時,第一欄欄位的順序必須完成符合。"), + React.DOM.p(null, "此功能適合使用在證明題的論證步驟與其理由的配對。"), + React.DOM.p(null, "當此功能關閉時,第一欄的欄位會固定下來,只讓使用者調整第二欄欄位的順序。") ) ), React.DOM.div(null, PropCheckBox( - {label:"Padding:", + {label:"留白:", padding:this.props.padding, onChange:this.props.onChange} ), InfoTip(null, - React.DOM.p(null, "Padding is good for text, but not needed for images.") + React.DOM.p(null, "建議在文字時加入「留白」,圖片模式不要加入。") ) ) ); @@ -18835,13 +18751,13 @@ var MatcherEditor = React.createClass({displayName: 'MatcherEditor', module.exports = { name: "matcher", - displayName: "Two column matcher", + displayName: "Two column matcher/配對題", widget: Matcher, editor: MatcherEditor, - hidden: true + hidden: false }; -},{"../components/prop-check-box.jsx":130,"../components/sortable.jsx":132,"../components/text-list-editor.jsx":135,"../renderer.jsx":165,"../util.js":168,"react":115,"react-components/info-tip":5}],184:[function(require,module,exports){ +},{"../components/prop-check-box.jsx":62,"../components/sortable.jsx":64,"../components/text-list-editor.jsx":67,"../renderer.jsx":97,"../util.js":100,"react":45,"react-components/info-tip":39}],116:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -18939,7 +18855,7 @@ var Measurer = React.createClass({displayName: 'Measurer', setupGraphie: function() { var graphieDiv = this.refs.graphieDiv.getDOMNode(); $(graphieDiv).empty(); - var graphie = this.graphie = KhanUtil.createGraphie(graphieDiv); + var graphie = this.graphie = KhanUtil.currentGraph = KhanUtil.createGraphie(graphieDiv); var scale = [40, 40]; var range = [ @@ -18959,7 +18875,7 @@ var Measurer = React.createClass({displayName: 'Measurer', } if (this.props.showProtractor) { - this.protractor = graphie.protractor([ + this.protractor = graphie.Protractor([ this.props.protractorX, this.props.protractorY ]); @@ -18970,7 +18886,7 @@ var Measurer = React.createClass({displayName: 'Measurer', } if (this.props.showRuler) { - this.ruler = graphie.ruler({ + this.ruler = graphie.Ruler({ center: [ (range[0][0] + range[0][1]) / 2, (range[1][0] + range[1][1]) / 2 @@ -19047,35 +18963,34 @@ var MeasurerEditor = React.createClass({displayName: 'MeasurerEditor', var image = _.extend({}, defaultImage, this.props.image); return React.DOM.div( {className:"perseus-widget-measurer"}, - React.DOM.div(null, "Image displayed under protractor and/or ruler:"), - React.DOM.div(null, "URL:",' ', + React.DOM.div(null, "背景圖片:"), + React.DOM.div(null, "圖片網址:",' ', React.DOM.input( {type:"text", className:"perseus-widget-measurer-url", ref:"image-url", - defaultValue:image.url, + value:image.url, onChange:this._changeUrl} ), InfoTip(null, - React.DOM.p(null, "Create an image in graphie, or use the \"Add image\" function"+' '+ - "to create a background.") + React.DOM.p(null, "插入圖片的連結網址。例如,先將圖片上傳至 http://imgur.com ,再分享其圖片網址 (Direct Link)。 " ) ) ), image.url && React.DOM.div( {className:"perseus-widget-row"}, React.DOM.div( {className:"perseus-widget-left-col"}, - NumberInput( {label:"Pixels from top:", + NumberInput( {label:"與上方的間隔畫素:", placeholder:0, onChange:this._changeTop, value:image.top, useArrowKeys:true} ) ), React.DOM.div( {className:"perseus-widget-right-col"}, - NumberInput( {label:"Pixels from left:", + NumberInput( {label:"與左方的間隔畫素:", placeholder:0, onChange:this._changeLeft, value:image.left, useArrowKeys:true} ) ) ), - React.DOM.div(null, "Containing area [width, height]:",' ', + React.DOM.div(null, "圖片大小 [寬, 高]:",' ', RangeInput( {onChange:this.change("box"), value:this.props.box, @@ -19083,12 +18998,12 @@ var MeasurerEditor = React.createClass({displayName: 'MeasurerEditor', ), React.DOM.div( {className:"perseus-widget-row"}, React.DOM.div( {className:"perseus-widget-left-col"}, - PropCheckBox( {label:"Show ruler", + PropCheckBox( {label:"顯示直尺", showRuler:this.props.showRuler, onChange:this.props.onChange} ) ), React.DOM.div( {className:"perseus-widget-right-col"}, - PropCheckBox( {label:"Show protractor", + PropCheckBox( {label:"顯示量角器", showProtractor:this.props.showProtractor, onChange:this.props.onChange} ) ) @@ -19096,26 +19011,26 @@ var MeasurerEditor = React.createClass({displayName: 'MeasurerEditor', this.props.showRuler && React.DOM.div(null, React.DOM.div(null, React.DOM.label(null, - ' ',"Ruler label:",' ', + ' ',"直尺單位:",' ', React.DOM.select( {onChange:function(e) {return this.change("rulerLabel", e.target.value);}.bind(this), value:this.props.rulerLabel} , - React.DOM.option( {value:""}, "None"), - React.DOM.optgroup( {label:"Metric"}, + React.DOM.option( {value:""}, "無"), + React.DOM.optgroup( {label:"公制"}, this.renderLabelChoices([ - ["milimeters", "mm"], - ["centimeters", "cm"], - ["meters", "m"], - ["kilometers", "km"] + ["厘米", "mm"], + ["公分", "cm"], + ["公尺", "m"], + ["公里", "km"] ]) ), - React.DOM.optgroup( {label:"Imperial"}, + React.DOM.optgroup( {label:"英制"}, this.renderLabelChoices([ - ["inches", "in"], - ["feet", "ft"], - ["yards", "yd"], - ["miles", "mi"] + ["英吋", "in"], + ["英呎", "ft"], + ["碼", "yd"], + ["英哩", "mi"] ]) ) ) @@ -19123,7 +19038,7 @@ var MeasurerEditor = React.createClass({displayName: 'MeasurerEditor', ), React.DOM.div(null, React.DOM.label(null, - ' ',"Ruler ticks:",' ', + ' ',"每單位分割數:",' ', React.DOM.select( {onChange:function(e) {return this.change("rulerTicks", +e.target.value);}.bind(this), @@ -19135,14 +19050,14 @@ var MeasurerEditor = React.createClass({displayName: 'MeasurerEditor', ) ), React.DOM.div(null, - NumberInput( {label:"Ruler pixels per unit:", + NumberInput( {label:"每單位長的畫素:", placeholder:40, onChange:this.change("rulerPixels"), value:this.props.rulerPixels, useArrowKeys:true} ) ), React.DOM.div(null, - NumberInput( {label:"Ruler length in units:", + NumberInput( {label:"直尺長度:", placeholder:10, onChange:this.change("rulerLength"), value:this.props.rulerLength, @@ -19195,15 +19110,15 @@ propUpgrades = { module.exports = { name: "measurer", - displayName: "Measurer", + displayName: "Measurer/直尺、量角器", widget: Measurer, editor: MeasurerEditor, version: {major: 1, minor: 0}, propUpgrades: propUpgrades, - hidden: true + hidden: false }; -},{"../components/number-input.jsx":129,"../components/prop-check-box.jsx":130,"../components/range-input.jsx":131,"../mixins/changeable.jsx":159,"../mixins/jsonify-props.jsx":160,"react":115,"react-components/info-tip":5}],185:[function(require,module,exports){ +},{"../components/number-input.jsx":61,"../components/prop-check-box.jsx":62,"../components/range-input.jsx":63,"../mixins/changeable.jsx":91,"../mixins/jsonify-props.jsx":92,"react":45,"react-components/info-tip":39}],117:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -19541,12 +19456,12 @@ var NumberLine = React.createClass({displayName: 'NumberLine', Line( {start:[tickCtrlLeft, 1.5], end:[tickCtrlRight, 1.5]} ), Label( {coord:[textLeft, 1.5], - text:"fewer ticks", + text:"較少刻度", direction:"center", tex:false} ), Label( {coord:[textRight, 1.5], - text:"more ticks", + text:"較多刻度", direction:"center", tex:false} ), MovablePoint( @@ -19682,6 +19597,17 @@ var NumberLine = React.createClass({displayName: 'NumberLine', graphie.line([center, 0], [left, 0], {arrows: "->"}); }, + setAnswerFromJSON: function(answerData) { + if (answerData === undefined) { + answerData = this.getDefaultProps(); + } + if (answerData.rel === "eq") { + answerData.rel = "ge"; + answerData.isInequality = false; + } + this.props.onChange(answerData); + }, + toJSON: function() { return { numLinePosition: this.props.numLinePosition, @@ -19777,17 +19703,17 @@ var NumberLineEditor = React.createClass({displayName: 'NumberLineEditor', } var labelStyleEditorButtons = [ - {value: "decimal", text: "0.75", title: "Decimals",}, + {value: "decimal", text: "0.75", title: "小數",}, {value: "improper", text: "\u2077\u2044\u2084", - title: "Improper fractions"}, + title: "假分數"}, {value: "mixed", text: "1\u00BE", - title: "Mixed numbers"}, + title: "帶分數"}, {value: "non-reduced", text: "\u2078\u2044\u2084", - title: "Non-reduced"}]; + title: "未化簡分數"}]; return React.DOM.div( {className:"perseus-widget-number-line-editor"}, React.DOM.div( {className:"perseus-widget-row"}, - React.DOM.label(null, "correct x"), + React.DOM.label(null, "正確的 x"), React.DOM.select( {value:this.props.correctRel, onChange:this.onChangeRelation}, React.DOM.option( {value:"eq"}, " = " ), @@ -19802,17 +19728,16 @@ var NumberLineEditor = React.createClass({displayName: 'NumberLineEditor', checkValidity:function(val) {return val >= range[0] && val <= range[1] && (!step || Math.abs(val - range[0]) % step === 0);}, - placeholder:"answer", size:"normal", + placeholder:"答案", size:"normal", useArrowKeys:true} ), InfoTip(null, React.DOM.p(null, - "This is the correct answer. The answer is validated"+' '+ - "(as right or wrong) by using only the end position of the"+' '+ - "point and the relation (=, <, >, ≤, ≥)" + "這是正確答案,會使用使用者移動的最終位置以及數學關係 (=, <, >, ≤, ≥) 來驗證答案是否正確。"+' '+ + "若底色變為紅色,代表使用者不可能透過操作得到這個答案。" )) ), React.DOM.div( {className:"perseus-widget-row"}, - NumberInput( {label:"position", + NumberInput( {label:"初始位置", value:this.props.initialX, format:this.props.labelStyle, onChange:this.onNumChange.bind(this, "initialX"), @@ -19825,15 +19750,12 @@ var NumberLineEditor = React.createClass({displayName: 'NumberLineEditor', format:this.props.labelStyle, useArrowKeys:true} ), InfoTip(null, React.DOM.p(null, - "This controls the initial position of the point along the"+' '+ - "number line and the ", React.DOM.strong(null, "range"),", the position"+' '+ - "of the endpoints of the number line. Setting the range"+' '+ - "constrains the position of the answer and the labels." + "這控制橘色點在數線上的初始位置,以及在數線上可移動的 ", React.DOM.strong(null, "範圍"),"。" )) ), React.DOM.div( {className:"perseus-widget-row"}, React.DOM.div( {className:"perseus-widget-left-col"}, - React.DOM.span(null, "labels " ), + React.DOM.span(null, "標籤 " ), NumberInput( {value:labelRange[0], placeholder:range[0], format:this.props.labelStyle, @@ -19850,43 +19772,37 @@ var NumberLineEditor = React.createClass({displayName: 'NumberLineEditor', onChange:this.onLabelRangeChange.bind(this, 1), useArrowKeys:true} ), InfoTip(null, React.DOM.p(null, - "This controls the position of the left / right labels."+' '+ - "By default, the labels are set by the range ", React.DOM.br(null ), - React.DOM.strong(null, "Note:"), " Ensure that the labels line up"+' '+ - "with the tick marks, or it may be confusing for users." + "這控制左右標籤的位置,預設為移動範圍的兩端。",React.DOM.br(null ), + React.DOM.strong(null, "注意:"), " 確保藍色標籤在黑色刻度線上,否則可能會讓使用者困惑。" )) ) ), React.DOM.div( {className:"perseus-widget-row"}, React.DOM.div( {className:"perseus-widget-left-col"}, - React.DOM.label(null, "style"), + React.DOM.label(null, "標籤格式"), ButtonGroup( {allowEmpty:false, value:this.props.labelStyle, buttons:labelStyleEditorButtons, onChange:this.onLabelStyleChange} ), InfoTip(null, React.DOM.p(null, - "This controls the styling of the labels for the two"+' '+ - "main labels as well as all the tick mark labels,"+' '+ - "if applicable. Your choices are decimal,"+' '+ - "improper fractions, mixed fractions, and non-reduced"+' '+ - "fractions." + "這控制標籤的格式,使用上,可以選擇「小數、假分數、帶分數、未化簡分數」。" )) ) ), React.DOM.div( {className:"perseus-widget-row"}, React.DOM.div( {className:"perseus-widget-left-col"}, - PropCheckBox( {label:"show tick controller", + PropCheckBox( {label:"顯示刻度線控制器", isTickCtrl:this.props.isTickCtrl, onChange:this.props.onChange} ) ), React.DOM.div( {className:"perseus-widget-right-col"}, - PropCheckBox( {label:"show label ticks", + PropCheckBox( {label:"顯示刻度代表的數字", labelTicks:this.props.labelTicks, onChange:this.props.onChange} ) ) ), React.DOM.div( {className:"perseus-widget-row"}, - NumberInput( {label:"num divisions", + NumberInput( {label:"分割數量", value:this.props.numDivisions || null, format:"decimal", onChange:this.onNumDivisionsChange, @@ -19902,15 +19818,11 @@ var NumberLineEditor = React.createClass({displayName: 'NumberLineEditor', onChange:this.onDivisionRangeChange, useArrowKeys:true} ), InfoTip(null, React.DOM.p(null, - "This controls the number (and position) of the tick marks."+' '+ - "The range dictates the minimum and maximum number of ticks"+' '+ - "that the user can make using the tick controller. ", React.DOM.br(null ), - React.DOM.strong(null, "Note:"), " There is no check to see if labels"+' '+ - "coordinate with the tick marks, which may be confusing for"+' '+ - "users if the blue labels and black ticks are off-step." + "這控制刻度線的數量,後面的範圍設定的是使用者用刻度線控制器可以調整的最大與最小分割數量。",React.DOM.br(null ), + React.DOM.strong(null, "注意:"), " 沒有特別檢查藍色的標籤是否會在黑色刻度線上,若不在刻度線上可能會讓使用者困惑。" ))), !isTickCtrl && React.DOM.span(null, - NumberInput( {label: " or tick step", + NumberInput( {label:"或一刻度為", value:this.props.tickStep || null, format:this.props.labelStyle, onChange:this.onTickStepChange, @@ -19918,28 +19830,21 @@ var NumberLineEditor = React.createClass({displayName: 'NumberLineEditor', placeholder:width / this.props.numDivisions, useArrowKeys:true} ), InfoTip(null, React.DOM.p(null, - "This controls the number (and position) of the tick marks;"+' '+ - "you can either set the number of divisions (2 divisions"+' '+ - "would split the entire range in two halves), or the"+' '+ - "tick step (the distance between ticks) and the other"+' '+ - "value will be updated accordingly. ", React.DOM.br(null ), - React.DOM.strong(null, "Note:"), " There is no check to see if labels"+' '+ - "coordinate with the tick marks, which may be confusing for"+' '+ - "users if the blue labels and black ticks are off-step." + "這控制刻度線的位置與數量,可以設定分割數量 (2 表示把整個範圍分割成兩半)"+' '+ + "或者設定一刻度為多少 (相鄰兩刻度之間的距離)。設定其中一個另一個會自動更新為對應的值。 ", React.DOM.br(null ), + React.DOM.strong(null, "注意:"), " 沒有特別檢查藍色的標籤是否會在黑色刻度線上,若不在刻度線上可能會讓使用者困惑。" ))) ), React.DOM.div( {className:"perseus-widget-row"}, - NumberInput( {label:"snap increments per tick", + NumberInput( {label:"刻度之間的分割數量", value:snapDivisions, checkValidity:function(val) {return val > 0;}, format:this.props.labelStyle, onChange:this.onNumChange.bind(this, "snapDivisions"), useArrowKeys:true} ), InfoTip(null, React.DOM.p(null, - "This determines the number of different places the point"+' '+ - "will snap between two adjacent tick marks. ", React.DOM.br(null ), - React.DOM.strong(null, "Note:"),"Ensure the required number of"+' '+ - "snap increments is provided to answer the question." + "這控制兩個相鄰的刻度之間,被分成了幾份,也就是使用者可以將橘點移動到的位置。 ", React.DOM.br(null ), + React.DOM.strong(null, "注意:"),"確保分割數量足夠讓使用者回答問題,即答案會落在某分割的位置。" )) ) @@ -20068,14 +19973,14 @@ var NumberLineTransform = function(editorProps) { module.exports = { name: "number-line", - displayName: "Number line", + displayName: "Number line/數線", widget: NumberLine, editor: NumberLineEditor, transform: NumberLineTransform, - hidden: true + hidden: false }; -},{"../components/graphie.jsx":125,"../components/number-input.jsx":129,"../components/prop-check-box.jsx":130,"../components/range-input.jsx":131,"../interactive2.js":148,"../interactive2/interactive-util.js":149,"../mixins/changeable.jsx":159,"../mixins/jsonify-props.jsx":160,"../util.js":168,"react":115,"react-components/button-group":3,"react-components/info-tip":5}],186:[function(require,module,exports){ +},{"../components/graphie.jsx":57,"../components/number-input.jsx":61,"../components/prop-check-box.jsx":62,"../components/range-input.jsx":63,"../interactive2.js":80,"../interactive2/interactive-util.js":81,"../mixins/changeable.jsx":91,"../mixins/jsonify-props.jsx":92,"../util.js":100,"react":45,"react-components/button-group":37,"react-components/info-tip":39}],118:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -20091,29 +19996,29 @@ var InputWithExamples = require("../components/input-with-examples.jsx"); var Editor = require("../editor.jsx"); -var firstNumericalParse = require("../util.js").firstNumericalParse; +var Util = require("../util.js"); var answerFormButtons = [ - {title: "Integers", value: "integer", text: "6"}, - {title: "Decimals", value: "decimal", text: "0.75"}, - {title: "Proper fractions", value: "proper", text: "\u2157"}, - {title: "Improper fractions", value: "improper", + {title: "整數", value: "integer", text: "6"}, + {title: "小樹", value: "decimal", text: "0.75"}, + {title: "真分數", value: "proper", text: "\u2157"}, + {title: "假分數", value: "improper", text: "\u2077\u2044\u2084"}, - {title: "Mixed numbers", value: "mixed", text: "1\u00BE"}, - {title: "Numbers with \u03C0", value: "pi", text: "\u03C0"} + {title: "帶分數", value: "mixed", text: "1\u00BE"}, + {title: "有 \u03C0 的數", value: "pi", text: "\u03C0"} ]; var formExamples = { - "integer": function(options) {return $._("an integer, like $6$");}, + "integer": function(options) {return $._("整數, 例 $6$");}, "proper": function(options) {return options.simplify === "optional" ? - $._("a *proper* fraction, like $1/2$ or $6/10$") : - $._("a *simplified proper* fraction, like $3/5$");}, + $._("真分數, 例 $1/2$ or $6/10$") : + $._("最簡真分數, 例 $3/5$");}, "improper": function(options) {return options.simplify === "optional" ? - $._("an *improper* fraction, like $10/7$ or $14/8$") : - $._("a *simplified improper* fraction, like $7/4$");}, - "mixed": function() {return $._("a mixed number, like $1\\ 3/4$");}, - "decimal": function() {return $._("an *exact* decimal, like $0.75$");}, - "pi": function() {return $._("a multiple of pi, like $12\\ \\text{pi}$ or " + + $._("假分數, 例 $10/7$ or $14/8$") : + $._("最簡假分數, 例 $7/4$");}, + "mixed": function() {return $._("帶分數, 例 $1\\ 3/4$");}, + "decimal": function() {return $._("精確的小數, 例 $0.75$");}, + "pi": function() {return $._("pi 的倍數, 例 $12\\ \\text{pi}$ or " + "$2/3\\ \\text{pi}$");} }; @@ -20140,7 +20045,7 @@ var NumericInput = React.createClass({displayName: 'NumericInput', }, handleChange: function(newValue) { - this.props.onChange({ currentValue: newValue }); + this.props.onChange({ currentValue: Util.asc(newValue) }); }, focus: function() { @@ -20152,6 +20057,13 @@ var NumericInput = React.createClass({displayName: 'NumericInput', return {currentValue: this.props.currentValue}; }, + setAnswerFromJSON: function(answerData) { + if (answerData === undefined) { + answerData = {currentValue: ""}; + } + this.props.onChange(answerData); + }, + simpleValidate: function(rubric) { return NumericInput.validate(this.toJSON(), rubric); }, @@ -20276,62 +20188,50 @@ var NumericInputEditor = React.createClass({displayName: 'NumericInputEditor', var answers = this.props.answers.concat(initAnswer(lastStatus)); var unsimplifiedAnswers = function(i) {return React.DOM.div( {className:"perseus-widget-row"}, - React.DOM.label(null, "Unsimplified answers are"), + React.DOM.label(null, "未化簡的答案是"), ButtonGroup( {value:answers[i]["simplify"], allowEmpty:false, buttons:[ - {value: "required", text: "ungraded"}, - {value: "optional", text: "accepted"}, - {value: "enforced", text: "wrong"}], + {value: "required", text: "不合適的"}, + {value: "optional", text: "可接受的"}, + {value: "enforced", text: "錯誤的"}], onChange:this.updateAnswer(i, "simplify")} ), InfoTip(null, - React.DOM.p(null, "Normally select \"ungraded\". This will give the"+' '+ - "user a message saying the answer is correct but not"+' '+ - "simplified. The user will then have to simplify it and"+' '+ - "re-enter, but will not be penalized. (5th grade and after)"), - React.DOM.p(null, "Select \"accepted\" only if the user is not"+' '+ - "expected to know how to simplify fractions yet. (Anything"+' '+ - "prior to 5th grade)"), - React.DOM.p(null, "Select \"wrong\" ", React.DOM.em(null, "only"), " if we are"+' '+ - "specifically assessing the ability to simplify.") + React.DOM.p(null, "預設是選取「不合適的」。會告訴使用者這個答案是對的但是沒有化簡。"+' '+ + "使用者必須化簡後再重新送出答案,但不會算錯。(適用於五年級以上)"), + React.DOM.p(null, "只有當使用者不知道如何化簡分數時才選取「可接受的」。(適用於五年級以下)"), + React.DOM.p(null, React.DOM.em(null, "只有"),"在要學會化簡時才選取「錯誤的」。") ) );}.bind(this); var suggestedAnswerTypes = function(i) {return React.DOM.div(null, React.DOM.div( {className:"perseus-widget-row"}, - React.DOM.label(null, "Choose the suggested answer formats"), + React.DOM.label(null, "選擇建議的答題格式"), MultiButtonGroup( {buttons:answerFormButtons, values:answers[i]["answerForms"], onChange:this.updateAnswer(i, "answerForms")} ), InfoTip(null, - React.DOM.p(null, "Formats will be autoselected for you based on the"+' '+ - "given answer; to show no suggested formats and"+' '+ - "accept all types, simply have a decimal/integer be"+' '+ - "the answer. Values with π will have format \"pi\","+' '+ - "and values that are fractions will have some subset"+' '+ - "(mixed will be \"mixed\" and \"proper\"; improper/proper"+' '+ - "will both be \"improper\" and \"proper\"). If you would"+' '+ - "like to specify that it is only a proper fraction"+' '+ - "(or only a mixed/improper fraction), deselect the"+' '+ - "other format. Except for specific cases, you should"+' '+ - "not need to change the autoselected formats."), - React.DOM.p(null, "To restrict the answer to ", React.DOM.em(null, "only"), " an improper"+' '+ - "fraction (i.e. 7/4), select the"+' '+ - "improper fraction and toggle \"strict\" to true."+' '+ - "This ", React.DOM.b(null, "will not"), " accept 1.75 as an answer. " ), - React.DOM.p(null, "Unless you are testing that specific skill, please"+' '+ - "do not restrict the answer format.") + React.DOM.p(null, "這邊選取的是學生在作答時,會顯示的答題建議格式。這邊會根據輸入的答案自動選取建議的格式。"+' '+ + "若輸入的答案為「小數、整數」則預設不顯示建議,同時不限制輸入的格式。"+' '+ + "若輸入的答案為帶有「π」的數值,則預設會顯示如何輸入 pi 的格式建議。"+' '+ + "若輸入的答案為「帶分數」,則預設會顯示帶分數以及真分數的格式建議。"+' '+ + "若輸入的答案為「假分數、真分數」,則預設會顯示假分數以及真分數的格式建議。"+' '+ + "因此若需要特別只顯示某個格式建議,再取消選取即可,一般使用不需要更動。"), + React.DOM.p(null, "例如,如果想要限制答案 ", React.DOM.em(null, "只能是"), " 假分數 (譬如 7/4),選取"+' '+ + "「假分數」並把「完全符合」打勾。"+' '+ + "這樣就 ", React.DOM.b(null, "不會"), " 接受輸入的答案為 1.75。"), + React.DOM.p(null, "除非你需要測試學生的某個技能 (例如:分數),一般使用請不要特別限制輸入的格式。") ) ), React.DOM.div( {className:"perseus-widget-row"}, - PropCheckBox( {label:"Strictly match only these formats", + PropCheckBox( {label:"完全符合選取的答題格式", strict:answers[i]["strict"], onChange:this.updateAnswer.bind(this, i)} ) ) );}.bind(this); var maxError = function(i) {return React.DOM.div( {className:"perseus-widget-row"}, - NumberInput( {label:"Max error", + NumberInput( {label:"最大誤差", className:"max-error", value:answers[i]["maxError"], onChange:this.updateAnswer(i, "maxError"), @@ -20340,29 +20240,27 @@ var NumericInputEditor = React.createClass({displayName: 'NumericInputEditor', var inputSize = React.DOM.div(null, - React.DOM.label(null, "Width:",' ', " " ), + React.DOM.label(null, "寬度:",' ', " " ), ButtonGroup( {value:this.props.size, allowEmpty:false, buttons:[ - {value: "normal", text: "Normal (80px)"}, - {value: "small", text: "Small (40px)"}], + {value: "normal", text: "一般 (80px)"}, + {value: "small", text: "較小 (40px)"}], onChange:this.change("size")} ), InfoTip(null, - React.DOM.p(null, "Use size \"Normal\" for all text boxes, unless there are"+' '+ - "multiple text boxes in one line and the answer area is too"+' '+ - "narrow to fit them.") + React.DOM.p(null, "預設使用一般大小,除非需要很多個答案格在同一行,會出現放不下的情況。") ) ); var instructions = { - "wrong": "(address the mistake/misconception)", - "ungraded": "(explain in detail to avoid confusion)", - "correct": "(reinforce the user's understanding)" + "wrong": "(說明這個答案的錯誤之處或迷思概念)", + "ungraded": "(進一步解釋避免混淆)", + "correct": "(加強使用者對觀念的理解)" }; var generateInputAnswerEditors = function() {return answers.map(function(answer, i) { var editor = Editor({ content: answer.message || "", - placeholder: "Why is this answer " + answer.status + "?\t" + + placeholder: "為什麼這個答案是" + answer.status + "?\t" + instructions[answer.status], widgetEnabled: false, onChange: function(newProps) { @@ -20389,13 +20287,13 @@ var NumericInputEditor = React.createClass({displayName: 'NumericInputEditor', forms = ["proper", "improper"]; } this.updateAnswer(i, { - value: firstNumericalParse(newValue), + value: Util.firstNumericalParse(newValue), answerForms: forms }); }.bind(this), onChange:function(newValue) { this.updateAnswer(i, { - value: firstNumericalParse(newValue)}); + value: Util.firstNumericalParse(newValue)}); }.bind(this)} ), answer.strict && React.DOM.div( {className:"is-strict-indicator", title:"strictly equivalent to"}, "≡"), @@ -20531,10 +20429,10 @@ module.exports = { widget: NumericInput, editor: NumericInputEditor, transform: propsTransform, - hidden: true + hidden: false }; -},{"../components/input-with-examples.jsx":126,"../components/multi-button-group.jsx":128,"../components/number-input.jsx":129,"../components/prop-check-box.jsx":130,"../editor.jsx":143,"../mixins/changeable.jsx":159,"../mixins/jsonify-props.jsx":160,"../util.js":168,"react":115,"react-components/button-group":3,"react-components/info-tip":5}],187:[function(require,module,exports){ +},{"../components/input-with-examples.jsx":58,"../components/multi-button-group.jsx":60,"../components/number-input.jsx":61,"../components/prop-check-box.jsx":62,"../editor.jsx":75,"../mixins/changeable.jsx":91,"../mixins/jsonify-props.jsx":92,"../util.js":100,"react":45,"react-components/button-group":37,"react-components/info-tip":39}],119:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -21098,11 +20996,9 @@ var OrdererEditor = React.createClass({displayName: 'OrdererEditor', return React.DOM.div( {className:"perseus-widget-orderer"}, React.DOM.div(null, - ' ',"Correct answer:",' ', + ' ',"正確答案:",' ', InfoTip(null, React.DOM.p(null, - "Place the cards in the correct order. The same card can be"+' '+ - "used more than once in the answer but will only be"+' '+ - "displayed once at the top of a stack of identical cards." + "請將卡片依正確答案的順序排列,答案中允許使用多張相同的卡片,但在候選卡片中僅會顯示一次。" )) ), TextListEditor( @@ -21111,9 +21007,9 @@ var OrdererEditor = React.createClass({displayName: 'OrdererEditor', layout:this.props.layout} ), React.DOM.div(null, - ' ',"Other cards:",' ', + ' ',"其他卡片:",' ', InfoTip(null, - React.DOM.p(null, "Create cards that are not part of the answer.") + React.DOM.p(null, "可在此增加不在答案中使用的卡片。") ) ), TextListEditor( @@ -21123,30 +21019,29 @@ var OrdererEditor = React.createClass({displayName: 'OrdererEditor', React.DOM.div(null, React.DOM.label(null, - ' ',"Layout:",' ', + ' ',"顯示方式:",' ', React.DOM.select( {value:this.props.layout, onChange:this.onLayoutChange}, - React.DOM.option( {value:HORIZONTAL}, "Horizontal"), - React.DOM.option( {value:VERTICAL}, "Vertical") + React.DOM.option( {value:HORIZONTAL}, "水平方式"), + React.DOM.option( {value:VERTICAL}, "垂直方式") ) ), InfoTip(null, - React.DOM.p(null, "Use the horizontal layout for short text and small"+' '+ - "images. The vertical layout is best for longer text (e.g."+' '+ - "proofs).") + React.DOM.p(null, "當卡片中的文字較短或是圖形較小時,建議可選用水平方式顯示,"+' '+ + "垂直方式較適用於較長的文字敘述 (如:證明) 或較大的圖形。") ) ), React.DOM.div(null, React.DOM.label(null, - ' ',"Height:",' ', + ' ',"顯示高度:",' ', React.DOM.select( {value:this.props.height, onChange:this.onHeightChange}, - React.DOM.option( {value:NORMAL}, "Normal"), - React.DOM.option( {value:AUTO}, "Automatic") + React.DOM.option( {value:NORMAL}, "一般大小"), + React.DOM.option( {value:AUTO}, "自動調整") ) ), InfoTip(null, - React.DOM.p(null, "Use \"Normal\" for text, \"Automatic\" for images.") + React.DOM.p(null, "若卡片內容為文字時,建議選用\"一般大小\";若卡片內容為圖片時,建議選用\"自動調整\"。") ) ) ); @@ -21204,13 +21099,13 @@ var OrdererEditor = React.createClass({displayName: 'OrdererEditor', module.exports = { name: "orderer", - displayName: "Orderer", + displayName: "Orderer/卡片重組", widget: Orderer, editor: OrdererEditor, - hidden: true + hidden: false }; -},{"../components/text-list-editor.jsx":135,"../renderer.jsx":165,"../util.js":168,"react":115,"react-components/info-tip":5}],188:[function(require,module,exports){ +},{"../components/text-list-editor.jsx":67,"../renderer.jsx":97,"../util.js":100,"react":45,"react-components/info-tip":39}],120:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -21895,7 +21790,7 @@ var PlotterEditor = React.createClass({displayName: 'PlotterEditor', var canChangeSnaps = !_.contains([PIC, DOTPLOT], this.props.type); return React.DOM.div( {className:"perseus-widget-plotter-editor"}, React.DOM.div(null, - "Chart type:",' ', + "圖表種類:",' ', _.map([BAR, LINE, PIC, HISTOGRAM, DOTPLOT], function(type) { return React.DOM.label( {key:type}, React.DOM.input( @@ -21908,8 +21803,8 @@ var PlotterEditor = React.createClass({displayName: 'PlotterEditor', }, this) ), React.DOM.div(null, - "Labels:",' ', - _.map(["x", "y"], function(axis, i) { + "標籤:",' ', + _.map(["x軸", "y軸"], function(axis, i) { return React.DOM.label( {key:axis}, axis + ":", React.DOM.input( @@ -21922,11 +21817,11 @@ var PlotterEditor = React.createClass({displayName: 'PlotterEditor', setFromScale && React.DOM.div( {className:"set-from-scale-box"}, React.DOM.span( {className:"categories-title"}, - "Set Categories From Scale" + "批次設定類別 (x軸)" ), React.DOM.div(null, React.DOM.label(null, - "Tick Step:",' ', + "間距:",' ', NumberInput( {placeholder:1, useArrowKeys:true, @@ -21934,12 +21829,12 @@ var PlotterEditor = React.createClass({displayName: 'PlotterEditor', onChange:this.handleChangeTickStep} ) ), InfoTip(null, - React.DOM.p(null, "The difference between adjacent ticks.") + React.DOM.p(null, "兩個資料點間的間距。") ) ), React.DOM.div(null, React.DOM.label(null, - "Range:",' ', + "範圍:",' ', RangeInput( {placeholder:[0, 10], useArrowKeys:true, @@ -21949,13 +21844,13 @@ var PlotterEditor = React.createClass({displayName: 'PlotterEditor', ), React.DOM.div(null, React.DOM.button( {onClick:this.setCategoriesFromScale}, - "Set Categories",' ' + "設定",' ' ) ) ), React.DOM.div(null, React.DOM.label(null, - "Label Interval:",' ', + "標籤範圍:",' ', NumberInput( {useArrowKeys:true, value:this.props.labelInterval, @@ -21969,7 +21864,7 @@ var PlotterEditor = React.createClass({displayName: 'PlotterEditor', ), this.props.type === PIC && React.DOM.div(null, React.DOM.label(null, - "Picture:",' ', + "圖例:",' ', React.DOM.input( {type:"text", className:"pic-url", @@ -21977,8 +21872,7 @@ var PlotterEditor = React.createClass({displayName: 'PlotterEditor', onKeyPress:this.changePicUrl, onBlur:this.changePicUrl} ), InfoTip(null, - React.DOM.p(null, "Use the default picture of Earth, or insert the URL for"+' '+ - "a different picture using the \"Add image\" function.") + React.DOM.p(null, "預設值為地球圖例,若要使用特定圖例,請於此處輸入圖片連結網址。") ) ), this.state.pic && @@ -21991,7 +21885,7 @@ var PlotterEditor = React.createClass({displayName: 'PlotterEditor', ), React.DOM.div(null, React.DOM.label(null, - "Categories:",' ', + "類別 (x軸):",' ', TextListEditor( {ref:"categories", layout:"horizontal", @@ -22001,7 +21895,7 @@ var PlotterEditor = React.createClass({displayName: 'PlotterEditor', ), React.DOM.div(null, React.DOM.label(null, - "Scale (y):",' ', + "組距 (y軸):",' ', React.DOM.input( {type:"text", onChange:this.changeScale, @@ -22010,7 +21904,7 @@ var PlotterEditor = React.createClass({displayName: 'PlotterEditor', ), React.DOM.div(null, React.DOM.label(null, - "Max y:",' ', + "最大值 (y軸):",' ', React.DOM.input( {type:"text", ref:"maxY", @@ -22020,21 +21914,21 @@ var PlotterEditor = React.createClass({displayName: 'PlotterEditor', ), canChangeSnaps && React.DOM.div(null, React.DOM.label(null, - "Snaps per line:",' ', + "垂直拖拉間距參數:",' ', React.DOM.input( {type:"text", onChange:this.changeSnaps, defaultValue:this.props.snapsPerLine} ) ), InfoTip(null, - React.DOM.p(null, "Creates the specified number of divisions between the"+' '+ - "horizontal lines. Fewer snaps between lines makes the graph"+' '+ - "easier for the student to create correctly.") + React.DOM.p(null, "用以調整學生在拖拉答案時的,y 軸的單位間距前進比例,舉例來說,"+' '+ + "當此參數設定為 2 之時,每次的拖拉時的前進單位為 1/2 個 y 軸"+' '+ + "單位間距。一般來說,為求學生作答時的便利性,此值不宜設定太大。") ) ), React.DOM.div(null, - "Editing values:",' ', - _.map(["correct", "starting"], function(editing) { + "圖表編輯值:",' ', + _.map(["答案值", "起始值"], function(editing) { return React.DOM.label( {key:editing}, React.DOM.input( {type:"radio", @@ -22045,10 +21939,7 @@ var PlotterEditor = React.createClass({displayName: 'PlotterEditor', ); }, this), InfoTip(null, React.DOM.p(null, - "Use this toggle to switch between editing the correct"+' '+ - "answer (what the student will be graded on) and the"+' '+ - "starting values (what the student will see plotted when"+' '+ - "they start the problem). Note: These cannot be the same." + "選用\"答案值\"編輯此題的圖表答案;選用\"起始值\"編輯此題作答前的圖表預設樣式。" )) ), this.transferPropsTo( @@ -22205,13 +22096,13 @@ var PlotterEditor = React.createClass({displayName: 'PlotterEditor', module.exports = { name: "plotter", - displayName: "Plotter", + displayName: "Plotter/統計圖", widget: Plotter, editor: PlotterEditor, - hidden: true + hidden: false }; -},{"../components/number-input.jsx":129,"../components/range-input.jsx":131,"../components/text-list-editor.jsx":135,"../util.js":168,"react":115,"react-components/info-tip":5}],189:[function(require,module,exports){ +},{"../components/number-input.jsx":61,"../components/range-input.jsx":63,"../components/text-list-editor.jsx":67,"../util.js":100,"react":45,"react-components/info-tip":39}],121:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -22255,7 +22146,7 @@ var BaseRadio = React.createClass({displayName: 'BaseRadio', "above-scratchpad blank-background"}, this.props.multipleSelect && React.DOM.div( {className:"instructions"}, - $_(null, "Select all that apply.") + $_(null, "請選擇所有正確的答案。") ), this.props.choices.map(function(choice, i) { @@ -22417,6 +22308,18 @@ var Radio = React.createClass({displayName: 'Radio', }); }, + setAnswerFromJSON: function(answerData) { + if (answerData === undefined) { + renderedAnswerData = {values: undefined}; + } else { + var renderedAnswerData = {'values': []}; + for (var i = 0; i < this.props.choices.length; i++) { + renderedAnswerData['values'].push(answerData['values'][this.props.choices[i].originalIndex]); + } + } + this.props.onChange(renderedAnswerData); + }, + toJSON: function(skipValidation) { // Return checked inputs in the form {values: [bool]}. (Dear future // timeline implementers: this used to be {value: i} before multiple @@ -22440,12 +22343,9 @@ var Radio = React.createClass({displayName: 'Radio', } } - return _.extend({}, this.props, { + return { values: values, - noneOfTheAboveIndex: noneOfTheAboveIndex, - noneOfTheAboveSelected: noneOfTheAboveSelected - } - ); + }; } else { // Nothing checked return { @@ -22469,7 +22369,7 @@ var Radio = React.createClass({displayName: 'Radio', }, statics: { - displayMode: "block" + displayMode: "inline-block" } }); @@ -22510,7 +22410,7 @@ _.extend(Radio, { type: "points", earned: correct ? 1 : 0, total: 1, - message: null + message: rubric.choices[_.indexOf(state.values, true)].clue }; } } @@ -22548,7 +22448,7 @@ var RadioEditor = React.createClass({displayName: 'RadioEditor', React.DOM.div( {className:"perseus-widget-row"}, React.DOM.div( {className:"perseus-widget-left-col"}, - PropCheckBox( {label:"Multiple selections", + PropCheckBox( {label:"多選題", labelAlignment:"right", multipleSelect:this.props.multipleSelect, onChange:this.onMultipleSelectChange} ) @@ -22569,7 +22469,7 @@ var RadioEditor = React.createClass({displayName: 'RadioEditor', ref: "editor" + i, content: choice.content || "", widgetEnabled: false, - placeholder: "Type a choice here...", + placeholder: "請輸入選項內容", onChange: function(newProps) { if ("content" in newProps) { this.onContentChange(i, newProps.content); @@ -22579,7 +22479,7 @@ var RadioEditor = React.createClass({displayName: 'RadioEditor', ref: "clue-editor-" + i, content: choice.clue || "", widgetEnabled: false, - placeholder: $._("Why is this choice " + + placeholder: $._("為什麼這個選項 " + checkedClass + "?"), onChange: function(newProps) { if ("content" in newProps) { @@ -22615,7 +22515,7 @@ var RadioEditor = React.createClass({displayName: 'RadioEditor', React.DOM.a( {href:"#", className:"simple-button orange", onClick:this.addChoice}, React.DOM.span( {className:"icon-plus"} ), - ' ',"Add a choice",' ' + ' ',"增加選項",' ' ) ) @@ -22755,13 +22655,13 @@ var choiceTransform = function(editorProps) { module.exports = { name: "radio", - displayName: "Multiple choice", + displayName: "Radio/選擇題", widget: Radio, editor: RadioEditor, transform: choiceTransform }; -},{"../components/prop-check-box.jsx":130,"../editor.jsx":143,"../mixins/changeable.jsx":159,"../perseus-api.jsx":162,"../renderer.jsx":165,"../util.js":168,"react":115,"react-components/button-group":3,"react-components/info-tip":5}],190:[function(require,module,exports){ +},{"../components/prop-check-box.jsx":62,"../editor.jsx":75,"../mixins/changeable.jsx":91,"../perseus-api.jsx":94,"../renderer.jsx":97,"../util.js":100,"react":45,"react-components/button-group":37,"react-components/info-tip":39}],122:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -22811,6 +22711,35 @@ var Sorter = React.createClass({displayName: 'Sorter', ); }, + setAnswerFromJSON: function(answerData) { + sortable = this.refs.sortable; + if (answerData === undefined) { + sortable.setState({ + items: sortable.clearItemMeasurements(sortable.state.items) + }); + } else { + items = sortable.state.items; + result = []; + + answerData.options.forEach(function(key) { + var found = false; + items = items.filter(function(item) { + if(!found && item['option'] == key) { + result.push(item); + found = true; + return false; + } else + return true; + }) + }); + sortable.setState({items: result}); + } + // HACK: We need to know *that* the widget changed, but currently it's + // not set up in a nice way to tell us *how* it changed, since the + // permutation of the items is stored in state. + this.props.onChange({}); + }, + toJSON: function(skipValidation) { return {options: this.refs.sortable.getOptions()}; }, @@ -22859,11 +22788,9 @@ var SorterEditor = React.createClass({displayName: 'SorterEditor', return React.DOM.div(null, React.DOM.div(null, - ' ',"Correct answer:",' ', + ' ',"正確答案:",' ', InfoTip(null, React.DOM.p(null, - "Enter the correct answer (in the correct order) here. The"+' '+ - "preview on the right will have the cards in a randomized"+' '+ - "order, which is how the student will see them." + "在這邊輸入正確的排序,右邊的預覽畫面會是隨機的排序,也就是學生會看到的畫面。" )) ), TextListEditor( @@ -22874,26 +22801,25 @@ var SorterEditor = React.createClass({displayName: 'SorterEditor', layout:this.props.layout} ), React.DOM.div(null, React.DOM.label(null, - ' ',"Layout:",' ', + ' ',"顯示方式:",' ', React.DOM.select( {value:this.props.layout, onChange:this.onLayoutChange}, - React.DOM.option( {value:HORIZONTAL}, "Horizontal"), - React.DOM.option( {value:VERTICAL}, "Vertical") + React.DOM.option( {value:HORIZONTAL}, "水平方式"), + React.DOM.option( {value:VERTICAL}, "垂直方式") ) ), InfoTip(null, - React.DOM.p(null, "Use the horizontal layout for short text and small"+' '+ - "images. The vertical layout is best for longer text and"+' '+ - "larger images.") + React.DOM.p(null, "當卡片中的文字較短或是圖形較小時,建議可選用水平方式顯示,垂直"+' '+ + "方式較適用於較長的文字敘述 (如:證明) 或較大的圖形。") ) ), React.DOM.div(null, PropCheckBox( - {label:"Padding:", + {label:"留白:", padding:this.props.padding, onChange:this.props.onChange} ), InfoTip(null, - React.DOM.p(null, "Padding is good for text, but not needed for images.") + React.DOM.p(null, "留白適合用在文字,若為圖片則不需要。") ) ) ); @@ -22910,13 +22836,518 @@ var SorterEditor = React.createClass({displayName: 'SorterEditor', module.exports = { name: "sorter", - displayName: "Sorter", + displayName: "Sorter/排序", widget: Sorter, editor: SorterEditor, - hidden: true + hidden: false +}; + +},{"../components/prop-check-box.jsx":62,"../components/sortable.jsx":64,"../components/text-list-editor.jsx":67,"../util.js":100,"react":45,"react-components/info-tip":39}],123:[function(require,module,exports){ +/** @jsx React.DOM */ + +var React = require('react'); +var Changeable = require("../mixins/changeable.jsx"); +var JsonifyProps = require("../mixins/jsonify-props.jsx"); +var classNames = require('classnames'); + +var textInputStyle = { + fontSize: "25px", + marginRight: "5px", + paddingTop: "15px", + paddingBottom: "15px", + marginTop: "15px", + marginBottom: "15px", +}; + +var TextInput = React.createClass({displayName: 'TextInput', + render: function() { + return React.DOM.input( {style:textInputStyle, ref:"input", value:this.props.value || "", onChange:this.changeValue, onPaste:this.pasteValue, onKeyPress:this.keypressValue}); + }, + + pasteValue: function(e) { + e.preventDefault(); + return false; + }, + + keypressValue: function(e) { + e.preventDefault(); + return false; + }, + + changeValue: function(e) { + // Chrome Speech API + if (e.target.value) { + this.props.setValue(e.target.value); + // iOS Siri Input + } else { + this.props.setValue(this.refs.input.value); + } + }, + + statics: { + displayMode: "inline-block" + } +}); + +var infoStyle = { + background: "#3498DB !important", + color: "#fff !important", + textShadow: "0px 0px #fff !important", + marginLeft: 10, + border: '1px solid #ccc', + borderBottom: '1px solid #bbb', + borderRadius: '5px', + backgroundRepeat: 'repeat-x', + cursor: 'pointer !important', + fontFamily: 'inherit', + lineHeight: '22px', + padding: '5px 10px', + position: 'relative', + textDecoration: 'none !important' +} + +var iconButtonStyle = { + width: "45px", + lineHeight: 1.5, +} + +var buttonStyle = { + +} + +var inlineStyle = { + display: 'inline-block' +} + +var SpeakingBtn = React.createClass({displayName: 'SpeakingBtn', + render: function() { + var btnIconCLass = classNames({ + 'fa fa-2x': true, + 'fa-microphone': !this.state.recognizing, + 'fa fa-spinner fa-spin fa-fw': this.state.recognizing + }); + return ( + React.DOM.div( {style:inlineStyle}, + this.recognition + ? React.DOM.button( {style:buttonStyle, onClick:this.startRecognizeOnClick, className:"simple-button orange"}, + React.DOM.i( {style:iconButtonStyle, className:btnIconCLass}) + ) + : React.DOM.div(null, + React.DOM.button( {style:buttonStyle, onClick:this.resetOnClick, className:"simple-button orange"}, + React.DOM.i( {style:iconButtonStyle, className:"fa fa-refresh fa-2x"}) + ), + React.DOM.span( {style:infoStyle}, this.state.status) + ) + + ) + ); + }, + // + + getInitialState: function() { + return {recognizing: false, status: ""} + }, + + startRecognize: function() { + if (this.state.recognizing == false) { + this.recognition.start(); + } + else{ + this.recognition.stop(); + } + }, + + // prevent trigger checking answer when clicking button + startRecognizeOnClick: function(e) { + this.startRecognize(); + e.preventDefault(); + return false; + }, + + // ignore clicking event + resetOnClick: function(e) { + this.props.setValue(''); + e.preventDefault(); + return false; + }, + + componentWillMount: function() { + var self = this; + var os = self.getMobileOperatingSystem(); + if (self.hasSpeechRecognition()) { + var recognition = new webkitSpeechRecognition(); + recognition.lang = 'en-US'; + recognition.continuous = false; + recognition.interimResults = true; + recognition.maxAlternatives = 20; + self.setState({recognizing: false}); + recognition.onstart = function() { + self.setState({recognizing: true}); + self.props.setValue(''); + }; + recognition.onend = function() { + self.setState({recognizing: false}); + }; + recognition.onresult = function(event) { + var res = ''; + for (var i = event.resultIndex; i < event.results.length; i++) { + if (event.results[i].isFinal) { + for (var j = 0; j < event.results[i].length; j++) { + if (j != 0) { + res = res + '/'; + } + res = res + event.results[i][j].transcript; + self.props.setValue(res); + } + } + } + } + self.recognition = recognition; + } else { + if (os == 'iOS') { + self.setState({status: "點選上面的框框 用Siri語音輸入"}); + } else if (os == 'Android') { + self.setState({status: "點選上面的框框 用Google語音輸入"}); + } else { + self.setState({status: "請切換至Chrome瀏覽器"}); + } + } + }, + + getMobileOperatingSystem: function() { + var userAgent = navigator.userAgent || navigator.vendor || window.opera; + if (userAgent.match(/iPad/i) || userAgent.match(/iPhone/i) || userAgent.match(/iPod/i)) { + return 'iOS'; + } else if (userAgent.match(/Android/i)) { + return 'Android'; + } else { + return userAgent; + } + }, + + hasSpeechRecognition: function() { + return ('webkitSpeechRecognition' in window); + }, + + statics: { + displayMode: "inline-block" + } +}); + +var SpeakingTextInput = React.createClass({displayName: 'SpeakingTextInput', + propTypes: { + value: React.PropTypes.string + }, + + getDefaultProps: function() { + return {value: ""}; + }, + + getInitialState: function() { + return {value: this.props.value} + }, + + // compare answer when setting value to prevent generate long atempt dict + setValue: function(val) { + val = val || ''; + var correntAns = SpeakingTextInput.parseAnswer(this.props.correct); + var userAnsList = val.split("/"); + var correntIdx = -1; + for (var i = 0, len = userAnsList.length; i < len; i++) { + if (SpeakingTextInput.arrIsEqual(SpeakingTextInput.parseAnswer(userAnsList[i]), correntAns)) { + correntIdx = i; + break; + } + } + // if the answer is wrong, set value to the first answer + if(correntIdx == -1 || correntIdx >= this.props.correctIdxLessThen){ + this.setState({value: userAnsList[0]}); + this.change("value")(userAnsList[0]); + } + // else set value to the correct answer + else{ + this.setState({value: this.props.correct}); + this.change("value")(this.props.correct); + } + }, + + mixins: [ + Changeable, JsonifyProps + ], + + render: function() { + return ( + React.DOM.div(null, + TextInput( {value:this.state.value, setValue:this.setValue}), + SpeakingBtn( {setValue:this.setValue}) + ) + ); + }, + + simpleValidate: function(rubric) { + return SpeakingTextInput.validate(this.toJSON(), rubric); + }, + + statics: { + displayMode: "inline-block" + } +}); + +_.extend(SpeakingTextInput, { + parseAnswer: function(s) { + var arr = s.split(" "); + var parsedArr = []; + for (var i = 0; i < arr.length; i++) { + if (arr[i].length > 0) { + parsedArr.push(arr[i].toLowerCase()); + } + } + return parsedArr; + }, + + arrIsEqual: function(arr1, arr2) { + if (arr1.length !== arr2.length) + return false; + for (var i = 0, len = arr1.length; i < len; i++) { + if (arr1[i] !== arr2[i]) { + return false; + } + } + return true; + }, + + validate: function(state, rubric) { + var correct = SpeakingTextInput.arrIsEqual( + SpeakingTextInput.parseAnswer(rubric.correct), + SpeakingTextInput.parseAnswer(state.value) + ); + if (state.value == '') { + return {type: 'invalid', message: '請重新再唸一次!'}; + } else if (correct) { + return {type: 'points', earned: 1, total: 1, message: null}; + } else { + return {type: 'points', earned: 0, total: 1, message: null}; + }} + }); + + var SpeakingTextInputEditor = React.createClass({displayName: 'SpeakingTextInputEditor', + mixins: [ + Changeable, JsonifyProps + ], + + getDefaultProps: function() { + return {correct: "", correctIdxLessThen: 5}; + }, + + handleAnswerChange: function(event) { + this.change({correct: event.target.value}); + }, + + handleCorrectIdxChange: function(event) { + this.change({ + correctIdxLessThen: parseInt(event.target.value) + }); + }, + + render: function() { + return React.DOM.div(null, + React.DOM.div(null, + React.DOM.label(null, + "正確答案:", + React.DOM.input( {value:this.props.correct, onChange:this.handleAnswerChange}) + ) + ), + React.DOM.div(null, + React.DOM.label(null, + "精準度 (1-20):", + React.DOM.input( {value:this.props.correctIdxLessThen, onChange:this.handleCorrectIdxChange, type:"integer"}) + ) + ) + ); + }, + }); + + module.exports = { + name: "speaking-text-input", + displayName: "English Speech Recognition/英文口說辨識", + widget: SpeakingTextInput, + editor: SpeakingTextInputEditor + }; + +},{"../mixins/changeable.jsx":91,"../mixins/jsonify-props.jsx":92,"classnames":3,"react":45}],124:[function(require,module,exports){ +/** @jsx React.DOM */ + +var React = require('react'); +var Changeable = require("../mixins/changeable.jsx"); +var JsonifyProps = require("../mixins/jsonify-props.jsx"); +var ResponsiveVoice = require('../../lib/responsivevoice.js'); + +var iconButtonStyle = { + width: "45px", + lineHeight: 1.5, +} + +var buttonStyle = { + marginTop: "5px", + marginBottom: "5px", +} + +var SpeakingVoice = React.createClass({displayName: 'SpeakingVoice', + componentDidMount: function() { + this.responsiveVoice = new ResponsiveVoice; + this.responsiveVoice.init(); // must manually init + }, + + speak: function() { + this.responsiveVoice.speak(this.props.voiceText, this.props.lang, { + pitch: parseFloat(this.props.pitch), + rate: parseFloat(this.props.rate), + volume: this.props.volume + }); + }, + + // prevent trigger checking answer when clicking button + speakOnClick: function(e) { + this.speak(); + e.preventDefault(); + return false; + }, + + mixins: [ + Changeable, JsonifyProps + ], + + render: function() { + return ( + React.DOM.div(null, + React.DOM.button( {style:buttonStyle, className:"simple-button green", onClick:this.speakOnClick}, React.DOM.i( {style:iconButtonStyle, className:"fa fa-volume-up fa-2x"})) + ) + ); + }, + + simpleValidate: function(rubric) { + return {type: "points", earned: 1, total: 1, message: null}; + }, + + statics: { + displayMode: "inline-block" + } +}); + +var SpeakingVoiceEditor = React.createClass({displayName: 'SpeakingVoiceEditor', + mixins: [ + Changeable, JsonifyProps + ], + + getDefaultProps: function() { + return {voiceText: "", pitch: "1.0", rate: "1.0", volume: "1.0", lang: "US English Female"} + }, + + getInitialState: function() { + return {voiceText: this.props.voiceText, pitch: this.props.pitch, rate: this.props.rate, volume: this.props.volume, lang: this.props.lang} + }, + + pitchChange: function(event) { + this.change({pitch: event.target.value}); + this.setState({pitch: event.target.value}); + }, + + voiceTextChange: function(event) { + this.change({voiceText: event.target.value}); + this.setState({voiceText: event.target.value}); + }, + + rateChange: function(event) { + this.change({rate: event.target.value}); + this.setState({rate: event.target.value}); + }, + + langChange: function(event) { + this.change({lang: event.target.value}); + this.setState({lang: event.target.value}); + }, + + render: function() { + return React.DOM.div(null, + React.DOM.div(null, + React.DOM.label(null, + "內容:", + React.DOM.input( {value:this.state.voiceText, onChange:this.voiceTextChange, defaultValue:this.state.voiceText}) + ) + ), + React.DOM.div(null, + React.DOM.label(null, + "速度:", + React.DOM.select( {value:this.state.rate, defaultValue:this.state.rate, onChange:this.rateChange}, + React.DOM.option( {value:"0.1"}, "0.1"), + React.DOM.option( {value:"0.2"}, "0.2"), + React.DOM.option( {value:"0.3"}, "0.3"), + React.DOM.option( {value:"0.4"}, "0.4"), + React.DOM.option( {value:"0.5"}, "0.5"), + React.DOM.option( {value:"0.6"}, "0.6"), + React.DOM.option( {value:"0.7"}, "0.7"), + React.DOM.option( {value:"0.8"}, "0.8"), + React.DOM.option( {value:"0.9"}, "0.9"), + React.DOM.option( {value:"1.0"}, "1.0"), + React.DOM.option( {value:"1.1"}, "1.1"), + React.DOM.option( {value:"1.2"}, "1.2"), + React.DOM.option( {value:"1.3"}, "1.3"), + React.DOM.option( {value:"1.4"}, "1.4"), + React.DOM.option( {value:"1.5"}, "1.5") + ) + ) + ), + React.DOM.div(null, + React.DOM.label(null, + "音調:", + React.DOM.select( {value:this.state.pitch, defaultValue:this.state.pitch, onChange:this.pitchChange}, + React.DOM.option( {value:"0"}, "0"), + React.DOM.option( {value:"0.1"}, "0.1"), + React.DOM.option( {value:"0.2"}, "0.2"), + React.DOM.option( {value:"0.3"}, "0.3"), + React.DOM.option( {value:"0.4"}, "0.4"), + React.DOM.option( {value:"0.5"}, "0.5"), + React.DOM.option( {value:"0.6"}, "0.6"), + React.DOM.option( {value:"0.7"}, "0.7"), + React.DOM.option( {value:"0.8"}, "0.8"), + React.DOM.option( {value:"0.9"}, "0.9"), + React.DOM.option( {value:"1.0"}, "1.0"), + React.DOM.option( {value:"1.1"}, "1.1"), + React.DOM.option( {value:"1.2"}, "1.2"), + React.DOM.option( {value:"1.3"}, "1.3"), + React.DOM.option( {value:"1.4"}, "1.4"), + React.DOM.option( {value:"1.5"}, "1.5"), + React.DOM.option( {value:"1.6"}, "1.6"), + React.DOM.option( {value:"1.7"}, "1.7"), + React.DOM.option( {value:"1.8"}, "1.8"), + React.DOM.option( {value:"1.9"}, "1.9"), + React.DOM.option( {value:"2"}, "2") + ) + ) + ), + React.DOM.div(null, + React.DOM.label(null, + "語言:", + React.DOM.select( {value:this.state.lang, defaultValue:this.state.lang, onChange:this.langChange}, + React.DOM.option( {value:"UK English Female"}, "UK English Female"), + React.DOM.option( {value:"UK English Male"}, "UK English Male"), + React.DOM.option( {value:"US English Female"}, "US English Female") + ) + ) + ) + ); + } +}); + +module.exports = { + name: "speaking-voice", + displayName: "English Text to Speech/英文發音工具", + widget: SpeakingVoice, + hidden: false, + editor: SpeakingVoiceEditor }; -},{"../components/prop-check-box.jsx":130,"../components/sortable.jsx":132,"../components/text-list-editor.jsx":135,"../util.js":168,"react":115,"react-components/info-tip":5}],191:[function(require,module,exports){ +},{"../../lib/responsivevoice.js":2,"../mixins/changeable.jsx":91,"../mixins/jsonify-props.jsx":92,"react":45}],125:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -23079,7 +23510,7 @@ var TableEditor = React.createClass({displayName: 'TableEditor', return React.DOM.div(null, React.DOM.div(null, React.DOM.label(null, - ' ',"Number of columns:",' ', + ' ',"欄數:",' ', React.DOM.input( {ref:"numberOfColumns", type:"text", @@ -23090,7 +23521,7 @@ var TableEditor = React.createClass({displayName: 'TableEditor', ), React.DOM.div(null, React.DOM.label(null, - ' ',"Number of rows:",' ', + ' ',"列數:",' ', React.DOM.input( {ref:"numberOfRows", type:"text", @@ -23100,7 +23531,7 @@ var TableEditor = React.createClass({displayName: 'TableEditor', ) ), React.DOM.div(null, - ' ',"Table of answers type:",' ', + ' ',"答案表格:",' ', React.DOM.ul(null, React.DOM.li(null, React.DOM.label(null, @@ -23108,13 +23539,12 @@ var TableEditor = React.createClass({displayName: 'TableEditor', {type:"radio", checked:"checked", readOnly:true} ), - "Set of values (complete)" + "設定值" ), InfoTip(null, - React.DOM.p(null, "The student has to fill out all cells in the"+' '+ - "table. For partially filled tables create a table"+' '+ - "using the template, and insert text input boxes"+' '+ - "as desired.") + React.DOM.p(null, "當表格欄數大於 1 的時候,答案表格中的所有欄位都需有值,也就"+' '+ + "是說,學生在作答時需填完所有的欄位,若有不需填完所有欄位的"+' '+ + "答案需求時,請再另行增加欄數為 1 的表格進行使用。") ) ) ) @@ -23231,13 +23661,13 @@ var TableEditor = React.createClass({displayName: 'TableEditor', module.exports = { name: "table", - displayName: "Table of values", + displayName: "Table of values/表格", widget: Table, editor: TableEditor, - hidden: true + hidden: false }; -},{"../editor.jsx":143,"../renderer.jsx":165,"../util.js":168,"react":115,"react-components/info-tip":5}],192:[function(require,module,exports){ +},{"../editor.jsx":75,"../renderer.jsx":97,"../util.js":100,"react":45,"react-components/info-tip":39}],126:[function(require,module,exports){ /** @jsx React.DOM */ var React = require('react'); @@ -23599,7 +24029,7 @@ var TransformOps = { var Transformations = { translation: { // I18N: As in the command, "Translate the polygon" - verbName: $._("Translate"), + verbName: $._("平移"), nounName: $._("Translation"), lowerNounName: $._("translation"), apply: function(transform) { @@ -23629,7 +24059,7 @@ var Transformations = { toTeX: function(transform) { // I18N: As in the command, "Translation by <3, 1>" return $_( {vector:texFromVector(transform.vector)}, - "Translation by %(vector)s" + "平移向量 %(vector)s" ); }, Input: React.createClass({displayName: 'Input', @@ -23671,7 +24101,7 @@ var Transformations = { ]; return React.DOM.div(null, $_( {vector:vector}, - "Translation by %(vector)s" + "平移向量 %(vector)s" ) ); }, @@ -23690,7 +24120,7 @@ var Transformations = { rotation: { // I18N: As in the command, "Rotate the polygon" - verbName: $._("Rotate"), + verbName: $._("旋轉"), nounName: $._("Rotation"), lowerNounName: $._("rotation"), apply: function(transform) { @@ -23724,7 +24154,7 @@ var Transformations = { toTeX: function(transform) { return $_( {degrees:texFromAngleDeg(transform.angleDeg), point:texFromPoint(transform.center)}, - "Rotation by %(degrees)s about %(point)s" + "旋轉 %(degrees)s 度 (以 %(point)s 為中心)" ); }, Input: React.createClass({displayName: 'Input', @@ -23804,7 +24234,7 @@ var Transformations = { reflection: { // I18N: As in the command, "Reflect the polygon" - verbName: $._("Reflect"), + verbName: $._("鏡射"), nounName: $._("Reflection"), lowerNounName: $._("reflection"), apply: function(transform) { @@ -23840,7 +24270,7 @@ var Transformations = { var point2 = transform.line[1]; return $_( {point1:texFromPoint(point1), point2:texFromPoint(point2)}, - "Reflection over the line from %(point1)s to %(point2)s" + "對應從 %(point1)s 至 %(point2)s 的線做鏡射" ); }, Input: React.createClass({displayName: 'Input', @@ -23885,7 +24315,7 @@ var Transformations = { ]; return React.DOM.div(null, $_( {point1:point1, point2:point2}, - "Reflection over the line from %(point1)s to %(point2)s" + "對應從 %(point1)s 至 %(point2)s 的線做鏡射" ) ); }, @@ -23912,7 +24342,7 @@ var Transformations = { dilation: { // I18N: As in the command, "Dilate the polygon" - verbName: $._("Dilate"), + verbName: $._("放大"), nounName: $._("Dilation"), lowerNounName: $._("dilation"), apply: function(transform) { @@ -23947,7 +24377,7 @@ var Transformations = { var scaleString = stringFromFraction(transform.scale); return $_( {scale:scaleString, point:texFromPoint(transform.center)}, - "Dilation of scale %(scale)s about %(point)s" + "放大 %(scale)s 倍 (以 %(point)s 為中心)" ); }, Input: React.createClass({displayName: 'Input', @@ -24456,34 +24886,31 @@ var ToolSettings = React.createClass({displayName: 'ToolSettings', this.props.name,":",' ', " ", PropCheckBox( - {label:"enabled:", + {label:"開啟:", enabled:this.props.settings.enabled, onChange:this.props.onChange} ), " ", this.props.settings.enabled && PropCheckBox( - {label:"required:", + {label:"必要:", required:this.props.settings.required, onChange:this.props.onChange} ), this.props.settings.enabled && InfoTip(null, - "'Required' will only grade the answer as correct if the"+' '+ - "student has used at least one such transformation." + "勾選\"必要\"表示學生至少要用過此轉換一次。" ), " ", this.props.allowFixed && this.props.settings.enabled && PropCheckBox( - {label:"fixed:", + {label:"固定:", fixed:this.props.settings.constraints.fixed, onChange:this.changeConstraints} ), this.props.allowFixed && this.props.settings.enabled && InfoTip(null, - "Enable 'fixed' to prevent the student from repositioning"+' '+ - "the tool. The tool will appear in the position at which it"+' '+ - "is placed in the editor below." + "勾選\"固定\"可防止學生重新定位此工具。" ) ); @@ -24503,61 +24930,54 @@ var TransformationExplorerSettings = React.createClass({displayName: 'Transforma return React.DOM.div( {className:"transformer-settings"}, React.DOM.div(null, - ' ',"Mode:",' ', + ' ',"模式:",' ', React.DOM.select( {value:this.getMode(), onChange:this.changeMode}, React.DOM.option( {value:"interactive,dynamic"}, - ' ',"Exploration with text",' ' + ' ',"顯示轉換的過程",' ' ), React.DOM.option( {value:"interactive,static"}, - ' ',"Exploration without text",' ' + ' ',"不顯示轉換的過程",' ' ), React.DOM.option( {value:"dynamic,interactive"}, - ' ',"Formal with movement",' ' + ' ',"指定轉換參數並即時顯示",' ' ), React.DOM.option( {value:"static,interactive"}, - ' ',"Formal without movement",' ' + ' ',"指定轉換參數但不即時顯示",' ' ) ), InfoTip(null, React.DOM.ul(null, React.DOM.li(null, - React.DOM.b(null, "Exploration:"), " Students create"+' '+ - "transformations with tools on the graph.",' ' - ), - React.DOM.li(null, - React.DOM.b(null, "Formal with movement:"), " Students specify"+' '+ - "transformations mathematically in the"+' '+ - "transformation list. Graph shows the results of"+' '+ - "these transformations.",' ' + React.DOM.b(null, "指定轉換參數並即時顯示:"), " 學生可自行指定轉換所需參數,"+' '+ + "而圖形可即時顯示轉換後的結果。",' ' ), React.DOM.li(null, - React.DOM.b(null, "Formal without movement:"), " Students specify"+' '+ - "transformations mathematically in the"+' '+ - "transformation list. Graph does not update.",' ' + React.DOM.b(null, "指定轉換參數但不即時顯示:"), " 學生可自行指定轉換所需參數,"+' '+ + "但圖形不即時顯示轉換後的結果。",' ' ) ) ) ), ToolSettings( - {name:"Translations", + {name:"平移", settings:this.props.tools.translation, allowFixed:false, onChange:this.changeHandlerFor("translation")} ), ToolSettings( - {name:"Rotations", + {name:"旋轉", settings:this.props.tools.rotation, onChange:this.changeHandlerFor("rotation")} ), ToolSettings( - {name:"Reflections", + {name:"鏡射", settings:this.props.tools.reflection, onChange:this.changeHandlerFor("reflection")} ), ToolSettings( - {name:"Dilations", + {name:"放大", settings:this.props.tools.dilation, onChange:this.changeHandlerFor("dilation")} ), PropCheckBox( - {label:"Draw Solution:", + {label:"畫出答案:", drawSolutionShape:this.props.drawSolutionShape, onChange:this.props.onChange} ) ); @@ -24608,18 +25028,18 @@ var TransformationsShapeEditor = React.createClass({displayName: 'Transformation {key:"type-select", value:this.getTypeString(this.props.shape.type), onChange:this.changeType} , - React.DOM.option( {value:"polygon-3"}, "Triangle"), - React.DOM.option( {value:"polygon-4"}, "Quadrilateral"), - React.DOM.option( {value:"polygon-5"}, "Pentagon"), - React.DOM.option( {value:"polygon-6"}, "Hexagon"), - React.DOM.option( {value:"line"}, "Line"), - React.DOM.option( {value:"line,line"}, "2 lines"), - React.DOM.option( {value:"lineSegment"}, "Line segment"), + React.DOM.option( {value:"polygon-3"}, "三角形"), + React.DOM.option( {value:"polygon-4"}, "四邊形"), + React.DOM.option( {value:"polygon-5"}, "五邊形"), + React.DOM.option( {value:"polygon-6"}, "六邊形"), + React.DOM.option( {value:"line"}, "線"), + React.DOM.option( {value:"line,line"}, "2 線"), + React.DOM.option( {value:"lineSegment"}, "線段"), React.DOM.option( {value:"lineSegment,lineSegment"}, - ' ',"2 line segments",' ' + ' ',"2 線段",' ' ), - React.DOM.option( {value:"angle"}, "Angle"), - React.DOM.option( {value:"circle"}, "Circle") + React.DOM.option( {value:"angle"}, "角度"), + React.DOM.option( {value:"circle"}, "圓形") ) ); }, @@ -24779,7 +25199,7 @@ var ToolsBar = React.createClass({displayName: 'ToolsBar', onTouchStart:captureScratchpadTouchStart}, React.DOM.span( {className:"icon-undo"} ), " ", - "Undo" + "回復" ), React.DOM.div( {className:"clear"}) ); @@ -24825,7 +25245,7 @@ var AddTransformBar = React.createClass({displayName: 'AddTransformBar', onTouchStart:captureScratchpadTouchStart}, React.DOM.span( {className:"icon-undo"} ), " ", - "Undo" + "回復" ), React.DOM.div( {className:"clear"}) ); @@ -24888,6 +25308,7 @@ var Transformer = React.createClass({displayName: 'Transformer', markings:graph.markings, backgroundImage:graph.backgroundImage, showProtractor:graph.showProtractor, + showRuler:graph.showRuler, onNewGraphie:this.setupGraphie} ), !interactiveToolsMode && ( @@ -25635,7 +26056,7 @@ var TransformerEditor = React.createClass({displayName: 'TransformerEditor', // so that we don't have all this duplication getDefaultProps: function() { return _.defaults({ - graph: defaultGraphProps(this.props.graph, 340) + graph: defaultGraphProps(null, 340) }, defaultTransformerProps); }, @@ -25644,33 +26065,24 @@ var TransformerEditor = React.createClass({displayName: 'TransformerEditor', // this can happen because the graph json doesn't include // box, for example var graph = _.extend( - defaultGraphProps(this.props.graph, 340), + defaultGraphProps(null, 340), this.props.graph ); return React.DOM.div(null, React.DOM.div(null, PropCheckBox( - {label:"Grade empty answers as wrong:", + {label:"將空的答案視為錯誤:", gradeEmpty:this.props.gradeEmpty, onChange:this.props.onChange} ), InfoTip(null, React.DOM.p(null, - "We generally do not grade empty answers. This usually"+' '+ - "works well, but sometimes can result in giving away"+' '+ - "part of an answer in a multi-part question." - ), - React.DOM.p(null, - "If this is a multi-part question (there is another"+' '+ - "widget), you probably want to enable this option."+' '+ - "Otherwise, you should leave it disabled." - ), - React.DOM.p(null, - "Confused? Talk to Elizabeth." + "基本上我們並不允許答案為空,但在具有多重填答需求的問題中"+' '+ + "(另一個 widget),此功能是需要的。" ) ) ), - React.DOM.div(null, "Graph settings:"), + React.DOM.div(null, "圖形設定:"), GraphSettings( {box:graph.box, labels:graph.labels, @@ -25681,8 +26093,9 @@ var TransformerEditor = React.createClass({displayName: 'TransformerEditor', backgroundImage:graph.backgroundImage, markings:graph.markings, showProtractor:graph.showProtractor, + showRuler:graph.showRuler, onChange:this.changeGraph} ), - React.DOM.div(null, "Transformation settings:"), + React.DOM.div(null, "變換設定:"), TransformationExplorerSettings( {ref:"transformationSettings", graphMode:this.props.graphMode, @@ -25690,13 +26103,13 @@ var TransformerEditor = React.createClass({displayName: 'TransformerEditor', tools:this.props.tools, drawSolutionShape:this.props.drawSolutionShape, onChange:this.props.onChange} ), - React.DOM.div(null, "Starting location:"), + React.DOM.div(null, "起始位置:"), TransformationsShapeEditor( {ref:"shapeEditor", graph:graph, shape:this.props.starting.shape, onChange:this.changeStarting} ), - React.DOM.div(null, "Solution transformations:"), + React.DOM.div(null, "答案:"), Transformer( {ref:"explorer", graph:graph, @@ -25752,13 +26165,11 @@ var TransformerEditor = React.createClass({displayName: 'TransformerEditor', module.exports = { name: "transformer", - displayName: "Transformer", + displayName: "Transformer/圖形變換", widget: Transformer, editor: TransformerEditor, - hidden: true + hidden: false }; -},{"../components/graph-settings.jsx":121,"../components/graph.jsx":122,"../components/number-input.jsx":129,"../components/prop-check-box.jsx":130,"../tex.jsx":167,"../util.js":168,"react":115,"react-components/info-tip":5}]},{},[163]) -(163) -}); -; \ No newline at end of file +},{"../components/graph-settings.jsx":53,"../components/graph.jsx":54,"../components/number-input.jsx":61,"../components/prop-check-box.jsx":62,"../tex.jsx":99,"../util.js":100,"react":45,"react-components/info-tip":39}]},{},[95])(95) +}); \ No newline at end of file diff --git a/ke b/ke index 797d4ddcad..05763fb687 160000 --- a/ke +++ b/ke @@ -1 +1 @@ -Subproject commit 797d4ddcadc04f3eb796a96c609b9b941497188e +Subproject commit 05763fb6874883218cc3522285753b7de3938bc6 diff --git a/lib/kas.js b/lib/kas.js index 99ee502391..c0f183039f 100644 --- a/lib/kas.js +++ b/lib/kas.js @@ -779,10 +779,12 @@ case 67:return "INVALID" break; case 68:console.log(yy_.yytext); break; +case 69:return "/" +break; } }, -rules: [/^(?:\s+)/,/^(?:\\space)/,/^(?:\\ )/,/^(?:[0-9]+\.?)/,/^(?:([0-9]+)?\.[0-9]+)/,/^(?:\*\*)/,/^(?:\*)/,/^(?:\\cdot|·)/,/^(?:\\times|×)/,/^(?:\/)/,/^(?:-)/,/^(?:−)/,/^(?:\+)/,/^(?:\^)/,/^(?:\()/,/^(?:\))/,/^(?:\\left\()/,/^(?:\\right\))/,/^(?:\{)/,/^(?:\})/,/^(?:\\left\{)/,/^(?:\\right\})/,/^(?:_)/,/^(?:\|)/,/^(?:\\left\|)/,/^(?:\\right\|)/,/^(?:\!)/,/^(?:<=|>=|<>|<|>|=)/,/^(?:\\le)/,/^(?:\\ge)/,/^(?:=\/=)/,/^(?:\\ne)/,/^(?:≠)/,/^(?:≤)/,/^(?:≥)/,/^(?:\\frac)/,/^(?:sqrt|\\sqrt)/,/^(?:abs|\\abs)/,/^(?:ln|\\ln)/,/^(?:log|\\log)/,/^(?:sin|cos|tan)/,/^(?:csc|sec|cot)/,/^(?:\\sin)/,/^(?:\\cos)/,/^(?:\\tan)/,/^(?:\\csc)/,/^(?:\\sec)/,/^(?:\\cot)/,/^(?:\\arcsin)/,/^(?:\\arccos)/,/^(?:\\arctan)/,/^(?:\\arccsc)/,/^(?:\\arcsec)/,/^(?:\\arccot)/,/^(?:arcsin|arccos|arctan)/,/^(?:arccsc|arcsec|arccot)/,/^(?:pi)/,/^(?:π)/,/^(?:\\pi)/,/^(?:theta)/,/^(?:θ)/,/^(?:\\theta)/,/^(?:phi)/,/^(?:φ)/,/^(?:\\phi)/,/^(?:[a-zA-Z])/,/^(?:$)/,/^(?:.)/,/^(?:.)/], -conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68],"inclusive":true}} +rules: [/^(?:\s+)/,/^(?:\\space)/,/^(?:\\ )/,/^(?:[0-9]+\.?)/,/^(?:([0-9]+)?\.[0-9]+)/,/^(?:\*\*)/,/^(?:\*)/,/^(?:\\cdot|·)/,/^(?:\\times|×)/,/^(?:\/)/,/^(?:-)/,/^(?:−)/,/^(?:\+)/,/^(?:\^)/,/^(?:\()/,/^(?:\))/,/^(?:\\left\()/,/^(?:\\right\))/,/^(?:\{)/,/^(?:\})/,/^(?:\\left\{)/,/^(?:\\right\})/,/^(?:_)/,/^(?:\|)/,/^(?:\\left\|)/,/^(?:\\right\|)/,/^(?:\!)/,/^(?:<=|>=|<>|<|>|=)/,/^(?:\\le)/,/^(?:\\ge)/,/^(?:=\/=)/,/^(?:\\ne)/,/^(?:≠)/,/^(?:≤)/,/^(?:≥)/,/^(?:\\frac)/,/^(?:sqrt|\\sqrt)/,/^(?:abs|\\abs)/,/^(?:ln|\\ln)/,/^(?:log|\\log)/,/^(?:sin|cos|tan)/,/^(?:csc|sec|cot)/,/^(?:\\sin)/,/^(?:\\cos)/,/^(?:\\tan)/,/^(?:\\csc)/,/^(?:\\sec)/,/^(?:\\cot)/,/^(?:\\arcsin)/,/^(?:\\arccos)/,/^(?:\\arctan)/,/^(?:\\arccsc)/,/^(?:\\arcsec)/,/^(?:\\arccot)/,/^(?:arcsin|arccos|arctan)/,/^(?:arccsc|arcsec|arccot)/,/^(?:pi)/,/^(?:π)/,/^(?:\\pi)/,/^(?:theta)/,/^(?:θ)/,/^(?:\\theta)/,/^(?:phi)/,/^(?:φ)/,/^(?:\\phi)/,/^(?:[a-zA-Z])/,/^(?:$)/,/^(?:.)/,/^(?:.)/,/^(?:\\div|÷)/], +conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69],"inclusive":true}} }; return lexer; })(); @@ -977,6 +979,30 @@ _.extend(Expr.prototype, { return this.equals(this.simplify()); }, + isMixedNumberRational: function() { + //判斷是否為帶分數 + if (this.terms.length !== 2){ + return false; + } + var left = this.terms[0]; + var right = this.terms[1]; + if(left instanceof Int && right instanceof Rational){ + return true; + }else { + return false; + } + }, + + getMixedNumberRationalVal: function() { + // for mixed number Rational: 1 1/2 = 1+1/2 , -1 1/2 = -1-1/2 + var left = this.terms[0]; + var right = this.terms[1]; + if (left.n < 0){ + return left.n - right.n/right.d; + } + return left.n + right.n/right.d; + }, + // return the child nodes of this node exprArgs: function() { return _.filter(this.args(), function(arg) { @@ -1441,7 +1467,13 @@ _.extend(Mul.prototype, { func: Mul, eval: function(vars) { - return _.reduce(this.terms, function(memo, term) { return memo * term.eval(vars); }, 1); + if(this.isMixedNumberRational()){ + return this.getMixedNumberRationalVal(); + } + + return _.reduce(this.terms, function(memo, term) { + return memo * term.eval(vars); + }, 1); }, print: function() { @@ -3545,7 +3577,7 @@ KAS.compare = function(expr1, expr2, options) { if (!vars.equal) { var message = null; if (vars.equalIgnoringCase) { - message = "Some of your variables are in the wrong case (upper vs. lower)."; + message = "你的答案其實很接近了!請將字母換成正確的大小寫噢!"; } return {equal: false, message: message}; } @@ -3557,12 +3589,12 @@ KAS.compare = function(expr1, expr2, options) { // syntactic check if (options.form && !expr1.sameForm(expr2)) { - return {equal: false, message: "Your answer is not in the correct form."}; + return {equal: false, message: "你的列式錯誤"}; } // syntactic check if (options.simplify && !expr1.isSimplified()) { - return {equal: false, message: "Your answer is not fully expanded and simplified."}; + return {equal: false, message: "你的答案未化簡或未展開"}; } return {equal: true, message: null}; diff --git a/lib/katex/katex.less.css b/lib/katex/katex.less.css new file mode 100644 index 0000000000..0a76d712f6 --- /dev/null +++ b/lib/katex/katex.less.css @@ -0,0 +1,321 @@ +/* +things to do: +\sum, \int, \lim +\sqrt +big parens +*/ +.katex { + font: normal 1.21em katex_main; + line-height: 1.2; + white-space: nowrap; +} +.katex .katex-inner { + display: inline-block; +} +.katex .base { + display: inline-block; +} +.katex .strut { + display: inline-block; +} +.katex .mathit { + font-family: katex_math; + font-style: italic; +} +.katex .amsrm { + font-family: katex_ams; +} +.katex .textstyle > .mord + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mord + .mbin { + margin-left: 0.22222em; +} +.katex .textstyle > .mord + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .mord + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .mop + .mord { + margin-left: 0.16667em; +} +.katex .textstyle > .mop + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mop + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .mop + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .mbin + .mord { + margin-left: 0.22222em; +} +.katex .textstyle > .mbin + .mop { + margin-left: 0.22222em; +} +.katex .textstyle > .mbin + .mopen { + margin-left: 0.22222em; +} +.katex .textstyle > .mbin + .minner { + margin-left: 0.22222em; +} +.katex .textstyle > .mrel + .mord { + margin-left: 0.27778em; +} +.katex .textstyle > .mrel + .mop { + margin-left: 0.27778em; +} +.katex .textstyle > .mrel + .mopen { + margin-left: 0.27778em; +} +.katex .textstyle > .mrel + .minner { + margin-left: 0.27778em; +} +.katex .textstyle > .mclose + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mclose + .mbin { + margin-left: 0.22222em; +} +.katex .textstyle > .mclose + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .mclose + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mord { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mrel { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mopen { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mclose { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mpunct { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mord { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mbin { + margin-left: 0.22222em; +} +.katex .textstyle > .minner + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .minner + .mopen { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mpunct { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .minner { + margin-left: 0.16667em; +} +.katex .mord + .mop { + margin-left: 0.16667em; +} +.katex .mop + .mord { + margin-left: 0.16667em; +} +.katex .mop + .mop { + margin-left: 0.16667em; +} +.katex .mclose + .mop { + margin-left: 0.16667em; +} +.katex .minner + .mop { + margin-left: 0.16667em; +} +.katex .reset-textstyle.textstyle { + font-size: 1em; +} +.katex .reset-textstyle.scriptstyle { + font-size: 0.66667em; +} +.katex .reset-textstyle.scriptscriptstyle { + font-size: 0.5em; +} +.katex .reset-scriptstyle.textstyle { + font-size: 1.5em; +} +.katex .reset-scriptstyle.scriptstyle { + font-size: 1em; +} +.katex .reset-scriptstyle.scriptscriptstyle { + font-size: 0.75em; +} +.katex .reset-scriptscriptstyle.textstyle { + font-size: 2em; +} +.katex .reset-scriptscriptstyle.scriptstyle { + font-size: 1.5em; +} +.katex .reset-scriptscriptstyle.scriptscriptstyle { + font-size: 1em; +} +.katex .msupsub { + display: inline-block; + text-align: left; + margin-left: 0.05em; +} +.katex .msupsub .msup, +.katex .msupsub .msub, +.katex .msupsub .fix-ie { + display: block; + height: 0; + position: relative; +} +.katex .msupsub .msup > span, +.katex .msupsub .msub > span, +.katex .msupsub .fix-ie > span { + display: inline-block; +} +.katex .msupsub .fix-ie { + display: inline-block; +} +.katex .mfrac { + display: inline-block; +} +.katex .mfrac .mfracnum, +.katex .mfrac .mfracmid, +.katex .mfrac .mfracden, +.katex .mfrac .fix-ie { + display: block; + height: 0; + position: relative; + text-align: center; +} +.katex .mfrac .mfracnum > span, +.katex .mfrac .mfracmid > span, +.katex .mfrac .mfracden > span, +.katex .mfrac .fix-ie > span { + display: inline-block; +} +.katex .mfrac .fix-ie { + display: inline-block; +} +.katex .mfrac .mfracmid > span { + width: 100%; +} +.katex .mfrac .mfracmid > span:before { + border-bottom-style: solid; + border-bottom-width: 1px; + content: ""; + display: block; +} +.katex .mfrac .mfracmid > span:after { + border-bottom-style: solid; + border-bottom-width: 0.05em; + content: ""; + display: block; + margin-top: -1px; +} +.katex .mspace { + display: inline-block; +} +.katex .mspace.negativethinspace { + margin-left: -0.16667em; +} +.katex .mspace.thinspace { + width: 0.16667em; +} +.katex .mspace.mediumspace { + width: 0.22222em; +} +.katex .mspace.thickspace { + width: 0.27778em; +} +.katex .mspace.enspace { + width: 0.5em; +} +.katex .mspace.quad { + width: 1em; +} +.katex .mspace.qquad { + width: 2em; +} +.katex .llap, +.katex .rlap { + width: 0; + position: relative; +} +.katex .llap > .inner, +.katex .rlap > .inner { + position: absolute; +} +.katex .llap > .fix, +.katex .rlap > .fix { + display: inline-block; +} +.katex .llap > .inner { + right: 0; +} +.katex .rlap > .inner { + left: 0; +} +.katex .katex-logo .a { + font-size: 0.75em; + margin-left: -0.32em; + position: relative; + top: -0.2em; +} +.katex .katex-logo .t { + margin-left: -0.23em; +} +.katex .katex-logo .e { + margin-left: -0.1667em; + position: relative; + top: 0.2155em; +} +.katex .katex-logo .x { + margin-left: -0.125em; +} +.katex .sizing { + display: inline-block; +} +.katex .reset-size5.size1 { + font-size: 0.5em; +} +.katex .reset-size5.size2 { + font-size: 0.7em; +} +.katex .reset-size5.size3 { + font-size: 0.8em; +} +.katex .reset-size5.size4 { + font-size: 0.9em; +} +.katex .reset-size5.size5 { + font-size: 1.0em; +} +.katex .reset-size5.size6 { + font-size: 1.2em; +} +.katex .reset-size5.size7 { + font-size: 1.44em; +} +.katex .reset-size5.size8 { + font-size: 1.73em; +} +.katex .reset-size5.size9 { + font-size: 2.07em; +} +.katex .reset-size5.size10 { + font-size: 2.49em; +} diff --git a/lib/mathquill/mathquill.js b/lib/mathquill/mathquill.js index 5bf12bf470..5ef916dd67 100644 --- a/lib/mathquill/mathquill.js +++ b/lib/mathquill/mathquill.js @@ -1435,6 +1435,7 @@ var saneKeyboardEvents = (function() { checkTextarea = noop; // key that clears the selection, then never clearTimeout(timeoutId); // again, 'cos next thing might be blur }); + onKeypress(e); } function onKeypress(e) { @@ -1505,7 +1506,6 @@ var saneKeyboardEvents = (function() { // -*- attach event handlers -*- // target.bind({ keydown: onKeydown, - keypress: onKeypress, focusout: onBlur, paste: onPaste }); diff --git a/lib/require.js b/lib/require.js new file mode 100644 index 0000000000..24b061e620 --- /dev/null +++ b/lib/require.js @@ -0,0 +1,2068 @@ +/** vim: et:ts=4:sw=4:sts=4 + * @license RequireJS 2.1.11 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. + * Available via the MIT or new BSD license. + * see: http://github.com/jrburke/requirejs for details + */ +//Not using strict: uneven strict support in browsers, #392, and causes +//problems with requirejs.exec()/transpiler plugins that may not be strict. +/*jslint regexp: true, nomen: true, sloppy: true */ +/*global window, navigator, document, importScripts, setTimeout, opera */ + +var requirejs, require, define; +(function (global) { + var req, s, head, baseElement, dataMain, src, + interactiveScript, currentlyAddingScript, mainScript, subPath, + version = '2.1.11', + commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg, + cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g, + jsSuffixRegExp = /\.js$/, + currDirRegExp = /^\.\//, + op = Object.prototype, + ostring = op.toString, + hasOwn = op.hasOwnProperty, + ap = Array.prototype, + apsp = ap.splice, + isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document), + isWebWorker = !isBrowser && typeof importScripts !== 'undefined', + //PS3 indicates loaded and complete, but need to wait for complete + //specifically. Sequence is 'loading', 'loaded', execution, + // then 'complete'. The UA check is unfortunate, but not sure how + //to feature test w/o causing perf issues. + readyRegExp = isBrowser && navigator.platform === 'PLAYSTATION 3' ? + /^complete$/ : /^(complete|loaded)$/, + defContextName = '_', + //Oh the tragedy, detecting opera. See the usage of isOpera for reason. + isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]', + contexts = {}, + cfg = {}, + globalDefQueue = [], + useInteractive = false; + + function isFunction(it) { + return ostring.call(it) === '[object Function]'; + } + + function isArray(it) { + return ostring.call(it) === '[object Array]'; + } + + /** + * Helper function for iterating over an array. If the func returns + * a true value, it will break out of the loop. + */ + function each(ary, func) { + if (ary) { + var i; + for (i = 0; i < ary.length; i += 1) { + if (ary[i] && func(ary[i], i, ary)) { + break; + } + } + } + } + + /** + * Helper function for iterating over an array backwards. If the func + * returns a true value, it will break out of the loop. + */ + function eachReverse(ary, func) { + if (ary) { + var i; + for (i = ary.length - 1; i > -1; i -= 1) { + if (ary[i] && func(ary[i], i, ary)) { + break; + } + } + } + } + + function hasProp(obj, prop) { + return hasOwn.call(obj, prop); + } + + function getOwn(obj, prop) { + return hasProp(obj, prop) && obj[prop]; + } + + /** + * Cycles over properties in an object and calls a function for each + * property value. If the function returns a truthy value, then the + * iteration is stopped. + */ + function eachProp(obj, func) { + var prop; + for (prop in obj) { + if (hasProp(obj, prop)) { + if (func(obj[prop], prop)) { + break; + } + } + } + } + + /** + * Simple function to mix in properties from source into target, + * but only if target does not already have a property of the same name. + */ + function mixin(target, source, force, deepStringMixin) { + if (source) { + eachProp(source, function (value, prop) { + if (force || !hasProp(target, prop)) { + if (deepStringMixin && typeof value === 'object' && value && + !isArray(value) && !isFunction(value) && + !(value instanceof RegExp)) { + + if (!target[prop]) { + target[prop] = {}; + } + mixin(target[prop], value, force, deepStringMixin); + } else { + target[prop] = value; + } + } + }); + } + return target; + } + + //Similar to Function.prototype.bind, but the 'this' object is specified + //first, since it is easier to read/figure out what 'this' will be. + function bind(obj, fn) { + return function () { + return fn.apply(obj, arguments); + }; + } + + function scripts() { + return document.getElementsByTagName('script'); + } + + function defaultOnError(err) { + throw err; + } + + //Allow getting a global that is expressed in + //dot notation, like 'a.b.c'. + function getGlobal(value) { + if (!value) { + return value; + } + var g = global; + each(value.split('.'), function (part) { + g = g[part]; + }); + return g; + } + + /** + * Constructs an error with a pointer to an URL with more information. + * @param {String} id the error ID that maps to an ID on a web page. + * @param {String} message human readable error. + * @param {Error} [err] the original error, if there is one. + * + * @returns {Error} + */ + function makeError(id, msg, err, requireModules) { + var e = new Error(msg + '\nhttp://requirejs.org/docs/errors.html#' + id); + e.requireType = id; + e.requireModules = requireModules; + if (err) { + e.originalError = err; + } + return e; + } + + if (typeof define !== 'undefined') { + //If a define is already in play via another AMD loader, + //do not overwrite. + return; + } + + if (typeof requirejs !== 'undefined') { + if (isFunction(requirejs)) { + //Do not overwrite and existing requirejs instance. + return; + } + cfg = requirejs; + requirejs = undefined; + } + + //Allow for a require config object + if (typeof require !== 'undefined' && !isFunction(require)) { + //assume it is a config object. + cfg = require; + require = undefined; + } + + function newContext(contextName) { + var inCheckLoaded, Module, context, handlers, + checkLoadedTimeoutId, + config = { + //Defaults. Do not set a default for map + //config to speed up normalize(), which + //will run faster if there is no default. + waitSeconds: 7, + baseUrl: './', + paths: {}, + bundles: {}, + pkgs: {}, + shim: {}, + config: {} + }, + registry = {}, + //registry of just enabled modules, to speed + //cycle breaking code when lots of modules + //are registered, but not activated. + enabledRegistry = {}, + undefEvents = {}, + defQueue = [], + defined = {}, + urlFetched = {}, + bundlesMap = {}, + requireCounter = 1, + unnormalizedCounter = 1; + + /** + * Trims the . and .. from an array of path segments. + * It will keep a leading path segment if a .. will become + * the first path segment, to help with module name lookups, + * which act like paths, but can be remapped. But the end result, + * all paths that use this function should look normalized. + * NOTE: this method MODIFIES the input array. + * @param {Array} ary the array of path segments. + */ + function trimDots(ary) { + var i, part, length = ary.length; + for (i = 0; i < length; i++) { + part = ary[i]; + if (part === '.') { + ary.splice(i, 1); + i -= 1; + } else if (part === '..') { + if (i === 1 && (ary[2] === '..' || ary[0] === '..')) { + //End of the line. Keep at least one non-dot + //path segment at the front so it can be mapped + //correctly to disk. Otherwise, there is likely + //no path mapping for a path starting with '..'. + //This can still fail, but catches the most reasonable + //uses of .. + break; + } else if (i > 0) { + ary.splice(i - 1, 2); + i -= 2; + } + } + } + } + + /** + * Given a relative module name, like ./something, normalize it to + * a real name that can be mapped to a path. + * @param {String} name the relative name + * @param {String} baseName a real name that the name arg is relative + * to. + * @param {Boolean} applyMap apply the map config to the value. Should + * only be done if this normalization is for a dependency ID. + * @returns {String} normalized name + */ + function normalize(name, baseName, applyMap) { + var pkgMain, mapValue, nameParts, i, j, nameSegment, lastIndex, + foundMap, foundI, foundStarMap, starI, + baseParts = baseName && baseName.split('/'), + normalizedBaseParts = baseParts, + map = config.map, + starMap = map && map['*']; + + //Adjust any relative paths. + if (name && name.charAt(0) === '.') { + //If have a base name, try to normalize against it, + //otherwise, assume it is a top-level require that will + //be relative to baseUrl in the end. + if (baseName) { + //Convert baseName to array, and lop off the last part, + //so that . matches that 'directory' and not name of the baseName's + //module. For instance, baseName of 'one/two/three', maps to + //'one/two/three.js', but we want the directory, 'one/two' for + //this normalization. + normalizedBaseParts = baseParts.slice(0, baseParts.length - 1); + name = name.split('/'); + lastIndex = name.length - 1; + + // If wanting node ID compatibility, strip .js from end + // of IDs. Have to do this here, and not in nameToUrl + // because node allows either .js or non .js to map + // to same file. + if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) { + name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, ''); + } + + name = normalizedBaseParts.concat(name); + trimDots(name); + name = name.join('/'); + } else if (name.indexOf('./') === 0) { + // No baseName, so this is ID is resolved relative + // to baseUrl, pull off the leading dot. + name = name.substring(2); + } + } + + //Apply map config if available. + if (applyMap && map && (baseParts || starMap)) { + nameParts = name.split('/'); + + outerLoop: for (i = nameParts.length; i > 0; i -= 1) { + nameSegment = nameParts.slice(0, i).join('/'); + + if (baseParts) { + //Find the longest baseName segment match in the config. + //So, do joins on the biggest to smallest lengths of baseParts. + for (j = baseParts.length; j > 0; j -= 1) { + mapValue = getOwn(map, baseParts.slice(0, j).join('/')); + + //baseName segment has config, find if it has one for + //this name. + if (mapValue) { + mapValue = getOwn(mapValue, nameSegment); + if (mapValue) { + //Match, update name to the new value. + foundMap = mapValue; + foundI = i; + break outerLoop; + } + } + } + } + + //Check for a star map match, but just hold on to it, + //if there is a shorter segment match later in a matching + //config, then favor over this star map. + if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) { + foundStarMap = getOwn(starMap, nameSegment); + starI = i; + } + } + + if (!foundMap && foundStarMap) { + foundMap = foundStarMap; + foundI = starI; + } + + if (foundMap) { + nameParts.splice(0, foundI, foundMap); + name = nameParts.join('/'); + } + } + + // If the name points to a package's name, use + // the package main instead. + pkgMain = getOwn(config.pkgs, name); + + return pkgMain ? pkgMain : name; + } + + function removeScript(name) { + if (isBrowser) { + each(scripts(), function (scriptNode) { + if (scriptNode.getAttribute('data-requiremodule') === name && + scriptNode.getAttribute('data-requirecontext') === context.contextName) { + scriptNode.parentNode.removeChild(scriptNode); + return true; + } + }); + } + } + + function hasPathFallback(id) { + var pathConfig = getOwn(config.paths, id); + if (pathConfig && isArray(pathConfig) && pathConfig.length > 1) { + //Pop off the first array value, since it failed, and + //retry + pathConfig.shift(); + context.require.undef(id); + context.require([id]); + return true; + } + } + + //Turns a plugin!resource to [plugin, resource] + //with the plugin being undefined if the name + //did not have a plugin prefix. + function splitPrefix(name) { + var prefix, + index = name ? name.indexOf('!') : -1; + if (index > -1) { + prefix = name.substring(0, index); + name = name.substring(index + 1, name.length); + } + return [prefix, name]; + } + + /** + * Creates a module mapping that includes plugin prefix, module + * name, and path. If parentModuleMap is provided it will + * also normalize the name via require.normalize() + * + * @param {String} name the module name + * @param {String} [parentModuleMap] parent module map + * for the module name, used to resolve relative names. + * @param {Boolean} isNormalized: is the ID already normalized. + * This is true if this call is done for a define() module ID. + * @param {Boolean} applyMap: apply the map config to the ID. + * Should only be true if this map is for a dependency. + * + * @returns {Object} + */ + function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) { + var url, pluginModule, suffix, nameParts, + prefix = null, + parentName = parentModuleMap ? parentModuleMap.name : null, + originalName = name, + isDefine = true, + normalizedName = ''; + + //If no name, then it means it is a require call, generate an + //internal name. + if (!name) { + isDefine = false; + name = '_@r' + (requireCounter += 1); + } + + nameParts = splitPrefix(name); + prefix = nameParts[0]; + name = nameParts[1]; + + if (prefix) { + prefix = normalize(prefix, parentName, applyMap); + pluginModule = getOwn(defined, prefix); + } + + //Account for relative paths if there is a base name. + if (name) { + if (prefix) { + if (pluginModule && pluginModule.normalize) { + //Plugin is loaded, use its normalize method. + normalizedName = pluginModule.normalize(name, function (name) { + return normalize(name, parentName, applyMap); + }); + } else { + normalizedName = normalize(name, parentName, applyMap); + } + } else { + //A regular module. + normalizedName = normalize(name, parentName, applyMap); + + //Normalized name may be a plugin ID due to map config + //application in normalize. The map config values must + //already be normalized, so do not need to redo that part. + nameParts = splitPrefix(normalizedName); + prefix = nameParts[0]; + normalizedName = nameParts[1]; + isNormalized = true; + + url = context.nameToUrl(normalizedName); + } + } + + //If the id is a plugin id that cannot be determined if it needs + //normalization, stamp it with a unique ID so two matching relative + //ids that may conflict can be separate. + suffix = prefix && !pluginModule && !isNormalized ? + '_unnormalized' + (unnormalizedCounter += 1) : + ''; + + return { + prefix: prefix, + name: normalizedName, + parentMap: parentModuleMap, + unnormalized: !!suffix, + url: url, + originalName: originalName, + isDefine: isDefine, + id: (prefix ? + prefix + '!' + normalizedName : + normalizedName) + suffix + }; + } + + function getModule(depMap) { + var id = depMap.id, + mod = getOwn(registry, id); + + if (!mod) { + mod = registry[id] = new context.Module(depMap); + } + + return mod; + } + + function on(depMap, name, fn) { + var id = depMap.id, + mod = getOwn(registry, id); + + if (hasProp(defined, id) && + (!mod || mod.defineEmitComplete)) { + if (name === 'defined') { + fn(defined[id]); + } + } else { + mod = getModule(depMap); + if (mod.error && name === 'error') { + fn(mod.error); + } else { + mod.on(name, fn); + } + } + } + + function onError(err, errback) { + var ids = err.requireModules, + notified = false; + + if (errback) { + errback(err); + } else { + each(ids, function (id) { + var mod = getOwn(registry, id); + if (mod) { + //Set error on module, so it skips timeout checks. + mod.error = err; + if (mod.events.error) { + notified = true; + mod.emit('error', err); + } + } + }); + + if (!notified) { + req.onError(err); + } + } + } + + /** + * Internal method to transfer globalQueue items to this context's + * defQueue. + */ + function takeGlobalQueue() { + //Push all the globalDefQueue items into the context's defQueue + if (globalDefQueue.length) { + //Array splice in the values since the context code has a + //local var ref to defQueue, so cannot just reassign the one + //on context. + apsp.apply(defQueue, + [defQueue.length, 0].concat(globalDefQueue)); + globalDefQueue = []; + } + } + + handlers = { + 'require': function (mod) { + if (mod.require) { + return mod.require; + } else { + return (mod.require = context.makeRequire(mod.map)); + } + }, + 'exports': function (mod) { + mod.usingExports = true; + if (mod.map.isDefine) { + if (mod.exports) { + return (defined[mod.map.id] = mod.exports); + } else { + return (mod.exports = defined[mod.map.id] = {}); + } + } + }, + 'module': function (mod) { + if (mod.module) { + return mod.module; + } else { + return (mod.module = { + id: mod.map.id, + uri: mod.map.url, + config: function () { + return getOwn(config.config, mod.map.id) || {}; + }, + exports: mod.exports || (mod.exports = {}) + }); + } + } + }; + + function cleanRegistry(id) { + //Clean up machinery used for waiting modules. + delete registry[id]; + delete enabledRegistry[id]; + } + + function breakCycle(mod, traced, processed) { + var id = mod.map.id; + + if (mod.error) { + mod.emit('error', mod.error); + } else { + traced[id] = true; + each(mod.depMaps, function (depMap, i) { + var depId = depMap.id, + dep = getOwn(registry, depId); + + //Only force things that have not completed + //being defined, so still in the registry, + //and only if it has not been matched up + //in the module already. + if (dep && !mod.depMatched[i] && !processed[depId]) { + if (getOwn(traced, depId)) { + mod.defineDep(i, defined[depId]); + mod.check(); //pass false? + } else { + breakCycle(dep, traced, processed); + } + } + }); + processed[id] = true; + } + } + + function checkLoaded() { + var err, usingPathFallback, + waitInterval = config.waitSeconds * 1000, + //It is possible to disable the wait interval by using waitSeconds of 0. + expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(), + noLoads = [], + reqCalls = [], + stillLoading = false, + needCycleCheck = true; + + //Do not bother if this call was a result of a cycle break. + if (inCheckLoaded) { + return; + } + + inCheckLoaded = true; + + //Figure out the state of all the modules. + eachProp(enabledRegistry, function (mod) { + var map = mod.map, + modId = map.id; + + //Skip things that are not enabled or in error state. + if (!mod.enabled) { + return; + } + + if (!map.isDefine) { + reqCalls.push(mod); + } + + if (!mod.error) { + //If the module should be executed, and it has not + //been inited and time is up, remember it. + if (!mod.inited && expired) { + if (hasPathFallback(modId)) { + usingPathFallback = true; + stillLoading = true; + } else { + noLoads.push(modId); + removeScript(modId); + } + } else if (!mod.inited && mod.fetched && map.isDefine) { + stillLoading = true; + if (!map.prefix) { + //No reason to keep looking for unfinished + //loading. If the only stillLoading is a + //plugin resource though, keep going, + //because it may be that a plugin resource + //is waiting on a non-plugin cycle. + return (needCycleCheck = false); + } + } + } + }); + + if (expired && noLoads.length) { + //If wait time expired, throw error of unloaded modules. + err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads); + err.contextName = context.contextName; + return onError(err); + } + + //Not expired, check for a cycle. + if (needCycleCheck) { + each(reqCalls, function (mod) { + breakCycle(mod, {}, {}); + }); + } + + //If still waiting on loads, and the waiting load is something + //other than a plugin resource, or there are still outstanding + //scripts, then just try back later. + if ((!expired || usingPathFallback) && stillLoading) { + //Something is still waiting to load. Wait for it, but only + //if a timeout is not already in effect. + if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) { + checkLoadedTimeoutId = setTimeout(function () { + checkLoadedTimeoutId = 0; + checkLoaded(); + }, 50); + } + } + + inCheckLoaded = false; + } + + Module = function (map) { + this.events = getOwn(undefEvents, map.id) || {}; + this.map = map; + this.shim = getOwn(config.shim, map.id); + this.depExports = []; + this.depMaps = []; + this.depMatched = []; + this.pluginMaps = {}; + this.depCount = 0; + + /* this.exports this.factory + this.depMaps = [], + this.enabled, this.fetched + */ + }; + + Module.prototype = { + init: function (depMaps, factory, errback, options) { + options = options || {}; + + //Do not do more inits if already done. Can happen if there + //are multiple define calls for the same module. That is not + //a normal, common case, but it is also not unexpected. + if (this.inited) { + return; + } + + this.factory = factory; + + if (errback) { + //Register for errors on this module. + this.on('error', errback); + } else if (this.events.error) { + //If no errback already, but there are error listeners + //on this module, set up an errback to pass to the deps. + errback = bind(this, function (err) { + this.emit('error', err); + }); + } + + //Do a copy of the dependency array, so that + //source inputs are not modified. For example + //"shim" deps are passed in here directly, and + //doing a direct modification of the depMaps array + //would affect that config. + this.depMaps = depMaps && depMaps.slice(0); + + this.errback = errback; + + //Indicate this module has be initialized + this.inited = true; + + this.ignore = options.ignore; + + //Could have option to init this module in enabled mode, + //or could have been previously marked as enabled. However, + //the dependencies are not known until init is called. So + //if enabled previously, now trigger dependencies as enabled. + if (options.enabled || this.enabled) { + //Enable this module and dependencies. + //Will call this.check() + this.enable(); + } else { + this.check(); + } + }, + + defineDep: function (i, depExports) { + //Because of cycles, defined callback for a given + //export can be called more than once. + if (!this.depMatched[i]) { + this.depMatched[i] = true; + this.depCount -= 1; + this.depExports[i] = depExports; + } + }, + + fetch: function () { + if (this.fetched) { + return; + } + this.fetched = true; + + context.startTime = (new Date()).getTime(); + + var map = this.map; + + //If the manager is for a plugin managed resource, + //ask the plugin to load it now. + if (this.shim) { + context.makeRequire(this.map, { + enableBuildCallback: true + })(this.shim.deps || [], bind(this, function () { + return map.prefix ? this.callPlugin() : this.load(); + })); + } else { + //Regular dependency. + return map.prefix ? this.callPlugin() : this.load(); + } + }, + + load: function () { + var url = this.map.url; + + //Regular dependency. + if (!urlFetched[url]) { + urlFetched[url] = true; + context.load(this.map.id, url); + } + }, + + /** + * Checks if the module is ready to define itself, and if so, + * define it. + */ + check: function () { + if (!this.enabled || this.enabling) { + return; + } + + var err, cjsModule, + id = this.map.id, + depExports = this.depExports, + exports = this.exports, + factory = this.factory; + + if (!this.inited) { + this.fetch(); + } else if (this.error) { + this.emit('error', this.error); + } else if (!this.defining) { + //The factory could trigger another require call + //that would result in checking this module to + //define itself again. If already in the process + //of doing that, skip this work. + this.defining = true; + + if (this.depCount < 1 && !this.defined) { + if (isFunction(factory)) { + //If there is an error listener, favor passing + //to that instead of throwing an error. However, + //only do it for define()'d modules. require + //errbacks should not be called for failures in + //their callbacks (#699). However if a global + //onError is set, use that. + if ((this.events.error && this.map.isDefine) || + req.onError !== defaultOnError) { + try { + exports = context.execCb(id, factory, depExports, exports); + } catch (e) { + err = e; + } + } else { + exports = context.execCb(id, factory, depExports, exports); + } + + // Favor return value over exports. If node/cjs in play, + // then will not have a return value anyway. Favor + // module.exports assignment over exports object. + if (this.map.isDefine && exports === undefined) { + cjsModule = this.module; + if (cjsModule) { + exports = cjsModule.exports; + } else if (this.usingExports) { + //exports already set the defined value. + exports = this.exports; + } + } + + if (err) { + err.requireMap = this.map; + err.requireModules = this.map.isDefine ? [this.map.id] : null; + err.requireType = this.map.isDefine ? 'define' : 'require'; + return onError((this.error = err)); + } + + } else { + //Just a literal value + exports = factory; + } + + this.exports = exports; + + if (this.map.isDefine && !this.ignore) { + defined[id] = exports; + + if (req.onResourceLoad) { + req.onResourceLoad(context, this.map, this.depMaps); + } + } + + //Clean up + cleanRegistry(id); + + this.defined = true; + } + + //Finished the define stage. Allow calling check again + //to allow define notifications below in the case of a + //cycle. + this.defining = false; + + if (this.defined && !this.defineEmitted) { + this.defineEmitted = true; + this.emit('defined', this.exports); + this.defineEmitComplete = true; + } + + } + }, + + callPlugin: function () { + var map = this.map, + id = map.id, + //Map already normalized the prefix. + pluginMap = makeModuleMap(map.prefix); + + //Mark this as a dependency for this plugin, so it + //can be traced for cycles. + this.depMaps.push(pluginMap); + + on(pluginMap, 'defined', bind(this, function (plugin) { + var load, normalizedMap, normalizedMod, + bundleId = getOwn(bundlesMap, this.map.id), + name = this.map.name, + parentName = this.map.parentMap ? this.map.parentMap.name : null, + localRequire = context.makeRequire(map.parentMap, { + enableBuildCallback: true + }); + + //If current map is not normalized, wait for that + //normalized name to load instead of continuing. + if (this.map.unnormalized) { + //Normalize the ID if the plugin allows it. + if (plugin.normalize) { + name = plugin.normalize(name, function (name) { + return normalize(name, parentName, true); + }) || ''; + } + + //prefix and name should already be normalized, no need + //for applying map config again either. + normalizedMap = makeModuleMap(map.prefix + '!' + name, + this.map.parentMap); + on(normalizedMap, + 'defined', bind(this, function (value) { + this.init([], function () { return value; }, null, { + enabled: true, + ignore: true + }); + })); + + normalizedMod = getOwn(registry, normalizedMap.id); + if (normalizedMod) { + //Mark this as a dependency for this plugin, so it + //can be traced for cycles. + this.depMaps.push(normalizedMap); + + if (this.events.error) { + normalizedMod.on('error', bind(this, function (err) { + this.emit('error', err); + })); + } + normalizedMod.enable(); + } + + return; + } + + //If a paths config, then just load that file instead to + //resolve the plugin, as it is built into that paths layer. + if (bundleId) { + this.map.url = context.nameToUrl(bundleId); + this.load(); + return; + } + + load = bind(this, function (value) { + this.init([], function () { return value; }, null, { + enabled: true + }); + }); + + load.error = bind(this, function (err) { + this.inited = true; + this.error = err; + err.requireModules = [id]; + + //Remove temp unnormalized modules for this module, + //since they will never be resolved otherwise now. + eachProp(registry, function (mod) { + if (mod.map.id.indexOf(id + '_unnormalized') === 0) { + cleanRegistry(mod.map.id); + } + }); + + onError(err); + }); + + //Allow plugins to load other code without having to know the + //context or how to 'complete' the load. + load.fromText = bind(this, function (text, textAlt) { + /*jslint evil: true */ + var moduleName = map.name, + moduleMap = makeModuleMap(moduleName), + hasInteractive = useInteractive; + + //As of 2.1.0, support just passing the text, to reinforce + //fromText only being called once per resource. Still + //support old style of passing moduleName but discard + //that moduleName in favor of the internal ref. + if (textAlt) { + text = textAlt; + } + + //Turn off interactive script matching for IE for any define + //calls in the text, then turn it back on at the end. + if (hasInteractive) { + useInteractive = false; + } + + //Prime the system by creating a module instance for + //it. + getModule(moduleMap); + + //Transfer any config to this other module. + if (hasProp(config.config, id)) { + config.config[moduleName] = config.config[id]; + } + + try { + req.exec(text); + } catch (e) { + return onError(makeError('fromtexteval', + 'fromText eval for ' + id + + ' failed: ' + e, + e, + [id])); + } + + if (hasInteractive) { + useInteractive = true; + } + + //Mark this as a dependency for the plugin + //resource + this.depMaps.push(moduleMap); + + //Support anonymous modules. + context.completeLoad(moduleName); + + //Bind the value of that module to the value for this + //resource ID. + localRequire([moduleName], load); + }); + + //Use parentName here since the plugin's name is not reliable, + //could be some weird string with no path that actually wants to + //reference the parentName's path. + plugin.load(map.name, localRequire, load, config); + })); + + context.enable(pluginMap, this); + this.pluginMaps[pluginMap.id] = pluginMap; + }, + + enable: function () { + enabledRegistry[this.map.id] = this; + this.enabled = true; + + //Set flag mentioning that the module is enabling, + //so that immediate calls to the defined callbacks + //for dependencies do not trigger inadvertent load + //with the depCount still being zero. + this.enabling = true; + + //Enable each dependency + each(this.depMaps, bind(this, function (depMap, i) { + var id, mod, handler; + + if (typeof depMap === 'string') { + //Dependency needs to be converted to a depMap + //and wired up to this module. + depMap = makeModuleMap(depMap, + (this.map.isDefine ? this.map : this.map.parentMap), + false, + !this.skipMap); + this.depMaps[i] = depMap; + + handler = getOwn(handlers, depMap.id); + + if (handler) { + this.depExports[i] = handler(this); + return; + } + + this.depCount += 1; + + on(depMap, 'defined', bind(this, function (depExports) { + this.defineDep(i, depExports); + this.check(); + })); + + if (this.errback) { + on(depMap, 'error', bind(this, this.errback)); + } + } + + id = depMap.id; + mod = registry[id]; + + //Skip special modules like 'require', 'exports', 'module' + //Also, don't call enable if it is already enabled, + //important in circular dependency cases. + if (!hasProp(handlers, id) && mod && !mod.enabled) { + context.enable(depMap, this); + } + })); + + //Enable each plugin that is used in + //a dependency + eachProp(this.pluginMaps, bind(this, function (pluginMap) { + var mod = getOwn(registry, pluginMap.id); + if (mod && !mod.enabled) { + context.enable(pluginMap, this); + } + })); + + this.enabling = false; + + this.check(); + }, + + on: function (name, cb) { + var cbs = this.events[name]; + if (!cbs) { + cbs = this.events[name] = []; + } + cbs.push(cb); + }, + + emit: function (name, evt) { + each(this.events[name], function (cb) { + cb(evt); + }); + if (name === 'error') { + //Now that the error handler was triggered, remove + //the listeners, since this broken Module instance + //can stay around for a while in the registry. + delete this.events[name]; + } + } + }; + + function callGetModule(args) { + //Skip modules already defined. + if (!hasProp(defined, args[0])) { + getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]); + } + } + + function removeListener(node, func, name, ieName) { + //Favor detachEvent because of IE9 + //issue, see attachEvent/addEventListener comment elsewhere + //in this file. + if (node.detachEvent && !isOpera) { + //Probably IE. If not it will throw an error, which will be + //useful to know. + if (ieName) { + node.detachEvent(ieName, func); + } + } else { + node.removeEventListener(name, func, false); + } + } + + /** + * Given an event from a script node, get the requirejs info from it, + * and then removes the event listeners on the node. + * @param {Event} evt + * @returns {Object} + */ + function getScriptData(evt) { + //Using currentTarget instead of target for Firefox 2.0's sake. Not + //all old browsers will be supported, but this one was easy enough + //to support and still makes sense. + var node = evt.currentTarget || evt.srcElement; + + //Remove the listeners once here. + removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange'); + removeListener(node, context.onScriptError, 'error'); + + return { + node: node, + id: node && node.getAttribute('data-requiremodule') + }; + } + + function intakeDefines() { + var args; + + //Any defined modules in the global queue, intake them now. + takeGlobalQueue(); + + //Make sure any remaining defQueue items get properly processed. + while (defQueue.length) { + args = defQueue.shift(); + if (args[0] === null) { + return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1])); + } else { + //args are id, deps, factory. Should be normalized by the + //define() function. + callGetModule(args); + } + } + } + + context = { + config: config, + contextName: contextName, + registry: registry, + defined: defined, + urlFetched: urlFetched, + defQueue: defQueue, + Module: Module, + makeModuleMap: makeModuleMap, + nextTick: req.nextTick, + onError: onError, + + /** + * Set a configuration for the context. + * @param {Object} cfg config object to integrate. + */ + configure: function (cfg) { + //Make sure the baseUrl ends in a slash. + if (cfg.baseUrl) { + if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') { + cfg.baseUrl += '/'; + } + } + + //Save off the paths since they require special processing, + //they are additive. + var shim = config.shim, + objs = { + paths: true, + bundles: true, + config: true, + map: true + }; + + eachProp(cfg, function (value, prop) { + if (objs[prop]) { + if (!config[prop]) { + config[prop] = {}; + } + mixin(config[prop], value, true, true); + } else { + config[prop] = value; + } + }); + + //Reverse map the bundles + if (cfg.bundles) { + eachProp(cfg.bundles, function (value, prop) { + each(value, function (v) { + if (v !== prop) { + bundlesMap[v] = prop; + } + }); + }); + } + + //Merge shim + if (cfg.shim) { + eachProp(cfg.shim, function (value, id) { + //Normalize the structure + if (isArray(value)) { + value = { + deps: value + }; + } + if ((value.exports || value.init) && !value.exportsFn) { + value.exportsFn = context.makeShimExports(value); + } + shim[id] = value; + }); + config.shim = shim; + } + + //Adjust packages if necessary. + if (cfg.packages) { + each(cfg.packages, function (pkgObj) { + var location, name; + + pkgObj = typeof pkgObj === 'string' ? { name: pkgObj } : pkgObj; + + name = pkgObj.name; + location = pkgObj.location; + if (location) { + config.paths[name] = pkgObj.location; + } + + //Save pointer to main module ID for pkg name. + //Remove leading dot in main, so main paths are normalized, + //and remove any trailing .js, since different package + //envs have different conventions: some use a module name, + //some use a file name. + config.pkgs[name] = pkgObj.name + '/' + (pkgObj.main || 'main') + .replace(currDirRegExp, '') + .replace(jsSuffixRegExp, ''); + }); + } + + //If there are any "waiting to execute" modules in the registry, + //update the maps for them, since their info, like URLs to load, + //may have changed. + eachProp(registry, function (mod, id) { + //If module already has init called, since it is too + //late to modify them, and ignore unnormalized ones + //since they are transient. + if (!mod.inited && !mod.map.unnormalized) { + mod.map = makeModuleMap(id); + } + }); + + //If a deps array or a config callback is specified, then call + //require with those args. This is useful when require is defined as a + //config object before require.js is loaded. + if (cfg.deps || cfg.callback) { + context.require(cfg.deps || [], cfg.callback); + } + }, + + makeShimExports: function (value) { + function fn() { + var ret; + if (value.init) { + ret = value.init.apply(global, arguments); + } + return ret || (value.exports && getGlobal(value.exports)); + } + return fn; + }, + + makeRequire: function (relMap, options) { + options = options || {}; + + function localRequire(deps, callback, errback) { + var id, map, requireMod; + + if (options.enableBuildCallback && callback && isFunction(callback)) { + callback.__requireJsBuild = true; + } + + if (typeof deps === 'string') { + if (isFunction(callback)) { + //Invalid call + return onError(makeError('requireargs', 'Invalid require call'), errback); + } + + //If require|exports|module are requested, get the + //value for them from the special handlers. Caveat: + //this only works while module is being defined. + if (relMap && hasProp(handlers, deps)) { + return handlers[deps](registry[relMap.id]); + } + + //Synchronous access to one module. If require.get is + //available (as in the Node adapter), prefer that. + if (req.get) { + return req.get(context, deps, relMap, localRequire); + } + + //Normalize module name, if it contains . or .. + map = makeModuleMap(deps, relMap, false, true); + id = map.id; + + if (!hasProp(defined, id)) { + return onError(makeError('notloaded', 'Module name "' + + id + + '" has not been loaded yet for context: ' + + contextName + + (relMap ? '' : '. Use require([])'))); + } + return defined[id]; + } + + //Grab defines waiting in the global queue. + intakeDefines(); + + //Mark all the dependencies as needing to be loaded. + context.nextTick(function () { + //Some defines could have been added since the + //require call, collect them. + intakeDefines(); + + requireMod = getModule(makeModuleMap(null, relMap)); + + //Store if map config should be applied to this require + //call for dependencies. + requireMod.skipMap = options.skipMap; + + requireMod.init(deps, callback, errback, { + enabled: true + }); + + checkLoaded(); + }); + + return localRequire; + } + + mixin(localRequire, { + isBrowser: isBrowser, + + /** + * Converts a module name + .extension into an URL path. + * *Requires* the use of a module name. It does not support using + * plain URLs like nameToUrl. + */ + toUrl: function (moduleNamePlusExt) { + var ext, + index = moduleNamePlusExt.lastIndexOf('.'), + segment = moduleNamePlusExt.split('/')[0], + isRelative = segment === '.' || segment === '..'; + + //Have a file extension alias, and it is not the + //dots from a relative path. + if (index !== -1 && (!isRelative || index > 1)) { + ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length); + moduleNamePlusExt = moduleNamePlusExt.substring(0, index); + } + + return context.nameToUrl(normalize(moduleNamePlusExt, + relMap && relMap.id, true), ext, true); + }, + + defined: function (id) { + return hasProp(defined, makeModuleMap(id, relMap, false, true).id); + }, + + specified: function (id) { + id = makeModuleMap(id, relMap, false, true).id; + return hasProp(defined, id) || hasProp(registry, id); + } + }); + + //Only allow undef on top level require calls + if (!relMap) { + localRequire.undef = function (id) { + //Bind any waiting define() calls to this context, + //fix for #408 + takeGlobalQueue(); + + var map = makeModuleMap(id, relMap, true), + mod = getOwn(registry, id); + + removeScript(id); + + delete defined[id]; + delete urlFetched[map.url]; + delete undefEvents[id]; + + //Clean queued defines too. Go backwards + //in array so that the splices do not + //mess up the iteration. + eachReverse(defQueue, function(args, i) { + if(args[0] === id) { + defQueue.splice(i, 1); + } + }); + + if (mod) { + //Hold on to listeners in case the + //module will be attempted to be reloaded + //using a different config. + if (mod.events.defined) { + undefEvents[id] = mod.events; + } + + cleanRegistry(id); + } + }; + } + + return localRequire; + }, + + /** + * Called to enable a module if it is still in the registry + * awaiting enablement. A second arg, parent, the parent module, + * is passed in for context, when this method is overridden by + * the optimizer. Not shown here to keep code compact. + */ + enable: function (depMap) { + var mod = getOwn(registry, depMap.id); + if (mod) { + getModule(depMap).enable(); + } + }, + + /** + * Internal method used by environment adapters to complete a load event. + * A load event could be a script load or just a load pass from a synchronous + * load call. + * @param {String} moduleName the name of the module to potentially complete. + */ + completeLoad: function (moduleName) { + var found, args, mod, + shim = getOwn(config.shim, moduleName) || {}, + shExports = shim.exports; + + takeGlobalQueue(); + + while (defQueue.length) { + args = defQueue.shift(); + if (args[0] === null) { + args[0] = moduleName; + //If already found an anonymous module and bound it + //to this name, then this is some other anon module + //waiting for its completeLoad to fire. + if (found) { + break; + } + found = true; + } else if (args[0] === moduleName) { + //Found matching define call for this script! + found = true; + } + + callGetModule(args); + } + + //Do this after the cycle of callGetModule in case the result + //of those calls/init calls changes the registry. + mod = getOwn(registry, moduleName); + + if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) { + if (config.enforceDefine && (!shExports || !getGlobal(shExports))) { + if (hasPathFallback(moduleName)) { + return; + } else { + return onError(makeError('nodefine', + 'No define call for ' + moduleName, + null, + [moduleName])); + } + } else { + //A script that does not call define(), so just simulate + //the call for it. + callGetModule([moduleName, (shim.deps || []), shim.exportsFn]); + } + } + + checkLoaded(); + }, + + /** + * Converts a module name to a file path. Supports cases where + * moduleName may actually be just an URL. + * Note that it **does not** call normalize on the moduleName, + * it is assumed to have already been normalized. This is an + * internal API, not a public one. Use toUrl for the public API. + */ + nameToUrl: function (moduleName, ext, skipExt) { + var paths, syms, i, parentModule, url, + parentPath, bundleId, + pkgMain = getOwn(config.pkgs, moduleName); + + if (pkgMain) { + moduleName = pkgMain; + } + + bundleId = getOwn(bundlesMap, moduleName); + + if (bundleId) { + return context.nameToUrl(bundleId, ext, skipExt); + } + + //If a colon is in the URL, it indicates a protocol is used and it is just + //an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?) + //or ends with .js, then assume the user meant to use an url and not a module id. + //The slash is important for protocol-less URLs as well as full paths. + if (req.jsExtRegExp.test(moduleName)) { + //Just a plain path, not module name lookup, so just return it. + //Add extension if it is included. This is a bit wonky, only non-.js things pass + //an extension, this method probably needs to be reworked. + url = moduleName + (ext || ''); + } else { + //A module that needs to be converted to a path. + paths = config.paths; + + syms = moduleName.split('/'); + //For each module name segment, see if there is a path + //registered for it. Start with most specific name + //and work up from it. + for (i = syms.length; i > 0; i -= 1) { + parentModule = syms.slice(0, i).join('/'); + + parentPath = getOwn(paths, parentModule); + if (parentPath) { + //If an array, it means there are a few choices, + //Choose the one that is desired + if (isArray(parentPath)) { + parentPath = parentPath[0]; + } + syms.splice(0, i, parentPath); + break; + } + } + + //Join the path parts together, then figure out if baseUrl is needed. + url = syms.join('/'); + url += (ext || (/^data\:|\?/.test(url) || skipExt ? '' : '.js')); + url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url; + } + + return config.urlArgs ? url + + ((url.indexOf('?') === -1 ? '?' : '&') + + config.urlArgs) : url; + }, + + //Delegates to req.load. Broken out as a separate function to + //allow overriding in the optimizer. + load: function (id, url) { + req.load(context, id, url); + }, + + /** + * Executes a module callback function. Broken out as a separate function + * solely to allow the build system to sequence the files in the built + * layer in the right sequence. + * + * @private + */ + execCb: function (name, callback, args, exports) { + return callback.apply(exports, args); + }, + + /** + * callback for script loads, used to check status of loading. + * + * @param {Event} evt the event from the browser for the script + * that was loaded. + */ + onScriptLoad: function (evt) { + //Using currentTarget instead of target for Firefox 2.0's sake. Not + //all old browsers will be supported, but this one was easy enough + //to support and still makes sense. + if (evt.type === 'load' || + (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) { + //Reset interactive script so a script node is not held onto for + //to long. + interactiveScript = null; + + //Pull out the name of the module and the context. + var data = getScriptData(evt); + context.completeLoad(data.id); + } + }, + + /** + * Callback for script errors. + */ + onScriptError: function (evt) { + var data = getScriptData(evt); + if (!hasPathFallback(data.id)) { + return onError(makeError('scripterror', 'Script error for: ' + data.id, evt, [data.id])); + } + } + }; + + context.require = context.makeRequire(); + return context; + } + + /** + * Main entry point. + * + * If the only argument to require is a string, then the module that + * is represented by that string is fetched for the appropriate context. + * + * If the first argument is an array, then it will be treated as an array + * of dependency string names to fetch. An optional function callback can + * be specified to execute when all of those dependencies are available. + * + * Make a local req variable to help Caja compliance (it assumes things + * on a require that are not standardized), and to give a short + * name for minification/local scope use. + */ + req = requirejs = function (deps, callback, errback, optional) { + + //Find the right context, use default + var context, config, + contextName = defContextName; + + // Determine if have config object in the call. + if (!isArray(deps) && typeof deps !== 'string') { + // deps is a config object + config = deps; + if (isArray(callback)) { + // Adjust args if there are dependencies + deps = callback; + callback = errback; + errback = optional; + } else { + deps = []; + } + } + + if (config && config.context) { + contextName = config.context; + } + + context = getOwn(contexts, contextName); + if (!context) { + context = contexts[contextName] = req.s.newContext(contextName); + } + + if (config) { + context.configure(config); + } + + return context.require(deps, callback, errback); + }; + + /** + * Support require.config() to make it easier to cooperate with other + * AMD loaders on globally agreed names. + */ + req.config = function (config) { + return req(config); + }; + + /** + * Execute something after the current tick + * of the event loop. Override for other envs + * that have a better solution than setTimeout. + * @param {Function} fn function to execute later. + */ + req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) { + setTimeout(fn, 4); + } : function (fn) { fn(); }; + + /** + * Export require as a global, but only if it does not already exist. + */ + if (!require) { + require = req; + } + + req.version = version; + + //Used to filter out dependencies that are already paths. + req.jsExtRegExp = /^\/|:|\?|\.js$/; + req.isBrowser = isBrowser; + s = req.s = { + contexts: contexts, + newContext: newContext + }; + + //Create default context. + req({}); + + //Exports some context-sensitive methods on global require. + each([ + 'toUrl', + 'undef', + 'defined', + 'specified' + ], function (prop) { + //Reference from contexts instead of early binding to default context, + //so that during builds, the latest instance of the default context + //with its config gets used. + req[prop] = function () { + var ctx = contexts[defContextName]; + return ctx.require[prop].apply(ctx, arguments); + }; + }); + + if (isBrowser) { + head = s.head = document.getElementsByTagName('head')[0]; + //If BASE tag is in play, using appendChild is a problem for IE6. + //When that browser dies, this can be removed. Details in this jQuery bug: + //http://dev.jquery.com/ticket/2709 + baseElement = document.getElementsByTagName('base')[0]; + if (baseElement) { + head = s.head = baseElement.parentNode; + } + } + + /** + * Any errors that require explicitly generates will be passed to this + * function. Intercept/override it if you want custom error handling. + * @param {Error} err the error object. + */ + req.onError = defaultOnError; + + /** + * Creates the node for the load command. Only used in browser envs. + */ + req.createNode = function (config, moduleName, url) { + var node = config.xhtml ? + document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') : + document.createElement('script'); + node.type = config.scriptType || 'text/javascript'; + node.charset = 'utf-8'; + node.async = true; + return node; + }; + + /** + * Does the request to load a module for the browser case. + * Make this a separate function to allow other environments + * to override it. + * + * @param {Object} context the require context to find state. + * @param {String} moduleName the name of the module. + * @param {Object} url the URL to the module. + */ + req.load = function (context, moduleName, url) { + var config = (context && context.config) || {}, + node; + if (isBrowser) { + //In the browser so use a script tag + node = req.createNode(config, moduleName, url); + + node.setAttribute('data-requirecontext', context.contextName); + node.setAttribute('data-requiremodule', moduleName); + + //Set up load listener. Test attachEvent first because IE9 has + //a subtle issue in its addEventListener and script onload firings + //that do not match the behavior of all other browsers with + //addEventListener support, which fire the onload event for a + //script right after the script execution. See: + //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution + //UNFORTUNATELY Opera implements attachEvent but does not follow the script + //script execution mode. + if (node.attachEvent && + //Check if node.attachEvent is artificially added by custom script or + //natively supported by browser + //read https://github.com/jrburke/requirejs/issues/187 + //if we can NOT find [native code] then it must NOT natively supported. + //in IE8, node.attachEvent does not have toString() + //Note the test for "[native code" with no closing brace, see: + //https://github.com/jrburke/requirejs/issues/273 + !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) && + !isOpera) { + //Probably IE. IE (at least 6-8) do not fire + //script onload right after executing the script, so + //we cannot tie the anonymous define call to a name. + //However, IE reports the script as being in 'interactive' + //readyState at the time of the define call. + useInteractive = true; + + node.attachEvent('onreadystatechange', context.onScriptLoad); + //It would be great to add an error handler here to catch + //404s in IE9+. However, onreadystatechange will fire before + //the error handler, so that does not help. If addEventListener + //is used, then IE will fire error before load, but we cannot + //use that pathway given the connect.microsoft.com issue + //mentioned above about not doing the 'script execute, + //then fire the script load event listener before execute + //next script' that other browsers do. + //Best hope: IE10 fixes the issues, + //and then destroys all installs of IE 6-9. + //node.attachEvent('onerror', context.onScriptError); + } else { + node.addEventListener('load', context.onScriptLoad, false); + node.addEventListener('error', context.onScriptError, false); + } + node.src = url; + + //For some cache cases in IE 6-8, the script executes before the end + //of the appendChild execution, so to tie an anonymous define + //call to the module name (which is stored on the node), hold on + //to a reference to this node, but clear after the DOM insertion. + currentlyAddingScript = node; + if (baseElement) { + head.insertBefore(node, baseElement); + } else { + head.appendChild(node); + } + currentlyAddingScript = null; + + return node; + } else if (isWebWorker) { + try { + //In a web worker, use importScripts. This is not a very + //efficient use of importScripts, importScripts will block until + //its script is downloaded and evaluated. However, if web workers + //are in play, the expectation that a build has been done so that + //only one script needs to be loaded anyway. This may need to be + //reevaluated if other use cases become common. + importScripts(url); + + //Account for anonymous modules + context.completeLoad(moduleName); + } catch (e) { + context.onError(makeError('importscripts', + 'importScripts failed for ' + + moduleName + ' at ' + url, + e, + [moduleName])); + } + } + }; + + function getInteractiveScript() { + if (interactiveScript && interactiveScript.readyState === 'interactive') { + return interactiveScript; + } + + eachReverse(scripts(), function (script) { + if (script.readyState === 'interactive') { + return (interactiveScript = script); + } + }); + return interactiveScript; + } + + //Look for a data-main script attribute, which could also adjust the baseUrl. + if (isBrowser && !cfg.skipDataMain) { + //Figure out baseUrl. Get it from the script tag with require.js in it. + eachReverse(scripts(), function (script) { + //Set the 'head' where we can append children by + //using the script's parent. + if (!head) { + head = script.parentNode; + } + + //Look for a data-main attribute to set main script for the page + //to load. If it is there, the path to data main becomes the + //baseUrl, if it is not already set. + dataMain = script.getAttribute('data-main'); + if (dataMain) { + //Preserve dataMain in case it is a path (i.e. contains '?') + mainScript = dataMain; + + //Set final baseUrl if there is not already an explicit one. + if (!cfg.baseUrl) { + //Pull off the directory of data-main for use as the + //baseUrl. + src = mainScript.split('/'); + mainScript = src.pop(); + subPath = src.length ? src.join('/') + '/' : './'; + + cfg.baseUrl = subPath; + } + + //Strip off any trailing .js since mainScript is now + //like a module name. + mainScript = mainScript.replace(jsSuffixRegExp, ''); + + //If mainScript is still a path, fall back to dataMain + if (req.jsExtRegExp.test(mainScript)) { + mainScript = dataMain; + } + + //Put the data-main script in the files to load. + cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript]; + + return true; + } + }); + } + + /** + * The function that handles definitions of modules. Differs from + * require() in that a string for the module should be the first argument, + * and the function to execute after dependencies are loaded should + * return a value to define the module corresponding to the first argument's + * name. + */ + define = function (name, deps, callback) { + var node, context; + + //Allow for anonymous modules + if (typeof name !== 'string') { + //Adjust args appropriately + callback = deps; + deps = name; + name = null; + } + + //This module may not have dependencies + if (!isArray(deps)) { + callback = deps; + deps = null; + } + + //If no name, and callback is a function, then figure out if it a + //CommonJS thing with dependencies. + if (!deps && isFunction(callback)) { + deps = []; + //Remove comments from the callback string, + //look for require calls, and pull them into the dependencies, + //but only if there are function args. + if (callback.length) { + callback + .toString() + .replace(commentRegExp, '') + .replace(cjsRequireRegExp, function (match, dep) { + deps.push(dep); + }); + + //May be a CommonJS thing even without require calls, but still + //could use exports, and module. Avoid doing exports and module + //work though if it just needs require. + //REQUIRES the function to expect the CommonJS variables in the + //order listed below. + deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps); + } + } + + //If in IE 6-8 and hit an anonymous define() call, do the interactive + //work. + if (useInteractive) { + node = currentlyAddingScript || getInteractiveScript(); + if (node) { + if (!name) { + name = node.getAttribute('data-requiremodule'); + } + context = contexts[node.getAttribute('data-requirecontext')]; + } + } + + //Always save off evaluating the def call until the script onload handler. + //This allows multiple modules to be in a file without prematurely + //tracing dependencies, and allows for anonymous module support, + //where the module name is not known until the script onload event + //occurs. If no context, use the global queue, and get it processed + //in the onscript load callback. + (context ? context.defQueue : globalDefQueue).push([name, deps, callback]); + }; + + define.amd = { + jQuery: true + }; + + + /** + * Executes the text. Normally just uses eval, but can be modified + * to use a better, environment-specific call. Only used for transpiling + * loader plugins, not for plain JS modules. + * @param {String} text the text to execute/evaluate. + */ + req.exec = function (text) { + /*jslint evil: true */ + return eval(text); + }; + + //Set up with config info. + req(cfg); +}(this)); diff --git a/lib/responsivevoice.js b/lib/responsivevoice.js new file mode 100644 index 0000000000..41166a74fd --- /dev/null +++ b/lib/responsivevoice.js @@ -0,0 +1,67 @@ +/* + ResponsiveVoice JS v1.4.7 + + (c) 2015 LearnBrite + + License: http://responsivevoice.org/license +*/ +if("undefined"!=typeof responsiveVoice)console.log("ResponsiveVoice already loaded"),console.log(responsiveVoice);else var ResponsiveVoice=function(){var a=this;a.version="1.4.7";console.log("ResponsiveVoice r"+a.version);a.responsivevoices=[{name:"UK English Female",flag:"gb",gender:"f",voiceIDs:[3,5,1,6,7,171,201,8]},{name:"UK English Male",flag:"gb",gender:"m",voiceIDs:[0,4,2,75,202,159,6,7]},{name:"US English Female",flag:"us",gender:"f",voiceIDs:[39,40,41,42,43,173,205,204,44]},{name:"Arabic Male", +flag:"ar",gender:"m",voiceIDs:[96,95,97,196,98],deprecated:!0},{name:"Arabic Female",flag:"ar",gender:"f",voiceIDs:[96,95,97,196,98]},{name:"Armenian Male",flag:"hy",gender:"f",voiceIDs:[99]},{name:"Australian Female",flag:"au",gender:"f",voiceIDs:[87,86,5,201,88]},{name:"Brazilian Portuguese Female",flag:"br",gender:"f",voiceIDs:[124,123,125,186,223,126]},{name:"Chinese Female",flag:"cn",gender:"f",voiceIDs:[58,59,60,155,191,231,61]},{name:"Czech Female",flag:"cz",gender:"f",voiceIDs:[101,100,102, +197,103]},{name:"Danish Female",flag:"dk",gender:"f",voiceIDs:[105,104,106,198,107]},{name:"Deutsch Female",flag:"de",gender:"f",voiceIDs:[27,28,29,30,31,78,170,199,32]},{name:"Dutch Female",flag:"nl",gender:"f",voiceIDs:[219,84,157,158,184,45]},{name:"Finnish Female",flag:"fi",gender:"f",voiceIDs:[90,89,91,209,92]},{name:"French Female",flag:"fr",gender:"f",voiceIDs:[21,22,23,77,178,210,26]},{name:"Greek Female",flag:"gr",gender:"f",voiceIDs:[62,63,80,200,64]},{name:"Hatian Creole Female",flag:"ht", +gender:"f",voiceIDs:[109]},{name:"Hindi Female",flag:"hi",gender:"f",voiceIDs:[66,154,179,213,67]},{name:"Hungarian Female",flag:"hu",gender:"f",voiceIDs:[9,10,81,214,11]},{name:"Indonesian Female",flag:"id",gender:"f",voiceIDs:[111,112,180,215,113]},{name:"Italian Female",flag:"it",gender:"f",voiceIDs:[33,34,35,36,37,79,181,216,38]},{name:"Japanese Female",flag:"jp",gender:"f",voiceIDs:[50,51,52,153,182,217,53]},{name:"Korean Female",flag:"kr",gender:"f",voiceIDs:[54,55,56,156,183,218,57]},{name:"Latin Female", +flag:"va",gender:"f",voiceIDs:[114]},{name:"Norwegian Female",flag:"no",gender:"f",voiceIDs:[72,73,221,74]},{name:"Polish Female",flag:"pl",gender:"f",voiceIDs:[120,119,121,185,222,122]},{name:"Portuguese Female",flag:"br",gender:"f",voiceIDs:[128,127,129,187,224,130]},{name:"Romanian Male",flag:"ro",gender:"m",voiceIDs:[151,150,152,225,46]},{name:"Russian Female",flag:"ru",gender:"f",voiceIDs:[47,48,83,188,226,49]},{name:"Slovak Female",flag:"sk",gender:"f",voiceIDs:[133,132,134,227,135]},{name:"Spanish Female", +flag:"es",gender:"f",voiceIDs:[19,16,17,18,20,76,174,207,15]},{name:"Spanish Latin American Female",flag:"es",gender:"f",voiceIDs:[137,136,138,175,208,139]},{name:"Swedish Female",flag:"sv",gender:"f",voiceIDs:[85,148,149,228,65]},{name:"Tamil Male",flag:"hi",gender:"m",voiceIDs:[141]},{name:"Thai Female",flag:"th",gender:"f",voiceIDs:[143,142,144,189,229,145]},{name:"Turkish Female",flag:"tr",gender:"f",voiceIDs:[69,70,82,190,230,71]},{name:"Afrikaans Male",flag:"af",gender:"m",voiceIDs:[93]},{name:"Albanian Male", +flag:"sq",gender:"m",voiceIDs:[94]},{name:"Bosnian Male",flag:"bs",gender:"m",voiceIDs:[14]},{name:"Catalan Male",flag:"catalonia",gender:"m",voiceIDs:[68]},{name:"Croatian Male",flag:"hr",gender:"m",voiceIDs:[13]},{name:"Czech Male",flag:"cz",gender:"m",voiceIDs:[161]},{name:"Danish Male",flag:"da",gender:"m",voiceIDs:[162],deprecated:!0},{name:"Esperanto Male",flag:"eo",gender:"m",voiceIDs:[108]},{name:"Finnish Male",flag:"fi",gender:"m",voiceIDs:[160],deprecated:!0},{name:"Greek Male",flag:"gr", +gender:"m",voiceIDs:[163],deprecated:!0},{name:"Hungarian Male",flag:"hu",gender:"m",voiceIDs:[164]},{name:"Icelandic Male",flag:"is",gender:"m",voiceIDs:[110]},{name:"Latin Male",flag:"va",gender:"m",voiceIDs:[165],deprecated:!0},{name:"Latvian Male",flag:"lv",gender:"m",voiceIDs:[115]},{name:"Macedonian Male",flag:"mk",gender:"m",voiceIDs:[116]},{name:"Moldavian Male",flag:"md",gender:"m",voiceIDs:[117]},{name:"Montenegrin Male",flag:"me",gender:"m",voiceIDs:[118]},{name:"Norwegian Male",flag:"no", +gender:"m",voiceIDs:[166]},{name:"Serbian Male",flag:"sr",gender:"m",voiceIDs:[12]},{name:"Serbo-Croatian Male",flag:"hr",gender:"m",voiceIDs:[131]},{name:"Slovak Male",flag:"sk",gender:"m",voiceIDs:[167],deprecated:!0},{name:"Swahili Male",flag:"sw",gender:"m",voiceIDs:[140]},{name:"Swedish Male",flag:"sv",gender:"m",voiceIDs:[168],deprecated:!0},{name:"Vietnamese Male",flag:"vi",gender:"m",voiceIDs:[146],deprecated:!0},{name:"Welsh Male",flag:"cy",gender:"m",voiceIDs:[147]},{name:"US English Male", +flag:"us",gender:"m",voiceIDs:[0,4,2,6,7,75,159]},{name:"Fallback UK Female",flag:"gb",gender:"f",voiceIDs:[8]}];a.voicecollection=[{name:"Google UK English Male"},{name:"Agnes"},{name:"Daniel Compact"},{name:"Google UK English Female"},{name:"en-GB",rate:.25,pitch:1},{name:"en-AU",rate:.25,pitch:1},{name:"ingl\u00e9s Reino Unido"},{name:"English United Kingdom"},{name:"Fallback en-GB Female",lang:"en-GB",fallbackvoice:!0},{name:"Eszter Compact"},{name:"hu-HU",rate:.4},{name:"Fallback Hungarian", +lang:"hu",fallbackvoice:!0,service:"g2"},{name:"Fallback Serbian",lang:"sr",fallbackvoice:!0},{name:"Fallback Croatian",lang:"hr",fallbackvoice:!0},{name:"Fallback Bosnian",lang:"bs",fallbackvoice:!0},{name:"Fallback Spanish",lang:"es",fallbackvoice:!0},{name:"Spanish Spain"},{name:"espa\u00f1ol Espa\u00f1a"},{name:"Diego Compact",rate:.3},{name:"Google Espa\u00f1ol"},{name:"es-ES",rate:.2},{name:"Google Fran\u00e7ais"},{name:"French France"},{name:"franc\u00e9s Francia"},{name:"Virginie Compact", +rate:.5},{name:"fr-FR",rate:.25},{name:"Fallback French",lang:"fr",fallbackvoice:!0},{name:"Google Deutsch"},{name:"German Germany"},{name:"alem\u00e1n Alemania"},{name:"Yannick Compact",rate:.5},{name:"de-DE",rate:.25},{name:"Fallback Deutsch",lang:"de",fallbackvoice:!0},{name:"Google Italiano"},{name:"Italian Italy"},{name:"italiano Italia"},{name:"Paolo Compact",rate:.5},{name:"it-IT",rate:.25},{name:"Fallback Italian",lang:"it",fallbackvoice:!0},{name:"Google US English",timerSpeed:1},{name:"English United States"}, +{name:"ingl\u00e9s Estados Unidos"},{name:"Vicki"},{name:"en-US",rate:.2,pitch:1,timerSpeed:1.3},{name:"Fallback English",lang:"en-US",fallbackvoice:!0,timerSpeed:0},{name:"Fallback Dutch",lang:"nl",fallbackvoice:!0,timerSpeed:0},{name:"Fallback Romanian",lang:"ro",fallbackvoice:!0},{name:"Milena Compact"},{name:"ru-RU",rate:.25},{name:"Fallback Russian",lang:"ru",fallbackvoice:!0},{name:"Google \u65e5\u672c\u4eba",timerSpeed:1},{name:"Kyoko Compact"},{name:"ja-JP",rate:.25},{name:"Fallback Japanese", +lang:"ja",fallbackvoice:!0},{name:"Google \ud55c\uad6d\uc758",timerSpeed:1},{name:"Narae Compact"},{name:"ko-KR",rate:.25},{name:"Fallback Korean",lang:"ko",fallbackvoice:!0},{name:"Google \u4e2d\u56fd\u7684",timerSpeed:1},{name:"Ting-Ting Compact"},{name:"zh-CN",rate:.25},{name:"Fallback Chinese",lang:"zh-CN",fallbackvoice:!0},{name:"Alexandros Compact"},{name:"el-GR",rate:.25},{name:"Fallback Greek",lang:"el",fallbackvoice:!0,service:"g2"},{name:"Fallback Swedish",lang:"sv",fallbackvoice:!0,service:"g2"}, +{name:"hi-IN",rate:.25},{name:"Fallback Hindi",lang:"hi",fallbackvoice:!0},{name:"Fallback Catalan",lang:"ca",fallbackvoice:!0},{name:"Aylin Compact"},{name:"tr-TR",rate:.25},{name:"Fallback Turkish",lang:"tr",fallbackvoice:!0},{name:"Stine Compact"},{name:"no-NO",rate:.25},{name:"Fallback Norwegian",lang:"no",fallbackvoice:!0,service:"g2"},{name:"Daniel"},{name:"Monica"},{name:"Amelie"},{name:"Anna"},{name:"Alice"},{name:"Melina"},{name:"Mariska"},{name:"Yelda"},{name:"Milena"},{name:"Xander"},{name:"Alva"}, +{name:"Lee Compact"},{name:"Karen"},{name:"Fallback Australian",lang:"en-AU",fallbackvoice:!0},{name:"Mikko Compact"},{name:"Satu"},{name:"fi-FI",rate:.25},{name:"Fallback Finnish",lang:"fi",fallbackvoice:!0,service:"g2"},{name:"Fallback Afrikans",lang:"af",fallbackvoice:!0},{name:"Fallback Albanian",lang:"sq",fallbackvoice:!0},{name:"Maged Compact"},{name:"Tarik"},{name:"ar-SA",rate:.25},{name:"Fallback Arabic",lang:"ar",fallbackvoice:!0,service:"g2"},{name:"Fallback Armenian",lang:"hy",fallbackvoice:!0, +service:"g2"},{name:"Zuzana Compact"},{name:"Zuzana"},{name:"cs-CZ",rate:.25},{name:"Fallback Czech",lang:"cs",fallbackvoice:!0,service:"g2"},{name:"Ida Compact"},{name:"Sara"},{name:"da-DK",rate:.25},{name:"Fallback Danish",lang:"da",fallbackvoice:!0,service:"g2"},{name:"Fallback Esperanto",lang:"eo",fallbackvoice:!0},{name:"Fallback Hatian Creole",lang:"ht",fallbackvoice:!0},{name:"Fallback Icelandic",lang:"is",fallbackvoice:!0},{name:"Damayanti"},{name:"id-ID",rate:.25},{name:"Fallback Indonesian", +lang:"id",fallbackvoice:!0},{name:"Fallback Latin",lang:"la",fallbackvoice:!0,service:"g2"},{name:"Fallback Latvian",lang:"lv",fallbackvoice:!0},{name:"Fallback Macedonian",lang:"mk",fallbackvoice:!0},{name:"Fallback Moldavian",lang:"mo",fallbackvoice:!0,service:"g2"},{name:"Fallback Montenegrin",lang:"sr-ME",fallbackvoice:!0},{name:"Agata Compact"},{name:"Zosia"},{name:"pl-PL",rate:.25},{name:"Fallback Polish",lang:"pl",fallbackvoice:!0},{name:"Raquel Compact"},{name:"Luciana"},{name:"pt-BR",rate:.25}, +{name:"Fallback Brazilian Portugese",lang:"pt-BR",fallbackvoice:!0,service:"g2"},{name:"Joana Compact"},{name:"Joana"},{name:"pt-PT",rate:.25},{name:"Fallback Portuguese",lang:"pt-PT",fallbackvoice:!0},{name:"Fallback Serbo-Croation",lang:"sh",fallbackvoice:!0,service:"g2"},{name:"Laura Compact"},{name:"Laura"},{name:"sk-SK",rate:.25},{name:"Fallback Slovak",lang:"sk",fallbackvoice:!0,service:"g2"},{name:"Javier Compact"},{name:"Paulina"},{name:"es-MX",rate:.25},{name:"Fallback Spanish (Latin American)", +lang:"es-419",fallbackvoice:!0,service:"g2"},{name:"Fallback Swahili",lang:"sw",fallbackvoice:!0},{name:"Fallback Tamil",lang:"ta",fallbackvoice:!0},{name:"Narisa Compact"},{name:"Kanya"},{name:"th-TH",rate:.25},{name:"Fallback Thai",lang:"th",fallbackvoice:!0},{name:"Fallback Vietnamese",lang:"vi",fallbackvoice:!0},{name:"Fallback Welsh",lang:"cy",fallbackvoice:!0},{name:"Oskar Compact"},{name:"sv-SE",rate:.25},{name:"Simona Compact"},{name:"Ioana"},{name:"ro-RO",rate:.25},{name:"Kyoko"},{name:"Lekha"}, +{name:"Ting-Ting"},{name:"Yuna"},{name:"Xander Compact"},{name:"nl-NL",rate:.25},{name:"Fallback UK English Male",lang:"en-GB",fallbackvoice:!0,service:"g1",voicename:"rjs"},{name:"Finnish Male",lang:"fi",fallbackvoice:!0,service:"g1",voicename:""},{name:"Czech Male",lang:"cs",fallbackvoice:!0,service:"g1",voicename:""},{name:"Danish Male",lang:"da",fallbackvoice:!0,service:"g1",voicename:""},{name:"Greek Male",lang:"el",fallbackvoice:!0,service:"g1",voicename:"",rate:.25},{name:"Hungarian Male", +lang:"hu",fallbackvoice:!0,service:"g1",voicename:""},{name:"Latin Male",lang:"la",fallbackvoice:!0,service:"g1",voicename:""},{name:"Norwegian Male",lang:"no",fallbackvoice:!0,service:"g1",voicename:""},{name:"Slovak Male",lang:"sk",fallbackvoice:!0,service:"g1",voicename:""},{name:"Swedish Male",lang:"sv",fallbackvoice:!0,service:"g1",voicename:""},{name:"Fallback US English Male",lang:"en",fallbackvoice:!0,service:"tts-api",voicename:""},{name:"German Germany",lang:"de_DE"},{name:"English United Kingdom", +lang:"en_GB"},{name:"English India",lang:"en_IN"},{name:"English United States",lang:"en_US"},{name:"Spanish Spain",lang:"es_ES"},{name:"Spanish Mexico",lang:"es_MX"},{name:"Spanish United States",lang:"es_US"},{name:"French Belgium",lang:"fr_BE"},{name:"French France",lang:"fr_FR"},{name:"Hindi India",lang:"hi_IN"},{name:"Indonesian Indonesia",lang:"in_ID"},{name:"Italian Italy",lang:"it_IT"},{name:"Japanese Japan",lang:"ja_JP"},{name:"Korean South Korea",lang:"ko_KR"},{name:"Dutch Netherlands", +lang:"nl_NL"},{name:"Polish Poland",lang:"pl_PL"},{name:"Portuguese Brazil",lang:"pt_BR"},{name:"Portuguese Portugal",lang:"pt_PT"},{name:"Russian Russia",lang:"ru_RU"},{name:"Thai Thailand",lang:"th_TH"},{name:"Turkish Turkey",lang:"tr_TR"},{name:"Chinese China",lang:"zh_CN_#Hans"},{name:"Chinese Hong Kong",lang:"zh_HK_#Hans"},{name:"Chinese Hong Kong",lang:"zh_HK_#Hant"},{name:"Chinese Taiwan",lang:"zh_TW_#Hant"},{name:"Alex"},{name:"Maged",lang:"ar-SA"},{name:"Zuzana",lang:"cs-CZ"},{name:"Sara", +lang:"da-DK"},{name:"Anna",lang:"de-DE"},{name:"Melina",lang:"el-GR"},{name:"Karen",lang:"en-AU"},{name:"Daniel",lang:"en-GB"},{name:"Moira",lang:"en-IE"},{name:"Samantha (Enhanced)",lang:"en-US"},{name:"Samantha",lang:"en-US"},{name:"Tessa",lang:"en-ZA"},{name:"Monica",lang:"es-ES"},{name:"Paulina",lang:"es-MX"},{name:"Satu",lang:"fi-FI"},{name:"Amelie",lang:"fr-CA"},{name:"Thomas",lang:"fr-FR"},{name:"Carmit",lang:"he-IL"},{name:"Lekha",lang:"hi-IN"},{name:"Mariska",lang:"hu-HU"},{name:"Damayanti", +lang:"id-ID"},{name:"Alice",lang:"it-IT"},{name:"Kyoko",lang:"ja-JP"},{name:"Yuna",lang:"ko-KR"},{name:"Ellen",lang:"nl-BE"},{name:"Xander",lang:"nl-NL"},{name:"Nora",lang:"no-NO"},{name:"Zosia",lang:"pl-PL"},{name:"Luciana",lang:"pt-BR"},{name:"Joana",lang:"pt-PT"},{name:"Ioana",lang:"ro-RO"},{name:"Milena",lang:"ru-RU"},{name:"Laura",lang:"sk-SK"},{name:"Alva",lang:"sv-SE"},{name:"Kanya",lang:"th-TH"},{name:"Yelda",lang:"tr-TR"},{name:"Ting-Ting",lang:"zh-CN"},{name:"Sin-Ji",lang:"zh-HK"},{name:"Mei-Jia", +lang:"zh-TW"}];a.iOS=/(iPad|iPhone|iPod)/g.test(navigator.userAgent);a.iOS9=/(iphone|ipod|ipad).* os 9_/.test(navigator.userAgent.toLowerCase());a.is_chrome=-1a.VOICESUPPORT_ATTEMPTLIMIT&&(clearInterval(b),null!=window.speechSynthesis?a.iOS?(a.iOS9?a.systemVoicesReady(a.cache_ios9_voices):a.systemVoicesReady(a.cache_ios_voices),console.log("RV: Voice support ready (cached)")):(console.log("RV: speechSynthesis present but no system voices found"),a.enableFallbackMode()):a.enableFallbackMode()))},100)},100);a.Dispatch("OnLoad")};a.systemVoicesReady=function(b){a.systemvoices=b;a.mapRVs();null!=a.OnVoiceReady&& +a.OnVoiceReady.call();a.Dispatch("OnReady");window.hasOwnProperty("dispatchEvent")&&window.dispatchEvent(new Event("ResponsiveVoice_OnReady"))};a.enableFallbackMode=function(){a.fallbackMode=!0;console.log("RV: Enabling fallback mode");a.mapRVs();null!=a.OnVoiceReady&&a.OnVoiceReady.call();a.Dispatch("OnReady");window.hasOwnProperty("dispatchEvent")&&window.dispatchEvent(new Event("ResponsiveVoice_OnReady"))};a.getVoices=function(){for(var b=[],c=0;ca.CHARACTER_LIMIT){for(var e=b;e.length>a.CHARACTER_LIMIT;){var g= +e.search(/[:!?.;]+/),d="";if(-1==g||g>=a.CHARACTER_LIMIT)g=e.search(/[,]+/);-1==g&&-1==e.search(" ")&&(g=99);if(-1==g||g>=a.CHARACTER_LIMIT)for(var k=e.split(" "),g=0;ga.CHARACTER_LIMIT);g++)d+=(0!=g?" ":"")+k[g];else d=e.substr(0,g+1);e=e.substr(d.length,e.length-d.length);h.push(d)}0=f)){var h=b.split(/\s+/).length,e=(b.match(/[^ ]/igm)||b).length,f=60/a.WORDS_PER_MINUTE*f*1E3*(e/h/5.1)*h;3>h&&(f=4E3);3E3>f&&(f=3E3);a.timeoutId=setTimeout(c,f)}};a.checkAndCancelTimeout=function(){null!=a.timeoutId&&(clearTimeout(a.timeoutId),a.timeoutId=null)};a.speech_timedout=function(){a.cancel();a.cancelled=!1;a.speech_onend()};a.speech_onend=function(){a.checkAndCancelTimeout(); +!0===a.cancelled?a.cancelled=!1:null!=a.msgparameters&&null!=a.msgparameters.onend&&1!=a.msgparameters.onendcalled&&(a.msgparameters.onendcalled=!0,a.msgparameters.onend())};a.speech_onstart=function(){if(!a.onstartFired){a.onstartFired=!0;if(a.iOS||a.is_safari||a.useTimer)a.fallbackMode||a.startTimeout(a.msgtext,a.speech_timedout);a.msgparameters.onendcalled=!1;if(null!=a.msgparameters&&null!=a.msgparameters.onstart)a.msgparameters.onstart()}};a.fallback_startPart=function(){0==a.fallback_part_index&& +a.speech_onstart();a.fallback_audio=a.fallback_parts[a.fallback_part_index];if(null==a.fallback_audio)console.log("RV: Fallback Audio is not available");else{var b=a.fallback_audio;a.fallback_audiopool.push(b);setTimeout(function(){b.playbackRate=a.fallback_playbackrate},50);b.onloadedmetadata=function(){b.play();b.playbackRate=a.fallback_playbackrate};a.fallback_audio.play();a.fallback_audio.addEventListener("ended",a.fallback_finishPart);a.useTimer&&a.startTimeout(a.multipartText[a.fallback_part_index], +a.fallback_finishPart)}};a.fallback_finishPart=function(b){a.checkAndCancelTimeout();a.fallback_part_indexa[e])&&clearTimeout(a[h])},50));return!1};a.AddEventListener=function(b,c){a.hasOwnProperty(b+"_callbacks")||(a[b+"_callbacks"]=[]);a[b+"_callbacks"].push(c)};a.addEventListener=a.AddEventListener;a.clickEvent=function(){if(a.iOS&& +!a.iOS_initialized){console.log("Initializing iOS click event");var b=new SpeechSynthesisUtterance(" ");speechSynthesis.speak(b);a.iOS_initialized=!0}};a.isPlaying=function(){return a.fallbackMode?null!=a.fallback_audio&&!a.fallback_audio.ended&&!a.fallback_audio.paused:speechSynthesis.speaking};a.clearFallbackPool=function(){for(var b=0;b
-
-
-
-
-
-
- {editor} + {this.state.showSolutionArea &&
+ {editor} +
}
; }, + getEditorInAnswerArea: function() { + if (this.refs !== undefined) { + return this.refs.editor; + } else { + return undefined; + } + }, + toJSON: function(skipValidation) { // Could be just _.pick(this.props, "type", "options"); but validation! + var editor = this.getEditorInAnswerArea(); return { type: this.props.type, - options: this.refs.editor.toJSON(skipValidation), + options: editor !== undefined ? this.refs.editor.toJSON(skipValidation) : {}, calculator: this.props.calculator }; } diff --git a/src/answer-area-renderer.jsx b/src/answer-area-renderer.jsx index 22ed1af3a7..bee53ec763 100644 --- a/src/answer-area-renderer.jsx +++ b/src/answer-area-renderer.jsx @@ -329,6 +329,32 @@ var AnswerAreaRenderer = React.createClass({ this.refs.widget.focus(); }, + showGuess: function(answerData) { + if( !answerData ) + return; + if (answerData instanceof Array) { + // Answer area contains no widgets. + } else if (this.refs.widget.setAnswerFromJSON === undefined) { + // Target widget cannot show answer. + console.log("Target widget cannot show in answerarea",answerData); + return 'no setAnswerFromJSON implemented for widgets in answer area.'; + } else { + console.log("Target widget show in answerarea") + // Just show the given answer. + this.refs.widget.setAnswerFromJSON(answerData); + } + }, + + canShowAllHistoryWidgets: function(answerData) { + if(!answerData) + return true; + if (this.refs.widget.setAnswerFromJSON === undefined) { + console.log('no setAnswerFromJSON implemented for widgets in answer area.'); + return false; + } + return true; + }, + guessAndScore: function() { // TODO(alpert): These should probably have the same signature... if (this.props.type === "multiple") { diff --git a/src/components/graph-settings.jsx b/src/components/graph-settings.jsx index ed217fbf59..2ba60b6d96 100644 --- a/src/components/graph-settings.jsx +++ b/src/components/graph-settings.jsx @@ -9,6 +9,7 @@ var NumberInput = require("../components/number-input.jsx"); var PropCheckBox = require("../components/prop-check-box.jsx"); var RangeInput = require("../components/range-input.jsx"); var Util = require("../util.js"); +var BlurInput = require("react-components/blur-input"); var defaultBoxSize = 400; var defaultBackgroundImage = { @@ -44,7 +45,7 @@ var GraphSettings = React.createClass({ step: [1, 1], gridStep: [1, 1], snapStep: Util.snapStepFromGridStep( - this.props.gridStep || [1, 1]), + this.gridStep || [1, 1]), valid: true, backgroundImage: defaultBackgroundImage, markings: "graph", @@ -59,14 +60,14 @@ var GraphSettings = React.createClass({ return
-
x Label +
x軸標籤
-
y Label +
y軸標籤
- x Range + x軸範圍
- y Range + y軸範圍
- Tick Step + 座標間距
- Grid Step + 網格間距
- Snap Step + 答案拖拉間距
- +
-
Background image:
+
背景圖:
Url:{' '} - + -

Create an image in graphie, or use the "Add image" - function to create a background.

+

請在圖形中增加圖片,或於欄中輸入圖片連結。

{this.props.backgroundImage.url &&
@@ -171,25 +167,25 @@ var GraphSettings = React.createClass({ {this.props.showRuler &&