├── e2e_test ├── Config.js ├── pages │ ├── Page.js │ ├── ResponsePage.js │ └── QuestionEditorPage.js ├── specs │ ├── questions │ │ ├── SingleTextQuestion.js │ │ ├── SelectQuestion.js │ │ └── RadioQuestion.js │ ├── JavaScriptEvent.js │ └── Node.js └── clearRequireCache.js ├── docs ├── images │ ├── delete.json │ ├── save.json │ └── images.json ├── survey-designer-js │ └── glyphicons-halflings-regular-e18bbf611f2a2e43afc071aa2f4e1512.ttf ├── survey │ └── init.json ├── image.html ├── detail.html ├── preview.html ├── index.html └── edit.html ├── .babelrc ├── lib ├── constants │ ├── parsleyConstants.js │ ├── dnd.js │ ├── editor.js │ ├── NumberValidationRuleConstants.js │ ├── ItemVisibility.js │ ├── states.js │ ├── prefectures.js │ ├── HelpMessages.js │ └── personalInfoFields.js ├── runtime │ ├── components │ │ ├── questions │ │ │ ├── RadioQuestion.js │ │ │ ├── CheckboxQuestion.js │ │ │ ├── ScreeningAgreementQuestion.js │ │ │ ├── DescriptionQuestion.js │ │ │ ├── Questions.js │ │ │ ├── TextQuestion.js │ │ │ ├── SingleTextQuestion.js │ │ │ └── SelectQuestion.js │ │ ├── plain │ │ │ ├── SingleTextQuestionJS.js │ │ │ ├── NumericInput.js │ │ │ ├── TextQuestionJS.js │ │ │ ├── ZeroSetting.js │ │ │ ├── SelectQuestionJS.js │ │ │ └── QuestionWithItemBaseJS.js │ │ └── parts │ │ │ ├── CommonQuestionParts.js │ │ │ ├── Value.js │ │ │ ├── PhotoSwipePart.js │ │ │ └── PageDetail.js │ ├── store.js │ ├── models │ │ ├── survey │ │ │ ├── questions │ │ │ │ ├── internal │ │ │ │ │ ├── ChoiceDefinition.js │ │ │ │ │ ├── PersonalInfoItemFieldDefinition.js │ │ │ │ │ ├── PersonalInfoItemDisplayTypeDefinition.js │ │ │ │ │ ├── NumberValidationDefinition.js │ │ │ │ │ ├── NumberValidationRuleDefinition.js │ │ │ │ │ └── OutputDefinition.js │ │ │ │ ├── DescriptionQuestionDefinition.js │ │ │ │ ├── TextQuestionDefinition.js │ │ │ │ ├── SingleTextQuestionDefinition.js │ │ │ │ ├── SelectQuestionDefinition.js │ │ │ │ ├── ScreeningAgreementQuestionDefinition.js │ │ │ │ └── QuestionDefinitions.js │ │ │ ├── NodeDefinition.js │ │ │ ├── FinisherDefinition.js │ │ │ ├── ConditionDefinition.js │ │ │ ├── ChildConditionDefinition.js │ │ │ └── LogicalVariableDefinition.js │ │ ├── ValidationErrorDefinition.js │ │ ├── migrator │ │ │ ├── 20171018000000_migratePersonalInfoQuestion.js │ │ │ ├── 20170921104000_migrateScheduleSubItems.js │ │ │ └── 20171002000000_migrateCssUrls.js │ │ ├── options │ │ │ └── CssOption.js │ │ └── view │ │ │ └── ViewSetting.js │ ├── reducers.js │ ├── SurveyDevIdGenerator.js │ ├── SurveyManager.js │ ├── css │ │ └── common.scss │ ├── actions.js │ └── PageManager.js ├── browserUtils.js ├── editor │ ├── components │ │ ├── question_editors │ │ │ ├── ScreeningAgreementQuestionEditor.js │ │ │ ├── DescriptionQuestionEditor.js │ │ │ ├── parts │ │ │ │ ├── HotTableDisabledEditor.js │ │ │ │ ├── ItemEditorPart.js │ │ │ │ ├── PersonalInfoItemEditorPart.js │ │ │ │ ├── ExSelect.js │ │ │ │ └── BulkAddItemsEditorPart.js │ │ │ ├── TextQuestionEditor.js │ │ │ ├── ScheduleQuestionEditor.js │ │ │ ├── SingleTextQuestionEditor.js │ │ │ ├── PersonalInfoQuestionEditor.js │ │ │ ├── SelectQuestionEditor.js │ │ │ ├── RadioQuestionEditor.js │ │ │ ├── CheckboxQuestionEditor.js │ │ │ ├── MultiNumberQuestionEditor.js │ │ │ ├── SubItemEditor.js │ │ │ ├── QuestionEditors.js │ │ │ └── MatrixQuestionEditor.js │ │ ├── flows │ │ │ └── FinisherInFlow.js │ │ ├── Help.js │ │ ├── editors │ │ │ ├── QuestionEditor.js │ │ │ ├── LogicalVariableEditor.js │ │ │ ├── MenuConfigModal.js │ │ │ ├── FinisherEditor.js │ │ │ ├── PageEditor.js │ │ │ ├── SurveySettingEditor.js │ │ │ └── PageSettingEditor.js │ │ └── Editor.js │ ├── tinymce_plugins │ │ ├── freeMode.js │ │ ├── reference.js │ │ └── imageManager.js │ ├── index.js │ ├── store.js │ ├── middlewares │ │ └── saveAsync.js │ ├── css │ │ ├── imageManager.scss │ │ ├── allJavaScriptEditor.scss │ │ └── bootstrap.less │ └── codemirror_plugins │ │ └── outputDefinitionHintFactory.js ├── Image.js ├── browserRequirements.js ├── preview │ └── css │ │ └── preview.scss ├── Runtime.js ├── Editor.js ├── Detail.js ├── Preview.js └── jquery.plugins.js ├── .gitignore ├── .travis.yml ├── resource └── sass │ └── simple │ ├── _common.scss │ └── preview.scss ├── .eslintrc.json ├── __tests__ ├── runtime │ ├── models │ │ ├── survey │ │ │ ├── FinisherDefinition_spec.js │ │ │ ├── questions │ │ │ │ ├── internal │ │ │ │ │ ├── OutputDefinition_spec.js │ │ │ │ │ └── PersonalInfoItemDefinition_spec.js │ │ │ │ ├── ScheduleQuestionDefinition_spec.js │ │ │ │ └── ConditionDefinition_spec.js │ │ │ ├── NodeDefinition_spec.js │ │ │ ├── SurveyDefinition │ │ │ │ ├── noBranchSurvey.json │ │ │ │ ├── hasJavaScriptSurvey.json │ │ │ │ ├── isValidPositionOfCompleteFinisherCase1.json │ │ │ │ ├── hasLogicalVariablesSurvey.json │ │ │ │ └── migrateScheduleQuestionNoSubItems.json │ │ │ └── BranchDefinition_radio.json │ │ ├── runtime │ │ │ └── RuntimeValue_spec.js │ │ └── SurveyDesignerState │ │ │ └── SurveyDesignerState_spec.js │ ├── SurveyManager │ │ └── SurveyManager_spec.js │ └── PageManager_spec.js └── utils_spec.js ├── LICENSE ├── e2e_test.js └── README.md /e2e_test/Config.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/images/delete.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /e2e_test/pages/Page.js: -------------------------------------------------------------------------------- 1 | module.exports = class Page { 2 | } 3 | 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "es2016", "es2017"] 3 | } 4 | -------------------------------------------------------------------------------- /lib/constants/parsleyConstants.js: -------------------------------------------------------------------------------- 1 | export const EXCLUDED_OPTION = 'input[type=button], input[type=submit], input[type=reset], input[type=hidden], :hidden, :disabled'; 2 | -------------------------------------------------------------------------------- /docs/survey-designer-js/glyphicons-halflings-regular-e18bbf611f2a2e43afc071aa2f4e1512.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jirokun/survey-designer-js/HEAD/docs/survey-designer-js/glyphicons-halflings-regular-e18bbf611f2a2e43afc071aa2f4e1512.ttf -------------------------------------------------------------------------------- /docs/survey/init.json: -------------------------------------------------------------------------------- 1 | {"_id":"65f1ca80-ff20-11e6-a3da-52d4834d462c","_createdAt":1488442529576,"_updatedAt":1488442529576,"status":"DRAFT","isPartnerSurvey":false,"title":"名称未設定","version":2,"pages":[],"finishers":[],"branches":[],"nodes":[]} -------------------------------------------------------------------------------- /lib/runtime/components/questions/RadioQuestion.js: -------------------------------------------------------------------------------- 1 | import ChoiceQuestionBase from './ChoiceQuestionBase'; 2 | 3 | /** 設問:単一選択肢 */ 4 | export default class RadioQuestion extends ChoiceQuestionBase { 5 | constructor(props) { 6 | super(props, 'radio'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/runtime/components/questions/CheckboxQuestion.js: -------------------------------------------------------------------------------- 1 | import ChoiceQuestionBase from './ChoiceQuestionBase'; 2 | 3 | /** 設問:複数選択肢 */ 4 | export default class CheckboxQuestion extends ChoiceQuestionBase { 5 | constructor(props) { 6 | super(props, 'checkbox'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/constants/dnd.js: -------------------------------------------------------------------------------- 1 | /** 2 | * react-dndで使用する定数 3 | */ 4 | export const DND_CONDITION = 'DND_CONDITION'; 5 | export const DND_ITEM = 'DND_ITEM'; 6 | export const DND_NODE = 'DND_NODE'; 7 | export const DND_QUESTION = 'DND_QUESTION'; 8 | export const DND_SUB_ITEM = 'DND_SUB_ITEM'; 9 | -------------------------------------------------------------------------------- /lib/constants/editor.js: -------------------------------------------------------------------------------- 1 | /** editorで使用する定数 */ 2 | export const TAB_QUESTIONS = 'questions'; 3 | export const TAB_LOGICAL_VARIABLES = 'logical-variables'; 4 | export const TAB_PAGE_OPTIONS = 'page-settings'; 5 | export const TAB_JAVASCRIPT = 'javascript'; 6 | export const TAB_HTML = 'html'; 7 | -------------------------------------------------------------------------------- /lib/runtime/components/questions/ScreeningAgreementQuestion.js: -------------------------------------------------------------------------------- 1 | import ChoiceQuestionBase from './ChoiceQuestionBase'; 2 | 3 | /** 設問:調査許諾 */ 4 | export default class ScreeningAgreementQuestion extends ChoiceQuestionBase { 5 | constructor(props) { 6 | super(props, 'radio'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/constants/NumberValidationRuleConstants.js: -------------------------------------------------------------------------------- 1 | export const COMPARISON_TYPES = { 2 | fixedValue: '固定値', 3 | answerValue: '回答値', 4 | }; 5 | 6 | export const OPERATORS = { 7 | '==': 'と等しい', 8 | '!=': 'と等しくない', 9 | '>': 'より大きい', 10 | '<': 'より小さい', 11 | '>=': '以上', 12 | '<=': '以下', 13 | }; 14 | -------------------------------------------------------------------------------- /lib/browserUtils.js: -------------------------------------------------------------------------------- 1 | import browser from 'detect-browser'; 2 | import { parseInteger } from './utils'; 3 | 4 | export function isIELowerEquals(version) { 5 | // browserが定義されていないときは未知のブラウザ 6 | if (!browser) return false; 7 | return browser.name === 'ie' && parseInteger(browser.version.substr(0, browser.version.indexOf('.'))) <= version; 8 | } 9 | -------------------------------------------------------------------------------- /lib/runtime/components/plain/SingleTextQuestionJS.js: -------------------------------------------------------------------------------- 1 | import TextQuestionJS from './TextQuestionJS'; 2 | 3 | /** 4 | * TextQuestionのためのJS 5 | */ 6 | export default class SingleTextQuestionJS extends TextQuestionJS { 7 | constructor(el, survey, page, runtime) { 8 | super(el, survey, page, runtime); 9 | this.dataType = 'SingleText'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | .DS_Store 6 | .vscode 7 | coverage 8 | 9 | ### Vim ### 10 | [._]*.s[a-w][a-z] 11 | [._]s[a-w][a-z] 12 | *.un~ 13 | Session.vim 14 | .netrwhist 15 | *~ 16 | 17 | 18 | # Dependency directory 19 | node_modules/ 20 | 21 | dist/ 22 | bower_components/ 23 | .sass-cache/ 24 | errorShots 25 | 26 | # envinronments 27 | .env 28 | 29 | www/ignore -------------------------------------------------------------------------------- /lib/constants/ItemVisibility.js: -------------------------------------------------------------------------------- 1 | export const HIDE = 'hide'; 2 | export const SHOW = 'show'; 3 | 4 | export const VISIBLITY_TYPE_OPTIONS = { 5 | [SHOW]: '表示', 6 | [HIDE]: '非表示', 7 | }; 8 | 9 | export const CLASS_NAME_HIDDEN = 'hidden'; 10 | export const CLASS_NAME_SHOW = 'show'; 11 | 12 | export const COMPARISON_TYPE_OPTIONS = { 13 | fixedValue: '固定値', 14 | answerValue: '回答値', 15 | }; 16 | -------------------------------------------------------------------------------- /lib/editor/components/question_editors/ScreeningAgreementQuestionEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseQuestionEditor from './BaseQuestionEditor'; 3 | 4 | export default function ScreeningAgreementQuestionEditor(props) { 5 | const { page, question } = props; 6 | 7 | return ( 8 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /lib/editor/components/question_editors/DescriptionQuestionEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseQuestionEditor from './BaseQuestionEditor'; 3 | 4 | export default function DescriptionQuestionEditor(props) { 5 | const { page, question } = props; 6 | 7 | return ( 8 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | language: node_js 3 | node_js: '7' 4 | sudo: required 5 | env: DISPLAY=':99.0' 6 | dist: trusty 7 | addons: 8 | apt: 9 | sources: 10 | - google-chrome 11 | packages: 12 | - google-chrome-stable 13 | before_script: 14 | - echo "RUNTIME_CSS_URL=${RUNTIME_CSS_URL}" > .env 15 | - sh -e /etc/init.d/xvfb start 16 | install: 17 | - npm install -g codecov 18 | - yarn 19 | script: 20 | - npm test 21 | - codecov 22 | -------------------------------------------------------------------------------- /lib/editor/components/question_editors/parts/HotTableDisabledEditor.js: -------------------------------------------------------------------------------- 1 | import Handsontable from 'handsontable'; 2 | 3 | /** 4 | * @private 5 | * @editor CheckboxEditor 6 | * @class CheckboxEditor 7 | */ 8 | export default class HotTableDisabledEditor extends Handsontable.editors.BaseEditor { 9 | beginEditing() {} 10 | finishEditing() {} 11 | init() {} 12 | open() {} 13 | close() {} 14 | getValue() {} 15 | setValue() {} 16 | focus() {} 17 | } 18 | -------------------------------------------------------------------------------- /lib/editor/components/question_editors/TextQuestionEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseQuestionEditor from './BaseQuestionEditor'; 3 | 4 | export default function TextQuestionEditor(props) { 5 | const { page, question } = props; 6 | 7 | return ( 8 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /lib/editor/components/question_editors/ScheduleQuestionEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseQuestionEditor from './BaseQuestionEditor'; 3 | 4 | export default function ScheduleQuestionEditor(props) { 5 | const { page, question } = props; 6 | 7 | return ( 8 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /lib/editor/components/question_editors/SingleTextQuestionEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseQuestionEditor from './BaseQuestionEditor'; 3 | 4 | export default function SingleTextQuestionEditor(props) { 5 | const { page, question } = props; 6 | 7 | return ( 8 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /lib/runtime/store.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import { createStore } from 'redux'; 3 | import reducer from './reducers'; 4 | 5 | export function configureStore(initialState) { 6 | const store = createStore( 7 | reducer, 8 | // currentNodeIdは最初のnodeにしておく 9 | initialState.setIn(['runtime', 'currentNodeId'], initialState.getSurvey().getNodes().get(0).getId()), 10 | window.devToolsExtension ? window.devToolsExtension() : undefined, 11 | ); 12 | 13 | return store; 14 | } 15 | -------------------------------------------------------------------------------- /lib/editor/components/question_editors/PersonalInfoQuestionEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseQuestionEditor from './BaseQuestionEditor'; 3 | 4 | export default function PersonalInfoQuestionEditor(props) { 5 | const { page, question } = props; 6 | 7 | return ( 8 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /lib/editor/components/question_editors/SelectQuestionEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseQuestionEditor from './BaseQuestionEditor'; 3 | 4 | export default function SelectQuestionEditor(props) { 5 | const { page, question } = props; 6 | 7 | return ( 8 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /lib/editor/components/question_editors/RadioQuestionEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseQuestionEditor from './BaseQuestionEditor'; 3 | 4 | export default function RadioQuestionEditor(props) { 5 | const { page, question } = props; 6 | 7 | return ( 8 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /resource/sass/simple/_common.scss: -------------------------------------------------------------------------------- 1 | $CONTAINER_SIZE: 1000px; 2 | $THEME_COLOR: #00bcd4; 3 | $THEME_BORDER: #006064; 4 | $ITEM_BACKGROUND_COLOR: lighten(#E0F7FA, 6%); 5 | $DISABLED_COLOR: #eee; 6 | $ERROR_COLOR: #fcc; 7 | 8 | @mixin button($backgroundColor) { 9 | background-color: $backgroundColor; 10 | border: none; 11 | border-radius: 4px; 12 | color: white; 13 | padding: 5px 20px; 14 | margin: 0 5px; 15 | text-align: center; 16 | text-decoration: none; 17 | display: inline-block; 18 | font-size: 16px; 19 | } 20 | -------------------------------------------------------------------------------- /lib/editor/components/question_editors/CheckboxQuestionEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseQuestionEditor from './BaseQuestionEditor'; 3 | 4 | export default function CheckboxQuestionEditor(props) { 5 | const { page, question } = props; 6 | 7 | return ( 8 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /lib/constants/states.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Stateで使う定数 3 | */ 4 | export const ANSWER_NOT_POSTED = 'ANSWER_NOT_POSTED'; 5 | export const ANSWER_POSTED_FAILED = 'ANSWER_POSTED_FAILED'; 6 | export const ANSWER_POSTED_SUCCESS = 'ANSWER_POSTED_SUCCESS'; 7 | export const ANSWER_POSTING = 'ANSWER_POSTING'; 8 | export const SURVEY_NOT_MODIFIED = 'SURVEY_NOT_MODIFIED'; 9 | export const SURVEY_NOT_POSTED = 'SURVEY_NOT_POSTED'; 10 | export const SURVEY_POSTED_FAILED = 'SURVEY_POSTED_FAILED'; 11 | export const SURVEY_POSTED_SUCCESS = 'SURVEY_POSTED_SUCCESS'; 12 | export const SURVEY_POSTING = 'SURVEY_POSTING'; 13 | -------------------------------------------------------------------------------- /lib/runtime/components/plain/NumericInput.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import { zenkakuNum2Hankaku } from '../../../utils'; 3 | 4 | /** 5 | * 数値の全角入力を半角に変換するクラス 6 | * 7 | * 有効となる条件 8 | * inputにclass="sdj-numeric" 9 | */ 10 | export default class NumericInput { 11 | constructor(el) { 12 | this.el = el; 13 | } 14 | 15 | initialize() { 16 | $(this.el).on('change', '.sdj-numeric', (e) => { 17 | const value = $(e.target).val(); 18 | const hankakuValue = zenkakuNum2Hankaku(value); 19 | $(e.target).val(hankakuValue); 20 | }); 21 | } 22 | 23 | deInitialize() { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/runtime/models/survey/questions/internal/ChoiceDefinition.js: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable'; 2 | 3 | /** OutputDefinitionのChoiceに格納するクラス */ 4 | export const ChoiceDefinitionRecord = Record({ 5 | _id: null, // 内部で使用するIDでitemの移動やnodeの移動があっても変わらない 6 | label: null, // 表示用のラベル 7 | value: null, // 対応する値 8 | }); 9 | 10 | export default class ChoiceDefinition extends ChoiceDefinitionRecord { 11 | getId() { 12 | return this.get('_id'); 13 | } 14 | 15 | getLabel() { 16 | return this.get('label'); 17 | } 18 | 19 | getValue() { 20 | return this.get('value'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/editor/components/flows/FinisherInFlow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import NodeInFlow from './NodeInFlow'; 4 | 5 | /** Flowの中に描画するFinisher */ 6 | function FinisherInFlow(props) { 7 | const { survey, node } = props; 8 | const nodeLabel = survey.calcNodeLabel(node.getId()); 9 | return ; 10 | } 11 | 12 | const stateToProps = state => ({ 13 | survey: state.getSurvey(), 14 | runtime: state.getRuntime(), 15 | view: state.getViewSetting(), 16 | }); 17 | 18 | export default connect( 19 | stateToProps, 20 | )(FinisherInFlow); 21 | -------------------------------------------------------------------------------- /lib/editor/tinymce_plugins/freeMode.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import tinymce from 'tinymce'; 3 | import '../../constants/tinymce_ja'; 4 | 5 | /** 6 | * フリーモードの保存とキャンセルボタンを追加する 7 | */ 8 | tinymce.PluginManager.add('free_mode', (editor) => { 9 | editor.addButton('free_mode_save', { 10 | text: '保存', 11 | icon: false, 12 | onclick: () => { 13 | editor.settings.freeModeSaveCallback(); 14 | }, 15 | }); 16 | 17 | editor.addButton('free_mode_cancel', { 18 | text: 'キャンセル', 19 | icon: false, 20 | onclick: () => { 21 | editor.settings.freeModeCancelCallback(); 22 | }, 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /lib/runtime/models/survey/questions/DescriptionQuestionDefinition.js: -------------------------------------------------------------------------------- 1 | import cuid from 'cuid'; 2 | import BaseQuestionDefinition from './internal/BaseQuestionDefinition'; 3 | import surveyDevIdGeneratorInstance from '../../../SurveyDevIdGenerator'; 4 | 5 | /** 設問定義:説明文 */ 6 | export default class DescriptionQuestionDefinition extends BaseQuestionDefinition { 7 | static create(pageDevId) { 8 | return new DescriptionQuestionDefinition({ 9 | _id: cuid(), 10 | devId: surveyDevIdGeneratorInstance.generateForQuestion(pageDevId), 11 | dataType: 'Description', 12 | plainTitle: '説明文', 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/editor/components/question_editors/MultiNumberQuestionEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseQuestionEditor from './BaseQuestionEditor'; 3 | 4 | export default function MultiNumberQuestionEditor(props) { 5 | const { page, question } = props; 6 | 7 | return ( 8 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /lib/Image.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser,jquery */ 2 | import 'babel-polyfill'; 3 | import 'classlist-polyfill'; 4 | import Raven from 'raven-js'; 5 | import React from 'react'; 6 | import { render } from 'react-dom'; 7 | import ImageManagerApp from './editor/containers/ImageManagerApp'; 8 | import { RequiredBrowserNoticeForRuntime } from './browserRequirements'; 9 | import { isIELowerEquals } from './browserUtils'; 10 | 11 | export function Image(el, options) { 12 | if (isIELowerEquals(10)) { 13 | render(, el); 14 | return null; 15 | } 16 | 17 | render( 18 | , 19 | el, 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /lib/runtime/components/parts/CommonQuestionParts.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import S from 'string'; 3 | 4 | export function title(question, replacer) { 5 | const html = question.getTitle(); 6 | if (S(html).isEmpty()) return null; 7 | return ( 8 |

13 | ); 14 | } 15 | 16 | export function description(question, replacer) { 17 | const html = question.getDescription(); 18 | if (S(html).isEmpty()) return null; 19 | return

; 20 | } 21 | -------------------------------------------------------------------------------- /lib/constants/prefectures.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 都道府県名の配列 3 | */ 4 | export default [ 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 | -------------------------------------------------------------------------------- /lib/editor/components/Help.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Glyphicon, Tooltip, OverlayTrigger } from 'react-bootstrap'; 3 | import * as HelpMessages from '../../constants/HelpMessages'; 4 | 5 | export default function Help(props) { 6 | const html = HelpMessages[props.messageId]; 7 | return ( 8 | 15 |
16 | 17 | } 18 | > 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "plugins": [ 4 | "jest" 5 | ], 6 | "global": { 7 | "ENV": false 8 | }, 9 | "env": { 10 | "mocha": true 11 | }, 12 | "rules": { 13 | "max-len": ["error", 140, 2, { 14 | "ignoreUrls": true, 15 | "ignoreComments": false, 16 | "ignoreRegExpLiterals": true, 17 | "ignoreStrings": true, 18 | "ignoreTemplateLiterals": true 19 | }], 20 | "class-methods-use-this": 0, 21 | "no-restricted-syntax": 0, 22 | "no-continue": 0, 23 | "no-plusplus": 0, 24 | "jsx-a11y/label-has-for": 0, 25 | "react/jsx-filename-extension": 0, 26 | "react/prop-types": 0, 27 | "react/no-danger": 0, 28 | "jest/no-exclusive-tests": 2, 29 | "jest/no-identical-title": 2 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/runtime/models/survey/questions/internal/PersonalInfoItemFieldDefinition.js: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable'; 2 | 3 | /** 設問定義の設問(PersonalInfoItem)のフィールドの要素 */ 4 | export const PersonalInfoItemFieldRecord = Record({ 5 | _id: null, // ID 6 | label: null, // フィールドのラベル 7 | outputType: null, // 出力形式 text / number 8 | prependValue: null, // 回答データDL時に先頭につける文字列 9 | }); 10 | 11 | export default class PersonalInfoItemFieldDefinition extends PersonalInfoItemFieldRecord { 12 | getId() { 13 | return this.get('_id'); 14 | } 15 | 16 | getLabel() { 17 | return this.get('label'); 18 | } 19 | 20 | getOutputType() { 21 | return this.get('outputType'); 22 | } 23 | 24 | getPrependValue() { 25 | return this.get('prependValue'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/runtime/components/questions/DescriptionQuestion.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import React, { Component } from 'react'; 3 | 4 | /** 設問:説明文 */ 5 | export default class DescriptionQuestion extends Component { 6 | render() { 7 | const { replacer, question } = this.props; 8 | const description = question.getDescription(); 9 | return ( 10 |
11 |

12 | {/* 13 | page.jsではページ内に一つもinput:visible,select:visible,textarea:visibleなエレメントがないと 14 | 自動でスキップしてしまうため、飛ばされないようにするためのエレメント 15 | */} 16 | 17 |

18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /e2e_test/specs/questions/SingleTextQuestion.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | /* global browser */ 3 | 4 | const clearRequireCache = require('../../clearRequireCache'); 5 | clearRequireCache(); 6 | 7 | const EditorPage = require('../../pages/EditorPage'); 8 | const expect = require('chai').expect; 9 | 10 | describe('SingleTextQuestion', () => { 11 | describe('editor', () => { 12 | it('エディタテキストクエスチョンが追加できる', () => { 13 | const editorPage = new EditorPage(); 14 | editorPage.addQuestion('ページ 1', 0, '1行テキスト'); 15 | editorPage.addQuestion('ページ 1', 0, '1行テキスト'); 16 | const questions = editorPage.findQuestionsInPage('ページ 1'); 17 | expect(questions).to.be.a('array'); 18 | expect(questions).to.have.lengthOf(2); 19 | expect(questions[0]).to.equal('1-1 1行テキスト'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /lib/editor/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser,jquery */ 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { Provider } from 'react-redux'; 5 | import EnqueteEditorApp from './containers/EnqueteEditorApp'; 6 | import { configureStore } from './store'; 7 | import SurveyDesignerState from '../runtime/models/SurveyDesignerState'; 8 | import ParsleyWrapper from '../ParsleyWrapper'; 9 | import './css/editor.scss'; 10 | 11 | $.getJSON('sample.json').done((json) => { 12 | const initialState = SurveyDesignerState.createFromJson(json); 13 | const store = configureStore(initialState); 14 | const rootElement = document.getElementById('root'); 15 | new ParsleyWrapper(rootElement); 16 | 17 | render( 18 | 19 | 20 | , 21 | rootElement, 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/runtime/models/survey/FinisherDefinition_spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import SurveyDesignerState from '../../../../lib/runtime/models/SurveyDesignerState'; 3 | import sample1 from '../sample1.json'; 4 | 5 | describe('FinisherDefinition', () => { 6 | let state; 7 | beforeAll(() => { 8 | state = SurveyDesignerState.createFromJson(sample1); 9 | }); 10 | 11 | describe('validate', () => { 12 | it('再掲で参照している値が存在していない場合にエラーメッセージが返る', () => { 13 | const survey = state.getSurvey().setIn(['finishers', 0, 'html'], '{{a.answer}}'); 14 | survey.refreshReplacer(); 15 | const result = survey.getFinishers().get(0).validate(survey); 16 | expect(result.size).toBe(1); 17 | expect(result.get(0).getType()).toBe('ERROR'); 18 | expect(result.get(0).getMessage()).toBe('存在しない参照があります'); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /lib/runtime/models/ValidationErrorDefinition.js: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable'; 2 | 3 | export const ERROR_TYPES = { ERROR: 'ERROR', WARNING: 'WARNING' }; 4 | 5 | export const ValidationErrorDefinitionRecord = Record({ 6 | type: null, // ERRORまたはWARNING 7 | message: null, // メッセージ 8 | }); 9 | 10 | /** Branchの定義 */ 11 | export default class ValidationErrorDefinition extends ValidationErrorDefinitionRecord { 12 | static createError(message) { 13 | return new ValidationErrorDefinition({ type: ERROR_TYPES.ERROR, message }); 14 | } 15 | 16 | static createWarning(message) { 17 | return new ValidationErrorDefinition({ type: ERROR_TYPES.WARNING, message }); 18 | } 19 | 20 | getType() { 21 | return this.get('type'); 22 | } 23 | 24 | getMessage() { 25 | return this.get('message'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /__tests__/runtime/SurveyManager/SurveyManager_spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import SurveyDesignerState from '../../../lib/runtime/models/SurveyDesignerState'; 3 | import SurveyManager from '../../../lib/runtime/SurveyManager'; 4 | import baseJson from './base.json'; 5 | 6 | describe('SurveyManager', () => { 7 | it('devId => name に変換できる', () => { 8 | const survey = SurveyDesignerState.createFromJson(baseJson).getSurvey(); 9 | const surveyManager = new SurveyManager(survey, {}); 10 | expect(surveyManager.getNameByDevId('ww1_xx1_yy1')).toBe('1__value1'); 11 | expect(surveyManager.getNameByDevId('ww1_xx1_yy2')).toBe('1__value2'); 12 | expect(surveyManager.getNameByDevId('ww1_xx1_yy3')).toBe('1__value3'); 13 | expect(surveyManager.getNameByDevId('ww1_xx2_yy4')).toBe('2__value1'); 14 | expect(surveyManager.getNameByDevId('ww1_xx2_yy5')).toBe('2__value2'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /lib/browserRequirements.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser,jquery */ 2 | import React from 'react'; 3 | 4 | export function RequiredBrowserNoticeForEditor() { 5 | return ( 6 |
7 |

8 | ご利用の環境ではこのページを表示することができません。
9 | 以下のいずれかのブラウザをご利用ください。

10 |
    11 |
  • Google Chrome 最新版
  • 12 |
  • Firefox 最新版
  • 13 |
  • Microsoft Edge
  • 14 |
  • Microsoft Internet Explorer 11
  • 15 |
16 |
17 | ); 18 | } 19 | 20 | export function RequiredBrowserNoticeForRuntime() { 21 | return ( 22 |
23 |

24 | ご利用の環境ではこのページを表示することができません。
25 | 以下のいずれかのブラウザをご利用ください。

26 |
    27 |
  • Google Chrome 最新版
  • 28 |
  • Firefox 最新版
  • 29 |
  • Microsoft Edge
  • 30 |
  • Microsoft Internet Explorer 9以上
  • 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /lib/editor/store.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import { createStore, applyMiddleware, compose } from 'redux'; 3 | import reducer from './reducers'; 4 | import saveAsync from './middlewares/saveAsync'; 5 | 6 | const nextReducer = require('./reducers'); 7 | 8 | export function configureStore(initialState) { 9 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 10 | const store = createStore( 11 | reducer, 12 | // currentNodeIdは最初のnodeにしておく 13 | initialState.setIn(['runtime', 'currentNodeId'], initialState.getSurvey().getNodes().get(0).getId()), 14 | composeEnhancers( 15 | applyMiddleware(saveAsync), 16 | ), 17 | ); 18 | if (module.hot) { 19 | // Enable Webpack hot module replacement for reducers 20 | module.hot.accept('./reducers', () => { 21 | store.replaceReducer(nextReducer); 22 | }); 23 | } 24 | 25 | return store; 26 | } 27 | -------------------------------------------------------------------------------- /lib/runtime/models/migrator/20171018000000_migratePersonalInfoQuestion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PersonalInfoQuestionのItemsを定義する 3 | * 4 | * 修正前 5 | * Itemsが存在しない 6 | * 7 | * 修正後 8 | * Itemsが定義される 9 | */ 10 | function shouldMigrate(question) { 11 | if (!question.getItems() || question.getItems().size === 0) { return true; } 12 | return question.getItems().size === 1 && question.getItems().get(0).getLabel() === '名称未設定'; 13 | } 14 | 15 | export function migratePersonalInfoQuestion(survey) { 16 | let tmpSurvey = survey; 17 | 18 | survey.getPages().forEach((page, pageIndex) => { 19 | page.getQuestions().forEach((question, questionIndex) => { 20 | if (question.dataType === 'PersonalInfo' && shouldMigrate(question)) { 21 | tmpSurvey = tmpSurvey.updateIn(['pages', pageIndex, 'questions', questionIndex], q => q.updateDefaultItems()); 22 | } 23 | }); 24 | }); 25 | 26 | return tmpSurvey; 27 | } 28 | -------------------------------------------------------------------------------- /lib/editor/middlewares/saveAsync.js: -------------------------------------------------------------------------------- 1 | import debounce from 'throttle-debounce/debounce'; 2 | import * as Actions from '../actions'; 3 | import { SURVEY_NOT_POSTED } from '../../constants/states'; 4 | 5 | /** 一定時間変更がなかった場合にだけ実際にsaveを行う関数 */ 6 | const debouncedSaveAsync = debounce(1500, (dispatch, saveSurveyUrl, newSurvey) => { 7 | //console.log(JSON.stringify(newSurvey.toJS(), null, ' ')); 8 | if (!saveSurveyUrl) return; 9 | Actions.saveSurvey(dispatch, saveSurveyUrl, newSurvey); 10 | }); 11 | 12 | /** surveyの定義が変わった際にサーバにsurveyを保存するmiddleware */ 13 | export default store => next => (action) => { 14 | const oldSurvey = store.getState().getSurvey(); 15 | next(action); 16 | const newSurvey = store.getState().getSurvey(); 17 | if (oldSurvey === newSurvey) return; 18 | store.dispatch(Actions.changeSaveSurveyStatus(SURVEY_NOT_POSTED)); 19 | const saveSurveyUrl = store.getState().getOptions().getSaveSurveyUrl(); 20 | debouncedSaveAsync(store.dispatch, saveSurveyUrl, newSurvey); 21 | }; 22 | -------------------------------------------------------------------------------- /lib/runtime/models/migrator/20170921104000_migrateScheduleSubItems.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 日程質問に関する制限の定義の修正 3 | * 修正前に作られた定義のものはこのメソッドで新しい定義に自動的に置き換える 4 | * 5 | * 修正前 6 | * 日程質問にSubItemsがなくデフォルト表示 7 | * 8 | * 修正後 9 | * 日程質問にSubItemsがあり、可変(初期値はデフォルト値) 10 | */ 11 | export function migrateScheduleSubItems(survey) { 12 | let tmpSurvey = survey; 13 | 14 | const shouldMigrate = (question) => { 15 | if (!question.getSubItems() || question.getSubItems().size === 0) { return true; } 16 | return question.getSubItems().size === 1 && question.getSubItems().get(0).getLabel() === '名称未設定'; 17 | }; 18 | 19 | survey.getPages().forEach((page, pageIndex) => { 20 | page.getQuestions().forEach((question, questionIndex) => { 21 | if (question.dataType === 'Schedule' && shouldMigrate(question)) { 22 | tmpSurvey = tmpSurvey.updateIn(['pages', pageIndex, 'questions', questionIndex], q => q.updateDefaultSubItems()); 23 | } 24 | }); 25 | }); 26 | 27 | return tmpSurvey; 28 | } 29 | -------------------------------------------------------------------------------- /lib/runtime/models/survey/NodeDefinition.js: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable'; 2 | 3 | export const NodeDefinitionRecord = Record({ 4 | _id: null, 5 | type: null, // nodeが参照するものがなにかを表す。page, branch, finisherのいずれか 6 | refId: null, // nodeが参照するpage, branch, finisherのid 7 | nextNodeId: null, // 次に遷移するnodeのid。typeが'branch'の場合は条件によってbranchの条件が優先される 8 | }); 9 | 10 | /** Nodeの定義 */ 11 | export default class NodeDefinition extends NodeDefinitionRecord { 12 | getId() { 13 | return this.get('_id'); 14 | } 15 | 16 | getType() { 17 | return this.get('type'); 18 | } 19 | 20 | isPage() { 21 | return this.get('type') === 'page'; 22 | } 23 | 24 | isBranch() { 25 | return this.get('type') === 'branch'; 26 | } 27 | 28 | isFinisher() { 29 | return this.get('type') === 'finisher'; 30 | } 31 | 32 | getRefId() { 33 | return this.get('refId'); 34 | } 35 | 36 | getNextNodeId() { 37 | return this.get('nextNodeId'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/runtime/components/parts/Value.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | /** 詳細プレビューで使用する値の表示のためのコンポーネント */ 5 | class Value extends Component { 6 | render() { 7 | const { survey, value, className } = this.props; 8 | const replacer = survey.getReplacer(); 9 | if (replacer.containsReferenceIdIn(value)) { 10 | return 再掲 {replacer.findReferenceOutputDefinitionsIn(value)[0].getOutputNo()}; 11 | } 12 | if (!replacer.validate(value, survey.getAllOutputDefinitions())) { 13 | return エラー 不正な参照です; 14 | } 15 | return {value}; 16 | } 17 | } 18 | 19 | const stateToProps = state => ({ 20 | survey: state.getSurvey(), 21 | runtime: state.getRuntime(), 22 | view: state.getViewSetting(), 23 | options: state.getOptions(), 24 | }); 25 | 26 | export default connect( 27 | stateToProps, 28 | )(Value); 29 | 30 | 31 | -------------------------------------------------------------------------------- /lib/editor/components/question_editors/SubItemEditor.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import { DragSource, DropTarget } from 'react-dnd'; 3 | import { connect } from 'react-redux'; 4 | import { DND_SUB_ITEM } from '../../../constants/dnd'; 5 | import BaseItemEditor, { conditionSource, conditionTarget, stateToProps, actionsToProps } from './BaseItemEditor'; 6 | 7 | /** questionのsubItemsを編集する際に使用するeditor */ 8 | class SubItemEditor extends BaseItemEditor { 9 | } 10 | 11 | const DropTargetItemEditor = DropTarget( 12 | DND_SUB_ITEM, 13 | conditionTarget, 14 | dndConnect => ({ connectDropTarget: dndConnect.dropTarget() }), 15 | )(SubItemEditor); 16 | const DragSourceItemEditor = DragSource( 17 | DND_SUB_ITEM, 18 | conditionSource, 19 | (dndConnect, monitor) => ({ 20 | connectDragSource: dndConnect.dragSource(), 21 | connectDragPreview: dndConnect.dragPreview(), 22 | dragging: monitor.isDragging(), 23 | }), 24 | )(DropTargetItemEditor); 25 | export default connect(stateToProps, actionsToProps)(DragSourceItemEditor); 26 | -------------------------------------------------------------------------------- /lib/runtime/components/plain/TextQuestionJS.js: -------------------------------------------------------------------------------- 1 | import { findElementsByOutputDefinitions } from '../../../utils'; 2 | 3 | /** 4 | * TextQuestionのためのJS 5 | */ 6 | export default class TextQuestionJS { 7 | constructor(el, survey, page, runtime) { 8 | this.el = el; 9 | this.survey = survey; 10 | this.page = page; 11 | this.runtime = runtime; 12 | this.dataType = 'Text'; 13 | } 14 | 15 | /** pageに含まれる対象のQuestionのみを取得する */ 16 | findQuestions() { 17 | return this.page.getQuestions().filter(question => question.getDataType() === this.dataType); 18 | } 19 | 20 | /** 設問を任意入力にする */ 21 | optionalize(question) { 22 | if (!question.isOptional()) return; 23 | const outputDefinition = question.getOutputDefinitions().get(0); 24 | findElementsByOutputDefinitions(outputDefinition).removeAttr('data-parsley-required'); 25 | } 26 | 27 | initialize() { 28 | this.findQuestions().forEach((question) => { 29 | this.optionalize(question); 30 | }); 31 | } 32 | 33 | deInitialize() { 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/editor/components/question_editors/parts/ItemEditorPart.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import { DragSource, DropTarget } from 'react-dnd'; 3 | import { connect } from 'react-redux'; 4 | import { DND_ITEM } from '../../../../constants/dnd'; 5 | import BaseItemEditor, { conditionSource, conditionTarget, stateToProps, actionsToProps } from '../BaseItemEditor'; 6 | 7 | /** questionのitemsを編集する際に使用するeditor */ 8 | class ItemEditorPart extends BaseItemEditor { 9 | } 10 | 11 | const DropTargetItemEditorPart = DropTarget( 12 | DND_ITEM, 13 | conditionTarget, 14 | dndConnect => ({ connectDropTarget: dndConnect.dropTarget() }), 15 | )(ItemEditorPart); 16 | const DragSourceItemEditorPart = DragSource( 17 | DND_ITEM, 18 | conditionSource, 19 | (dndConnect, monitor) => ({ 20 | connectDragSource: dndConnect.dragSource(), 21 | connectDragPreview: dndConnect.dragPreview(), 22 | dragging: monitor.isDragging(), 23 | }), 24 | )(DropTargetItemEditorPart); 25 | export default connect(stateToProps, actionsToProps)(DragSourceItemEditorPart); 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2017 Jiro Iwamoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /lib/editor/components/question_editors/parts/PersonalInfoItemEditorPart.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import { DragSource, DropTarget } from 'react-dnd'; 3 | import { connect } from 'react-redux'; 4 | import { DND_ITEM } from '../../../../constants/dnd'; 5 | import PersonalInfoItemEditor, { conditionSource, conditionTarget, stateToProps, actionsToProps } from '../PersonalInfoItemEditor'; 6 | 7 | /** questionのitemsを編集する際に使用するeditor */ 8 | class PersonalItemEditorPart extends PersonalInfoItemEditor { 9 | } 10 | 11 | const DropTargetPersonalItemEditorPart = DropTarget( 12 | DND_ITEM, 13 | conditionTarget, 14 | dndConnect => ({ connectDropTarget: dndConnect.dropTarget() }), 15 | )(PersonalItemEditorPart); 16 | const DragSourcePersonalItemEditorPart = DragSource( 17 | DND_ITEM, 18 | conditionSource, 19 | (dndConnect, monitor) => ({ 20 | connectDragSource: dndConnect.dragSource(), 21 | connectDragPreview: dndConnect.dragPreview(), 22 | dragging: monitor.isDragging(), 23 | }), 24 | )(DropTargetPersonalItemEditorPart); 25 | export default connect(stateToProps, actionsToProps)(DragSourcePersonalItemEditorPart); 26 | -------------------------------------------------------------------------------- /lib/runtime/components/questions/Questions.js: -------------------------------------------------------------------------------- 1 | import CheckboxQuestion from './CheckboxQuestion'; 2 | import RadioQuestion from './RadioQuestion'; 3 | import SelectQuestion from './SelectQuestion'; 4 | import MultiNumberQuestion from './MultiNumberQuestion'; 5 | import SingleTextQuestion from './SingleTextQuestion'; 6 | import TextQuestion from './TextQuestion'; 7 | import MatrixQuestion from './MatrixQuestion'; 8 | import DescriptionQuestion from './DescriptionQuestion'; 9 | import ScreeningAgreementQuestion from './ScreeningAgreementQuestion'; 10 | import ScheduleQuestion from './ScheduleQuestion'; 11 | import PersonalInfoQuestion from './PersonalInfoQuestion'; 12 | 13 | const questions = { 14 | CheckboxQuestion, 15 | RadioQuestion, 16 | SelectQuestion, 17 | MultiNumberQuestion, 18 | SingleTextQuestion, 19 | TextQuestion, 20 | MatrixQuestion, 21 | DescriptionQuestion, 22 | ScreeningAgreementQuestion, 23 | ScheduleQuestion, 24 | PersonalInfoQuestion, 25 | }; 26 | 27 | /** dataTypeから対応するQuestionを取得する */ 28 | export function findQuestionClass(className) { 29 | return questions[`${className}Question`]; 30 | } 31 | -------------------------------------------------------------------------------- /lib/preview/css/preview.scss: -------------------------------------------------------------------------------- 1 | .optional-area { 2 | border: 1px solid #666; 3 | border-radius: 5px; 4 | background: #fff; 5 | padding: 5px; 6 | width: 800px; 7 | margin: 10px auto 0 auto; 8 | box-sizing: border-box; 9 | .caption { 10 | text-align: left; 11 | font-size: 120%; 12 | font-weight: bold; 13 | } 14 | > .current-page { 15 | text-align: left; 16 | } 17 | > .help { 18 | text-align: left; 19 | line-height: 16px; 20 | color: #777; 21 | margin-top: 3px; 22 | margin-left: 5px; 23 | } 24 | } 25 | .dd-menu-items { 26 | li { 27 | padding: 3px 20px; 28 | width: 100px; 29 | text-align: left; 30 | } 31 | li.checked:before { 32 | content: "✓" 33 | } 34 | } 35 | .warning-area { 36 | font-size: 0.875rem; 37 | margin-top: 5px; 38 | background-color: #f9f3e0; 39 | border: 1px solid gold; 40 | text-align: left; 41 | ul { 42 | padding-left: 10px; 43 | list-style-position: inside; 44 | list-style-type: decimal; 45 | } 46 | } 47 | .error-area { 48 | @extend .warning-area; 49 | background-color: #fcc; 50 | border: 1px solid #f7024b; 51 | } -------------------------------------------------------------------------------- /e2e_test/clearRequireCache.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | function walk(p, fileCallback, errCallback) { 5 | fs.readdir(p, (err, files) => { 6 | if (err) { 7 | errCallback(err); 8 | return; 9 | } 10 | 11 | files.forEach((f) => { 12 | const fp = path.join(p, f); // to full-path 13 | if (fp.indexOf('node_modules') !== -1) return; 14 | if (fs.statSync(fp).isDirectory()) { 15 | walk(fp, fileCallback); // ディレクトリなら再帰 16 | } else { 17 | fileCallback(path.resolve(fp)); // ファイルならコールバックで通知 18 | } 19 | }); 20 | }); 21 | } 22 | 23 | function clearRequireCache() { 24 | walk( 25 | '.', 26 | (filePath) => { 27 | try { 28 | if (require.cache[filePath]) { 29 | // console.log("clear require cache " + filePath); 30 | delete require.cache[filePath]; 31 | } 32 | } catch (e) { 33 | // do nothing 34 | } 35 | }, 36 | (err) => { 37 | console.error(err); 38 | } 39 | ); 40 | } 41 | 42 | //clearRequireCache(); 43 | module.exports = clearRequireCache; 44 | 45 | -------------------------------------------------------------------------------- /__tests__/runtime/models/survey/questions/internal/OutputDefinition_spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { Map } from 'immutable'; 3 | import SurveyDesignerState from '../../../../../../lib/runtime/models/SurveyDesignerState'; 4 | import OutputDefinition from '../../../../../../lib/runtime/models/survey/questions/internal/OutputDefinition'; 5 | 6 | describe('OutputDefinition', () => { 7 | describe('isOutputTypeSingleChoice', () => { 8 | function testIsOutputTypeSingleChoice(outputType, expectResult) { 9 | expect(new OutputDefinition({ outputType }).isOutputTypeSingleChoice()).toBe(expectResult); 10 | } 11 | it('radioの場合trueを返す', () => { 12 | testIsOutputTypeSingleChoice('radio', true) 13 | }); 14 | it('selectの場合trueを返す', () => { 15 | testIsOutputTypeSingleChoice('select', true) 16 | }); 17 | it('textの場合falseを返す', () => { 18 | testIsOutputTypeSingleChoice('text', false) 19 | }); 20 | it('numberの場合falseを返す', () => { 21 | testIsOutputTypeSingleChoice('number', false) 22 | }); 23 | it('checkboxの場合falseを返す', () => { 24 | testIsOutputTypeSingleChoice('checkbox', false) 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /docs/image.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SurveyDesinger Editor 5 | 8 | 9 | 10 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /e2e_test.js: -------------------------------------------------------------------------------- 1 | const process = require('process'); 2 | const spawn = require('child_process').spawn; 3 | const http = require('http'); 4 | 5 | let exitStatus = 0; 6 | function startE2ETest(server) { 7 | const e2eTest = spawn('yarn', ['test:e2e'], { stdio: 'inherit', stdrr: 'inherit' }); 8 | 9 | e2eTest.on('exit', (code) => { 10 | exitStatus = code; 11 | server.kill(); 12 | }); 13 | } 14 | 15 | function startCheck(server) { 16 | const req = http.request({ 17 | host: 'localhost', 18 | port: 3000, 19 | path: '/static/editor.bundle.js', 20 | }, (res) => { 21 | let startTestFlag = false; 22 | res.on('data', () => { 23 | if (startTestFlag) return; 24 | startTestFlag = true; 25 | startE2ETest(server); 26 | }); 27 | }); 28 | 29 | req.on('error', () => setTimeout(startCheck.bind(null, server), 1000)); 30 | req.end(); 31 | } 32 | 33 | const server = spawn('./node_modules/.bin/webpack-dev-server', { stdio: 'inherit', stdrr: 'inherit' }); 34 | 35 | server.on('exit', (code) => { 36 | if (code !== 0) { 37 | process.exit(code); 38 | } else { 39 | process.exit(exitStatus); 40 | } 41 | }); 42 | 43 | startCheck(server); 44 | -------------------------------------------------------------------------------- /lib/editor/components/editors/QuestionEditor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Element } from 'react-scroll'; 4 | import { findQuestionEditorClass } from '../question_editors/QuestionEditors'; 5 | 6 | /** 7 | * 各Questionのエディタをレンダリングするラッパー。 8 | * questionのdataTypeに応じたエディタが描画される 9 | */ 10 | class QuestionEditor extends Component { 11 | /** questionのdataTypeに応じたエディタを取得する */ 12 | findEditorComponent(name) { 13 | const { page, question } = this.props; 14 | const Editor = findQuestionEditorClass(name); 15 | if (!Editor) return
undefined component type: {name}
; 16 | return ; 17 | } 18 | 19 | /** 描画 */ 20 | render() { 21 | const { question } = this.props; 22 | return ( 23 | 24 |
25 | {this.findEditorComponent(question.getDataType())} 26 |
27 |
28 | ); 29 | } 30 | } 31 | 32 | const stateToProps = state => ({ 33 | state, 34 | }); 35 | 36 | export default connect( 37 | stateToProps, 38 | )(QuestionEditor); 39 | -------------------------------------------------------------------------------- /lib/runtime/components/questions/TextQuestion.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import React, { Component } from 'react'; 3 | import QuestionDetail from '../parts/QuestionDetail'; 4 | import * as CommonQuestionParts from '../parts/CommonQuestionParts'; 5 | 6 | /** 設問:複数行テキスト */ 7 | export default class TextQuestion extends Component { 8 | render() { 9 | const { survey, options, replacer, question } = this.props; 10 | const name = question.getOutputName(); 11 | 12 | return ( 13 |
14 | { CommonQuestionParts.title(question, replacer) } 15 | { CommonQuestionParts.description(question, replacer) } 16 |
17 |