├── 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 |
28 |
29 | { options.isShowDetail() ?
: null }
30 |
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/runtime/components/questions/SingleTextQuestion.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 | /** 設問:1行テキスト */
7 | export default class SingleTextQuestion 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 |
29 | { options.isShowDetail() ?
: null }
30 |
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/editor/css/imageManager.scss:
--------------------------------------------------------------------------------
1 | .image-manager {
2 | .upload-form {
3 | margin-bottom: 20px;
4 | input[type="file"] {
5 | display: inline;
6 | max-width: 400px;
7 | margin-right: 20px;
8 | }
9 | }
10 |
11 | .image-container {
12 | .image-block {
13 | display: inline-block;
14 | margin-right: 10px;
15 | margin-bottom: 10px;
16 | .image-link {
17 | display: table-cell;
18 | height: 150px;
19 | border: 1px solid #888;
20 | padding: 3px;
21 | vertical-align: middle;
22 | &.selected {
23 | border: 3px solid blue;
24 | }
25 | img {
26 | max-height: 140px;
27 | max-width: 140px;
28 | }
29 | }
30 | .image-delete {
31 | display: block;
32 | }
33 | }
34 | }
35 | .image-manager-block-layer {
36 | background: black;
37 | opacity: 0.5;
38 | position: fixed;
39 | width: 100vw;
40 | height: 100vh;
41 | top: 0;
42 | left: 0;
43 | }
44 | .image-manager-spinner {
45 | position: fixed;
46 | width: 100vw;
47 | height: 100vh;
48 | top: 0;
49 | left: 0;
50 | display: flex;
51 | justify-content: center;
52 | align-items: center;
53 | }
54 | }
--------------------------------------------------------------------------------
/lib/runtime/models/survey/questions/internal/PersonalInfoItemDisplayTypeDefinition.js:
--------------------------------------------------------------------------------
1 | import { Record, List } from 'immutable';
2 | import PersonalInfoItemFieldDefinition from './PersonalInfoItemFieldDefinition';
3 |
4 | /** 設問定義の設問(PersonalInfoItem)の表示形式のセット */
5 | export const PersonalInfoItemDisplayTypeRecord = Record({
6 | _id: null, // ID
7 | label: null, // 選択肢の日本語表記
8 | personalItemFields: List(), // フィールド配列
9 | });
10 |
11 | export default class PersonalInfoItemDisplayTypeDefinition extends PersonalInfoItemDisplayTypeRecord {
12 | static create(displayType) {
13 | let fieldDefinitions = List();
14 | if (displayType.fields) {
15 | fieldDefinitions = displayType.fields.map((field) => {
16 | return new PersonalInfoItemFieldDefinition({ _id: field.id, label: field.label, outputType: field.outputType, prependValue: field.prependValue });
17 | });
18 | }
19 | return new PersonalInfoItemDisplayTypeDefinition({ _id: displayType.id, label: displayType.label, personalItemFields: List(fieldDefinitions) });
20 | }
21 |
22 | getId() {
23 | return this.get('_id');
24 | }
25 |
26 | getLabel() {
27 | return this.get('label');
28 | }
29 |
30 | getPersonalItemFields() {
31 | return this.get('personalItemFields');
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/runtime/reducers.js:
--------------------------------------------------------------------------------
1 | import Raven from 'raven-js';
2 | import * as C from '../constants/actions';
3 |
4 | export default function reducer(state, action) {
5 | try {
6 | switch (action.type) {
7 | case '@@INIT':
8 | state.getSurvey().refreshReplacer();
9 | return state;
10 | default:
11 | return state.update('runtime', (runtime) => {
12 | switch (action.type) {
13 | case C.RESTART:
14 | return runtime.restart(state.getSurvey());
15 | case C.CHANGE_CURRENT_NODE_ID:
16 | return runtime.setCurrentNodeId(action.nodeId);
17 | case C.NEXT_PAGE:
18 | return runtime.nextPage(state.getSurvey());
19 | case C.CHANGE_POST_ANSWER_STATUS:
20 | return runtime.updatePostAnswerStatus(action.postAnswerStatus);
21 | case C.CHANGE_ANSWERS:
22 | return runtime.updateAnswers(state.getSurvey(), action.answers);
23 | case C.REPLACE_ANSWERS:
24 | return runtime.replaceAnswers(state.getSurvey(), action.answers);
25 | default:
26 | return runtime;
27 | }
28 | });
29 | }
30 | } catch (e) {
31 | Raven.captureException(e);
32 | console.error(e);
33 | alert(e);
34 | return state;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/runtime/models/survey/questions/TextQuestionDefinition.js:
--------------------------------------------------------------------------------
1 | import cuid from 'cuid';
2 | import { List } from 'immutable';
3 | import BaseQuestionDefinition from './internal/BaseQuestionDefinition';
4 | import OutputDefinition from './internal/OutputDefinition';
5 | import surveyDevIdGeneratorInstance from '../../../SurveyDevIdGenerator';
6 |
7 | /** 設問定義:複数行テキスト */
8 | export default class TextQuestionDefinition extends BaseQuestionDefinition {
9 | static create(pageDevId) {
10 | return new TextQuestionDefinition({
11 | _id: cuid(),
12 | devId: surveyDevIdGeneratorInstance.generateForQuestion(pageDevId),
13 | dataType: 'Text',
14 | });
15 | }
16 |
17 | /** 出力に使用する名前を取得する */
18 | getOutputName() {
19 | return this.getId();
20 | }
21 |
22 | /** 設問が出力する項目の一覧を返す */
23 | getOutputDefinitions(pageNo, questionNo) {
24 | const name = this.getOutputName();
25 | const ret = List();
26 | return ret.push(new OutputDefinition({
27 | _id: name,
28 | questionId: this.getId(),
29 | devId: this.getDevId(),
30 | name,
31 | label: `${this.getPlainTitle()}`,
32 | dlLabel: `${this.getPlainTitle()}`,
33 | question: this,
34 | outputType: 'text',
35 | outputNo: BaseQuestionDefinition.createOutputNo(pageNo, questionNo),
36 | }));
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/editor/components/question_editors/QuestionEditors.js:
--------------------------------------------------------------------------------
1 | import CheckboxQuestionEditor from './CheckboxQuestionEditor';
2 | import RadioQuestionEditor from './RadioQuestionEditor';
3 | import SelectQuestionEditor from './SelectQuestionEditor';
4 | import MultiNumberQuestionEditor from './MultiNumberQuestionEditor';
5 | import SingleTextQuestionEditor from './SingleTextQuestionEditor';
6 | import TextQuestionEditor from './TextQuestionEditor';
7 | import MatrixQuestionEditor from './MatrixQuestionEditor';
8 | import DescriptionQuestionEditor from './DescriptionQuestionEditor';
9 | import ScreeningAgreementQuestionEditor from './ScreeningAgreementQuestionEditor';
10 | import ScheduleQuestionEditor from './ScheduleQuestionEditor';
11 | import PersonalInfoQuestionEditor from './PersonalInfoQuestionEditor';
12 |
13 | const editors = {
14 | CheckboxQuestionEditor,
15 | RadioQuestionEditor,
16 | SelectQuestionEditor,
17 | MultiNumberQuestionEditor,
18 | SingleTextQuestionEditor,
19 | TextQuestionEditor,
20 | MatrixQuestionEditor,
21 | DescriptionQuestionEditor,
22 | ScreeningAgreementQuestionEditor,
23 | ScheduleQuestionEditor,
24 | PersonalInfoQuestionEditor,
25 | };
26 |
27 | /** dataTypeからQuestionEditorを取得する */
28 | export function findQuestionEditorClass(className) {
29 | return editors[`${className}QuestionEditor`];
30 | }
31 |
--------------------------------------------------------------------------------
/__tests__/runtime/models/survey/NodeDefinition_spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import SurveyDesignerState from '../../../../lib/runtime/models/SurveyDesignerState';
3 | import sample1 from '../sample1.json';
4 |
5 | describe('NodeDefinition', () => {
6 | let state;
7 | beforeAll(() => {
8 | state = SurveyDesignerState.createFromJson(sample1);
9 | });
10 |
11 | describe('addChildCondition', () => {
12 | it('childConditionを追加できる', () => {
13 | const result = state.getSurvey().findBranch('B001').addChildCondition('C002', 1);
14 | expect(result.getIn(['conditions', 1, 'childConditions']).size).toBe(2);
15 | expect(result.getIn(['conditions', 1, 'childConditions', 1, 'outputId'])).toBe('');
16 | });
17 | });
18 |
19 | describe('removeChildCondition', () => {
20 | it('指定したchildConditionが削除できること', () => {
21 | const branch = state.getSurvey().findBranch('B001');
22 | const result = branch.addChildCondition('C002', 1);
23 | const childConditionId = result.getIn(['conditions', 1, 'childConditions', 1, '_id']);
24 | const result2 = result.removeChildCondition('C002', childConditionId);
25 | expect(result2.getIn(['conditions', 1, 'childConditions']).size).toBe(1);
26 | expect(result2.getIn(['conditions', 1, 'childConditions', 0, '_id'])).toBe('CC002');
27 | });
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/lib/runtime/components/questions/SelectQuestion.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 | /** 設問:単一選択肢(select) */
7 | export default class SelectQuestion extends Component {
8 | createItem(item) {
9 | return ;
10 | }
11 |
12 | render() {
13 | const { survey, replacer, question, options } = this.props;
14 | const name = question.getOutputName();
15 |
16 | return (
17 |
18 | { CommonQuestionParts.title(question, replacer) }
19 | { CommonQuestionParts.description(question, replacer) }
20 |
21 |
30 |
31 | { options.isShowDetail() ?
: null }
32 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/lib/runtime/models/survey/questions/SingleTextQuestionDefinition.js:
--------------------------------------------------------------------------------
1 | import cuid from 'cuid';
2 | import { List } from 'immutable';
3 | import BaseQuestionDefinition from './internal/BaseQuestionDefinition';
4 | import OutputDefinition from './internal/OutputDefinition';
5 | import surveyDevIdGeneratorInstance from '../../../SurveyDevIdGenerator';
6 |
7 | /** 設問定義:1行テキスト */
8 | export default class SingleTextQuestionDefinition extends BaseQuestionDefinition {
9 | static create(pageDevId) {
10 | return new SingleTextQuestionDefinition({
11 | _id: cuid(),
12 | devId: surveyDevIdGeneratorInstance.generateForQuestion(pageDevId),
13 | dataType: 'SingleText',
14 | });
15 | }
16 |
17 | /** 出力に使用する名前を取得する */
18 | getOutputName() {
19 | return this.getId();
20 | }
21 |
22 | /** 設問が出力する項目の一覧を返す */
23 | getOutputDefinitions(pageNo, questionNo) {
24 | const name = this.getOutputName();
25 | const ret = List();
26 | return ret.push(new OutputDefinition({
27 | _id: name,
28 | questionId: this.getId(),
29 | devId: this.getDevId(),
30 | name,
31 | label: `${this.getPlainTitle()}`,
32 | dlLabel: `${this.getPlainTitle()}`,
33 | question: this,
34 | outputType: 'text',
35 | outputNo: BaseQuestionDefinition.createOutputNo(pageNo, questionNo),
36 | }));
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/Runtime.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 { Provider } from 'react-redux';
8 | import 'tooltipster/dist/css/tooltipster.bundle.min.css';
9 | import EnqueteRuntimeApp from './runtime/containers/EnqueteRuntimeApp';
10 | import { configureStore } from './runtime/store';
11 | import SurveyDesignerState from './runtime/models/SurveyDesignerState';
12 | import ParsleyWrapper from './ParsleyWrapper';
13 | import { RequiredBrowserNoticeForRuntime } from './browserRequirements';
14 | import { isIELowerEquals } from './browserUtils';
15 | // /static/css/runtime.css を生成。別途CSSの読み込みが必要です
16 | import './runtime/css/runtime.scss';
17 |
18 | /** Runtimeのエントリポイント */
19 | export function Runtime(el, json) {
20 | if (isIELowerEquals(8)) {
21 | render(, el);
22 | return;
23 | }
24 |
25 | if (json.options.sentryInitFn) {
26 | json.options.sentryInitFn(Raven);
27 | }
28 |
29 | new ParsleyWrapper(el);
30 | const initialState = SurveyDesignerState.createFromJson(json);
31 | const store = configureStore(initialState);
32 |
33 | render(
34 |
35 |
36 | ,
37 | el,
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/lib/editor/components/editors/LogicalVariableEditor.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import React, { Component } from 'react';
3 | import { connect } from 'react-redux';
4 | import { Button } from 'react-bootstrap';
5 | import LogicalVariableForm from './LogicalVariableForm';
6 | import * as EditorActions from '../../actions';
7 |
8 | class LogicalVariableEditor extends Component {
9 | handleClickAddButton() {
10 | const { page, addLogicalVariable } = this.props;
11 | addLogicalVariable(page.getId());
12 | }
13 |
14 | render() {
15 | const { page } = this.props;
16 | const disabled = page.isEditDisabled();
17 | return (
18 |
19 |
20 |
21 |
22 | { page.getLogicalVariables().map(logicalVariable =>
) }
23 |
24 | );
25 | }
26 | }
27 |
28 | const stateToProps = state => ({
29 | state,
30 | });
31 | const actionsToProps = dispatch => ({
32 | addLogicalVariable: pageId => dispatch(EditorActions.addLogicalVariable(pageId)),
33 | });
34 |
35 | export default connect(
36 | stateToProps,
37 | actionsToProps,
38 | )(LogicalVariableEditor);
39 |
--------------------------------------------------------------------------------
/lib/runtime/models/survey/questions/SelectQuestionDefinition.js:
--------------------------------------------------------------------------------
1 | import cuid from 'cuid';
2 | import { List } from 'immutable';
3 | import ItemDefinition from './internal/ItemDefinition';
4 | import RadioQuestionDefinition from './RadioQuestionDefinition';
5 | import surveyDevIdGeneratorInstance from '../../../SurveyDevIdGenerator';
6 |
7 | /** 設問定義:単一選択肢(select) */
8 | export default class SelectQuestionDefinition extends RadioQuestionDefinition {
9 | static create(pageDevId, options) {
10 | const questionDevId = surveyDevIdGeneratorInstance.generateForQuestion(pageDevId);
11 | if (options && options.defaultItems) {
12 | return new SelectQuestionDefinition({
13 | _id: cuid(),
14 | devId: questionDevId,
15 | dataType: 'Select',
16 | items: List(options.defaultItems.map((label, index) => {
17 | const itemDevId = surveyDevIdGeneratorInstance.generateForItem(questionDevId);
18 | return ItemDefinition.create(itemDevId, index, `${index + 1}`, label);
19 | })),
20 | });
21 | }
22 | const itemDevId = surveyDevIdGeneratorInstance.generateForItem(questionDevId);
23 | return new SelectQuestionDefinition({
24 | _id: cuid(),
25 | devId: questionDevId,
26 | dataType: 'Select',
27 | items: List().push(ItemDefinition.create(itemDevId, 0, '1')),
28 | });
29 | }
30 |
31 | getOutputType() {
32 | return 'select';
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # survey-designer-js
[](https://codecov.io/gh/jirokun/survey-designer-js)
2 |
3 | アンケートデザインのためのJavaScript
4 |
5 | # 環境構築
6 | ## Node.jsのインストール
7 | 動作確認は7.4.0で行っています。nodebrewなどでインストールしてください。
8 |
9 | インストールの例
10 | ```bash
11 | brew install nodebrew
12 | mkdir -p ~/.nodebrew/src/v7.4.0
13 | nodebrew install-binary v7.4.0
14 | echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> .bashrc
15 | ```
16 |
17 | ## yarnのインストール
18 |
19 | ```
20 | npm install -g yarn
21 | ```
22 |
23 | ## 必要なライブラリのインストール
24 |
25 | ```
26 | yarn
27 | ```
28 |
29 | ## 環境変数の設定
30 | runtimeに必要なCSSを設定する必要があります。PJに合わせた設定をしてください
31 |
32 | ```
33 | cat RUNTIME_CSS_URL=,[css_url2] > $PJ_ROOT/.env
34 | ```
35 |
36 | ## 開発用の起動方法
37 |
38 | ```
39 | yarn start
40 | ```
41 |
42 | ## その他の起動コマンド
43 | |コマンド |説明 |
44 | |:-----------------|-------------------------------------------|
45 | | yarn start | 開発用サーバの起動。ファイルは出力しない |
46 | | yarn build | ビルド。distにファイルを出力する |
47 | | yarn build:watch | ビルド。変更を検知して自動テストを行う |
48 | | yarn test | テスト |
49 | | yarn test:watch | テスト。変更を検知して自動テストを行う |
50 |
--------------------------------------------------------------------------------
/lib/editor/css/allJavaScriptEditor.scss:
--------------------------------------------------------------------------------
1 | .allJavaScriptEditor {
2 | display: flex;
3 | flex-direction: column;
4 |
5 | &__title {
6 | padding: 0 10px 5px;
7 | font-size: 18px;
8 | height: 38px;
9 | margin: 5px 0 0 0;
10 | }
11 |
12 | &__btn-save, &__btn-close {
13 | margin: 0px 5px;
14 | }
15 |
16 | &__notification {
17 | -moz-animation: cssAnimation 0s ease-in 5s forwards;
18 | /* Firefox */
19 | -webkit-animation: cssAnimation 0s ease-in 5s forwards;
20 | /* Safari and Chrome */
21 | -o-animation: cssAnimation 0s ease-in 5s forwards;
22 | /* Opera */
23 | animation: cssAnimation 0s ease-in 5s forwards;
24 | -webkit-animation-fill-mode: forwards;
25 | animation-fill-mode: forwards;
26 | display: inline-block;
27 | margin: 0 0 0 10px;
28 | padding: 1px 5px;
29 | font-size: 13px;
30 | }
31 |
32 | @keyframes cssAnimation {
33 | to {
34 | padding: 0;
35 | margin: 0;
36 | width:0;
37 | height:0;
38 | overflow:hidden;
39 | }
40 | }
41 | @-webkit-keyframes cssAnimation {
42 | to {
43 | padding: 0;
44 | margin: 0;
45 | width:0;
46 | height:0;
47 | visibility:hidden;
48 | }
49 | }
50 |
51 | .ReactCodeMirror {
52 | height: calc(100% - 38px);
53 | .CodeMirror {
54 | height: 100%;
55 | border: #AAA solid 1px;
56 | box-sizing: border-box;
57 | }
58 | }
59 | }
60 |
61 |
62 |
--------------------------------------------------------------------------------
/lib/runtime/components/plain/ZeroSetting.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import S from 'string';
3 |
4 | /**
5 | * 進むボタンを押したときにnumberの入力要素で空のものは0を入力する
6 | */
7 | export default class ZeroSetting {
8 | constructor(el, survey, page, SurveyJS) {
9 | this.el = el;
10 | this.survey = survey;
11 | this.page = page;
12 | this.SurveyJS = SurveyJS;
13 | }
14 |
15 | handleValidateFormError() {
16 | const surveyZeroSetting = this.survey.getZeroSetting();
17 | const pageZeroSetting = this.page.getZeroSetting();
18 | if (pageZeroSetting === false) return;
19 | if (surveyZeroSetting === false && pageZeroSetting === null) return;
20 |
21 | // 進むボタンを連続で押してしまわないように一度押してから1秒は押せないように調整
22 | const $nextPage = $(this.el).find('.next-page');
23 | $nextPage.disable(true);
24 | setTimeout(() => $nextPage.disable(false), 1000);
25 |
26 | const outputDefinitions = this.page.getOutputDefinitionsFromThisPage(this.survey, true);
27 | outputDefinitions.filter(od => od.getOutputType() === 'number').forEach((od) => {
28 | const name = od.getName();
29 | const $el = $(`[name="${name}"]:visible:enabled`);
30 | if ($el.length !== 1 || !S($el.val()).isEmpty() || !$el.is('[data-parsley-required]')) return;
31 | $el.val('0');
32 | });
33 | }
34 |
35 | initialize() {
36 | const $parsley = $(this.el).parsley();
37 | $parsley.on('form:error', this.handleValidateFormError.bind(this));
38 | }
39 |
40 | deInitialize() {
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/lib/runtime/models/migrator/20171002000000_migrateCssUrls.js:
--------------------------------------------------------------------------------
1 | import { List } from 'immutable';
2 |
3 | /**
4 | * CSSの定義をmigrateする
5 | *
6 | * 修正前
7 | * 定義が存在しない
8 | *
9 | * 修正後
10 | * optionのdefaultCssの値が設定される
11 | */
12 | export function migrateCssUrls(survey, options) {
13 | if (!options) return survey;
14 | if (survey.getCssRuntimeUrls().size > 0 || survey.getCssPreviewUrls().size > 0 || survey.getCssDetailUrls().size > 0) return survey;
15 |
16 | const defaultCss = options.getDefaultCss();
17 |
18 | if (!defaultCss) {
19 | if (!ENV.RUNTIME_CSS_URL) throw new Error('環境変数にRUNTIME_CSS_URLが設定されていません');
20 | if (!ENV.PREVIEW_CSS_URL) throw new Error('環境変数にPREVIEW_CSS_URLが設定されていません');
21 | if (!ENV.DETAIL_CSS_URL) throw new Error('環境変数にDETAIL_CSS_URLが設定されていません');
22 | const runtimeUrls = ENV.RUNTIME_CSS_URL.split(/,/);
23 | const previewUrls = ENV.PREVIEW_CSS_URL.split(/,/);
24 | const detailUrls = ENV.DETAIL_CSS_URL.split(/,/);
25 | return survey
26 | .set('cssRuntimeUrls', List(runtimeUrls))
27 | .set('cssPreviewUrls', List(previewUrls))
28 | .set('cssDetailUrls', List(detailUrls));
29 | }
30 |
31 | const runtimeUrls = defaultCss.get('runtimeUrls');
32 | const previewUrls = defaultCss.get('previewUrls');
33 | const detailUrls = defaultCss.get('detailUrls');
34 | return survey
35 | .set('cssRuntimeUrls', List(runtimeUrls))
36 | .set('cssPreviewUrls', List(previewUrls))
37 | .set('cssDetailUrls', List(detailUrls));
38 | }
39 |
--------------------------------------------------------------------------------
/__tests__/runtime/models/survey/questions/internal/PersonalInfoItemDefinition_spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import PersonalInfoQuestionDefinition from '../../../../../../lib/runtime/models/survey/questions/PersonalInfoQuestionDefinition';
3 | import * as FIELDS from '../../../../../../lib/constants/personalInfoFields';
4 |
5 | describe('PersonalInfoItemDefinition', () => {
6 | describe('getDisplayTypeCandidates', () => {
7 | it('candidates を返す', () => {
8 | const pq = new PersonalInfoQuestionDefinition().updateDefaultItems();
9 |
10 | const item = pq.getItems().get(0);
11 | const candidates = item.getDisplayTypeCandidates();
12 |
13 | expect(candidates.size).toBe(3);
14 | expect(candidates.get(0).getId()).toBe('none');
15 | expect(candidates.get(1).getId()).toBe('1input');
16 | expect(candidates.get(2).getId()).toBe('2input');
17 | });
18 | });
19 |
20 | describe('getDisplayTypeCandidates', () => {
21 | it('fields を返す', () => {
22 | const pq = new PersonalInfoQuestionDefinition().updateDefaultItems();
23 |
24 | const item = pq.getItems().find(i => i.getRowType() === 'InterviewRow');
25 | const fields = item.getFields();
26 |
27 | expect(fields.size).toBe(3);
28 | expect(fields.get(0).getId()).toBe(FIELDS.interviewContactMobileTel);
29 | expect(fields.get(1).getId()).toBe(FIELDS.interviewContactHomeTel);
30 | expect(fields.get(2).getId()).toBe(FIELDS.interviewContactWorkTel);
31 | });
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/__tests__/runtime/models/survey/questions/ScheduleQuestionDefinition_spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import ScheduleQuestionDefinition from '../../../../../lib/runtime/models/survey/questions/ScheduleQuestionDefinition';
3 |
4 | describe('ScheduleQuestionDefinition', () => {
5 | describe('updateDefaultItems', () => {
6 | it('itemsに初期値を設定できる', () => {
7 | let scheduleQuestionDefinition = new ScheduleQuestionDefinition();
8 |
9 | scheduleQuestionDefinition = scheduleQuestionDefinition.updateDefaultItems();
10 | expect(scheduleQuestionDefinition.getItems().size).toBe(2);
11 | expect(scheduleQuestionDefinition.getItems().get(0).getLabel()).toBe('9月2日(月)');
12 | expect(scheduleQuestionDefinition.getItems().get(1).getLabel()).toBe('9月3日(火)');
13 | });
14 | });
15 |
16 | describe('updateDefaultSubItems', () => {
17 | it('subItemsに初期値を設定できる', () => {
18 | let scheduleQuestionDefinition = new ScheduleQuestionDefinition();
19 |
20 | scheduleQuestionDefinition = scheduleQuestionDefinition.updateDefaultSubItems();
21 | expect(scheduleQuestionDefinition.getSubItems().size).toBe(3);
22 | expect(scheduleQuestionDefinition.getSubItems().get(0).getLabel()).toBe('A.
午前
9:00~12:00');
23 | expect(scheduleQuestionDefinition.getSubItems().get(1).getLabel()).toBe('B.
午後
12:00~16:00');
24 | expect(scheduleQuestionDefinition.getSubItems().get(2).getLabel()).toBe('C.
夜間
16:00 以降');
25 | });
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/e2e_test/specs/questions/SelectQuestion.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 QuestionEditorPage = require('../../pages/QuestionEditorPage');
9 | const expect = require('chai').expect;
10 |
11 | describe('SelectQuestion', () => {
12 | let editorPage;
13 | beforeEach(() => {
14 | editorPage = new EditorPage();
15 | });
16 |
17 | /*
18 | describe('editor', () => {
19 | it('単一選択肢(select)を追加できる', () => {
20 | editorPage.addQuestion('ページ 1', 0, '単一選択肢(select)');
21 | editorPage.addQuestion('ページ 1', 0, '単一選択肢(select)');
22 | const questions = editorPage.findQuestionsInPage('ページ 1');
23 | expect(questions).to.be.a('array');
24 | expect(questions).to.have.lengthOf(2);
25 | expect(questions[0]).to.equal('1-1 単一選択肢(select)');
26 | expect(questions[1]).to.equal('1-2 単一選択肢(select)');
27 | });
28 |
29 | it('質問タイトル・補足・選択肢・表示制御が設定できる', () => {
30 | editorPage.addQuestion('ページ 1', 0, '単一選択肢(select)');
31 | const questionEditorPage = new QuestionEditorPage(editorPage, '1-1');
32 | expect(questionEditorPage.hasTitleEditor()).to.equal(true);
33 | expect(questionEditorPage.hasDescriptionEditor()).to.equal(true);
34 | expect(questionEditorPage.hasChoiceEditor()).to.equal(true);
35 | expect(questionEditorPage.hasVisibilityConditionEditor()).to.equal(true);
36 | });
37 | });
38 | */
39 | });
40 |
--------------------------------------------------------------------------------
/docs/images/save.json:
--------------------------------------------------------------------------------
1 | {
2 | "imageList": [
3 | {
4 | "title": "",
5 | "thumbnailUrl": "https://fastsurvey-image-manager-qa.s3-ap-northeast-1.amazonaws.com/images/31c641d9-87e2-45fc-8d62-3976a3506bb5.thumb.png",
6 | "imageUrl": "https://fastsurvey-image-manager-qa.s3-ap-northeast-1.amazonaws.com/images/31c641d9-87e2-45fc-8d62-3976a3506bb5.png",
7 | "width": 600,
8 | "height": 109,
9 | "originalWidth": 1546,
10 | "originalHeight": 282,
11 | "format": "PNG",
12 | "createdBy": "9da738d0-59cb-11e0-a80d-0024e8bd0a22",
13 | "id": 16,
14 | "_id": "a1a07310-76b2-11e7-a2ea-d215a84a0c22",
15 | "_createdAt": "2017/08/01 21:14:18",
16 | "_updatedAt": "2017/08/01 21:14:18",
17 | "_rev": 1,
18 | "document": "{}"
19 | },
20 | {
21 | "title": "",
22 | "thumbnailUrl": "https://fastsurvey-image-manager-qa.s3-ap-northeast-1.amazonaws.com/images/31c641d9-87e2-45fc-8d62-3976a3506bb5.thumb.png",
23 | "imageUrl": "https://fastsurvey-image-manager-qa.s3-ap-northeast-1.amazonaws.com/images/31c641d9-87e2-45fc-8d62-3976a3506bb5.png",
24 | "width": 600,
25 | "height": 109,
26 | "originalWidth": 1546,
27 | "originalHeight": 282,
28 | "format": "PNG",
29 | "createdBy": "9da738d0-59cb-11e0-a80d-0024e8bd0a22",
30 | "id": 17,
31 | "_id": "b1a07310-76b2-11e7-a2ea-d215a84a0c22",
32 | "_createdAt": "2017/08/01 21:14:18",
33 | "_updatedAt": "2017/08/01 21:14:18",
34 | "_rev": 1,
35 | "document": "{}"
36 | }
37 | ]
38 | }
--------------------------------------------------------------------------------
/lib/runtime/models/options/CssOption.js:
--------------------------------------------------------------------------------
1 | import { Record, List } from 'immutable';
2 | import cuid from 'cuid';
3 |
4 | export const CssOptionRecord = Record({
5 | _id: null,
6 | title: null,
7 | runtimeUrls: List(),
8 | previewUrls: List(),
9 | detailUrls: List(),
10 | });
11 |
12 | export default class CssOption extends CssOptionRecord {
13 | static create(title, runtimeUrls, previewUrls, detailUrls) {
14 | return new CssOption({ _id: cuid(), title, runtimeUrls, previewUrls, detailUrls });
15 | }
16 |
17 | getId() {
18 | return this.get('_id');
19 | }
20 |
21 | getTitle() {
22 | return this.get('title');
23 | }
24 |
25 | getRuntimeUrls() {
26 | return this.get('runtimeUrls');
27 | }
28 |
29 | getPreviewUrls() {
30 | return this.get('previewUrls');
31 | }
32 |
33 | getDetailUrls() {
34 | return this.get('detailUrls');
35 | }
36 |
37 | matchRuntimeUrls(runtimeUrls) {
38 | const inStr = runtimeUrls.toArray().sort().toString();
39 | const currentStr = this.getRuntimeUrls().toArray().sort().toString();
40 | return inStr === currentStr;
41 | }
42 |
43 | matchPreviewUrls(previewUrls) {
44 | const inStr = previewUrls.toArray().sort().toString();
45 | const currentStr = this.getPreviewUrls().toArray().sort().toString();
46 | return inStr === currentStr;
47 | }
48 |
49 | matchDetailUrls(detailUrls) {
50 | const inStr = detailUrls.toArray().sort().toString();
51 | const currentStr = this.getDetailUrls().toArray().sort().toString();
52 | return inStr === currentStr;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lib/editor/tinymce_plugins/reference.js:
--------------------------------------------------------------------------------
1 | import tinymce from 'tinymce';
2 | import S from 'string';
3 | import '../../constants/tinymce_ja';
4 |
5 | tinymce.PluginManager.add('reference_answer', (editor) => {
6 | editor.addButton('reference_answer', {
7 | text: '再掲',
8 | icon: false,
9 | onclick: () => {
10 | const survey = editor.settings.survey;
11 | const outputDefinitions = editor.settings.outputDefinitions;
12 | const outputDefinitionIdValues = outputDefinitions
13 | .map(od => ({ text: od.getLabelWithOutputNo(), value: od.getId() })).toArray();
14 | editor.windowManager.open({
15 | title: '再掲',
16 | // bodyの作り方は次のURLを参照
17 | // https://makina-corpus.com/blog/metier/2016/how-to-create-a-custom-dialog-in-tinymce-4
18 | body: [
19 | { type: 'listbox', name: 'outputDefinitionId', label: '参照する設問', values: outputDefinitionIdValues },
20 | ],
21 | onsubmit: (e) => {
22 | const id = e.data.outputDefinitionId;
23 | if (S(id).isEmpty()) return; // 選択されていない場合は何もしない
24 | const outputDefinition = outputDefinitions.find(od => od.getId() === id);
25 | const type = outputDefinition.getOutputType();
26 | if (type === 'checkbox' || outputDefinition.isOutputTypeSingleChoice()) editor.insertContent(`{{${id}.answer_label}}`);
27 | else editor.insertContent(`{{${id}.answer}}`);
28 | const replacer = survey.getReplacer();
29 | editor.setContent(replacer.id2No(editor.getContent()));
30 | },
31 | });
32 | },
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/docs/images/images.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title": "",
4 | "thumbnailUrl": "https://user-images.githubusercontent.com/1380767/29124562-3abbe08c-7d54-11e7-9781-ace2e0535437.png",
5 | "imageUrl": "https://user-images.githubusercontent.com/1380767/29124563-3ac0cc78-7d54-11e7-96ba-393b996d5b71.png",
6 | "originalUrl": "https://user-images.githubusercontent.com/1380767/29065210-53e36aa6-7c66-11e7-9bc5-b6a735ad1d87.png",
7 | "width": 600,
8 | "height": 109,
9 | "originalWidth": 1546,
10 | "originalHeight": 282,
11 | "format": "PNG",
12 | "createdBy": "9da738d0-59cb-11e0-a80d-0024e8bd0a22",
13 | "id": 1,
14 | "_id": "f1a07310-76b2-11e7-a2ea-d215a84a0c22",
15 | "_createdAt": "2017/08/01 21:14:18",
16 | "_updatedAt": "2017/08/01 21:14:18",
17 | "_rev": 1,
18 | "document": "{}"
19 | },
20 | {
21 | "title": "",
22 | "thumbnailUrl": "https://user-images.githubusercontent.com/1380767/29124562-3abbe08c-7d54-11e7-9781-ace2e0535437.png",
23 | "imageUrl": "https://user-images.githubusercontent.com/1380767/29124563-3ac0cc78-7d54-11e7-96ba-393b996d5b71.png",
24 | "originalUrl": "https://user-images.githubusercontent.com/1380767/29065210-53e36aa6-7c66-11e7-9bc5-b6a735ad1d87.png",
25 | "width": 600,
26 | "height": 109,
27 | "originalWidth": 1546,
28 | "originalHeight": 282,
29 | "format": "PNG",
30 | "createdBy": "8da738d0-59cb-11e0-a80d-0024e8bd0a22",
31 | "id": 2,
32 | "_id": "a3207310-76b2-11e7-a2ea-d215a84a0c22",
33 | "_createdAt": "2017/08/01 21:14:18",
34 | "_updatedAt": "2017/08/01 21:14:18",
35 | "_rev": 1,
36 | "document": "{}"
37 | }
38 | ]
--------------------------------------------------------------------------------
/e2e_test/pages/ResponsePage.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | /* global browser */
3 |
4 | class ResponsePage {
5 | getAnswers() {
6 | return browser.execute(() => {
7 | const readableAnswers = {};
8 | SurveyJS.surveyManager.survey.getAllOutputDefinitions().forEach(function(od) {
9 | readableAnswers[od.getOutputNo()] = SurveyJS.surveyManager.answers[od.getName()];
10 | });
11 | return readableAnswers;
12 | }).value;
13 | }
14 |
15 | transformAnswers(answers) {
16 | return browser.execute((a) => {
17 | const readableAnswers = {};
18 | SurveyJS.surveyManager.survey.getAllOutputDefinitions().forEach(function(od) {
19 | readableAnswers[od.getOutputNo()] = a[od.getName()];
20 | });
21 | return readableAnswers;
22 | }, answers).value;
23 | }
24 |
25 | findElementsByOutputNo(outputNo) {
26 | return browser.elements(`*[data-output-no="${outputNo}"]`);
27 | }
28 |
29 | clickByOutputNo(outputNo, index = 0) {
30 | const element = this.findElementsByOutputNo(outputNo).value[index];
31 | browser.pause(10);
32 | element.click();
33 | }
34 |
35 | setValue(outputNo, value) {
36 | const element = this.findElementsByOutputNo(outputNo).value[0];
37 | element.setValue(value);
38 | }
39 |
40 | nextPage() {
41 | const nextButton = browser.elements('button').value.find(button => button.getText() === '進む');
42 | nextButton.click();
43 | }
44 |
45 | getPageLabel() {
46 | return browser.elements('.finisher-no,.page-no').getText();
47 | }
48 |
49 | close() {
50 | browser.close();
51 | browser.window(browser.windowHandles().value[0]);
52 | }
53 | }
54 |
55 | module.exports = ResponsePage;
56 |
--------------------------------------------------------------------------------
/lib/editor/components/Editor.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import React, { Component } from 'react';
3 | import { connect } from 'react-redux';
4 | import * as EditorActions from '../actions';
5 | import PageEditor from './editors/PageEditor';
6 | import BranchEditor from './editors/BranchEditor';
7 | import FinisherEditor from './editors/FinisherEditor';
8 |
9 | /** エディタの領域を描画する */
10 | class Editor extends Component {
11 | /** currentNodeの種類に対応するeditorを作成する */
12 | createEditor() {
13 | const { runtime, survey } = this.props;
14 | const node = runtime.findCurrentNode(survey);
15 | if (node === null) {
16 | return 削除されたノードです
;
17 | } else if (node.isPage()) {
18 | const page = runtime.findCurrentPage(survey);
19 | return ;
20 | } else if (node.isBranch()) {
21 | const branch = runtime.findCurrentBranch(survey);
22 | return ;
23 | } else if (node.isFinisher()) {
24 | const finisher = runtime.findCurrentFinisher(survey);
25 | return ;
26 | }
27 | throw new Error(`不明なnodeTypeです。type: ${node.getType()}`);
28 | }
29 |
30 | render() {
31 | return {this.createEditor()}
;
32 | }
33 | }
34 |
35 | const stateToProps = state => ({
36 | survey: state.getSurvey(),
37 | runtime: state.getRuntime(),
38 | view: state.getViewSetting(),
39 | });
40 | const actionsToProps = dispatch => ({
41 | changeCodemirror: value => dispatch(EditorActions.changeCodemirror(value)),
42 | });
43 |
44 | export default connect(
45 | stateToProps,
46 | actionsToProps,
47 | )(Editor);
48 |
--------------------------------------------------------------------------------
/e2e_test/specs/JavaScriptEvent.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | /* global browser */
3 |
4 | const expect = require('chai').expect;
5 | const clearRequireCache = require('../clearRequireCache');
6 | clearRequireCache();
7 |
8 | const EditorPage = require('../pages/EditorPage');
9 | const ResponsePage = require('../pages/ResponsePage');
10 | const eventSurvey = require('./eventSurvey.json');
11 |
12 | describe('JavaScriptEvent', () => {
13 | let editorPage;
14 | let responsePage;
15 | beforeEach(() => {
16 | editorPage = new EditorPage();
17 | editorPage.loadSurvey(eventSurvey);
18 | editorPage.preview();
19 | responsePage = new ResponsePage();
20 |
21 | responsePage.clickByOutputNo('1-1-1');
22 | responsePage.nextPage();
23 |
24 | responsePage.setValue('2-1-1', 10);
25 | responsePage.nextPage();
26 | });
27 |
28 | afterEach(() => {
29 | responsePage.close();
30 | });
31 |
32 | it('pageUnloadではロジック変数の値を取得することができる', () => {
33 | const page2PageUnloadAnswers = responsePage.transformAnswers(browser.execute(() => window.test.page2PageUnloadAnswers).value);
34 | expect(page2PageUnloadAnswers['1-1-1']).to.equal('1');
35 | expect(page2PageUnloadAnswers['2-1-1']).to.equal('10');
36 | expect(page2PageUnloadAnswers['2-L-000']).to.equal(20);
37 | });
38 |
39 | it('pageLoadでは前のページのpageUnloadのJavaScriptで設定した値を取得することができる', () => {
40 | const page3PageLoadAnswers = responsePage.transformAnswers(browser.execute(() => window.test.page3PageLoadAnswers).value);
41 | expect(page3PageLoadAnswers['1-1-1']).to.equal('1');
42 | expect(page3PageLoadAnswers['2-1-1']).to.equal('10');
43 | expect(page3PageLoadAnswers['2-L-000']).to.equal('abc');
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/lib/Editor.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 { Provider } from 'react-redux';
8 | import EnqueteEditorApp from './editor/containers/EnqueteEditorApp';
9 | import { configureStore } from './editor/store';
10 | import SurveyDesignerState from './runtime/models/SurveyDesignerState';
11 | import ParsleyWrapper from './ParsleyWrapper';
12 | import { RequiredBrowserNoticeForEditor } from './browserRequirements';
13 | import { isIELowerEquals } from './browserUtils';
14 | import './editor/tinymce_plugins/reference';
15 | import './editor/tinymce_plugins/imageManager';
16 | import './editor/css/editor.scss';
17 |
18 | /** 編集画面のエントリポイント */
19 | export function Editor(el, json) {
20 | if (isIELowerEquals(10)) {
21 | render(, el);
22 | return;
23 | }
24 |
25 | if (json.options.sentryInitFn) {
26 | json.options.sentryInitFn(Raven);
27 | }
28 |
29 | // デフォルトのオプション
30 | const defaultOptions = {
31 | visibilityConditionDisabled: true,
32 | };
33 |
34 | json.options = Object.assign(defaultOptions, json.options);
35 |
36 | new ParsleyWrapper(el);
37 | let initialState = SurveyDesignerState.createFromJson(json);
38 | if (initialState.getSurvey().getNodes().size === 0) {
39 | // nodesがない場合には初期データを追加する
40 | initialState = initialState.update('survey', survey => survey.addNode(0, 'page').addNode(1, 'finisher'));
41 | }
42 | const store = configureStore(initialState);
43 |
44 | render(
45 |
46 |
47 | ,
48 | el,
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/lib/editor/components/editors/MenuConfigModal.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import React, { Component } from 'react';
3 | import { connect } from 'react-redux';
4 | import { Modal } from 'react-bootstrap';
5 | import * as Action from '../../actions';
6 |
7 | /** 画面上部のMenuの設定 */
8 | class MenuConfigModal extends Component {
9 | handleChangeCssUrls(event) {
10 | const { options, changeSurveyCssOption } = this.props;
11 |
12 | const id = event.target.value;
13 | const cssOption = options.getCssOptionById(id);
14 | changeSurveyCssOption(cssOption);
15 | }
16 |
17 | render() {
18 | const { survey, options, view, hideMenuConfig } = this.props;
19 |
20 | const id = options.getCssOptionIdByUrls(survey.getCssRuntimeUrls(), survey.getCssPreviewUrls(), survey.getCssDetailUrls()) || '';
21 |
22 | return (
23 | hideMenuConfig()}>
24 |
25 | CSSの選択:
26 |
29 |
30 |
31 |
32 | );
33 | }
34 | }
35 |
36 | const stateToProps = state => ({
37 | survey: state.getSurvey(),
38 | options: state.getOptions(),
39 | view: state.getViewSetting(),
40 | });
41 | const actionsToProps = dispatch => ({
42 | changeSurveyCssOption: cssOption => dispatch(Action.changeSurveyCssOption(cssOption)),
43 | hideMenuConfig: () => dispatch(Action.changeShowMenuConfig(false)),
44 | });
45 |
46 | export default connect(
47 | stateToProps,
48 | actionsToProps,
49 | )(MenuConfigModal);
50 |
--------------------------------------------------------------------------------
/lib/Detail.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 { Provider } from 'react-redux';
8 | import 'tooltipster/dist/css/tooltipster.bundle.min.css';
9 | import EnqueteDetailApp from './preview/containers/EnqueteDetailApp';
10 | import { configureStore } from './runtime/store';
11 | import SurveyDesignerState from './runtime/models/SurveyDesignerState';
12 | import ParsleyWrapper from './ParsleyWrapper';
13 | import { RequiredBrowserNoticeForRuntime } from './browserRequirements';
14 | import { isIELowerEquals } from './browserUtils';
15 | // /static/css/detail.css を生成。別途CSSの読み込みが必要です
16 | import './preview/css/detail.scss';
17 |
18 | /** 初期stateを取得する */
19 | export function getInitialState(json) {
20 | return SurveyDesignerState.createFromJson(json);
21 | }
22 |
23 | /** 1ページにすべてのページ、分岐、終了ページを詳細表示するページのエントリポイント */
24 | export function Detail(el, json) {
25 | if (isIELowerEquals(8)) {
26 | render(, el);
27 | return null;
28 | }
29 |
30 | if (json.options.sentryInitFn) {
31 | json.options.sentryInitFn(Raven);
32 | }
33 |
34 | // 詳細プレビューのデフォルトのオプション
35 | const defaultOptions = {
36 | showPageNo: true,
37 | visibilityConditionDisabled: true,
38 | };
39 | json.options = Object.assign(defaultOptions, json.options);
40 |
41 | const parsleyWrapper = new ParsleyWrapper(el);
42 | const initialState = getInitialState(json);
43 | const store = configureStore(initialState);
44 |
45 | render(
46 |
47 |
48 | ,
49 | el,
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/lib/runtime/models/survey/FinisherDefinition.js:
--------------------------------------------------------------------------------
1 | import cuid from 'cuid';
2 | import { Record, List } from 'immutable';
3 | import ValidationErrorDefinition from '../ValidationErrorDefinition';
4 |
5 | export const FinisherDefinitionRecord = Record({
6 | _id: null,
7 | finishType: 'SCREEN', // 終了タイプ。COMPLETEまたはSCREEN
8 | point: 0, // 付与するポイント
9 | html: 'ご回答ありがとうございました。
またのご協力をお待ちしております。', // 画面に表示するHTML
10 | });
11 |
12 | /** Finisherの定義 */
13 | export default class FinisherDefinition extends FinisherDefinitionRecord {
14 | static create() {
15 | return new FinisherDefinition({ _id: cuid() });
16 | }
17 |
18 | getId() {
19 | return this.get('_id');
20 | }
21 |
22 | getFinishType() {
23 | return this.get('finishType');
24 | }
25 |
26 | getPoint() {
27 | return this.get('point');
28 | }
29 |
30 | getHtml() {
31 | return this.get('html');
32 | }
33 |
34 | isComplete() {
35 | return this.get('finishType') === 'COMPLETE';
36 | }
37 |
38 | validate(survey) {
39 | let errors = List();
40 | const replacer = survey.getReplacer();
41 | const node = survey.findNodeFromRefId(this.getId());
42 | const outputDefinitions = survey.findPrecedingOutputDefinition(node.getId(), false);
43 | if (!replacer.validate(this.getHtml(), outputDefinitions)) errors = errors.push(ValidationErrorDefinition.createError('存在しない参照があります'));
44 |
45 | return errors;
46 | }
47 |
48 | // ------------------------- 更新系 -----------------------------
49 | /** finisherの属性の更新 */
50 | updateFinisherAttribute(attributeName, value, replacer) {
51 | return this.set(attributeName, replacer.no2Id(value));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/lib/Preview.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 $ from 'jquery';
7 | import { render } from 'react-dom';
8 | import { Provider } from 'react-redux';
9 | import 'tooltipster/dist/css/tooltipster.bundle.min.css';
10 | import EnquetePreviewApp from './preview/containers/EnquetePreviewApp';
11 | import { configureStore } from './runtime/store';
12 | import SurveyDesignerState from './runtime/models/SurveyDesignerState';
13 | import ParsleyWrapper from './ParsleyWrapper';
14 | import { RequiredBrowserNoticeForRuntime } from './browserRequirements';
15 | import { isIELowerEquals } from './browserUtils';
16 | // /static/css/preview.css を生成。別途CSSの読み込みが必要です
17 | import './preview/css/preview.scss';
18 |
19 | /** プレビュー画面のエントリポイント */
20 | export function Preview(el, json) {
21 | if (isIELowerEquals(8)) {
22 | render(, el);
23 | return;
24 | }
25 |
26 | if (json.options.sentryInitFn) {
27 | json.options.sentryInitFn(Raven);
28 | }
29 |
30 | // Preview画面のコンソールでjQueryを使えるようにする
31 | window.$ = $;
32 | window.jQuery = $;
33 |
34 | // プレビューのデフォルトのオプション
35 | const defaultOptions = {
36 | useBrowserHistory: true,
37 | showPageNo: true,
38 | exposeSurveyJS: true,
39 | };
40 | json.options = Object.assign(defaultOptions, json.options);
41 |
42 | const parsleyWrapper = new ParsleyWrapper(el);
43 | const initialState = SurveyDesignerState.createFromJson(json);
44 | const store = configureStore(initialState);
45 | el.innerHTML = ''; // 一度削除
46 |
47 | render(
48 |
49 |
50 | ,
51 | el,
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/lib/runtime/models/survey/questions/internal/NumberValidationDefinition.js:
--------------------------------------------------------------------------------
1 | import { Record, List } from 'immutable';
2 | import cuid from 'cuid';
3 | import S from 'string';
4 | import ValidationErrorDefinition from '../../../ValidationErrorDefinition';
5 |
6 | /** 数値のValidation */
7 | export const NumberValidationDefinitionRecord = Record({
8 | _id: null, // 内部で使用するID
9 | value: '', // 比較する値
10 | operator: '', // 対応する値
11 | });
12 |
13 | export default class NumberValidationDefinition extends NumberValidationDefinitionRecord {
14 | static create(opts) {
15 | return new NumberValidationDefinition(Object.assign({}, opts, { _id: cuid() }));
16 | }
17 |
18 | getId() {
19 | return this.get('_id');
20 | }
21 |
22 | getValue() {
23 | return this.get('value');
24 | }
25 |
26 | getOperator() {
27 | return this.get('operator');
28 | }
29 |
30 | validate(survey, page, question) {
31 | const replacer = survey.getReplacer();
32 | const errors = [];
33 | const node = survey.findNodeFromRefId(page.getId());
34 | const outputDefinitions = survey.findPrecedingOutputDefinition(node.getId(), true, true);
35 | if (S(this.getValue()).isEmpty()) {
36 | errors.push(ValidationErrorDefinition.createError('数値制限で値が設定されていません'));
37 | } else if (!replacer.validate(this.getValue(), outputDefinitions)) {
38 | errors.push(ValidationErrorDefinition.createError('数値制限で不正な参照が設定されています'));
39 | }
40 | if (S(this.getOperator()).isEmpty()) {
41 | errors.push(ValidationErrorDefinition.createError('数値制限で比較方法が設定されていません'));
42 | }
43 | return List(errors);
44 | }
45 |
46 | /** Immutable.jsが等値かどうかを判断するためのメソッド */
47 | equals(other) {
48 | return other.getValue() === this.getValue()
49 | && other.getOperator() === this.getOperator();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/__tests__/runtime/models/runtime/RuntimeValue_spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import SurveyDesignerState from '../../../../lib/runtime/models/SurveyDesignerState';
3 | import sample1 from '../sample1.json';
4 |
5 | describe('RuntimeValue', () => {
6 | let state;
7 | beforeAll(() => {
8 | state = SurveyDesignerState.createFromJson(sample1);
9 | });
10 |
11 | describe('findCurrentPage', () => {
12 | it('現在のpageを返す', () => {
13 | const runtime = state.getRuntime();
14 | const page = runtime.findCurrentPage(state.getSurvey());
15 | expect(page).not.toBeNull();
16 | expect(page.getId()).toBe('P001');
17 | });
18 | });
19 |
20 | describe('findCurrentNode', () => {
21 | it('現在のnodeを返す', () => {
22 | const runtime = state.getRuntime();
23 | const node = runtime.findCurrentNode(state.getSurvey());
24 | expect(node).not.toBeNull();
25 | expect(node.getId()).toBe('F001');
26 | });
27 | });
28 |
29 | describe('findCurrentBranch', () => {
30 | it('現在のbranchを返す', () => {
31 | const runtime = state.getRuntime().setCurrentNodeId('F002');
32 | const branch = runtime.findCurrentBranch(state.getSurvey());
33 | expect(branch).not.toBeNull();
34 | expect(branch.getId()).toBe('B001');
35 | });
36 | });
37 |
38 | describe('submitPage', () => {
39 | it('入力値が追加される', () => {
40 | const survey = state.getSurvey();
41 | survey.refreshReplacer();
42 | const result1 = state.getRuntime().updateAnswers(survey, { '1__value1': 'abc' });
43 | expect(result1.getAnswers().get('1__value1')).toBe('abc');
44 | const result2 = result1.updateAnswers(state.getSurvey(), { q2: 'def' });
45 | expect(result2.getAnswers().get('1__value1')).toBe('abc');
46 | expect(result2.getAnswers().get('q2')).toBe('def');
47 | });
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/lib/jquery.plugins.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 |
3 | /**
4 | * jQuery extended functions
5 | */
6 |
7 | /** 要素の有効無効を切り替える */
8 | $.fn.disable = function disable(disabled) {
9 | if (this.length === 0) return this;
10 | this.prop('disabled', disabled);
11 | if (disabled) {
12 | this.addClass('disabled');
13 | // すでにエラーが出ているものを消すためにfield:successをtriggerする
14 | const instances = this.parsley();
15 | if (Array.isArray(instances)) {
16 | instances.forEach((instance) => {
17 | instance.trigger('field:success');
18 | });
19 | } else {
20 | const instance = instances;
21 | instance.trigger('field:success');
22 | }
23 | } else {
24 | this.removeClass('disabled');
25 | }
26 | return this;
27 | };
28 |
29 | /** 表示されており、なおかつdisabledがfalseかどうかを確認する */
30 | $.fn.isEnabled = function isEnabled() {
31 | return this.is(':visible:enabled');
32 | };
33 |
34 | /** 要素のoutputNoを取得する */
35 | $.fn.no = function no() {
36 | return $(this).data('output-no');
37 | };
38 |
39 | /**
40 | * jQuery selectors
41 | */
42 | $.extend($.expr[':'], {
43 | /**
44 | * outputNoでセレクタを指定出来るようにする。
45 | * @example $(':no(1-1-1)')
46 | **/
47 | no: (elem, idx, args) => {
48 | const regex = new RegExp(`^${args[3]}$`);
49 | return regex.test($(elem).data('output-no'));
50 | },
51 |
52 | /**
53 | * valでセレクタを指定出来るようにする。(ラジオボタン、コンボボックス)
54 | * @example $(':val(1)')
55 | **/
56 | val: (elem, idx, args) => {
57 | const regex = new RegExp(`^${args[3]}$`);
58 | return (elem.type === 'radio' || elem.tagName.toLowerCase() === 'select') && regex.test(elem.value);
59 | },
60 |
61 | /**
62 | * dev-idでセレクタを指定出来るようにする。
63 | * @example $(':dev(xxx_yyy)')
64 | **/
65 | dev: (elem, idx, args) => {
66 | const regex = new RegExp(`^${args[3]}$`);
67 | return regex.test($(elem).data('dev-id'));
68 | },
69 | });
70 |
71 |
72 |
--------------------------------------------------------------------------------
/lib/editor/css/bootstrap.less:
--------------------------------------------------------------------------------
1 | @import "variables.less";
2 |
3 | @import "~bootstrap/less/mixins.less";
4 |
5 | // Reset and dependencies
6 | @import "~bootstrap/less/normalize.less";
7 | @import "~bootstrap/less/print.less";
8 | @import "~bootstrap/less/glyphicons.less";
9 |
10 | // Core CSS
11 | @import "~bootstrap/less/scaffolding.less";
12 | @import "~bootstrap/less/type.less";
13 | @import "~bootstrap/less/code.less";
14 | @import "~bootstrap/less/grid.less";
15 | @import "~bootstrap/less/tables.less";
16 | @import "~bootstrap/less/forms.less";
17 | @import "~bootstrap/less/buttons.less";
18 |
19 | // Components
20 | @import "~bootstrap/less/component-animations.less";
21 | @import "~bootstrap/less/dropdowns.less";
22 | @import "~bootstrap/less/button-groups.less";
23 | @import "~bootstrap/less/input-groups.less";
24 | @import "~bootstrap/less/navs.less";
25 | @import "~bootstrap/less/navbar.less";
26 | @import "~bootstrap/less/breadcrumbs.less";
27 | @import "~bootstrap/less/pagination.less";
28 | @import "~bootstrap/less/pager.less";
29 | @import "~bootstrap/less/labels.less";
30 | @import "~bootstrap/less/badges.less";
31 | @import "~bootstrap/less/jumbotron.less";
32 | @import "~bootstrap/less/thumbnails.less";
33 | @import "~bootstrap/less/alerts.less";
34 | @import "~bootstrap/less/progress-bars.less";
35 | @import "~bootstrap/less/media.less";
36 | @import "~bootstrap/less/list-group.less";
37 | @import "~bootstrap/less/panels.less";
38 | @import "~bootstrap/less/responsive-embed.less";
39 | @import "~bootstrap/less/wells.less";
40 | @import "~bootstrap/less/close.less";
41 |
42 | // Components w/ JavaScript
43 | @import "~bootstrap/less/modals.less";
44 | @import "~bootstrap/less/tooltip.less";
45 | @import "~bootstrap/less/popovers.less";
46 | @import "~bootstrap/less/carousel.less";
47 |
48 | // Utility classes
49 | @import "~bootstrap/less/utilities.less";
50 | @import "~bootstrap/less/responsive-utilities.less";
--------------------------------------------------------------------------------
/lib/runtime/SurveyDevIdGenerator.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable jest/no-exclusive-tests */
2 | import { List } from 'immutable';
3 |
4 | const SURVEY_ID_LENGTH = 3; // 文字列の長さ
5 | const SURVEY_ID_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; // 利用可能な文字
6 |
7 | export class SurveyDevIdGenerator {
8 | constructor() {
9 | this.existIds = new List();
10 | }
11 |
12 | // Id のリストを保持
13 | updateExistingIds(survey) {
14 | this.existIds = survey.getAllPageDevIds().concat(
15 | survey.getAllQuestionDevIds().map(devId => devId.split('_').slice(-1)[0]),
16 | survey.getAllItemDevIds().map(devId => devId.split('_').slice(-1)[0]),
17 | survey.getAllSubItemDevIds().map(devId => devId.split('_').slice(-1)[0]),
18 | );
19 | return this.existIds;
20 | }
21 |
22 | generateForPage() {
23 | const newId = this.getUniqStr();
24 | return newId;
25 | }
26 |
27 | generateForQuestion(pageDevId) {
28 | const newDevId = this.getUniqStr(this.existIds);
29 | return `${pageDevId}_${newDevId}`;
30 | }
31 |
32 | generateForItem(questionDevId) {
33 | const newDevId = this.getUniqStr(this.existIds);
34 | return `${questionDevId}_${newDevId}`;
35 | }
36 |
37 | getUniqStr() {
38 | let newId = null;
39 | while (true) {
40 | newId = this.generateIdStr();
41 | if (!this.existIds.some(id => id === newId)) { // eslint-disable-line no-loop-func
42 | this.existIds = this.existIds.push(newId);
43 | return newId;
44 | }
45 | }
46 | }
47 |
48 | generateIdStr() {
49 | const cl = SURVEY_ID_CHARS.length;
50 | let str = '';
51 | for (let i = 0; i < SURVEY_ID_LENGTH; i++) {
52 | str += SURVEY_ID_CHARS[Math.floor(Math.random() * cl)];
53 | }
54 | return str;
55 | }
56 | }
57 |
58 | // Singletonのオブジェクトを生成して使う
59 | const surveyDevIdGeneratorInstance = new SurveyDevIdGenerator();
60 | export default surveyDevIdGeneratorInstance;
61 |
--------------------------------------------------------------------------------
/resource/sass/simple/preview.scss:
--------------------------------------------------------------------------------
1 | @import '../../../node_modules/react-dd-menu/src/scss/react-dd-menu.scss';
2 | @import '../../../node_modules/handsontable/dist/handsontable.full.min.css';
3 | @import "_common.scss";
4 |
5 | $PREVIEW_COLOR: #673AB7;
6 | $ERROR_COLOR: #B71C1C;
7 | $ERROR_BACKGROUND_COLOR: #FFEBEE;
8 |
9 | @mixin nodeNo($color, $backgroundColor, $borderColor) {
10 | background: $backgroundColor;
11 | position: absolute;
12 | right: 0;
13 | top: 0;
14 | line-height: 2em;
15 | padding: 0 10px;
16 | border-left: 1px solid $borderColor;
17 | border-bottom: 1px solid $borderColor;
18 | border-radius: 0 0 0 4px;
19 | z-index: 1;
20 | color: $color;
21 | font-size: 0.875rem;
22 | z-index: 1;
23 | }
24 | .preview-main {
25 | .formButtons.preview {
26 | margin: auto;
27 | color: #fff;
28 | text-align: center;
29 | background-color: #D1C4E9;
30 | padding: 5px 0;
31 | p {
32 | margin: 0;
33 | }
34 | button {
35 | @include button($PREVIEW_COLOR);
36 | }
37 | }
38 | }
39 |
40 | .page-no {
41 | @include nodeNo(#333, $ITEM_BACKGROUND_COLOR, $THEME_COLOR);
42 | }
43 |
44 | .finisher-no {
45 | @include nodeNo(#333, $ITEM_BACKGROUND_COLOR, $THEME_COLOR);
46 | }
47 |
48 | .branch-no {
49 | @include nodeNo(#333, $ITEM_BACKGROUND_COLOR, $THEME_COLOR);
50 | }
51 |
52 | .optional-area {
53 | padding: 9px;
54 | position: relative;
55 | width: $CONTAINER_SIZE;
56 | box-sizing: border-box;
57 | border: 1px solid $PREVIEW_COLOR;
58 | margin: 10px auto;
59 | font-size: 14px;
60 |
61 | .help {
62 | color: #888;
63 | }
64 |
65 | .error-area {
66 | border: 1px solid $ERROR_COLOR;
67 | background-color: $ERROR_BACKGROUND_COLOR;
68 | ul {
69 | margin: 3px;
70 | list-style-type: decimal;
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/lib/editor/components/question_editors/parts/ExSelect.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List } from 'immutable';
3 | import S from 'string';
4 |
5 | /** 渡されたchildrenからフラットなobjectを生成する */
6 | function childrenToObject(children) {
7 | return List(children).filter(option => !!option).flatMap((option) => {
8 | // childrenが多重配列で渡ってくることも有る
9 | if (Array.isArray(option)) {
10 | return List(option.map(childOption => (
11 | {
12 | label: childOption.props.children,
13 | value: childOption.props.value,
14 | error: !!childOption.props.error,
15 | }
16 | )));
17 | }
18 | const optionProps = option.props;
19 | return [{ label: optionProps.children, value: optionProps.value, error: !!optionProps.error }];
20 | }).toArray();
21 | }
22 |
23 | /**
24 | * select表示とプレーンテキスト表示を簡単に切り替えるためのコンポーネント
25 | */
26 | export default function ExSelect(props) {
27 | const { detailMode, notExistsLabel } = props;
28 |
29 | const values = childrenToObject(props.children);
30 | const selectedObj = values.find(obj => obj.value === props.value);
31 | if (detailMode) {
32 | if (!selectedObj) return {notExistsLabel || '未選択'};
33 | const label = S(selectedObj.label).isEmpty() ? '未選択' : selectedObj.label;
34 | if (selectedObj['data-error']) return {label};
35 | return {label};
36 | }
37 |
38 | // detailModeは渡さない
39 | const passProps = Object.assign({}, props);
40 | delete passProps.detailMode;
41 | delete passProps.notExistsLabel;
42 | if (notExistsLabel && !selectedObj) passProps.value = 'notExists';
43 | return (
44 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/lib/runtime/models/view/ViewSetting.js:
--------------------------------------------------------------------------------
1 | import { Record } from 'immutable';
2 | import { SURVEY_NOT_MODIFIED } from '../../../constants/states';
3 | import { TAB_QUESTIONS } from '../../../constants/editor';
4 |
5 | export const ViewSettingRecord = Record({
6 | flowPane: true,
7 | editorPane: true,
8 | previewPane: true,
9 | showAllJsEditor: false,
10 | saveSurveyStatus: SURVEY_NOT_MODIFIED,
11 | selectedPageEditorTab: TAB_QUESTIONS,
12 | allJavaScriptSavedAt: null,
13 | showMenuConfig: false,
14 | });
15 |
16 | /** editorのviewの定義 */
17 | export default class ViewSetting extends ViewSettingRecord {
18 | getFlowPane() {
19 | return this.get('flowPane');
20 | }
21 |
22 | getEditorPane() {
23 | return this.get('editorPane');
24 | }
25 |
26 | getPreviewPane() {
27 | return this.get('previewPane');
28 | }
29 |
30 | getSaveSurveyStatus() {
31 | return this.get('saveSurveyStatus');
32 | }
33 |
34 | getSelectedPageEditorTab() {
35 | return this.get('selectedPageEditorTab');
36 | }
37 |
38 | getShowAllJsEditor() {
39 | return this.get('showAllJsEditor');
40 | }
41 |
42 | getAllJavaScriptSavedAt() {
43 | return this.get('allJavaScriptSavedAt');
44 | }
45 |
46 | getShowMenuConfig() {
47 | return this.get('showMenuConfig');
48 | }
49 |
50 | // ---------------------- 更新系 --------------------------
51 | /** 属性値を変更する */
52 | updateViewAttribute(attribute, value) {
53 | return this.set(attribute, value);
54 | }
55 |
56 | /** surveyの保存リクエスト状態を更新する */
57 | updateSaveSurveyStatus(saveStatus) {
58 | return this.set('saveSurveyStatus', saveStatus);
59 | }
60 |
61 | /** pageEditorの選択しているタブを変更する */
62 | updateSelectedPageEditorTab(eventKey) {
63 | return this.set('selectedPageEditorTab', eventKey);
64 | }
65 |
66 | updateShowAllJsEditorAndSetNullToSavedAt(value) {
67 | return this.merge({
68 | showAllJsEditor: value,
69 | allJavaScriptSavedAt: null,
70 | });
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/lib/runtime/models/survey/ConditionDefinition.js:
--------------------------------------------------------------------------------
1 | import cuid from 'cuid';
2 | import { Record, List } from 'immutable';
3 | import ChildConditionDefinition from './ChildConditionDefinition';
4 | import ValidationErrorDefinition from '../ValidationErrorDefinition';
5 |
6 | export const ConditionDefinitionRecord = Record({
7 | _id: null,
8 | conditionType: 'all', // allまたはsome。childConditionを評価する際にandになるかorになるかが変わる
9 | satisfactionType: 'satisfy', // 満たす場合を条件とする場合 satisfy、満たさない場合を条件とする場合notSatisfy
10 | nextNodeId: '', // 条件にマッチしたときに遷移するnodeのid。node.nextNodeIdよりも優先される
11 | childConditions: List(), // 実際の条件
12 | });
13 |
14 | /** Conditionの定義 */
15 | export default class ConditionDefinition extends ConditionDefinitionRecord {
16 | static create() {
17 | return new ConditionDefinition({
18 | _id: cuid(),
19 | childConditions: List().push(ChildConditionDefinition.create()),
20 | });
21 | }
22 |
23 | getId() {
24 | return this.get('_id');
25 | }
26 |
27 | getSatisfactionType() {
28 | return this.get('satisfactionType');
29 | }
30 |
31 | getConditionType() {
32 | return this.get('conditionType');
33 | }
34 |
35 | getNextNodeId() {
36 | return this.get('nextNodeId');
37 | }
38 |
39 | getChildConditions() {
40 | return this.get('childConditions');
41 | }
42 |
43 | findChildConditionIndex(childConditionId) {
44 | return this.getChildConditions().findIndex(cc => cc.getId() === childConditionId);
45 | }
46 |
47 | /** 設定の検証を行う */
48 | validate(survey, branchId) {
49 | let errors = List();
50 | const currentNode = survey.findNodeFromRefId(branchId);
51 | const followingNodeIds = survey.findFollowingPageAndFinisherNodeIds(currentNode.getId());
52 | if (followingNodeIds.indexOf(this.getNextNodeId()) === -1) errors = errors.push(ValidationErrorDefinition.createError('分岐設定の遷移先が存在しません'));
53 | return errors.concat(this.getChildConditions().flatMap(cc => cc.validate(survey, branchId)));
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/lib/runtime/components/parts/PhotoSwipePart.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | /**
4 | * PhotoSwipeを使用するために必要なDOM構造を定義
5 | */
6 | export default class PhotoSwipePart extends Component {
7 | render() {
8 | return (
9 | { this.pswpEl = el; }} className="pswp" tabIndex="-1" role="dialog" aria-hidden="true">
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 |
34 |
35 |
36 |
39 |
40 |
41 |
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/docs/detail.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | SurveyDesinger Preview
5 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/docs/preview.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | SurveyDesinger Preview
5 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | SurveyDesigner Editor
5 |
6 |
7 |
8 |
9 |
10 |
11 |
survey-designer-js
12 |
DEMO
13 |
14 |
15 |
16 |
17 | | サンプル名 |
18 | 編集画面 |
19 | 動作プレビュー画面 |
20 | 詳細プレビュー画面 |
21 |
22 |
23 |
24 |
25 | | 空のアンケート |
26 |
27 | LINK
28 | (dev mode)
29 | |
30 | |
31 | |
32 |
33 |
34 | | アンケートに関するアンケート |
35 |
36 | LINK
37 | (dev mode)
38 | |
39 |
40 | LINK
41 | (dev mode)
42 | |
43 |
44 | LINK
45 | (dev mode)
46 | |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/__tests__/runtime/models/survey/questions/ConditionDefinition_spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import { Map, List } from 'immutable';
3 | import SurveyDesignerState from '../../../../../lib/runtime/models/SurveyDesignerState';
4 | import nextNodeIdIsPreviousPageJson from './ConditionDefinition_nextNodeIdIsPreviousPage.json';
5 |
6 | describe('ConditionDefinition', () => {
7 | describe('validate', () => {
8 | it('ConditionDefinitionよりも前のページに遷移しようとした場合エラーが返る', () => {
9 | const survey = SurveyDesignerState.createFromJson({ survey: nextNodeIdIsPreviousPageJson }).getSurvey();
10 | const branch = survey.getIn(['branches', 0]);
11 | const condition = branch.getIn(['conditions', 0]);
12 | const result = condition.validate(survey, branch.getId());
13 | expect(result.size).toBe(1);
14 | expect(result.get(0).getType()).toBe('ERROR');
15 | expect(result.get(0).getMessage()).toBe('分岐設定の遷移先が存在しません');
16 | });
17 |
18 | it('ConditionDefinitionよりも後のページに遷移しようとした場合成功する', () => {
19 | const survey = SurveyDesignerState
20 | .createFromJson({ survey: nextNodeIdIsPreviousPageJson })
21 | .getSurvey()
22 | .setIn(['branches', 0, 'conditions', 0, 'nextNodeId'], 'cj98d9exg00033j731183r8mj');
23 | const branch = survey.getIn(['branches', 0]);
24 | const condition = branch.getIn(['conditions', 0]);
25 | const result = condition.validate(survey, branch.getId());
26 | expect(result.size).toBe(0);
27 | });
28 |
29 | it('遷移先のページが存在しない場合エラーが返る', () => {
30 | const survey = SurveyDesignerState
31 | .createFromJson({ survey: nextNodeIdIsPreviousPageJson })
32 | .getSurvey()
33 | .setIn(['branches', 0, 'conditions', 0, 'nextNodeId'], 'dummy');
34 | const branch = survey.getIn(['branches', 0]);
35 | const condition = branch.getIn(['conditions', 0]);
36 | const result = condition.validate(survey, branch.getId());
37 | expect(result.size).toBe(1);
38 | expect(result.get(0).getMessage()).toBe('分岐設定の遷移先が存在しません');
39 | });
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/lib/runtime/components/plain/SelectQuestionJS.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import QuestionWithItemBaseJS from './QuestionWithItemBaseJS';
3 | import { findElementsByOutputDefinitions } from '../../../utils';
4 | import * as ItemVisibility from '../../../constants/ItemVisibility';
5 |
6 | /**
7 | * SelectQuestionのためのJS
8 | */
9 | export default class SelectQuestionJS extends QuestionWithItemBaseJS {
10 | constructor(el, survey, page, runtime) {
11 | super(el, survey, page, runtime, 'Select');
12 | }
13 |
14 | /**
15 | * itemに対応する要素を取得する
16 | *
17 | * @param {Question} question 対象の設問
18 | * @param {ItemDefinition} item 対象のItem
19 | */
20 | findItemElement(question, item) {
21 | const outputDefinition = question.getOutputDefinitions().get(0);
22 | return findElementsByOutputDefinitions(outputDefinition).find(`option[value="${item.getValue()}"]`);
23 | }
24 |
25 | /**
26 | * 項目の表示・非表示の制御を行う
27 | *
28 | * @param {BaseQuestionDefinition} question 対象の設問
29 | */
30 | applyItemVisibility(question) {
31 | question
32 | .getItems()
33 | .forEach((item) => {
34 | const $option = this.findItemElement(question, item);
35 | if ($option.length === 0) return; // option要素がない場合はスキップ
36 | const className = item.calcVisibilityClassName(this.survey, this.runtime.getAnswers());
37 | if (className === ItemVisibility.CLASS_NAME_HIDDEN) {
38 | // RadioやCheckboxはclass=hiddenとしているが、selectの場合要素を消さないと選択を外すことができない
39 | // したがってCLASS_NAME_HIDDENの場合には要素を削除する
40 | $option.remove();
41 | }
42 | });
43 | }
44 |
45 | /** 設問を任意入力にする */
46 | optionalize(question) {
47 | if (!question.isOptional()) return;
48 | const outputDefinition = question.getOutputDefinitions().get(0);
49 | findElementsByOutputDefinitions(outputDefinition).removeAttr('data-parsley-required');
50 | }
51 |
52 | initialize() {
53 | this.findQuestions().forEach((question) => {
54 | this.optionalize(question);
55 | this.applyItemVisibility(question);
56 | });
57 | }
58 |
59 | deInitialize() {
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/lib/editor/components/question_editors/MatrixQuestionEditor.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import BaseQuestionEditor from './BaseQuestionEditor';
3 |
4 | export default function MatrixQuestionEditor(props) {
5 | const { page, question } = props;
6 |
7 | const isTypeCheckbox = question.getMatrixType() === 'checkbox';
8 | const itemExclusive = isTypeCheckbox && question.isMatrixReverse();
9 | const subItemExclusive = isTypeCheckbox && !question.isMatrixReverse();
10 | const matrixSum = question.getMatrixType() === 'number';
11 | /*
12 | その他記入はadditionalInputを再利用する形で実装するため、コメントアウトして残している。
13 | その他記入実装時に綺麗にすること
14 | const itemAdditionalInput = matrixReverse && (question.getMatrixType() === 'checkbox' || question.getMatrixType() === 'radio');
15 | const subItemAdditionalInput = !matrixReverse && (question.getMatrixType() === 'checkbox' || question.getMatrixType() === 'radio');
16 | */
17 | const matrixReverse = question.isMatrixReverse();
18 | const createsUnitLabel = question.getMatrixType() === 'number';
19 | const createsSubUnitLabel = question.getMatrixType() === 'number';
20 | const matrixRowAndColumnUnique = question.getMatrixType() === 'radio';
21 | return (
22 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/lib/runtime/SurveyManager.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser, jquery */
2 | export default class SurveyManager {
3 | constructor(survey, setAnswerFunc) {
4 | this.survey = survey;
5 | this.setAnswerFunc = setAnswerFunc;
6 | }
7 |
8 | /** 使用する前にanswersをリフレッシュする */
9 | refresh(answers) {
10 | this.answers = answers;
11 | }
12 |
13 | /**
14 | * 単位を付ける
15 | */
16 | unit(outputNo, str) {
17 | $(`:no(${outputNo})`).after(`${str}`);
18 | }
19 |
20 | /**
21 | * outputNoからinputなどに設定されるnameを取得する。存在しない場合はnullを返す
22 | */
23 | getName(outputNo) {
24 | const outputDefinition = this.survey.getAllOutputDefinitions().find(od => od.getOutputNo() === outputNo);
25 | if (!outputDefinition) return null;
26 | return outputDefinition.getName();
27 | }
28 |
29 | getNameByDevId(devId) {
30 | const definitions = this.survey.getAllOutputDefinitions();
31 | const outputDefinition = definitions.find(od => od.getDevId() === devId);
32 | if (!outputDefinition) return null;
33 | return outputDefinition.getName();
34 | }
35 |
36 | /**
37 | * outputNoの回答値を設定する
38 | *
39 | * 設定したいkeyだけを指定する
40 | *
41 | * 下記のような形で使用する
42 | * sm.setAnswers({
43 | * '1-1-1': 'abc', // outputNoの形で設問番号を指定できる
44 | * 'cj2v795s900063k676uz4nb1g__value1': 'def', // 直接nameの形でも指定可能
45 | * });
46 | */
47 | setAnswers(answers) {
48 | const translatedAnswers = {};
49 | Object.keys(answers).forEach((key) => {
50 | const nameKey = this.getName(key);
51 | if (nameKey === null) translatedAnswers[key] = answers[key];
52 | else translatedAnswers[nameKey] = answers[key];
53 | });
54 | this.setAnswerFunc(translatedAnswers);
55 | }
56 |
57 | /**
58 | * 指定したoutputNoか、dev-idに対応する回答を取得する
59 | */
60 | getAnswer(key) {
61 | // answersがない場合にはundefinedを返す
62 | if (!this.answers) return undefined;
63 |
64 | if (key.includes('_')) {
65 | const devId = key;
66 | return this.answers[this.getNameByDevId(devId)];
67 | }
68 |
69 | const outputNo = key;
70 | return this.answers[this.getName(outputNo)];
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/lib/runtime/css/common.scss:
--------------------------------------------------------------------------------
1 | $page-list-item-controller-width: 21px;
2 |
3 | $backgroud-color-finisher: #e6f6d7;
4 | $backgroud-color-branch: #e7e1f9;
5 | $backgroud-color-page: #f9f3e0;
6 | $backgroud-color-page-free-edit: #ffe8e8;
7 |
8 | $border-color-finisher: green;
9 | $border-color-branch: purple;
10 | $border-color-page: gold;
11 |
12 | $background-color-question-detail: #e5f0ff;
13 | $border-color-question-detail: #08198e;
14 |
15 | $background-color-page-detail: $backgroud-color-finisher;
16 | $border-color-page-detail: $border-color-finisher;
17 |
18 | $background-color-detail-function: #426ef4;
19 | $color-detail-function: #fff;
20 |
21 | $background-color-answer-value: #fcc;
22 | $border-color-answer-value: #f7024b;
23 | $color-answer-value: #f7024b;
24 |
25 | $background-color-fixed-value: #009e14;
26 | $color-fixed-value: #fff;
27 |
28 | $background-color-validation-detail: #009e14;
29 | $color-validation-detail: #fff;
30 |
31 | $background-color-alert-value: #c11;
32 | $color-alert-value: #fff;
33 |
34 | $number-input-width: 4em;
35 |
36 | $disabled-background-color: #eeeeee;
37 | $disabled-opacity-color: 0.5;
38 |
39 | @mixin tag($bgColor, $color, $borderColor) {
40 | background-color: $bgColor;
41 | border: 1px solid $borderColor;
42 | border-radius: 20px;
43 | color: $color;
44 | padding: 1px 7px;
45 | margin: 0 3px;
46 | white-space: nowrap;
47 | text-overflow: ellipsis;
48 | max-width: 250px;
49 | overflow: hidden;
50 | }
51 |
52 | @mixin no($bgColor, $borderColor) {
53 | position: absolute;
54 | right: 0;
55 | top: 0;
56 | background: $bgColor;
57 | border-left: 1px solid $borderColor;
58 | border-bottom: 1px solid $borderColor;
59 | border-radius: 0 4px 0 4px;
60 | padding: 0 10px;
61 | z-index: 1;
62 | }
63 |
64 | /** 縦書き対応 */
65 | .vertical-writing {
66 | writing-mode: vertical-rl;
67 | -ms-writing-mode: tb-rl;
68 | -webkit-writing-mode: vertical-rl;
69 | height: 100px;
70 | word-wrap: break-word;
71 | vertical-align: middle;
72 | text-align: center;
73 | display: inline;
74 | }
75 |
76 | .col-vertical-writing {
77 | vertical-align: middle;
78 | text-align: center;
79 | }
80 |
--------------------------------------------------------------------------------
/lib/editor/tinymce_plugins/imageManager.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import tinymce from 'tinymce';
3 | import S from 'string';
4 | import cuid from 'cuid';
5 | import '../../constants/tinymce_ja';
6 |
7 | tinymce.PluginManager.add('image_manager', (editor) => {
8 | editor.addButton('image_manager', {
9 | text: '画像',
10 | icon: false,
11 | onclick: () => {
12 | const win = editor.windowManager.open({
13 | title: '画像管理',
14 | url: editor.settings.imageManagerUrl,
15 | width: 1000,
16 | height: 600,
17 | buttons: [{
18 | text: 'Close',
19 | onclick: 'close',
20 | },
21 | {
22 | text: '挿入',
23 | onclick: () => {
24 | const $iframe = win.$el.find('iframe');
25 | const childWindow = $iframe[0].contentWindow;
26 | childWindow.postMessage({ type: 'submitInsertForm' }, '*');
27 | },
28 | }],
29 | });
30 |
31 | function onMessage(e) {
32 | if (e.origin !== location.origin) {
33 | alert('オリジンが一致しません');
34 | return;
35 | }
36 | if (e.data.type !== 'insertImage') return;
37 | const { params } = JSON.parse(e.data.value);
38 | const image = params.image;
39 | const src = params.size === 'normal' ? image.imageUrl : image.thumbnailUrl;
40 | let html = `
`;
50 | editor.insertContent(html);
51 | win.close();
52 | }
53 |
54 | win.on('close', () => {
55 | window.removeEventListener('message', onMessage);
56 | });
57 | window.addEventListener('message', onMessage, false);
58 | },
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/lib/runtime/models/survey/ChildConditionDefinition.js:
--------------------------------------------------------------------------------
1 | import cuid from 'cuid';
2 | import { Record, List } from 'immutable';
3 | import ValidationErrorDefinition from '../ValidationErrorDefinition';
4 |
5 | export const ChildConditionDefinitionRecord = Record({
6 | _id: null,
7 | outputId: '', // 参照先の設問のID.要素のname属性とは異なるので注意
8 | operator: '==', // どういう条件か
9 | value: '', // 比較値
10 | });
11 |
12 | /** ChildConditionの定義 */
13 | export default class ChildConditionDefinition extends ChildConditionDefinitionRecord {
14 | static create() {
15 | return new ChildConditionDefinition({ _id: cuid() });
16 | }
17 |
18 | getId() {
19 | return this.get('_id');
20 | }
21 |
22 | getOutputId() {
23 | return this.get('outputId');
24 | }
25 |
26 | getOperator() {
27 | return this.get('operator');
28 | }
29 |
30 | getValue() {
31 | return this.get('value');
32 | }
33 |
34 | /** 値を検証する */
35 | validate(survey, branchId) {
36 | let errors = List();
37 | const allOutputDefinitionMap = survey.getAllOutputDefinitionMap();
38 | if (!allOutputDefinitionMap.has(this.getOutputId())) {
39 | errors = errors.push(ValidationErrorDefinition.createError('設定されていない分岐条件があります'));
40 | } else {
41 | const od = allOutputDefinitionMap.get(this.getOutputId());
42 | const replacer = survey.getReplacer();
43 | const node = survey.findNodeFromRefId(branchId);
44 | const precedingOutputDefinitions = survey.findPrecedingOutputDefinition(node.getId(), false);
45 | if (od.getOutputType() === 'number' && this.getValue() === '') {
46 | errors = errors.push(ValidationErrorDefinition.createError('分岐条件の入力値が空欄です'));
47 | } else if (od.isOutputTypeSingleChoice() && this.getValue() === '') {
48 | errors = errors.push(ValidationErrorDefinition.createError('分岐条件の入力値が選択されていません'));
49 | } else if (
50 | (od.getOutputType() === 'number' || od.isOutputTypeSingleChoice())
51 | && !replacer.validate(this.getValue(), precedingOutputDefinitions)
52 | ) {
53 | errors = errors.push(ValidationErrorDefinition.createError('分岐条件の参照先が存在しません'));
54 | }
55 | }
56 | return errors;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/__tests__/runtime/models/SurveyDesignerState/SurveyDesignerState_spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import SurveyDesignerState from '../../../../lib/runtime/models/SurveyDesignerState';
3 | import PageDefinition from '../../../../lib/runtime/models/survey/PageDefinition';
4 | import json from './SurveyDesignerState.json';
5 | import StateWithPersonalInfoQuestionJson from './SurveyDesignerStateWithPersonalInfoQuestion.json';
6 |
7 | describe('SurveyDesignerState', () => {
8 | describe('createFromJson', () => {
9 | it('ロードできる', () => {
10 | const survey = SurveyDesignerState.createFromJson({ survey: json }).getSurvey();
11 | const pages = survey.getPages();
12 | pages.forEach(page => expect(page instanceof PageDefinition).toBe(true));
13 |
14 | // NumberValidationRuleが復元できている
15 | const numberValidationRuleMap = pages.get(0).getQuestions().get(0).getNumberValidationRuleMap().get('cj6kee1sg00073j68j2v9clne').get(0);
16 | expect(numberValidationRuleMap.getId()).toBe('cj6kr0dn000163j685rrx2x7g');
17 | expect(numberValidationRuleMap.getNumberValidations().get(0).getId()).toBe('cj6kr0dn000173j68jniau8a5');
18 | expect(numberValidationRuleMap.getNumberValidations().get(0).getValue()).toBe('1');
19 | expect(numberValidationRuleMap.getNumberValidations().get(0).getOperator()).toBe('==');
20 | });
21 |
22 | it('個人情報設問のロードできる', () => {
23 | const survey = SurveyDesignerState.createFromJson({ survey: StateWithPersonalInfoQuestionJson }).getSurvey();
24 | const pages = survey.getPages();
25 | pages.forEach(page => expect(page instanceof PageDefinition).toBe(true));
26 |
27 | // PersonalInfoQuestionが復元できている
28 | const personalInfoQUestion = pages.get(0).getQuestions().get(0);
29 | expect(personalInfoQUestion.getId()).toBe('cj987gwlo00043h73qdwllvgf');
30 | const items = personalInfoQUestion.getItems();
31 | expect(items.size).toBe(23);
32 | expect(items.get(0).getId()).toBe('cj987gwlp00053h735nqka7dv');
33 | expect(items.get(0).getRowType()).toBe('NameRow');
34 | expect(items.get(22).getId()).toBe('cj987gwlp000r3h73n6riir96');
35 | expect(items.get(22).getRowType()).toBe('InterviewRow');
36 | });
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/e2e_test/specs/Node.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('Node', () => {
11 | let editorPage;
12 | beforeEach(() => {
13 | editorPage = new EditorPage();
14 | });
15 |
16 | function addNodeAndVerify(type, title) {
17 | editorPage.addNode(0, type);
18 | const titles = editorPage.findNodeTitles();
19 | expect(titles).to.be.a('array');
20 | expect(titles).to.have.lengthOf(3);
21 | expect(titles[0]).to.equal(title);
22 | }
23 |
24 | describe('Nodeの追加', () => {
25 | it('ページを1ページ目に追加できる', () => {
26 | addNodeAndVerify('page', 'ページ 1');
27 | });
28 |
29 | it('分岐を1ページ目に追加できる', () => {
30 | addNodeAndVerify('branch', '分岐 1');
31 | });
32 |
33 | it('終了ページを1ページ目に追加できる', () => {
34 | addNodeAndVerify('finisher', '終了 1 COMPLETE');
35 | });
36 | });
37 |
38 | describe('Nodeの削除', () => {
39 | function removeNodeCommon(type, title) {
40 | addNodeAndVerify(type, title);
41 | editorPage.removeNode(title);
42 | const titles = editorPage.findNodeTitles();
43 | expect(titles).to.be.a('array');
44 | expect(titles).to.have.lengthOf(2);
45 | expect(titles[0]).to.equal('ページ 1');
46 | expect(titles[1]).to.equal('終了 1 COMPLETE');
47 | }
48 |
49 | it('ページを削除できる', () => {
50 | removeNodeCommon('page', 'ページ 1');
51 | });
52 |
53 | it('分岐を削除できる', () => {
54 | removeNodeCommon('branch', '分岐 1');
55 | });
56 |
57 | it('終了ページを削除できる', () => {
58 | removeNodeCommon('finisher', '終了 1 COMPLETE');
59 | });
60 |
61 | it('最後の終了ページは削除できない', () => {
62 | expect(editorPage.isNodeDeleteButtonVisible('終了 1 COMPLETE')).to.equal(false);
63 | editorPage.addNode(1, 'finisher');
64 | expect(editorPage.isNodeDeleteButtonVisible('終了 1 COMPLETE')).to.equal(true);
65 | expect(editorPage.isNodeDeleteButtonVisible('終了 2 COMPLETE')).to.equal(true);
66 | editorPage.removeNode('終了 1 COMPLETE');
67 | expect(editorPage.isNodeDeleteButtonVisible('終了 1 COMPLETE')).to.equal(false);
68 | });
69 | });
70 | });
71 |
72 |
--------------------------------------------------------------------------------
/lib/runtime/actions.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import Raven from 'raven-js';
3 | import * as C from '../constants/actions';
4 | import { ANSWER_POSTED_SUCCESS, ANSWER_POSTED_FAILED, ANSWER_POSTING } from '../constants/states';
5 |
6 | export function nextPage() {
7 | return { type: C.NEXT_PAGE };
8 | }
9 |
10 | export function restart() {
11 | return { type: C.RESTART };
12 | }
13 |
14 | export function changeCurrentNodeId(nodeId) {
15 | return { type: C.CHANGE_CURRENT_NODE_ID, nodeId };
16 | }
17 |
18 | export function updatePostAnswerStatus(postAnswerStatus) {
19 | return { type: C.CHANGE_POST_ANSWER_STATUS, postAnswerStatus };
20 | }
21 |
22 | export function changeAnswers(answers) {
23 | return { type: C.CHANGE_ANSWERS, answers };
24 | }
25 |
26 | export function replaceAnswers(answers) {
27 | return { type: C.REPLACE_ANSWERS, answers };
28 | }
29 |
30 | /** ************** 非同期 ******************/
31 | export function asyncPostAnswer(dispatch, survey, finisher, answers, isComplete, options) {
32 | const postAnswerUrl = options.getPostAnswerUrl();
33 | const extraPostParameters = options.getExtraPostParameters().toJS();
34 | const answerRegisteredFn = options.getAnswerRegisteredFn();
35 |
36 | const timeForAnswer = options.calcTimeForAnswer();
37 | const postData = {
38 | _doc: JSON.stringify(Object.assign(answers, extraPostParameters, { timeForAnswer })),
39 | is_complete: isComplete,
40 | };
41 |
42 | dispatch(updatePostAnswerStatus(ANSWER_POSTING));
43 | $.ajax({
44 | url: postAnswerUrl,
45 | method: 'POST',
46 | data: postData,
47 | dataType: 'json',
48 | timeout: 10000,
49 | }).done((response) => {
50 | dispatch(updatePostAnswerStatus(ANSWER_POSTED_SUCCESS));
51 | if (answerRegisteredFn) answerRegisteredFn(survey, response);
52 | }).fail((xhr, status, error) => {
53 | const finisherId = finisher.getId();
54 | const finisherNo = survey.calcFinisherNo(finisherId);
55 | Raven.captureMessage('回答の登録に失敗しました', {
56 | level: 'error',
57 | tags: { finisherId, finisherNo },
58 | extra: {
59 | responseText: xhr.responseText,
60 | status: xhr.status,
61 | error,
62 | answers,
63 | },
64 | });
65 | dispatch(updatePostAnswerStatus(ANSWER_POSTED_FAILED));
66 | });
67 | }
68 |
--------------------------------------------------------------------------------
/__tests__/utils_spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import { Record } from 'immutable';
3 | import BaseQuestionDefinition from '../lib/runtime/models/survey/questions/internal/BaseQuestionDefinition';
4 | import NumberValidationDefinition from '../lib/runtime/models/survey/questions/internal/NumberValidationDefinition';
5 | import NumberValidationRuleDefinition from '../lib/runtime/models/survey/questions/internal/NumberValidationRuleDefinition';
6 | import { cloneRecord } from '../lib/utils';
7 |
8 | const ID_SIZE = 25;
9 | describe('utils', () => {
10 | describe('cloneRecord', () => {
11 | it('複数層のrecordをコピーできる', () => {
12 | const record = new NumberValidationRuleDefinition({ _id: 'NVRD' })
13 | .update('numberValidations', list => list.push(new NumberValidationDefinition({ _id: 'NV', value: 'HOGE' })));
14 | const result = cloneRecord(record);
15 | expect(result.getId()).not.toBe('NVRD');
16 | expect(result.getId().length).toBe(ID_SIZE);
17 | expect(result.getIn(['numberValidations', 0, '_id'])).not.toBe('NV');
18 | expect(result.getIn(['numberValidations', 0, '_id']).length).toBe(ID_SIZE);
19 | expect(result.getIn(['numberValidations', 0, 'value'])).toBe('HOGE');
20 | });
21 |
22 | it('コピーしたデータの中にIDの参照がある場合は書き換えたものに置き換わる', () => {
23 | const record = new NumberValidationRuleDefinition({ _id: 'NVRD' })
24 | .update('numberValidations', list => list.push(new NumberValidationDefinition({ _id: 'NV', value: '{{NVRD}}{{NVRD}}{{NV}}' })));
25 | const result = cloneRecord(record);
26 | const newNvId = result.getIn(['numberValidations', 0, '_id']);
27 | expect(result.getIn(['numberValidations', 0, 'value'])).toBe(`{{${result.getId()}}}{{${result.getId()}}}{{${newNvId}}}`);
28 | });
29 |
30 | it('Mapの項目がある場合も問題なく変換できる', () => {
31 | const question = new BaseQuestionDefinition({ _id: 'dummy' })
32 | .addNumberValidation('ODID1');
33 | const result = cloneRecord(question);
34 | expect(result.getIn(['numberValidationRuleMap', 'ODID1', 0, 'numberValidations', 0, '_id'])).not.toBe(
35 | question.getIn(['numberValidationRuleMap', 'ODID1', 0, 'numberValidations', 0, '_id']),
36 | );
37 | expect(result.getIn(['numberValidationRuleMap', 'ODID1', 0, 'numberValidations', 0, '_id']).length).toBe(ID_SIZE);
38 | });
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/lib/runtime/models/survey/questions/ScreeningAgreementQuestionDefinition.js:
--------------------------------------------------------------------------------
1 | import cuid from 'cuid';
2 | import { List } from 'immutable';
3 | import RadioQuestionDefinition from './RadioQuestionDefinition';
4 | import ItemDefinition from './internal/ItemDefinition';
5 | import surveyIdGeneratorInstance from '../../../SurveyDevIdGenerator';
6 |
7 | /** 設問定義:調査許諾 */
8 | export default class ScreeningAgreementQuestionDefinition extends RadioQuestionDefinition {
9 | static create(pageDevId) {
10 | const questionDevId = surveyIdGeneratorInstance.generateForQuestion(pageDevId);
11 | const items = List([
12 | 'はい、協力します。',
13 | 'はい、協力します。有害事象の報告の際には匿名を希望します。',
14 | 'いいえ、調査には協力できません。',
15 | ]).map((label, index) => {
16 | const itemDevId = surveyIdGeneratorInstance.generateForItem(questionDevId);
17 | return ItemDefinition.create(itemDevId, index)
18 | .set('label', label)
19 | .set('plainLabel', label)
20 | .set('value', `${index + 1}`);
21 | }).toList();
22 | return new ScreeningAgreementQuestionDefinition({
23 | _id: cuid(),
24 | devId: questionDevId,
25 | dataType: 'ScreeningAgreement',
26 | title: '調査許諾',
27 | plainTitle: '調査許諾',
28 | description: `
29 |
30 |
このインタビューの目的は市場調査であり、販売促進活動ではありません。
31 |
32 | ■個人情報及び匿名性の保護について
33 | 回答の機密性は市場調査のプライバシー保護原則に則って厳密に保護され、個人の身元を特定できる情報が本調査の依頼元に開示されることはありません。なお、弊社では内容分析のためインタビューを録音し、本調査の依頼主に提供させて頂きます。また、調査を依頼した企業の関係者が拝聴させていただく場合もございます。予めご了承下さい。お伺い致しましたご意見が内容分析以外に使用されることはなく、調査の結果によりご迷惑をおかけすることは一切ございません。
34 |
35 | ■有害事象について
36 | 本調査中に副作用、毒性、パッケージに関する問題点などの有害事象について言及があった場合、既に当該企業または規制当局に報告されている内容であったとしても、私どもから依頼元の医薬品医療機器安全性評価部門に全ての情報を報告する必要があります。匿名での報告も可能です。
37 |
38 | ■機密情報の取り扱いについて
39 | この調査にご参加頂くに当たり、独占的かつ極秘とみなされる情報に触れる可能性がありますが(現段階ではまた開発中で、政府規制機関による検証または認可が行われていない実験的または仮説的概念、製品の説明またはデータ等)、
40 | この情報は調査目的のためのみに共有されたものであり、臨床的使用のための製品販売促進の意図はございません。
41 |
42 | 1) 上記の情報すべての機密を保持すること
43 | 2) 事前の書面による同意なしで、任意の人物や企業体に当該情報を開示しないこと
44 | 3) 事前の書面による同意なしで当該情報を使用しないこと
45 | 以上に合意頂きますようお願い致します。
46 |
47 |
この条件に基づいて、調査にご協力いただけますか。
48 |
`,
49 | items,
50 | });
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/lib/runtime/components/plain/QuestionWithItemBaseJS.js:
--------------------------------------------------------------------------------
1 | import { swapNodes, findElementsByOutputDefinitions } from '../../../utils';
2 |
3 | /**
4 | * itemを利用する下記の設問のベースクラスJS
5 | *
6 | * * CheckboxQuestion
7 | * * RadioQuestion
8 | * * MultiNumberQuestion
9 | */
10 | export default class QuestionWithItemBaseJS {
11 | constructor(el, survey, page, runtime, dataType, itemClass) {
12 | this.el = el;
13 | this.survey = survey;
14 | this.page = page;
15 | this.runtime = runtime;
16 | this.dataType = dataType;
17 | this.itemClass = itemClass;
18 | }
19 |
20 | /** pageに含まれる対象のQuestionのみを取得する */
21 | findQuestions() {
22 | return this.page.getQuestions().filter(question => question.getDataType() === this.dataType);
23 | }
24 |
25 | /**
26 | * itemはitemClassで一つ一つくるまれている必要がある
27 | * @param {BaseQuestionDefinition} question 対象の設問
28 | */
29 | randomize(question) {
30 | if (!question.isRandom()) return;
31 | // 見えているliのエレメントをかき集める
32 | const visibleItemElements = question
33 | .getItems()
34 | .filter(item => !item.isRandomFixed()) // 固定は除く
35 | .map(item => this.findItemElement(question, item))
36 | .filter($el => $el.length > 0) // 要素が見つからないものは除く
37 | .filter($el => $el.is(':visible')); // 見えていないものも除く
38 |
39 | // ランダムに入れ替える
40 | visibleItemElements
41 | .forEach(($itemEl) => {
42 | const $referenceElement = visibleItemElements.get(Math.floor(Math.random() * visibleItemElements.size));
43 | swapNodes($itemEl[0], $referenceElement[0]);
44 | });
45 | }
46 |
47 | /**
48 | * itemに対応する要素を取得する
49 | *
50 | * @param {BaseQuestionDefinition} question 対象の設問
51 | * @param {ItemDefinition} item 対象のItem
52 | */
53 | findItemElement(question, item) {
54 | const outputDefinition = question.getOutputDefinitionsFromItem(item).get(0);
55 | return findElementsByOutputDefinitions(outputDefinition).parents(`.${this.itemClass}`);
56 | }
57 |
58 | /**
59 | * 項目の表示・非表示の制御を行う
60 | *
61 | * @param {BaseQuestionDefinition} question 対象の設問
62 | */
63 | applyItemVisibility(question) {
64 | question
65 | .getItems()
66 | .forEach((item) => {
67 | const $li = this.findItemElement(question, item);
68 | if ($li.length === 0) return; // li要素がない場合はスキップ
69 | const className = item.calcVisibilityClassName(this.survey, this.runtime.getAnswers());
70 | $li.addClass(className);
71 | });
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/lib/constants/HelpMessages.js:
--------------------------------------------------------------------------------
1 | export const freeMode = `
2 | 自由にHTMLを編集できる機能です。
3 | 有効にすると設問定義での変更は画面に反映されません。
4 |
5 | また、一般のユーザは該当ページを編集することはできなくなります。
6 | 編集するには開発者モードで開く必要があります。
7 |
8 | 有効にした後に設問定義を変更してもHTMLには反映されないので、別途HTMLを修正ください。
9 | `;
10 | export const itemVisibility = `
11 | 項目の表示・非表示を設定した条件によって切り替えます。
12 | 表示を設定した場合は条件にマッチすると表示され、条件にマッチしないと非表示となります。
13 | 非表示を設定した場合は条件にマッチすると非表示となり、条件にマッチしないと表示されます。
14 | `;
15 | export const matrixHtmlEnabled = `
16 | テーブルを自由にレイアウト変更できる機能です。セルの結合・行列の追加などが行なえます。
17 | ただし、ダウンロードできる設問の内容は行項目・列項目に定義したものだけとなります。
18 |
19 | 有効にすると設問定義の編集内容に制限がかかり、行項目・列項目の追加・削除・移動などが行なえません。
20 | 項目がFIXした後に有効にすることをおすすめします。
21 |
22 | 定義を変更したい場合には一度このチェックボックスを解除して編集してください。
23 | ただし、レイアウトの編集内容は失われます。
24 | `;
25 | export const matrixReverse = `
26 | 通常要素をZ方向に並べますが、N方向に並べます。
27 | これによって設問タイプのラジオは縦方向の中から一つ値を選ぶように動作が変わります。
28 | その他の設問タイプではダウンロード時にデータの並び方が変わります。
29 | `;
30 | export const javaScriptEditor = `
31 | 下記のショートカットが利用できます。
32 |
33 |
34 | | CTRL-F | ソースコードのフォーマット |
35 | | CTRL-Space | ソースコードの補完
36 |
37 | | 先行する文字 | 補完される値 |
38 | | name: | 要素の名前の補完 |
39 | | no: | outputNoの補完 |
40 | | dev: | devIdの補完 |
41 | | form: | 要素(HTML)の補完 |
42 |
43 | |
44 |
45 |
46 | `;
47 | export const pageHtmlEditor = `
48 | 下記のショートカットが利用できます。
49 |
50 |
51 | | CTRL-F | ソースコードのフォーマット |
52 | | CTRL-Space | ソースコードの補完
53 |
54 | | 先行する文字 | 補完される値 |
55 | | name: | 要素の名前の補完 |
56 | | no: | outputNoの補完 |
57 | | dev: | devIdの補完 |
58 | | form: | 要素(HTML)の補完 |
59 | | reprint: | 再掲の補完 |
60 |
61 | |
62 |
63 |
64 | `;
65 |
66 | export const zeroSetting = '検証エラー時に数値の未入力フィールドに0を埋める機能です';
67 |
--------------------------------------------------------------------------------
/docs/edit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | SurveyDesigner Editor
5 |
8 |
9 |
10 |
20 |
21 |
22 |
23 | Header
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/lib/runtime/models/survey/questions/QuestionDefinitions.js:
--------------------------------------------------------------------------------
1 | import CheckboxQuestionDefinition from './CheckboxQuestionDefinition';
2 | import RadioQuestionDefinition from './RadioQuestionDefinition';
3 | import SelectQuestionDefinition from './SelectQuestionDefinition';
4 | import MultiNumberQuestionDefinition from './MultiNumberQuestionDefinition';
5 | import SingleTextQuestionDefinition from './SingleTextQuestionDefinition';
6 | import TextQuestionDefinition from './TextQuestionDefinition';
7 | import MatrixQuestionDefinition from './MatrixQuestionDefinition';
8 | import DescriptionQuestionDefinition from './DescriptionQuestionDefinition';
9 | import ScreeningAgreementQuestionDefinition from './ScreeningAgreementQuestionDefinition';
10 | import ScheduleQuestionDefinition from './ScheduleQuestionDefinition';
11 | import PersonalInfoQuestionDefinition from './PersonalInfoQuestionDefinition';
12 | import PREFECTURES from '../../../../constants/prefectures';
13 |
14 | const questionDefinitions = {
15 | CheckboxQuestionDefinition: { definitionClass: CheckboxQuestionDefinition, options: null },
16 | RadioQuestionDefinition: { definitionClass: RadioQuestionDefinition, options: null },
17 | SelectQuestionDefinition: { definitionClass: SelectQuestionDefinition, options: null },
18 | MultiNumberQuestionDefinition: { definitionClass: MultiNumberQuestionDefinition, options: null },
19 | SingleTextQuestionDefinition: { definitionClass: SingleTextQuestionDefinition, options: null },
20 | TextQuestionDefinition: { definitionClass: TextQuestionDefinition, options: null },
21 | MatrixQuestionDefinition: { definitionClass: MatrixQuestionDefinition, options: null },
22 | PrefectureQuestionDefinition: { definitionClass: SelectQuestionDefinition, options: { defaultItems: PREFECTURES } },
23 | DescriptionQuestionDefinition: { definitionClass: DescriptionQuestionDefinition, options: null },
24 | ScreeningAgreementQuestionDefinition: { definitionClass: ScreeningAgreementQuestionDefinition, options: null },
25 | ScheduleQuestionDefinition: { definitionClass: ScheduleQuestionDefinition, options: null },
26 | PersonalInfoQuestionDefinition: { definitionClass: PersonalInfoQuestionDefinition, options: null },
27 | };
28 |
29 | /** dataTypeから対応するDefinitionを取得する */
30 | export function findQuestionDefinitionMap(type) {
31 | return questionDefinitions[`${type}QuestionDefinition`]; // eslint-disable-line prefer-const
32 | }
33 |
34 | /** dataTypeから対応するDefinitionを取得する */
35 | export function findQuestionDefaultValuesFromdefinitionClass(type) {
36 | return questionDefinitions[`${type}QuestionDefinition`];
37 | }
38 |
--------------------------------------------------------------------------------
/__tests__/runtime/PageManager_spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import PageManager from '../../lib/runtime/PageManager';
3 |
4 | describe('PageManager', () => {
5 | describe('firePageLoad', () => {
6 | it('pageLoadを発火する', () => {
7 | const pm = new PageManager();
8 | let isCalled = false;
9 | pm.on('pageLoad', () => { isCalled = true; });
10 | pm.firePageLoad();
11 | expect(isCalled).toBe(true);
12 | });
13 | });
14 |
15 | describe('firePageUnload', () => {
16 | it('pageUnloadを発火する', () => {
17 | const pm = new PageManager();
18 | let isCalled = false;
19 | pm.on('pageUnload', () => { isCalled = true; });
20 | pm.firePageUnload();
21 | expect(isCalled).toBe(true);
22 | });
23 | });
24 |
25 | describe('fireValidate', () => {
26 | it('validateを発火する', () => {
27 | const pm = new PageManager();
28 | let isCalled = false;
29 | pm.on('validate', (resolve) => {
30 | isCalled = true;
31 | resolve();
32 | });
33 | return pm.fireValidate().then(() => {
34 | expect(isCalled).toBe(true);
35 | });
36 | });
37 |
38 | it('validate結果を取得できる', () => {
39 | const pm = new PageManager();
40 | pm.on('validate', resolve => resolve('失敗しました'));
41 | return pm.fireValidate().then((result) => {
42 | expect(result.length).toBe(1);
43 | expect(result[0]).toBe('失敗しました');
44 | });
45 | });
46 |
47 | it('複数のvalidate結果を取得できる', () => {
48 | const pm = new PageManager();
49 | pm.on('validate', resolve => resolve('失敗しました1'));
50 | pm.on('validate', resolve => resolve('失敗しました2'));
51 | return pm.fireValidate().then((result) => {
52 | expect(result.length).toBe(2);
53 | expect(result[0]).toBe('失敗しました1');
54 | expect(result[1]).toBe('失敗しました2');
55 | });
56 | });
57 |
58 | it('validate中に例外が発生したときは例外が取得できる', () => {
59 | const pm = new PageManager();
60 | const error = new Error('HOGE');
61 | pm.on('validate', () => { throw error; });
62 | return pm.fireValidate().then(() => {
63 | expect(true).toBe(false); // force error
64 | }).catch((reason) => {
65 | expect(reason.message).toBe(error.message);
66 | });
67 | });
68 |
69 | it('指定時間内に帰ってこない場合エラーとなる', () => {
70 | const pm = new PageManager();
71 | pm.on('validate', () => {});
72 | return pm.fireValidate().then(() => {
73 | expect(true).toBe(false); // force error
74 | }).catch((reason) => {
75 | expect(reason).toBe('バリデーション処理がタイムアウトしました');
76 | });
77 | });
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/__tests__/runtime/models/survey/SurveyDefinition/noBranchSurvey.json:
--------------------------------------------------------------------------------
1 | {
2 | "_id": "65f1ca80-ff20-11e6-a3da-52d4834d462c",
3 | "title": "名称未設定",
4 | "version": 2,
5 | "pages": [
6 | {
7 | "_id": "cj1pzhzdg00023j66pvgv5plq",
8 | "questions": [
9 | {
10 | "_id": "cj1pzhzdg00033j669199lgv2",
11 | "dataType": "Checkbox",
12 | "title": "設問タイトルa
",
13 | "plainTitle": "設問タイトルa",
14 | "description": "",
15 | "random": false,
16 | "subItemsRandom": false,
17 | "unit": "",
18 | "items": [
19 | {
20 | "_id": "cj1pzhzdg00043j662v6dune9",
21 | "index": 0,
22 | "label": "名称未設定",
23 | "plainLabel": "名称未設定",
24 | "value": "1",
25 | "additionalInput": false,
26 | "additionalInputType": "text",
27 | "unit": "",
28 | "randomFixed": false,
29 | "exclusive": false,
30 | "totalEqualTo": ""
31 | }
32 | ],
33 | "subItems": [
34 | {
35 | "_id": "cj1pzhzc000013j66ecd872d1",
36 | "index": 0,
37 | "label": "名称未設定",
38 | "plainLabel": "名称未設定",
39 | "value": "",
40 | "additionalInput": false,
41 | "additionalInputType": "text",
42 | "unit": "",
43 | "randomFixed": false,
44 | "exclusive": false,
45 | "totalEqualTo": ""
46 | }
47 | ],
48 | "showTotal": false,
49 | "matrixType": null,
50 | "matrixReverse": false,
51 | "matrixSumRows": false,
52 | "matrixSumCols": false,
53 | "totalEqualTo": "",
54 | "minCheckCount": 1,
55 | "maxCheckCount": 0,
56 | "min": "",
57 | "max": ""
58 | }
59 | ],
60 | "logicalVariables": [],
61 | "javaScriptCode": ""
62 | }
63 | ],
64 | "branches": [],
65 | "finishers": [
66 | {
67 | "_id": "cj1pzhzdh00063j66kerjvl2a",
68 | "finishType": "COMPLETE",
69 | "point": 0,
70 | "html": "ご回答ありがとうございました。
またのご協力をお待ちしております。"
71 | }
72 | ],
73 | "nodes": [
74 | {
75 | "_id": "cj1pzhzdg00053j66dou92ayf",
76 | "type": "page",
77 | "refId": "cj1pzhzdg00023j66pvgv5plq",
78 | "nextNodeId": "cj1pzhzdi00073j6639h3xdix"
79 | },
80 | {
81 | "_id": "cj1pzhzdi00073j6639h3xdix",
82 | "type": "finisher",
83 | "refId": "cj1pzhzdh00063j66kerjvl2a",
84 | "nextNodeId": null
85 | }
86 | ],
87 | "panel": null
88 | }
--------------------------------------------------------------------------------
/lib/editor/components/question_editors/parts/BulkAddItemsEditorPart.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import React, { Component } from 'react';
3 | import { connect } from 'react-redux';
4 | import S from 'string';
5 | import * as EditorActions from '../../../actions';
6 |
7 | class BulkAddItemsEditorPart extends Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = { inputText: '' };
11 | }
12 |
13 | /** 一括追加を実行する */
14 | handleBulkAddItems() {
15 | const { bulkAddItems, bulkAddSubItems, handleExecute, page, question, isTargetSubItems } = this.props;
16 | if (isTargetSubItems) {
17 | bulkAddSubItems(page.getId(), question.getId(), this.state.inputText);
18 | } else {
19 | bulkAddItems(page.getId(), question.getId(), this.state.inputText);
20 | }
21 | handleExecute();
22 | }
23 |
24 | /** タブを改行に置換する */
25 | handleClickConvertTabToLineBreak() {
26 | const replacedStr = this.state.inputText.replace(/\t/g, '\n');
27 | this.setState({ inputText: replacedStr });
28 | }
29 |
30 | /** 空行を削除する */
31 | handleClickDeleteEmptyLines() {
32 | const lines = this.state.inputText.split(/\n/);
33 | const replacedStr = lines.filter(line => !S(line).isEmpty()).join('\n');
34 | this.setState({ inputText: replacedStr });
35 | }
36 |
37 | render() {
38 | return (
39 |
40 |
1行に1項目を指定してください。HTMLも指定可能です。
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | }
52 | }
53 |
54 | const stateToProps = state => ({
55 | survey: state.getSurvey(),
56 | runtime: state.getRuntime(),
57 | view: state.getViewSetting(),
58 | options: state.getOptions(),
59 | });
60 | const actionsToProps = dispatch => ({
61 | bulkAddItems: (pageId, questionId, text) => dispatch(EditorActions.bulkAddItems(pageId, questionId, text)),
62 | bulkAddSubItems: (pageId, questionId, text) => dispatch(EditorActions.bulkAddSubItems(pageId, questionId, text)),
63 | });
64 |
65 | export default connect(
66 | stateToProps,
67 | actionsToProps,
68 | )(BulkAddItemsEditorPart);
69 |
--------------------------------------------------------------------------------
/__tests__/runtime/models/survey/SurveyDefinition/hasJavaScriptSurvey.json:
--------------------------------------------------------------------------------
1 | {
2 | "_id": "65f1ca80-ff20-11e6-a3da-52d4834d462c",
3 | "title": "名称未設定",
4 | "version": 2,
5 | "pages": [
6 | {
7 | "_id": "cj1pzhzdg00023j66pvgv5plq",
8 | "questions": [
9 | {
10 | "_id": "cj1q0u7da00213j66ffh315m8",
11 | "dataType": "MultiNumber",
12 | "title": "設問タイトル
",
13 | "plainTitle": "設問タイトル",
14 | "description": "",
15 | "random": false,
16 | "subItemsRandom": false,
17 | "unit": "",
18 | "items": [
19 | {
20 | "_id": "cj1q0u7da00223j66lfpz28ys",
21 | "index": 0,
22 | "label": "名称未設定",
23 | "plainLabel": "名称未設定",
24 | "value": "",
25 | "additionalInput": false,
26 | "additionalInputType": "text",
27 | "unit": "",
28 | "randomFixed": false,
29 | "exclusive": false,
30 | "totalEqualTo": ""
31 | }
32 | ],
33 | "subItems": [
34 | {
35 | "_id": "cj1pzhzc000013j66ecd872d1",
36 | "index": 0,
37 | "label": "名称未設定",
38 | "plainLabel": "名称未設定",
39 | "value": "",
40 | "additionalInput": false,
41 | "additionalInputType": "text",
42 | "unit": "",
43 | "randomFixed": false,
44 | "exclusive": false,
45 | "totalEqualTo": ""
46 | }
47 | ],
48 | "showTotal": false,
49 | "matrixType": null,
50 | "matrixReverse": false,
51 | "matrixSumRows": false,
52 | "matrixSumCols": false,
53 | "totalEqualTo": "",
54 | "minCheckCount": 1,
55 | "maxCheckCount": 0,
56 | "min": "",
57 | "max": ""
58 | }
59 | ],
60 | "logicalVariables": [],
61 | "javaScriptCode": "alert('HOGE');"
62 | }
63 | ],
64 | "branches": [],
65 | "finishers": [
66 | {
67 | "_id": "cj1pzhzdh00063j66kerjvl2a",
68 | "finishType": "SCREEN",
69 | "point": 0,
70 | "html": "ご回答ありがとうございました。
またのご協力をお待ちしております。"
71 | }
72 | ],
73 | "nodes": [
74 | {
75 | "_id": "cj1pzhzdg00053j66dou92ayf",
76 | "type": "page",
77 | "refId": "cj1pzhzdg00023j66pvgv5plq",
78 | "nextNodeId": "cj1pzhzdi00073j6639h3xdix"
79 | },
80 | {
81 | "_id": "cj1pzhzdi00073j6639h3xdix",
82 | "type": "finisher",
83 | "refId": "cj1pzhzdh00063j66kerjvl2a",
84 | "nextNodeId": null
85 | }
86 | ],
87 | "panel": null
88 | }
--------------------------------------------------------------------------------
/__tests__/runtime/models/survey/SurveyDefinition/isValidPositionOfCompleteFinisherCase1.json:
--------------------------------------------------------------------------------
1 | {
2 | "_id": "65f1ca80-ff20-11e6-a3da-52d4834d462c",
3 | "title": "名称未設定",
4 | "version": 2,
5 | "pages": [
6 | {
7 | "_id": "cj1pzhzdg00023j66pvgv5plq",
8 | "questions": [
9 | {
10 | "_id": "cj1pzhzdg00033j669199lgv2",
11 | "dataType": "Checkbox",
12 | "title": "設問タイトルa
",
13 | "plainTitle": "設問タイトルa",
14 | "description": "",
15 | "random": false,
16 | "subItemsRandom": false,
17 | "unit": "",
18 | "items": [
19 | {
20 | "_id": "cj1pzhzdg00043j662v6dune9",
21 | "index": 0,
22 | "label": "名称未設定",
23 | "plainLabel": "名称未設定",
24 | "value": "1",
25 | "additionalInput": false,
26 | "additionalInputType": "text",
27 | "unit": "",
28 | "randomFixed": false,
29 | "exclusive": false,
30 | "totalEqualTo": ""
31 | }
32 | ],
33 | "subItems": [
34 | {
35 | "_id": "cj1pzhzc000013j66ecd872d1",
36 | "index": 0,
37 | "label": "名称未設定",
38 | "plainLabel": "名称未設定",
39 | "value": "",
40 | "additionalInput": false,
41 | "additionalInputType": "text",
42 | "unit": "",
43 | "randomFixed": false,
44 | "exclusive": false,
45 | "totalEqualTo": ""
46 | }
47 | ],
48 | "showTotal": false,
49 | "matrixType": null,
50 | "matrixReverse": false,
51 | "matrixSumRows": false,
52 | "matrixSumCols": false,
53 | "totalEqualTo": "",
54 | "minCheckCount": 1,
55 | "maxCheckCount": 0,
56 | "min": "",
57 | "max": ""
58 | }
59 | ],
60 | "logicalVariables": [],
61 | "javaScriptCode": ""
62 | }
63 | ],
64 | "branches": [],
65 | "finishers": [
66 | {
67 | "_id": "cj1pzhzdh00063j66kerjvl2a",
68 | "finishType": "COMPLETE",
69 | "point": 0,
70 | "html": "ご回答ありがとうございました。
またのご協力をお待ちしております。"
71 | }
72 | ],
73 | "nodes": [
74 | {
75 | "_id": "cj1pzhzdg00053j66dou92ayf",
76 | "type": "page",
77 | "refId": "cj1pzhzdg00023j66pvgv5plq",
78 | "nextNodeId": "cj1pzhzdi00073j6639h3xdix"
79 | },
80 | {
81 | "_id": "cj1pzhzdi00073j6639h3xdix",
82 | "type": "finisher",
83 | "refId": "cj1pzhzdh00063j66kerjvl2a",
84 | "nextNodeId": null
85 | }
86 | ],
87 | "panel": null
88 | }
--------------------------------------------------------------------------------
/lib/runtime/PageManager.js:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events';
2 | import Promise from 'es6-promise';
3 |
4 | const EVENT_NAMES = {
5 | EVENT_PAGE_LOAD: 'pageLoad',
6 | EVENT_PAGE_UNLOAD: 'pageUnload',
7 | EVENT_VALIDATE: 'validate',
8 | };
9 |
10 | /**
11 | * 開発者に公開するオブジェクト
12 | *
13 | * Pageの操作に関連するイベントが発火される
14 | * イベントは3種類
15 | * - pageLoad
16 | * 使用方法
17 | * function func(survey, page, answers) {
18 | * // 何かの処理
19 | * };
20 | * SurveyJS.pageManager.on('pageLoad', func);
21 | *
22 | * - pageUnload
23 | * 使用方法
24 | * function func(survey, page) {
25 | * // 何かの処理
26 | * };
27 | * SurveyJS.pageManager.on('pageUnload', func);
28 | *
29 | * - validate
30 | * 使用方法
31 | * function func(resolve, survey, page, answers) {
32 | * const result = ['失敗しました']; // バリデーション結果を配列に格納
33 | * resolve(result); // 必ずresolveにバリデーション結果を渡して実行すること
34 | * // 配列が空、またはundefinedの場合、validationが成功したとみなす
35 | * };
36 | * SurveyJS.pageManager.on('validate', func);
37 | */
38 | export default class PageManager extends EventEmitter {
39 | constructor(page) {
40 | super();
41 | this.page = page;
42 | this.validationTimeout = 3000;
43 | }
44 |
45 | static makePromise(event, timeout, timeoutMessage, listener, ...args) {
46 | const promise = new Promise((resolve, reject) => {
47 | listener(resolve, ...args);
48 | setTimeout(() => {
49 | reject(timeoutMessage);
50 | }, timeout);
51 | });
52 | return promise;
53 | }
54 |
55 | /** listenerの実行結果をpromiseで返す */
56 | emitWithResult(event, timeout, timeoutMessage, ...args) {
57 | const asyncFuncList = this.listeners(event).map(listener => PageManager.makePromise(event, timeout, timeoutMessage, listener, ...args));
58 | return Promise.all(asyncFuncList);
59 | }
60 |
61 | /** 初期化してすべてのイベントリスナを削除する */
62 | init() {
63 | for (const prop in EVENT_NAMES) {
64 | if (!Object.prototype.hasOwnProperty.call(EVENT_NAMES, prop)) continue;
65 | this.removeAllListeners(EVENT_NAMES[prop]);
66 | }
67 | }
68 |
69 | /** pageLoadイベントを発火する */
70 | firePageLoad(...args) {
71 | return this.emit(EVENT_NAMES.EVENT_PAGE_LOAD, ...args);
72 | }
73 |
74 | /** beforeNextPageイベントを発火する */
75 | firePageUnload(...args) {
76 | this.emit(EVENT_NAMES.EVENT_PAGE_UNLOAD, ...args);
77 | }
78 |
79 | /** validateイベントを発火する */
80 | fireValidate(...args) {
81 | return this.emitWithResult(EVENT_NAMES.EVENT_VALIDATE, this.validationTimeout, 'バリデーション処理がタイムアウトしました', ...args);
82 | }
83 |
84 | /** validationエラーや、スクリプトエラーがあったときのポップアップ処理 */
85 | showMessage(message) {
86 | alert(message);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/lib/runtime/components/parts/PageDetail.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import React, { Component } from 'react';
3 | import { connect } from 'react-redux';
4 | import S from 'string';
5 | import Value from './Value';
6 |
7 | class PageDetail extends Component {
8 | createLogicalVariables() {
9 | const { survey, page } = this.props;
10 | const logicalVariables = page.getLogicalVariables();
11 | if (logicalVariables.size === 0) return null;
12 |
13 | const details = logicalVariables.map((lv) => {
14 | const variableName = survey.calcLogicalVariableNo(page.getId(), lv.getId());
15 | const operands = lv.getOperands();
16 | const operators = lv.getOperators();
17 | const logicalVariableDetail = operands.map((operand, i) => {
18 | const key = `PageDetail_${page.getId()}_${lv.getId()}_${i}`;
19 | if (S(operand).isEmpty()) return ;
20 | const value = `{{${operand}.answer}}`;
21 | if (i === 0) {
22 | return ;
23 | }
24 | return ;
25 | }).toArray();
26 |
27 | const key = `PageDetail_${page.getId()}_${lv.getId()}`;
28 | return | {variableName} | {logicalVariableDetail} |
;
29 | });
30 |
31 | return (
32 |
33 | ロジック変数
34 |
35 |
36 | | 変数名 | 定義 |
37 |
38 |
39 | {details}
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | createPageSettings() {
47 | const { page } = this.props;
48 | let zeroSetting;
49 | if (page.getZeroSetting() === null) zeroSetting = '全体設定に従う';
50 | if (page.getZeroSetting() === true) zeroSetting = '有効';
51 | if (page.getZeroSetting() === false) zeroSetting = '無効';
52 | return (
53 |
54 | ページオプション
55 |
56 | | フリーモード | {page.isFreeMode() ? '◯' : ''} |
57 | | ゼロ埋め | {zeroSetting} |
58 |
59 |
60 | );
61 | }
62 |
63 | render() {
64 | return (
65 |
66 |
ページ設定
67 |
68 | {this.createPageSettings()}
69 | {this.createLogicalVariables()}
70 |
71 |
72 | );
73 | }
74 | }
75 |
76 | const stateToProps = state => ({
77 | survey: state.getSurvey(),
78 | runtime: state.getRuntime(),
79 | view: state.getViewSetting(),
80 | });
81 |
82 | export default connect(
83 | stateToProps,
84 | )(PageDetail);
85 |
--------------------------------------------------------------------------------
/lib/constants/personalInfoFields.js:
--------------------------------------------------------------------------------
1 | /**
2 | * PersonalInfoQuestionで使用するフィールドの一覧
3 | */
4 | export const name = 'name';
5 | export const firstName = 'firstName';
6 | export const lastName = 'lastName';
7 | export const furigana = 'furigana';
8 | export const furiganaFirst = 'furiganaFirst';
9 | export const furiganaLast = 'furiganaLast';
10 | export const hospitalName = 'hospitalName';
11 | export const hospitalPrefecture = 'hospitalPrefecture';
12 | export const hospitalPrefectureText = 'hospitalPrefectureText';
13 | export const specialty = 'specialty';
14 | export const position = 'position';
15 | export const age = 'age';
16 | export const sex = 'sex';
17 | export const mobileTel = 'mobileTel';
18 | export const mobileTel1 = 'mobileTel1';
19 | export const mobileTel2 = 'mobileTel2';
20 | export const mobileTel3 = 'mobileTel3';
21 | export const homeTel = 'homeTel';
22 | export const homeTel1 = 'homeTel1';
23 | export const homeTel2 = 'homeTel2';
24 | export const homeTel3 = 'homeTel3';
25 | export const workTel = 'workTel';
26 | export const workTel1 = 'workTel1';
27 | export const workTel2 = 'workTel2';
28 | export const workTel3 = 'workTel3';
29 | export const email = 'email';
30 | export const scheduleContactMobileTel = 'scheduleContactMobileTel';
31 | export const scheduleContactHomeTel = 'scheduleContactHomeTel';
32 | export const scheduleContactWorkTel = 'scheduleContactWorkTel';
33 | export const scheduleContactEmail = 'scheduleContactEmail';
34 | export const contactTimeWeekday = 'contactTimeWeekday';
35 | export const contactTimeWeekend = 'contactTimeWeekend';
36 | export const contactMeansTel = 'contactMeansTel';
37 | export const contactMeansEmail = 'contactMeansEmail';
38 | export const contactEasyTime = 'contactEasyTime';
39 | export const contactMeans = 'contactMeans';
40 | export const interviewContactMobileTel = 'interviewContactMobileTel';
41 | export const interviewContactHomeTel = 'interviewContactHomeTel';
42 | export const interviewContactWorkTel = 'interviewContactWorkTel';
43 | export const interviewMeans = 'interviewMeans';
44 | export const interviewPlace = 'interviewPlace';
45 | export const workPostalCode = 'workPostalCode';
46 | export const professionalArea = 'professionalArea';
47 | export const requestEtc = 'requestEtc';
48 | export const fax = 'fax';
49 | export const fax1 = 'fax1';
50 | export const fax2 = 'fax2';
51 | export const fax3 = 'fax3';
52 | export const birthYear = 'birthYear';
53 |
54 |
55 | export const homeTelCheckboxList = [scheduleContactHomeTel, interviewContactHomeTel];
56 | export const mobileTelCheckboxList = [scheduleContactMobileTel, interviewContactMobileTel];
57 | export const workTelCheckboxList = [scheduleContactWorkTel, interviewContactWorkTel];
58 | export const emailCheckboxList = [scheduleContactEmail];
59 |
--------------------------------------------------------------------------------
/lib/runtime/models/survey/questions/internal/NumberValidationRuleDefinition.js:
--------------------------------------------------------------------------------
1 | import { Record, List, is } from 'immutable';
2 | import cuid from 'cuid';
3 | import S from 'string';
4 | import NumberValidationDefinition from './NumberValidationDefinition';
5 | import ValidationErrorDefinition from '../../../ValidationErrorDefinition';
6 | import { OPERATORS } from '../../../../../constants/NumberValidationRuleConstants';
7 |
8 | /** 数値へのValidationRule */
9 | export const NumberValidationRuleDefinitionRecord = Record({
10 | _id: null, // 内部で使用するID
11 | validationTypeInQuestion: null, // 設問に閉じた検証タイプ。二桁の大文字アルファベット
12 | numberValidations: List(), // 実際の条件
13 | });
14 |
15 | export default class NumberValidationRuleDefinition extends NumberValidationRuleDefinitionRecord {
16 | static create(opts) {
17 | return new NumberValidationRuleDefinition(Object.assign({}, opts, { _id: cuid() }));
18 | }
19 |
20 | getId() {
21 | return this.get('_id');
22 | }
23 |
24 | getValidationTypeInQuestion() {
25 | return this.get('validationTypeInQuestion');
26 | }
27 |
28 | getNumberValidations() {
29 | return this.get('numberValidations');
30 | }
31 |
32 | validate(survey, page, question) {
33 | const errors = this.getNumberValidations().flatMap(numberValidation => numberValidation.validate(survey, page, question));
34 | // 重複したものをリスト
35 | const duplicatedList = this.getNumberValidations()
36 | .map(numberValidation => numberValidation.getOperator())
37 | .filter(operator => !S(operator).isEmpty()) // 未入力はNumbeValidation.validateでチェックしているため除外
38 | .filter((x, i, self) => self.indexOf(x) !== self.lastIndexOf(x));
39 | return errors.concat(duplicatedList.map(duplicatedOperator => ValidationErrorDefinition.createError(`入力値制限に重複した条件があります(${OPERATORS[duplicatedOperator]})`)));
40 | }
41 |
42 | equals(other) {
43 | return is(this.getNumberValidations(), other.getNumberValidations());
44 | }
45 |
46 | /** numberValidationを追加する */
47 | addNumberValidation(opts = {}) {
48 | return this.update('numberValidations', numberValidations => numberValidations.push(NumberValidationDefinition.create(opts)));
49 | }
50 |
51 | /** numberValidationを削除する */
52 | removeNumberValidation(numberValidationId) {
53 | return this.update('numberValidations', numberValidations => numberValidations.filter(nv => nv.getId() !== numberValidationId));
54 | }
55 |
56 | /** numberValidationの属性を更新する */
57 | updateNumberValidationAttribute(numberValidationId, attr, value) {
58 | const index = this.getNumberValidations().findIndex(nv => nv.getId() === numberValidationId);
59 | return this.setIn(['numberValidations', index, attr], value);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/__tests__/runtime/models/survey/BranchDefinition_radio.json:
--------------------------------------------------------------------------------
1 | {
2 | "_id": "ef6005c2-0024-413e-9fb4-488a75bbba1a",
3 | "title": "sample enquete",
4 | "version": 1,
5 | "pages": [{
6 | "_id": "c9137304-bfba-4d94-b24f-da6760c3b5f0",
7 | "questions": [{
8 | "_id": "a7808505-f243-4ef4-a7f1-f7a8fcabe455",
9 | "dataType": "Radio",
10 | "title": "設問タイトル",
11 | "plainTitle": "設問タイトル",
12 | "description": "",
13 | "random": false,
14 | "unit": "",
15 | "items": [{
16 | "_id": "b6ad6c40-431d-432f-93c6-5270c421b609",
17 | "index": 0,
18 | "label": "",
19 | "plainLabel": "",
20 | "value": "value1",
21 | "additionalInput": false,
22 | "additionalInputType": "text",
23 | "unit": "",
24 | "randomFixed": false,
25 | "exclusive": false
26 | }],
27 | "showTotal": false,
28 | "totalEqualTo": null,
29 | "minCheckCount": 1,
30 | "maxCheckCount": 0,
31 | "min": null,
32 | "max": null
33 | }],
34 | "layout": "flow_layout"
35 | }],
36 | "branches": [{
37 | "_id": "805905f0-ef30-4a7c-949b-4f1e6f48f212",
38 | "type": null,
39 | "conditions": [{
40 | "_id": "7ee06f54-71f1-4dd1-9643-6d4ae91b3fc6",
41 | "conditionType": "all",
42 | "nextNodeId": "09d5a018-45d1-4dc4-9d72-34a33a1475de",
43 | "childConditions": [{
44 | "_id": "3f025909-dee8-4beb-9a8f-e7ddb371b18e",
45 | "outputId": "a7808505-f243-4ef4-a7f1-f7a8fcabe455",
46 | "operator": "==",
47 | "value": ""
48 | }]
49 | }]
50 | }],
51 | "finishers": [{
52 | "_id": "ab9d449c-80e5-4077-86f7-ce8f912fe278",
53 | "finishType": "SCREEN",
54 | "point": "0",
55 | "html": "ご回答ありがとうございました。
またのご協力をお待ちしております。
\n0pt
"
56 | }],
57 | "nodes": [{
58 | "_id": "09d5a018-45d1-4dc4-9d72-34a33a1475de",
59 | "type": "page",
60 | "refId": "c9137304-bfba-4d94-b24f-da6760c3b5f0",
61 | "nextNodeId": "a9d5a018-45d1-4dc4-9d72-34a33a1475df"
62 | },
63 | {
64 | "_id": "a9d5a018-45d1-4dc4-9d72-34a33a1475df",
65 | "type": "branch",
66 | "refId": "805905f0-ef30-4a7c-949b-4f1e6f48f212",
67 | "nextNodeId": "dce07ee5-fa63-4a74-a81d-fa117ed62ada"
68 | },
69 | {
70 | "_id": "dce07ee5-fa63-4a74-a81d-fa117ed62ada",
71 | "type": "finisher",
72 | "refId": "ab9d449c-80e5-4077-86f7-ce8f912fe278",
73 | "nextNodeId": null
74 | }],
75 | "panel": {}
76 | }
77 |
--------------------------------------------------------------------------------
/lib/editor/codemirror_plugins/outputDefinitionHintFactory.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import CodeMirror from 'codemirror';
3 |
4 | export default function outputDefinitionHintFactory(overrideOptions = {}) {
5 | // どの項目をオートコンプリートするか
6 | const defaultOptions = {
7 | dev: true,
8 | no: true,
9 | name: true,
10 | form: true,
11 | reprint: true,
12 | };
13 |
14 | const options = Object.assign(defaultOptions, overrideOptions);
15 |
16 | return function outputDefinitionHint(survey, cm) {
17 | const cur = cm.getCursor();
18 | const line = cm.getLine(cur.line);
19 |
20 | // OutputNoのlistを作る
21 | const list = [];
22 |
23 | // 文字以外の箇所で一度区切る
24 | const beforeString = line.substring(0, cur.ch).split(/[^:-\w]/).pop();
25 | // dev:, no:, name:, form:のいずれかにマッチするか確認
26 | const m = beforeString.match(/(\b(?:[^ ]+)\b)/);
27 | if (!m) return { list, from: 0, to: 0 };
28 | const start = line.lastIndexOf(m[1]);
29 | const end = start + m[1].length;
30 | const from = CodeMirror.Pos(cur.line, start);
31 | const to = CodeMirror.Pos(cur.line, end);
32 |
33 | survey.getAllOutputDefinitions().forEach((od) => {
34 | if (options.dev) list.push({ displayText: `dev:${od.getOutputNo()} ${od.getLabel()}`, text: od.getDevId() });
35 | if (options.no) list.push({ displayText: `no:${od.getOutputNo()} ${od.getLabel()}`, text: od.getOutputNo() });
36 | if (options.name) list.push({ displayText: `name:${od.getOutputNo()} ${od.getLabel()}`, text: od.getName() });
37 |
38 | if (options.reprint) {
39 | const propertyName = od.getOutputType() === 'checkbox' || od.isOutputTypeSingleChoice() ? 'answer_label' : 'answer';
40 | list.push({ displayText: `reprint:${od.getOutputNo()} ${od.getLabel()}`, text: `{{${od.getOutputNo()}.${propertyName}}}` });
41 | }
42 |
43 | if (options.form) {
44 | if (od.getOutputType() === 'checkbox') {
45 | list.push({
46 | displayText: `form:${od.getOutputNo()} ${od.getLabel()}`,
47 | text: ``,
48 | });
49 | } else if (od.getOutputType() === 'radio') {
50 | od.getChoices().forEach((choice) => {
51 | list.push({
52 | displayText: `form:${od.getOutputNo()}(${choice.getValue()}) ${od.getLabel()}`,
53 | text: ``,
54 | });
55 | });
56 | } else {
57 | list.push({ displayText: `form:${od.getOutputNo()} ${od.getLabel()}`, text: `` });
58 | }
59 | }
60 | });
61 | return { list: list.filter(obj => obj.displayText.indexOf(m[1]) !== -1), from, to };
62 | };
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/lib/editor/components/editors/FinisherEditor.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import cuid from 'cuid';
4 | import { Col, FormGroup, ControlLabel, FormControl } from 'react-bootstrap';
5 | import tinymce from 'tinymce';
6 | import HtmlEditorPart from '../question_editors/parts/HtmlEditorPart';
7 | import * as EditorActions from '../../actions';
8 | import './../../../constants/tinymce_ja';
9 |
10 | /** Finisherの編集画面 */
11 | class FinisherEditor extends Component {
12 | /** コンストラクタ */
13 | constructor(props) {
14 | super(props);
15 | // tinymceがかぶらないようにするためにcuidを生成
16 | this.cuid = cuid();
17 | }
18 |
19 | /** tinymceの値が変わったときの処理 */
20 | handleHtmlChange(finisherId, html) {
21 | const { changeFinisherAttribute } = this.props;
22 | changeFinisherAttribute(finisherId, 'html', html);
23 | }
24 |
25 | /** tinymce以外のコントロールの値が変わったときの処理 */
26 | handleChangeFinisherAttribute(finisherId, attribute, value) {
27 | const { changeFinisherAttribute } = this.props;
28 | changeFinisherAttribute(finisherId, attribute, value);
29 | }
30 |
31 | /** 描画 */
32 | render() {
33 | const { survey, finisher } = this.props;
34 | const finisherId = finisher.getId();
35 |
36 | const finisherNo = survey.calcFinisherNo(finisher.getId());
37 | return (
38 |
39 |
{finisherNo} 終了ページ設定
40 |
41 | 表示内容
42 |
43 | this.handleHtmlChange(finisherId, html)}
46 | content={finisher.getHtml()}
47 | />
48 |
49 |
50 |
51 | 終了タイプ
52 |
53 | this.handleChangeFinisherAttribute(finisherId, 'finishType', e.target.value)}>
54 |
55 |
56 |
57 |
58 |
59 |
60 | );
61 | }
62 | }
63 |
64 | const stateToProps = state => ({
65 | survey: state.getSurvey(),
66 | runtime: state.getRuntime(),
67 | view: state.getViewSetting(),
68 | });
69 | const actionsToProps = dispatch => ({
70 | changeFinisherAttribute: (finisherId, attribute, value) =>
71 | dispatch(EditorActions.changeFinisherAttribute(finisherId, attribute, value)),
72 | });
73 |
74 | export default connect(
75 | stateToProps,
76 | actionsToProps,
77 | )(FinisherEditor);
78 |
--------------------------------------------------------------------------------
/lib/editor/components/editors/PageEditor.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import React, { Component } from 'react';
3 | import { connect } from 'react-redux';
4 | import { Tabs, Tab } from 'react-bootstrap';
5 | import QuestionEditor from './QuestionEditor';
6 | import JavaScriptEditor from './JavaScriptEditor';
7 | import PageSettingEditor from './PageSettingEditor';
8 | import LogicalVariableEditor from './LogicalVariableEditor';
9 | import PageHtmlEditor from './PageHtmlEditor';
10 | import * as Actions from '../../actions';
11 | import * as EditorConstants from '../../../constants/editor';
12 | import { isDevelopment } from '../../../utils';
13 |
14 | /** エディタの領域を描画する */
15 | class PageEditor extends Component {
16 | /** currentNodeの種類に対応するeditorを取得する */
17 | getQuestionEditors() {
18 | const { page } = this.props;
19 | const questionEditors = page.getQuestions().map(question =>
);
20 | return {questionEditors}
;
21 | }
22 |
23 | handleSelect(key) {
24 | const { changePageEditorTab } = this.props;
25 | changePageEditorTab(key);
26 | }
27 |
28 | render() {
29 | const { page, view } = this.props;
30 |
31 | const activeKey = view.getSelectedPageEditorTab();
32 |
33 | return (
34 |
35 |
this.handleSelect(key)}>
36 | {this.getQuestionEditors()}
37 |
38 |
39 | { activeKey === EditorConstants.TAB_JAVASCRIPT ? : null }
40 | { isDevelopment() && page.isFreeMode() ?
41 | { activeKey === EditorConstants.TAB_HTML ? : null } : null }
42 |
43 |
44 | );
45 | }
46 | }
47 |
48 | const stateToProps = state => ({
49 | survey: state.getSurvey(),
50 | runtime: state.getRuntime(),
51 | view: state.getViewSetting(),
52 | options: state.getOptions(),
53 | });
54 | const actionsToProps = dispatch => ({
55 | changePageEditorTab: eventKey => dispatch(Actions.changePageEditorTab(eventKey)),
56 | });
57 |
58 | export default connect(
59 | stateToProps,
60 | actionsToProps,
61 | )(PageEditor);
62 |
63 |
--------------------------------------------------------------------------------
/lib/editor/components/editors/SurveySettingEditor.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import React, { Component } from 'react';
3 | import { connect } from 'react-redux';
4 | import { Radio, FormControl, Col, ControlLabel, Modal, FormGroup } from 'react-bootstrap';
5 | import Help from '../Help';
6 | import * as Action from '../../actions';
7 |
8 | /** 画面上部のMenuの設定 */
9 | class SurveySettingEditor extends Component {
10 | handleChangeCssUrls(event) {
11 | const { options, changeSurveyCssOption } = this.props;
12 |
13 | const id = event.target.value;
14 | const cssOption = options.getCssOptionById(id);
15 | changeSurveyCssOption(cssOption);
16 | }
17 |
18 | render() {
19 | const { survey, options, view, hideMenuConfig, changeSurveyAttribute } = this.props;
20 |
21 | const cssOptionId = options.getCssOptionIdByUrls(survey.getCssRuntimeUrls(), survey.getCssPreviewUrls(), survey.getCssDetailUrls());
22 |
23 | return (
24 | hideMenuConfig()}>
25 |
26 |
27 |
28 | スタイル定義
29 |
30 | this.handleChangeCssUrls(e)}>
31 | {options.getCssOptions().toArray().map(o => )}
32 |
33 |
34 |
35 |
36 | ゼロ埋め
37 |
38 | changeSurveyAttribute('zeroSetting', true)}
42 | >有効
43 | changeSurveyAttribute('zeroSetting', false)}
47 | >無効
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | const stateToProps = state => ({
58 | survey: state.getSurvey(),
59 | options: state.getOptions(),
60 | view: state.getViewSetting(),
61 | });
62 | const actionsToProps = dispatch => ({
63 | changeSurveyAttribute: (attributeName, value) => dispatch(Action.changeSurveyAttribute(attributeName, value)),
64 | changeSurveyCssOption: cssOption => dispatch(Action.changeSurveyCssOption(cssOption)),
65 | hideMenuConfig: () => dispatch(Action.changeShowMenuConfig(false)),
66 | });
67 |
68 | export default connect(
69 | stateToProps,
70 | actionsToProps,
71 | )(SurveySettingEditor);
72 |
--------------------------------------------------------------------------------
/e2e_test/pages/QuestionEditorPage.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | /* global browser */
3 |
4 | const PAUSE_TIME = 200;
5 |
6 | class QuestionEditorPage {
7 | constructor(editorPage, questionNo) {
8 | this.editorPage = editorPage;
9 | this.questionNo = questionNo;
10 | }
11 |
12 | findQuestionElement() {
13 | const pageNo = this.questionNo.split('-')[0];
14 | this.editorPage.selectNode(`ページ ${pageNo}`);
15 | const questionEditors = browser.elements('.editor-pane .question-editor');
16 | return questionEditors.value.find(el => el.getText('.enq-title').indexOf(this.questionNo) !== -1);
17 | }
18 |
19 | hasTitleEditor() {
20 | const questionElement = this.findQuestionElement();
21 | return questionElement.getText().indexOf('質問タイトル') !== -1;
22 | }
23 |
24 | hasDescriptionEditor() {
25 | const questionElement = this.findQuestionElement();
26 | return questionElement.getText().indexOf('補足') !== -1;
27 | }
28 |
29 | hasChoiceEditor() {
30 | const questionElement = this.findQuestionElement();
31 | return questionElement.getText().indexOf('選択肢') !== -1;
32 | }
33 |
34 | hasVisibilityConditionEditor() {
35 | const questionElement = this.findQuestionElement();
36 | return questionElement.getText().indexOf('表示制御') !== -1;
37 | }
38 |
39 | hasOption() {
40 | const questionElement = this.findQuestionElement();
41 | return questionElement.getText().indexOf('オプション') !== -1;
42 | }
43 |
44 | setLabel(index, label) {
45 | const questionEl = this.findQuestionElement();
46 | browser.pause(PAUSE_TIME);
47 | const itemEditorRowEl = questionEl.elements('.item-editor-row').value[index];
48 | browser.pause(PAUSE_TIME);
49 | itemEditorRowEl.click('.html-editor');
50 | browser.pause(PAUSE_TIME);
51 | itemEditorRowEl.click('.item-editor-tinymce');
52 | browser.pause(PAUSE_TIME);
53 | itemEditorRowEl.setValue('.item-editor-tinymce', label);
54 | }
55 |
56 | addItem(index) {
57 | if (index === 0) throw new Error('QuestionEditorPage.addItemに0は指定できません');
58 | const questionEl = this.findQuestionElement();
59 | browser.pause(PAUSE_TIME);
60 | questionEl.elements('.item-editor-row').value[index - 1].click('.glyphicon-plus-sign');
61 | browser.pause(PAUSE_TIME);
62 | }
63 |
64 | removeItem(index) {
65 | const questionEl = this.findQuestionElement();
66 | questionEl.elements('.item-editor-row').value[index].click('.glyphicon-remove-sign');
67 | }
68 |
69 | getItems() {
70 | const questionElement = this.findQuestionElement();
71 | const itemEditorRows = questionElement.elements('.item-editor-row').value;
72 | const items = itemEditorRows.map(el => ({
73 | label: el.element('.html-editor').getText(),
74 | additionalInput: el.element('.additional-input').isSelected(),
75 | randomFixed: el.isExisting('.random-fixed') ? el.element('.random-fixed').isSelected() : false,
76 | }));
77 | return items;
78 | }
79 | }
80 |
81 | module.exports = QuestionEditorPage;
82 |
--------------------------------------------------------------------------------
/e2e_test/specs/questions/RadioQuestion.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 QuestionEditorPage = require('../../pages/QuestionEditorPage');
9 | const expect = require('chai').expect;
10 |
11 | describe('RadioQuestion', () => {
12 | let editorPage;
13 | describe('editor', () => {
14 | beforeEach(() => {
15 | editorPage = new EditorPage();
16 | editorPage.addQuestion('ページ 1', 0, '単一選択肢(ラジオボタン)');
17 | });
18 |
19 | it('単一選択肢が追加できる', () => {
20 | const questions = editorPage.findQuestionsInPage('ページ 1');
21 | expect(questions).to.be.a('array');
22 | expect(questions).to.have.lengthOf(1);
23 | expect(questions[0]).to.equal('1-1 単一選択肢(ラジオボタン)');
24 | });
25 |
26 | describe('question', () => {
27 | let questionEditorPage;
28 | beforeEach(() => {
29 | questionEditorPage = new QuestionEditorPage(editorPage, '1-1');
30 | });
31 |
32 | it('選択肢を追加できる', () => {
33 | questionEditorPage.addItem(1);
34 | const items = questionEditorPage.getItems();
35 | expect(items).to.be.a('array');
36 | expect(items).to.have.lengthOf(2);
37 | });
38 |
39 | it('0番目の選択肢のラベルを変更できる', () => {
40 | questionEditorPage.addItem(1);
41 | questionEditorPage.setLabel(0, '0番目');
42 | const items = questionEditorPage.getItems();
43 | expect(items).to.be.a('array');
44 | expect(items).to.have.lengthOf(2);
45 | expect(items[0].label).to.equal('0番目');
46 | expect(items[1].label).to.equal('名称未設定');
47 | });
48 |
49 | it('1番目の選択肢のラベルを変更できる', () => {
50 | questionEditorPage.addItem(1);
51 | questionEditorPage.setLabel(1, '1番目');
52 | const items = questionEditorPage.getItems();
53 | expect(items).to.be.a('array');
54 | expect(items).to.have.lengthOf(2);
55 | expect(items[0].label).to.equal('名称未設定');
56 | expect(items[1].label).to.equal('1番目');
57 | });
58 |
59 | it('0番目の選択肢を削除できる', () => {
60 | questionEditorPage.addItem(1);
61 | questionEditorPage.setLabel(0, '0番目');
62 | questionEditorPage.setLabel(1, '1番目');
63 | questionEditorPage.removeItem(0);
64 | const items = questionEditorPage.getItems();
65 | expect(items).to.be.a('array');
66 | expect(items).to.have.lengthOf(1);
67 | expect(items[0].label).to.equal('1番目');
68 | });
69 |
70 | it('1番目の選択肢を削除できる', () => {
71 | questionEditorPage.addItem(1, '追加したタイトル');
72 | questionEditorPage.setLabel(0, '0番目');
73 | questionEditorPage.setLabel(1, '1番目');
74 | questionEditorPage.removeItem(1);
75 | const items = questionEditorPage.getItems();
76 | expect(items).to.be.a('array');
77 | expect(items).to.have.lengthOf(1);
78 | expect(items[0].label).to.equal('0番目');
79 | });
80 | });
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/__tests__/runtime/models/survey/SurveyDefinition/hasLogicalVariablesSurvey.json:
--------------------------------------------------------------------------------
1 | {
2 | "_id": "65f1ca80-ff20-11e6-a3da-52d4834d462c",
3 | "title": "名称未設定",
4 | "version": 2,
5 | "pages": [
6 | {
7 | "_id": "cj1pzhzdg00023j66pvgv5plq",
8 | "questions": [
9 | {
10 | "_id": "cj1q0u7da00213j66ffh315m8",
11 | "dataType": "MultiNumber",
12 | "title": "設問タイトル
",
13 | "plainTitle": "設問タイトル",
14 | "description": "",
15 | "random": false,
16 | "subItemsRandom": false,
17 | "unit": "",
18 | "items": [
19 | {
20 | "_id": "cj1q0u7da00223j66lfpz28ys",
21 | "index": 0,
22 | "label": "名称未設定",
23 | "plainLabel": "名称未設定",
24 | "value": "",
25 | "additionalInput": false,
26 | "additionalInputType": "text",
27 | "unit": "",
28 | "randomFixed": false,
29 | "exclusive": false,
30 | "totalEqualTo": ""
31 | }
32 | ],
33 | "subItems": [
34 | {
35 | "_id": "cj1pzhzc000013j66ecd872d1",
36 | "index": 0,
37 | "label": "名称未設定",
38 | "plainLabel": "名称未設定",
39 | "value": "",
40 | "additionalInput": false,
41 | "additionalInputType": "text",
42 | "unit": "",
43 | "randomFixed": false,
44 | "exclusive": false,
45 | "totalEqualTo": ""
46 | }
47 | ],
48 | "showTotal": false,
49 | "matrixType": null,
50 | "matrixReverse": false,
51 | "matrixSumRows": false,
52 | "matrixSumCols": false,
53 | "totalEqualTo": "",
54 | "minCheckCount": 1,
55 | "maxCheckCount": 0,
56 | "min": "",
57 | "max": ""
58 | }
59 | ],
60 | "logicalVariables": [
61 | {
62 | "_id": "cj1q0u12u00203j6649grjip1",
63 | "variableName": "000",
64 | "operands": [
65 | "cj1q0u7da00223j66lfpz28ys",
66 | "cj1q0u7da00223j66lfpz28ys"
67 | ],
68 | "operators": [
69 | "+"
70 | ]
71 | }
72 | ],
73 | "javaScriptCode": ""
74 | }
75 | ],
76 | "branches": [],
77 | "finishers": [
78 | {
79 | "_id": "cj1pzhzdh00063j66kerjvl2a",
80 | "finishType": "SCREEN",
81 | "point": 0,
82 | "html": "ご回答ありがとうございました。
またのご協力をお待ちしております。"
83 | }
84 | ],
85 | "nodes": [
86 | {
87 | "_id": "cj1pzhzdg00053j66dou92ayf",
88 | "type": "page",
89 | "refId": "cj1pzhzdg00023j66pvgv5plq",
90 | "nextNodeId": "cj1pzhzdi00073j6639h3xdix"
91 | },
92 | {
93 | "_id": "cj1pzhzdi00073j6639h3xdix",
94 | "type": "finisher",
95 | "refId": "cj1pzhzdh00063j66kerjvl2a",
96 | "nextNodeId": null
97 | }
98 | ],
99 | "panel": null
100 | }
--------------------------------------------------------------------------------
/lib/runtime/models/survey/questions/internal/OutputDefinition.js:
--------------------------------------------------------------------------------
1 | import { Record } from 'immutable';
2 |
3 | /** 回答データの1列に相当する定義 */
4 | export const OutputDefinitionRecord = Record({
5 | _id: null, // 内部で使用するIDでitemの移動やnodeの移動があっても変わらない
6 | devId: null, // 開発者向けのid(JavaScriptで利用)
7 | name: null, // サーバにpostするときにキーとなる属性に使われる値
8 | label: null, // 表示用のラベル
9 | dlLabel: null, // DL時に使用するカラムのラベル
10 | questionId: null, // 対応するquestionId ####questionとの紐付けをサーバに保存するために定義
11 | question: null, // 対応するquestion ####この項目は容量が大きくなるためサーバサイドに保存しない
12 | outputType: null, // この出力データの種類。checkbox, radio, text, numberがある
13 | choices: null, // 選択肢
14 | outputNo: null, // ユーザフレンドリなoutputDefitionのユニークな番号
15 | downloadable: true, // 回答データDL時に出力する項目かどうか
16 | prependValue: null, // 回答データDL時に先頭につける文字列
17 | });
18 |
19 | export default class OutputDefinition extends OutputDefinitionRecord {
20 | getId() {
21 | return this.get('_id');
22 | }
23 |
24 | getDevId() {
25 | return this.get('devId');
26 | }
27 |
28 | getName() {
29 | return this.get('name');
30 | }
31 |
32 | getLabel() {
33 | return this.get('label');
34 | }
35 |
36 | getDlLabel() {
37 | return this.get('dlLabel');
38 | }
39 |
40 | getOutputType() {
41 | return this.get('outputType');
42 | }
43 |
44 | getChoices() {
45 | return this.get('choices');
46 | }
47 |
48 | getPageId() {
49 | return this.get('pageId');
50 | }
51 |
52 | getQuestion() {
53 | return this.get('question');
54 | }
55 |
56 | getOutputNo() {
57 | return this.get('outputNo');
58 | }
59 |
60 | /** outputNoがついたlabelを返す. 例: 1-1-1 選択肢1 */
61 | getLabelWithOutputNo() {
62 | return `${this.getOutputNo()} ${this.getLabel()}`;
63 | }
64 |
65 | /** outputNoがついたchoiceのlabelを返す. 例: 1-1(1) 選択肢1 */
66 | getChoiceLabelWithOutputNo(choiceIndex) {
67 | return `${this.getOutputNo()}(${choiceIndex + 1}) ${this.getChoices().get(choiceIndex).getLabel()}`;
68 | }
69 |
70 | getPrependValue() {
71 | return this.get('prependValue');
72 | }
73 |
74 | // 条件のoptionで使用するラベルを返す
75 | getLabelForCondition(natural = false) {
76 | const label = this.getLabelWithOutputNo();
77 | if (!natural) return label;
78 | const outputType = this.getOutputType();
79 | switch (outputType) {
80 | case 'checkbox':
81 | return label;
82 | case 'radio':
83 | case 'select':
84 | case 'text':
85 | case 'number':
86 | return `${label} の値が`;
87 | default:
88 | throw new Error(`不正なoutputTypeです: ${outputType}`);
89 | }
90 | }
91 |
92 | /** outputTypeが単一選択肢かどうかを返す */
93 | isOutputTypeSingleChoice() {
94 | switch (this.getOutputType()) {
95 | case 'select':
96 | case 'radio':
97 | return true;
98 | default:
99 | return false;
100 | }
101 | }
102 |
103 | /** ロジック変数のOutputDefinitionかどうかを判定する */
104 | isLogicalVariableOutputDefinition() {
105 | return this.getOutputNo().split('-')[1] === 'L';
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/__tests__/runtime/models/survey/SurveyDefinition/migrateScheduleQuestionNoSubItems.json:
--------------------------------------------------------------------------------
1 | {
2 | "_id": "65f1ca80-ff20-11e6-a3da-52d4834d462c",
3 | "title": "ScheduleQuestionのマイグレーション確認用",
4 | "version": 2,
5 | "pages": [
6 | {
7 | "_id": "cj7ibzyse00003h6xypvsjgez",
8 | "devId": "EWN",
9 | "questions": [
10 | {
11 | "_id": "cj7ic07ie00063h6x8rs4zlbm",
12 | "devId": "EWN_Tq4",
13 | "dataType": "Schedule",
14 | "title": "以下の日時の中よりインタビュー調査にご参加が可能な日時枠と、具体的な時間帯をお知らせください。",
15 | "plainTitle": "日程",
16 | "description": "可能な限り、複数日時をお知らせ頂きたくお願い致します。
\n (回答例)●月●日(曜日)午後 レ(14:00-16:00の間)等
",
17 | "random": false,
18 | "subItemsRandom": false,
19 | "unit": "",
20 | "items": [
21 | {
22 | "_id": "cj7ic07ie00043h6xyqb0m7lu",
23 | "index": 0,
24 | "devId": "EWN_Tq4_hkq",
25 | "label": "9月2日(月)",
26 | "plainLabel": "9月2日(月)",
27 | "value": "value1",
28 | "additionalInput": false,
29 | "additionalInputType": "text",
30 | "unit": "",
31 | "randomFixed": false,
32 | "exclusive": false,
33 | "visibilityCondition": null,
34 | "totalEqualTo": ""
35 | },
36 | {
37 | "_id": "cj7ic07ie00053h6xabdsm2j2",
38 | "index": 1,
39 | "devId": "EWN_Tq4_REc",
40 | "label": "9月3日(火)",
41 | "plainLabel": "9月3日(火)",
42 | "value": "value2",
43 | "additionalInput": false,
44 | "additionalInputType": "text",
45 | "unit": "",
46 | "randomFixed": false,
47 | "exclusive": false,
48 | "visibilityCondition": null,
49 | "totalEqualTo": ""
50 | }
51 | ],
52 | "subItems": [],
53 | "showTotal": false,
54 | "matrixType": null,
55 | "matrixReverse": false,
56 | "matrixSumRows": false,
57 | "matrixSumCols": false,
58 | "totalEqualTo": "",
59 | "minCheckCount": 1,
60 | "maxCheckCount": 0,
61 | "min": "",
62 | "max": "",
63 | "numberValidationRuleMap": {}
64 | }
65 | ],
66 | "logicalVariables": [],
67 | "javaScriptCode": ""
68 | }
69 | ],
70 | "branches": [],
71 | "finishers": [
72 | {
73 | "_id": "cj7ibzysh00023h6x0182f6a1",
74 | "finishType": "COMPLETE",
75 | "point": 0,
76 | "html": "ご回答ありがとうございました。
またのご協力をお待ちしております。"
77 | }
78 | ],
79 | "nodes": [
80 | {
81 | "_id": "cj7ibzysf00013h6xdiyq9snx",
82 | "type": "page",
83 | "refId": "cj7ibzyse00003h6xypvsjgez",
84 | "nextNodeId": "cj7ibzysh00033h6xqvifw7mo"
85 | },
86 | {
87 | "_id": "cj7ibzysh00033h6xqvifw7mo",
88 | "type": "finisher",
89 | "refId": "cj7ibzysh00023h6x0182f6a1",
90 | "nextNodeId": null
91 | }
92 | ],
93 | "panel": null
94 | }
95 |
--------------------------------------------------------------------------------
/lib/runtime/models/survey/LogicalVariableDefinition.js:
--------------------------------------------------------------------------------
1 | import { Record, List } from 'immutable';
2 | import cuid from 'cuid';
3 | import ValidationErrorDefinition from '../ValidationErrorDefinition';
4 |
5 | const LogicalVariableDefinitionRecord = Record({
6 | _id: null,
7 | variableName: '', // 変数名
8 | operands: List(), // オペランド。outputDefinitionのIDが入る
9 | operators: List(), // オペレータ。+,-,*,/のいずれかが入る
10 | });
11 |
12 | /** ロジック変数の定義 */
13 | export default class LogicalVariableDefinition extends LogicalVariableDefinitionRecord {
14 | static create() {
15 | const id = cuid();
16 | const operands = List().push('').push('');
17 | const operators = List().push('');
18 | return new LogicalVariableDefinition({ _id: id, operands, operators });
19 | }
20 |
21 | getId() {
22 | return this.get('_id');
23 | }
24 |
25 | getVariableName() {
26 | return this.get('variableName');
27 | }
28 |
29 | getOperators() {
30 | return this.get('operators');
31 | }
32 |
33 | getOperands() {
34 | return this.get('operands');
35 | }
36 |
37 | /** 定義をもとに関数の文字列を作成する */
38 | createFunctionCode(survey, page) {
39 | const node = survey.findNodeFromRefId(page.getId());
40 | // このページよりも前にある設問のOutputDefinition
41 | const precedingOutputDefinition = survey.findPrecedingOutputDefinition(node.getId(), false, false);
42 | // すべてのページの設問のOutputDefinition
43 | const allOutputDefinitionMap = survey.getAllOutputDefinitionMap();
44 | let code = 'return ';
45 | const operands = this.getOperands();
46 | const operators = this.getOperators();
47 |
48 | operands.forEach((operand, i) => {
49 | const outputDefinition = precedingOutputDefinition.find(od => od.getId() === operand);
50 | if (i > 0) {
51 | code += `${operators.get(i - 1)}`;
52 | }
53 | if (outputDefinition === undefined) {
54 | // 現在のページの値は入力値から取得する
55 | // ページ遷移時はanswersからも取得はできるが、ページ内バリデーションのタイミングでは画面から値を取得する必要がある
56 | // 数値全角は半角に変換してから計算する
57 | code += `(parseInt(($('*[name="${allOutputDefinitionMap.get(operand).getName()}"]:enabled:visible').val() || '').replace(/[0-9]/g, function(ch) { return String.fromCharCode(ch.charCodeAt(0) - 65248); }), 10) || 0)`;
58 | } else {
59 | // 過去のページの値はanswersから取得する
60 | code += `(parseInt(answers['${outputDefinition.getName()}'], 10) || 0)`;
61 | }
62 | });
63 | return code;
64 | }
65 |
66 | /** 正しく設定されているかチェックする */
67 | validate(survey, page) {
68 | let errors = List();
69 | if (this.getOperators().findIndex(ope => ope === '') !== -1) {
70 | errors = errors.push(ValidationErrorDefinition.createError(`${this.getVariableName()}で選択されていない演算子があります`));
71 | }
72 | const currentNode = survey.findNodeFromRefId(page.getId());
73 | const precedingOutputDefinitions = survey.findPrecedingOutputDefinition(currentNode.getId());
74 | if (!this.getOperands().every(opr => !!precedingOutputDefinitions.find(od => od.getId() === opr))) {
75 | errors = errors.push(ValidationErrorDefinition.createError(`${this.getVariableName()}で選択されていない設問があります`));
76 | }
77 | return errors;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/lib/editor/components/editors/PageSettingEditor.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import React, { Component } from 'react';
3 | import ReactDOMServer from 'react-dom/server';
4 | import { connect } from 'react-redux';
5 | import $ from 'jquery';
6 | import { prettyPrint } from 'html';
7 | import { ControlLabel, FormGroup, Checkbox, Col, Radio } from 'react-bootstrap';
8 | import Help from '../Help';
9 | import Page from '../../../runtime/components/Page';
10 | import * as EditorActions from '../../actions';
11 | import { isDevelopment } from '../../../utils';
12 |
13 | class PageSettingEditor extends Component {
14 | handleChangeFreeMode(freeMode) {
15 | const { survey, options, runtime, view, page, changePageAttribute } = this.props;
16 |
17 | if (!freeMode && !confirm('フリーモードで編集した内容は失われますが本当によろしいですか?')) return;
18 |
19 | const props = {
20 | survey,
21 | options,
22 | runtime,
23 | view,
24 | page,
25 | };
26 | const pageHtml = ReactDOMServer.renderToStaticMarkup();
27 | changePageAttribute(page.getId(), 'freeMode', freeMode);
28 | changePageAttribute(page.getId(), 'html', prettyPrint($(pageHtml).html(), {
29 | indent_size: 2,
30 | }));
31 | }
32 |
33 | render() {
34 | const { page, changePageAttribute } = this.props;
35 | return (
36 |
37 | { isDevelopment() ? (
38 |
39 | フリーモード
40 |
41 | this.handleChangeFreeMode(e.target.checked)}>フリーモード
42 |
43 |
44 | ) : null }
45 |
46 | ゼロ埋め
47 |
48 | changePageAttribute(page.getId(), 'zeroSetting', null)}
52 | >全体設定に従う
53 | changePageAttribute(page.getId(), 'zeroSetting', true)}
57 | >有効
58 | changePageAttribute(page.getId(), 'zeroSetting', false)}
62 | >無効
63 |
64 |
65 |
66 | );
67 | }
68 | }
69 |
70 | const stateToProps = state => ({
71 | survey: state.getSurvey(),
72 | runtime: state.getRuntime(),
73 | options: state.getOptions(),
74 | view: state.getViewSetting(),
75 | });
76 | const actionsToProps = dispatch => ({
77 | changePageAttribute: (pageId, attributeName, value) => dispatch(EditorActions.changePageAttribute(pageId, attributeName, value)),
78 | });
79 |
80 | export default connect(
81 | stateToProps,
82 | actionsToProps,
83 | )(PageSettingEditor);
84 |
--------------------------------------------------------------------------------