├── .babelrc ├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .gitlab-ci.yml ├── README.md ├── README_EN.md ├── TODO.md ├── __mocks__ ├── fileMock.js └── styleMock.js ├── create.html ├── font-awesome.config.js ├── index.html ├── list.html ├── news.html ├── package-lock.json ├── package.json ├── src ├── css │ ├── base │ │ ├── _base.scss │ │ ├── _mixins.scss │ │ ├── _normalize.scss │ │ ├── _variables.scss │ │ ├── colors.scss │ │ ├── main.scss │ │ └── modules │ │ │ ├── _alert.scss │ │ │ ├── _arguments.scss │ │ │ ├── _barometer.scss │ │ │ ├── _discussion-list.scss │ │ │ ├── _discussion-participant.scss │ │ │ ├── _discussion.scss │ │ │ ├── _dropdown.scss │ │ │ ├── _forms.scss │ │ │ ├── _messages.scss │ │ │ ├── _modal.scss │ │ │ ├── _pagination.scss │ │ │ ├── _star-ratings.scss │ │ │ ├── _statement-list.scss │ │ │ ├── _tags.scss │ │ │ ├── _toggle.scss │ │ │ └── _view.scss │ └── themes │ │ ├── bjv.scss │ │ ├── brabbl.scss │ │ ├── brabbl2021.scss │ │ ├── eyp.scss │ │ ├── jugendinfo.scss │ │ ├── main.scss │ │ └── vorwaerts.scss ├── fonts │ ├── FontAwesome.otf │ ├── _animated.scss │ ├── _bordered-pulled.scss │ ├── _core.scss │ ├── _fixed-width.scss │ ├── _icons.scss │ ├── _larger.scss │ ├── _list.scss │ ├── _mixins.scss │ ├── _path.scss │ ├── _rotated-flipped.scss │ ├── _stacked.scss │ ├── _variables.scss │ ├── font-awesome.scss │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── img │ ├── .keep │ ├── Checkmark.png │ └── arrow.png └── js │ ├── actions │ ├── app.js │ └── async.js │ ├── api.js │ ├── components │ ├── __mocks__ │ │ └── app.mock.js │ ├── __tests__ │ │ └── app.spec.js │ ├── app.jsx │ ├── argument │ │ ├── argument-buttons.jsx │ │ ├── argument-list.jsx │ │ ├── argument.jsx │ │ ├── index.js │ │ └── starrating.jsx │ ├── avatar-container.jsx │ ├── base-detail-view.jsx │ ├── common │ │ ├── atoms │ │ │ ├── CAutoHeightAnimator │ │ │ │ └── index.js │ │ │ ├── CButton │ │ │ │ └── index.js │ │ │ ├── CCircularProgress │ │ │ │ └── index.jsx │ │ │ ├── CCollapsable │ │ │ │ └── index.jsx │ │ │ ├── CImageLoader │ │ │ │ └── index.js │ │ │ ├── CMarkupParser │ │ │ │ └── index.jsx │ │ │ ├── CPagination │ │ │ │ └── index.js │ │ │ ├── CRemovableList │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── custom-widgets │ │ │ └── CKEditor.jsx │ │ ├── index.js │ │ └── newforms │ │ │ ├── BoundField.js │ │ │ ├── ErrorList.js │ │ │ ├── ErrorObject.js │ │ │ ├── Field.js │ │ │ ├── Form.js │ │ │ ├── FormSet.js │ │ │ ├── Widget.js │ │ │ ├── components │ │ │ ├── FormRow.jsx │ │ │ ├── ProgressMixin.jsx │ │ │ ├── RenderForm.jsx │ │ │ └── RenderFormSet.jsx │ │ │ ├── constants.js │ │ │ ├── env.js │ │ │ ├── fields │ │ │ ├── BaseTemporalField.js │ │ │ ├── BooleanField.js │ │ │ ├── CharField.js │ │ │ ├── ChoiceField.js │ │ │ ├── ComboField.js │ │ │ ├── DateField.js │ │ │ ├── DateTimeField.js │ │ │ ├── DecimalField.js │ │ │ ├── EmailField.js │ │ │ ├── FileField.js │ │ │ ├── FilePathField.js │ │ │ ├── FloatField.js │ │ │ ├── GenericIPAddressField.js │ │ │ ├── IPAddressField.js │ │ │ ├── ImageField.js │ │ │ ├── IntegerField.js │ │ │ ├── MultiValueField.js │ │ │ ├── MultipleChoiceField.js │ │ │ ├── MultipleFileField.js │ │ │ ├── NullBooleanField.js │ │ │ ├── RegexField.js │ │ │ ├── SlugField.js │ │ │ ├── SplitDateTimeField.js │ │ │ ├── TimeField.js │ │ │ ├── TypedChoiceField.js │ │ │ ├── TypedMultipleChoiceField.js │ │ │ └── URLField.js │ │ │ ├── formats.js │ │ │ ├── forms │ │ │ ├── DeclarativeFieldsMeta.js │ │ │ └── isFormAsync.js │ │ │ ├── locales.js │ │ │ ├── newforms.js │ │ │ ├── util.js │ │ │ └── widgets │ │ │ ├── CheckboxInput.js │ │ │ ├── CheckboxSelectMultiple.js │ │ │ ├── ClearableFileInput.js │ │ │ ├── DateInput.js │ │ │ ├── DateTimeBaseInput.js │ │ │ ├── DateTimeInput.js │ │ │ ├── EmailInput.js │ │ │ ├── FileInput.js │ │ │ ├── HiddenInput.js │ │ │ ├── Input.js │ │ │ ├── MultiWidget.js │ │ │ ├── MultipleHiddenInput.js │ │ │ ├── NullBooleanSelect.js │ │ │ ├── NumberInput.js │ │ │ ├── PasswordInput.js │ │ │ ├── RadioSelect.js │ │ │ ├── Select.js │ │ │ ├── SelectMultiple.js │ │ │ ├── SplitDateTimeWidget.js │ │ │ ├── SplitHiddenDateTimeWidget.js │ │ │ ├── SubWidget.js │ │ │ ├── TextInput.js │ │ │ ├── Textarea.js │ │ │ ├── TimeInput.js │ │ │ ├── URLInput.js │ │ │ ├── inputs │ │ │ ├── CheckboxChoiceInput.js │ │ │ ├── ChoiceInput.js │ │ │ └── RadioChoiceInput.js │ │ │ └── renderers │ │ │ ├── CheckboxFieldRenderer.js │ │ │ ├── ChoiceFieldRenderer.js │ │ │ ├── RadioFieldRenderer.js │ │ │ └── RendererMixin.js │ ├── discussion │ │ ├── __tests__ │ │ │ ├── barometer.spec.js │ │ │ └── discussion-button.spec.js │ │ ├── barometer.jsx │ │ ├── countdown-timer.jsx │ │ ├── discussion-argument.jsx │ │ ├── discussion-button.jsx │ │ ├── discussion-item.jsx │ │ ├── discussion-meta.jsx │ │ ├── discussion-time-left.jsx │ │ ├── discussion.jsx │ │ ├── index.js │ │ ├── list-discussion-item.jsx │ │ ├── statement-detail.jsx │ │ ├── statement-item.jsx │ │ ├── survey-list.jsx │ │ ├── survey-statement-item.jsx │ │ └── utils.js │ ├── dropdown.js │ ├── forms │ │ ├── argument.jsx │ │ ├── containers │ │ │ ├── argument.jsx │ │ │ ├── discussion.jsx │ │ │ ├── discussion_list.jsx │ │ │ ├── discussion_participant.jsx │ │ │ ├── invite_participant.jsx │ │ │ ├── login.jsx │ │ │ ├── profile.jsx │ │ │ ├── resetpassword.jsx │ │ │ ├── signup.jsx │ │ │ ├── social_buttons.jsx │ │ │ ├── statement.jsx │ │ │ └── utils.js │ │ ├── discussion.jsx │ │ ├── discussion_list.jsx │ │ ├── index.js │ │ ├── invite_participant.jsx │ │ ├── login.jsx │ │ ├── profile.jsx │ │ ├── resetpassword.jsx │ │ ├── signup.jsx │ │ ├── statement.jsx │ │ └── toggle.jsx │ ├── header.jsx │ ├── login.jsx │ ├── markup_wording.jsx │ ├── media.jsx │ ├── modals │ │ ├── alert.jsx │ │ ├── argument.jsx │ │ ├── barometer.jsx │ │ ├── data_policy.jsx │ │ ├── delete_discussion.jsx │ │ ├── delete_statement.jsx │ │ ├── discussion.jsx │ │ ├── discussion_list.jsx │ │ ├── export_discussion.jsx │ │ ├── export_statistics.jsx │ │ ├── iframe.jsx │ │ ├── invite_participant.jsx │ │ ├── login.jsx │ │ ├── media.jsx │ │ ├── modal-selector.jsx │ │ ├── modal.jsx │ │ ├── password.jsx │ │ ├── profile.jsx │ │ ├── reset_discussion.jsx │ │ ├── signup.jsx │ │ └── statement.jsx │ ├── navigations │ │ └── navigator.jsx │ ├── options.jsx │ ├── services │ │ └── live-update-manager.jsx │ ├── tag-select.jsx │ ├── timelimit-select.jsx │ ├── user-message.jsx │ └── views │ │ ├── detail-view.jsx │ │ ├── info-view.jsx │ │ ├── list-view.jsx │ │ ├── login-view.jsx │ │ ├── news-view.jsx │ │ ├── reset-password-view.jsx │ │ └── signup-view.jsx │ ├── config │ ├── development.config.js │ ├── index.js │ ├── new_staging.config.js │ └── production.config.js │ ├── constants.js │ ├── helpers │ ├── discussionExcelExport.js │ ├── index.js │ └── statisticsExcelExport.js │ ├── i18n.js │ ├── init.dev.js │ ├── init.js │ ├── init.prod.js │ ├── reducers │ ├── alert.js │ ├── argument.js │ ├── customer.js │ ├── discussion.js │ ├── index.js │ ├── modal.js │ ├── notification.js │ ├── translators.js │ └── user.js │ ├── store │ ├── configureStore.dev.js │ ├── configureStore.js │ └── configureStore.prod.js │ └── utils.js ├── test └── setup.js ├── themes.md ├── webpack.config.js ├── webpack.new_staging.config.js └── webpack.production.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react", "@babel/preset-env"], 3 | "plugins": ["transform-class-properties"] 4 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | trim_trailing_whitespace = true 8 | 9 | [*.{py,rst,ini}] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [*.{html,js,coffee,yml,jsx,scss}] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | APP_ENV=new_staging 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # node_modules are ignored by default 2 | 3 | # Ignore build folder 4 | build 5 | dist 6 | .git 7 | 8 | # Ignore config stuff 9 | server.*.js 10 | *config.js 11 | serviceworker.js 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "globals": { 8 | "Atomics": "readonly", 9 | "SharedArrayBuffer": "readonly" 10 | }, 11 | "parser": "babel-eslint", 12 | "parserOptions": { 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "ecmaVersion": 2018, 17 | "sourceType": "module" 18 | }, 19 | "rules": { 20 | "max-len": [1, 120, 2, {ignoreComments: true}], 21 | "react/display-name": 0, 22 | "prefer-const": 0, 23 | "prefer-template": 0, 24 | "no-console": 0, 25 | "new-cap": 0, 26 | "no-param-reassign": 0, 27 | "no-else-return": 0, 28 | "object-shorthand": 0, 29 | "prefer-arrow-callback": 0, 30 | "camelcase": 0, 31 | "quote-props": 0, 32 | "one-var": 0, 33 | "consistent-return": 0, 34 | "react/no-underscore-dangle": 0, 35 | "react/jsx-pascal-case": 0, 36 | "react/prefer-stateless-function": 0, 37 | "react/prefer-es6-class": 0, 38 | "react/no-multi-comp": 0, 39 | "react/prop-types": 0, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ------------------------- Linux 2 | *~ 3 | 4 | # KDE 5 | .directory 6 | 7 | # ------------------------- OSX 8 | .DS_Store 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear on external disk 14 | .Spotlight-V100 15 | .Trashes 16 | 17 | # ------------------------- vim 18 | .*.sw[a-z] 19 | *.un~ 20 | Session.vim 21 | 22 | 23 | # ------------------------- Python 24 | *.py[co] 25 | 26 | # Packages 27 | *.egg 28 | *.egg-info 29 | dist 30 | build 31 | eggs 32 | parts 33 | bin 34 | var 35 | sdist 36 | coverage 37 | develop-eggs 38 | .installed.cfg 39 | 40 | # Installer logs 41 | pip-log.txt 42 | 43 | # Unit test / coverage reports 44 | .coverage 45 | .tox 46 | 47 | #Translations 48 | *.mo 49 | 50 | 51 | # ------------------------ Django 52 | *.log 53 | *.pot 54 | *.pyc 55 | settings_local.py 56 | 57 | # ------------------------ Custom 58 | .cache 59 | .tmp 60 | *.sqlite3 61 | htmlcov 62 | node_modules 63 | frontend/lib 64 | docs/build 65 | *.orig 66 | *.sql 67 | *.tox/* 68 | media 69 | *.log 70 | npm-debug* 71 | .idea 72 | package-backup.json 73 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - export LC_ALL=en_US.UTF-8 3 | - export PATH=$ENV_PATH/bin:$PATH 4 | 5 | stages: 6 | - test 7 | 8 | test_job: 9 | stage: test 10 | script: 11 | - bash -c '. ~/.nvm/nvm.sh ; nvm use 5.0 ; npm install -s > /dev/null' 12 | - bash -c '. ~/.nvm/nvm.sh ; nvm use 5.0 ; npm test' 13 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redblame315/brabbl-frontend/100f4b761f450273395c62ccfb033123b80f5ae4/README_EN.md -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | Webpack / Deployment 2 | -------------------- 3 | * Better seperation of configs and commands for the different environments 4 | 5 | AXIOS / API-Interaction 6 | ----------------------- 7 | * Improve API client once this is available in a stable release: https://github.com/mzabriskie/axios#axioscreateconfig 8 | * Better organization of API module or use API-Client in directly in actions and remove API module 9 | * Global intercept for server errors (5xx) => Decide how to handle them best (user message, error page, ...) 10 | 11 | REDUX/State-Management 12 | ---------------------- 13 | 14 | * Use redux for async stuff (More granular actions) 15 | 16 | Testing 17 | ------- 18 | 19 | * Setup test tools 20 | * Add tests for reducers 21 | * Add tests for everything else 22 | 23 | Deployment/Setup 24 | ---------------- 25 | 26 | * Check babel stage (really 0?: https://babeljs.algolia.com/docs/usage/experimental/) 27 | * Check raven/sentry integration 28 | * Improve build size: 29 | - http://moduscreate.com/optimizing-react-es6-webpack-production-build/ 30 | - https://github.com/robertknight/webpack-bundle-size-analyzer 31 | * Check better separation of client builds (deploy new features/versions only for some clients) 32 | - Tag versions and map clients to tags? 33 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redblame315/brabbl-frontend/100f4b761f450273395c62ccfb033123b80f5ae4/__mocks__/fileMock.js -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /create.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Brabbl 6 | 7 | 8 |
9 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /font-awesome.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Default for the style loading 3 | styleLoader: 'style-loader!css-loader!sass-loader', 4 | 5 | styles: { 6 | mixins: true, 7 | 'bordered-pulled': true, 8 | core: true, 9 | 'fixed-width': true, 10 | icons: true, 11 | larger: true, 12 | list: true, 13 | path: true, 14 | 'rotated-flipped': true, 15 | animated: true, 16 | stacked: true 17 | } 18 | }; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Brabbl 10 | 11 | 12 |

Below this simple paragraph, there should be the faboulous 'brabbl' widget, showing a discussion with externalID 'demo-discussion-1' (customer 'brabbl-dev-customer-1').

13 | 14 |
15 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Brabbl 10 | 11 | 12 |

Below this simple paragraph, there should be the faboulous 'brabbl' widget, showing a list of all existing discussions for customer 'brabbl-dev-customer-1'.

13 |
14 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /news.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Brabbl 10 | 11 | 12 |

Below this simple paragraph, there should be the faboulous 'brabbl' widget, showing a list of all existing discussions for customer 'brabbl-dev-customer-1'.

13 |
14 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/css/base/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Media query generator - copied from https://github.com/paranoida/sass-mediaqueries 2 | @mixin mq($args...) { 3 | $media-type: 'only screen'; 4 | $media-type-key: 'media-type'; 5 | $args: keywords($args); 6 | $expr: ''; 7 | 8 | @if map-has-key($args, $media-type-key) { 9 | $media-type: map-get($args, $media-type-key); 10 | $args: map-remove($args, $media-type-key); 11 | } 12 | 13 | @each $key, $value in $args { 14 | @if $value { 15 | $expr: "#{$expr} and (#{$key}: #{$value})"; 16 | } 17 | } 18 | 19 | @media #{$media-type} #{$expr} { 20 | @content; 21 | } 22 | } 23 | 24 | @mixin bp-large { 25 | @include mq($min-width: $breakpoint-large) { 26 | @content; 27 | } 28 | } 29 | 30 | @mixin bp-ml-large { 31 | @include mq($min-width: $breakpoint-ml) { 32 | @content; 33 | } 34 | } 35 | 36 | @mixin bp-ml { 37 | @include mq($min-width: $breakpoint-ml, $max-width: $breakpoint-large - 1px) { 38 | @content; 39 | } 40 | } 41 | 42 | @mixin bp-medium-large{ 43 | @include mq($min-width: $breakpoint-medium) { 44 | @content; 45 | } 46 | } 47 | 48 | @mixin bp-medium { 49 | @include mq($min-width: $breakpoint-medium, $max-width: $breakpoint-large - 1px) { 50 | @content; 51 | } 52 | } 53 | 54 | @mixin bp-small-medium { 55 | @include mq($max-width: $breakpoint-ml - 1px) { 56 | @content; 57 | } 58 | } 59 | 60 | @mixin bp-small{ 61 | @include mq($max-width: $breakpoint-small) { 62 | @content; 63 | } 64 | } 65 | 66 | %clearfix { 67 | *zoom: 1; 68 | &:before, &:after { 69 | content: " "; 70 | display: table; 71 | } 72 | &:after { 73 | clear: both; 74 | } 75 | } 76 | 77 | @mixin fadeIn($duration: 200ms) { 78 | opacity: 0; 79 | transition: opacity $duration ease-in-out; 80 | 81 | &.show { 82 | opacity: 1; 83 | } 84 | } 85 | 86 | @mixin vertical-align($position: relative) { 87 | position: $position; 88 | top: 50%; 89 | transform: translateY(-50%); 90 | } 91 | 92 | @mixin tooltip-arrow($color: $black, $size: 5px) { 93 | &:after { 94 | top: 100%; 95 | left: 50%; 96 | border: solid transparent; 97 | content: " "; 98 | height: 0; 99 | width: 0; 100 | position: absolute; 101 | pointer-events: none; 102 | border-color: transparent; 103 | border-top-color: $color; 104 | border-width: $size; 105 | margin-left: -$size; 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/css/base/_variables.scss: -------------------------------------------------------------------------------- 1 | // Breakpoints 2 | $breakpoint-large: 960px; 3 | $breakpoint-ml: 780px; 4 | $breakpoint-medium: 480px; 5 | $breakpoint-small: 320px; 6 | 7 | $modal-border-radius: 10px; 8 | $button-border-radius: 6px; 9 | -------------------------------------------------------------------------------- /src/css/base/colors.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | $white: #FFFFFF; 3 | $black: #111111; 4 | $dark-grey: #858585; 5 | $medium-grey: #EAEAEA; 6 | $light-grey: #F5F7F7; 7 | $red: #D31114; 8 | $green: #0F9601; 9 | $yellow: #F5A71B; 10 | $widget-background-color: #f2f2f2; 11 | $modal-background-color: #f2f2f2; 12 | $red-orange: #FF741A; -------------------------------------------------------------------------------- /src/css/base/main.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'colors'; 3 | @import 'mixins'; 4 | @import 'modules/modal'; 5 | @import 'modules/alert'; 6 | 7 | #brabbl-widget, 8 | .ReactModal__Overlay { 9 | @import 'normalize'; 10 | @import 'base'; 11 | @import 'modules/barometer'; 12 | @import 'modules/discussion'; 13 | @import 'modules/discussion-list'; 14 | @import 'modules/statement-list'; 15 | @import 'modules/discussion-participant'; 16 | @import 'modules/arguments'; 17 | @import 'modules/forms'; 18 | @import 'modules/star-ratings'; 19 | @import 'modules/toggle'; 20 | @import 'modules/messages'; 21 | @import 'modules/dropdown'; 22 | @import 'modules/tags'; 23 | @import 'modules/view'; 24 | @import 'modules/pagination'; 25 | } 26 | 27 | #brabbl-widget { 28 | background-color: $widget-background-color; 29 | border-radius: $modal-border-radius; 30 | font-weight: 400; 31 | padding: 16px; 32 | @include bp-small-medium { 33 | padding: 8px; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/css/base/modules/_alert.scss: -------------------------------------------------------------------------------- 1 | 2 | .alert_modal_content { 3 | left:0 !important; 4 | right:0 !important; 5 | width: 380px; 6 | padding: 5px 5px 15px !important; 7 | margin-left: auto; 8 | margin-right: auto; 9 | top: 35%; 10 | background-color: white; 11 | > div { 12 | margin: 5px 5px 5px; 13 | max-width: 380px; 14 | } 15 | -webkit-transition: 0.2s ease-in-out; 16 | -moz-transition: 0.2s ease-in-out; 17 | -o-transition: 0.2s ease-in-out; 18 | transition: 0.2s ease-in-out; 19 | border-radius: 6px; 20 | } 21 | .alert-modal { 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | width: 360px; 26 | button { 27 | flex: 1; 28 | align-items: center; 29 | justify-content: center; 30 | height: 40px; 31 | width: 240px; 32 | margin-bottom: 16px; 33 | } 34 | } 35 | 36 | .alert-modal-title { 37 | font-size: 14px; 38 | margin-top: 16px; 39 | } 40 | .alert-modal-message { 41 | font-size: 14px; 42 | margin-left: 16px; 43 | margin-right: 16px; 44 | margin-top: 16px; 45 | margin-bottom: 16px; 46 | } -------------------------------------------------------------------------------- /src/css/base/modules/_discussion-participant.scss: -------------------------------------------------------------------------------- 1 | .participant-section { 2 | border-bottom: 1px solid $dark-grey; 3 | margin-bottom: 24px; 4 | } 5 | .manage-participant { 6 | .collapsable-section { 7 | .collapsable-section-title { 8 | h2 { 9 | font-size: 14px; 10 | } 11 | .icon { 12 | font-size: 20px; 13 | margin-left: 8px; 14 | } 15 | } 16 | } 17 | } 18 | .participant-names { 19 | font-style: italic; 20 | font-size: 14px; 21 | } 22 | .participant-list { 23 | margin-top: 32px; 24 | .participant-checkbox { 25 | width: 32px; 26 | color: #9BCF67; 27 | } 28 | .participant-item { 29 | margin-left: 32px; 30 | display: flex; 31 | flex-direction: row; 32 | align-items: center; 33 | .avatar-non-select { 34 | opacity: 0.8; 35 | } 36 | .participant-info { 37 | display: flex; 38 | flex: 1; 39 | flex-direction: column; 40 | align-items: left; 41 | margin-top: 4px; 42 | margin-left: 4px; 43 | .name { 44 | font-size: 12px; 45 | font-weight: normal; 46 | } 47 | .email { 48 | margin-top: -6px; 49 | font-size: 11px; 50 | } 51 | .selected { 52 | color: $black; 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/css/base/modules/_dropdown.scss: -------------------------------------------------------------------------------- 1 | .dropdown-select { 2 | font-size: 1.07em; 3 | 4 | .dropdown-active { 5 | display: block; 6 | cursor: pointer; 7 | height: 20px; 8 | 9 | i { 10 | margin-left: 2px; 11 | vertical-align: top; 12 | } 13 | } 14 | 15 | ul { 16 | position: absolute; 17 | right: 0; 18 | z-index: 50; 19 | padding: 1em; 20 | margin: 0; 21 | background-color: $light-grey; 22 | border: 1px solid $medium-grey; 23 | display: none; 24 | list-style: none; 25 | 26 | &.open { 27 | display: block; 28 | } 29 | 30 | li { 31 | padding: 5px; 32 | color: $dark-grey; 33 | cursor: pointer; 34 | list-style: none; 35 | font-size: 1em; 36 | line-height: 1.2em; 37 | 38 | &:hover { 39 | color: darken($dark-grey, 20%); 40 | } 41 | } 42 | } 43 | } 44 | 45 | .discussion-header-tags { 46 | float: left; 47 | ul { 48 | left: 0 !important; 49 | width: 120px; 50 | } 51 | } 52 | 53 | 54 | .discussion-header-time-limit { 55 | float: right; 56 | ul { 57 | right: 0; 58 | width: 120px; 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/css/base/modules/_messages.scss: -------------------------------------------------------------------------------- 1 | .crouton { 2 | font-size: 1.33em; 3 | color: #FFF; 4 | line-height: 140%; 5 | border-radius: 2px; 6 | position: fixed; 7 | top: 0px; 8 | left: 0px; 9 | right: 0px; 10 | width: 100%; 11 | text-align: center; 12 | z-index: 999999999; 13 | margin: 0 auto; 14 | 15 | div { 16 | padding: 10px; 17 | &.user-message {background-color: $base-color; color: $white} 18 | &.info { background-color: #1F8DD6; } 19 | &.success { background-color: #50CD84; } 20 | &.warning { background-color: #FF8859; } 21 | &.error { background-color: #FF5F5F; } 22 | span { 23 | display: block; 24 | margin-bottom: 0; 25 | margin-left: 3px; 26 | } 27 | 28 | .buttons { 29 | height: 0; 30 | padding: 0; 31 | 32 | button { 33 | position: absolute; 34 | right: 2px; 35 | top: 2px; 36 | color: $white; 37 | padding: 0; 38 | margin: 6px 10px 0 0; 39 | border-color: $base-color; 40 | 41 | &:hover { 42 | background-color: $base-color; 43 | color: $white 44 | } 45 | 46 | &.btn, 47 | &.retry, 48 | &.close, 49 | &.cancel, 50 | &.ignore { 51 | padding: 1px; 52 | border: 1px solid transparent; 53 | } 54 | &.retry { 55 | border-color: #FFF; 56 | } 57 | &.btn { 58 | &.close { 59 | border-color: $base-color; 60 | margin-left: 10px; 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/css/base/modules/_statement-list.scss: -------------------------------------------------------------------------------- 1 | .statement-item-container { 2 | background-color: $white; 3 | margin-top: 0px; 4 | position: relative; 5 | cursor: pointer; 6 | border-radius: $modal-border-radius; 7 | -webkit-box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2); 8 | -moz-box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2); 9 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2); 10 | min-height: 100px; 11 | &:hover { 12 | h3 a { 13 | color: $list-entry-title-color !important; 14 | } 15 | } 16 | 17 | } 18 | 19 | 20 | .statement-detail { 21 | padding: 16px; 22 | } 23 | 24 | .c-auto-height-animator { 25 | overflow: hidden; 26 | padding: 5px; 27 | transition: height 0.4s ease-out; 28 | } -------------------------------------------------------------------------------- /src/css/base/modules/_tags.scss: -------------------------------------------------------------------------------- 1 | .react-tagsinput { 2 | background-color: #fff; 3 | border: 1px solid #ccc; 4 | overflow: hidden; 5 | padding-left: 5px; 6 | padding-top: 5px; 7 | border-radius: $button-border-radius; 8 | } 9 | 10 | .react-tagsinput-tag { 11 | background-color: #f6f8f2; 12 | border: 1px solid #b9bcb5; 13 | color: #2c2c2c; 14 | border-radius: 2px; 15 | display: inline-block; 16 | font-family: sans-serif; 17 | font-size: 1em; 18 | font-weight: 400; 19 | margin-bottom: 5px; 20 | margin-right: 5px; 21 | padding: 5px; 22 | border-radius: $button-border-radius; 23 | } 24 | 25 | .react-tagsinput-remove { 26 | cursor: pointer; 27 | font-weight: bold; 28 | } 29 | 30 | .react-tagsinput-tag a::before { 31 | content: " ×"; 32 | } 33 | 34 | .react-tagsinput-input { 35 | background: transparent; 36 | border: 0; 37 | color: #777; 38 | font-family: sans-serif; 39 | font-size: 1em; 40 | font-weight: 400; 41 | margin-bottom: 6px; 42 | margin-top: 1px; 43 | outline: none; 44 | padding: 5px; 45 | width: 80px; 46 | } 47 | -------------------------------------------------------------------------------- /src/css/base/modules/_toggle.scss: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-tap-highlight-color: rgba(0,0,0,0); /* Stops flash on tap iOS */ 3 | } 4 | 5 | .toggle { 6 | max-height: 0; 7 | max-width: 0; 8 | opacity: 0; 9 | } 10 | 11 | .toggle + label { 12 | cursor: pointer; 13 | display: block; 14 | position: relative; 15 | box-shadow: inset 0 0 0px 1px #d5d5d5; 16 | text-indent: -5000px; 17 | height: 30px; width: 50px; 18 | border-radius: 15px; 19 | background-color: $base-color; 20 | } 21 | 22 | .toggle + label:before { 23 | content: ''; 24 | position: absolute; 25 | display: block; 26 | height: 30px; width: 30px; 27 | top: 0; left: 0; 28 | border-radius: 15px; 29 | background: $green; 30 | transition: .25s ease-in-out; 31 | } 32 | 33 | .toggle + label:after { 34 | content: ''; 35 | position: absolute; 36 | display: block; 37 | height: 30px; width: 30px; 38 | top: 0; left: 0; 39 | border-radius: 15px; 40 | background: white; 41 | box-shadow: inset 0 0 0 1px rgba(0,0,0,.2), 0 2px 4px rgba(0,0,0,.2); 42 | transition: .25s ease-in-out; 43 | } 44 | 45 | .toggle:checked + label:before { 46 | width: 50px; 47 | background: rgba(19,191,17,1); 48 | } 49 | 50 | .toggle:checked + label:after { 51 | left: 20px; 52 | box-shadow: inset 0 0 0 1px rgba(19,191,17,1), 0 2px 4px rgba(0,0,0,.2); 53 | } 54 | 55 | .toggleDiscussion, .toggleMedia { 56 | 57 | .toggle + label { 58 | background-color: #E4E4E4; 59 | ; 60 | } 61 | 62 | .toggle + label:before { 63 | background: #E4E4E4; 64 | } 65 | 66 | .toggle:checked + label:after { 67 | left: 20px; 68 | box-shadow: inset 0 0 0 1px rgb(180, 191, 180), 0 2px 4px rgba(0,0,0,.2); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/css/base/modules/_view.scss: -------------------------------------------------------------------------------- 1 | .view-form { 2 | max-width: 690px; 3 | padding-top: 80px; 4 | padding-bottom: 80px; 5 | margin-left: auto; 6 | margin-right: auto; 7 | } -------------------------------------------------------------------------------- /src/css/themes/bjv.scss: -------------------------------------------------------------------------------- 1 | @import '../base/colors.scss'; 2 | 3 | $base-color: #3f3f3f; 4 | $text-color: $black; 5 | $link-color: $dark-grey; 6 | $hover-color: $dark-grey; 7 | 8 | $bar-positive1: #49D02EB2; 9 | $bar-positive2: #49D02E80; 10 | $bar-positive3: #49D02E33; 11 | $bar-neutral: #D8D8D8; 12 | $bar-negative3: #FF751A33; 13 | $bar-negative2: #FF751A80; 14 | $bar-negative1: #FF751AB2; 15 | 16 | $font-family-1: Roboto, serif; 17 | $font-family-2: Roboto, sans-serif; 18 | $font-family-icons: FontAwesome; 19 | 20 | $label-rating-positive: $bar-positive1; 21 | $label-rating-negative: $bar-negative1; 22 | $rating-star-rated: $dark-grey; 23 | $rating-star-active: $base-color; 24 | $rating-star-base: #e3e3e3; 25 | $rating-average-label: $dark-grey; 26 | $argument-list-header-text-transform: uppercase; 27 | $argument-list-header-color: black; 28 | $barometer-background-color: $light-grey; 29 | $barometer-bottom-splitter-color: $dark-grey; 30 | 31 | $discussion-title-color: $text-color; 32 | $list-entry-title-color: $text-color; 33 | 34 | @import '../base/main.scss'; 35 | -------------------------------------------------------------------------------- /src/css/themes/brabbl.scss: -------------------------------------------------------------------------------- 1 | @import '../base/colors.scss'; 2 | 3 | $base-color: green; 4 | $text-color: $black; 5 | $link-color: $dark-grey; 6 | $hover-color: darken($base-color, 20%); 7 | 8 | $bar-positive1: #49D02EB2; 9 | $bar-positive2: #49D02E80; 10 | $bar-positive3: #49D02E33; 11 | $bar-neutral: #D8D8D8; 12 | $bar-negative3: #FF751A33; 13 | $bar-negative2: #FF751A80; 14 | $bar-negative1: #FF751AB2; 15 | 16 | $font-family-1: Georgia, serif; 17 | $font-family-2: Arial, sans-serif; 18 | $font-family-icons: FontAwesome; 19 | 20 | $label-rating-positive: $bar-positive1; 21 | $label-rating-negative: $bar-negative1; 22 | $rating-star-rated: $dark-grey; 23 | $rating-star-active: $base-color; 24 | $rating-star-base: #e3e3e3; 25 | $rating-average-label: $dark-grey; 26 | $argument-list-header-text-transform: uppercase; 27 | $argument-list-header-color: black; 28 | $barometer-background-color: $light-grey; 29 | $barometer-bottom-splitter-color: $dark-grey; 30 | 31 | $discussion-title-color: $text-color; 32 | $list-entry-title-color: $text-color; 33 | 34 | @import '../base/main.scss'; 35 | -------------------------------------------------------------------------------- /src/css/themes/brabbl2021.scss: -------------------------------------------------------------------------------- 1 | @import '../base/colors.scss'; 2 | 3 | $base-color: #1E4E8A; 4 | $text-color: $black; 5 | $link-color: $dark-grey; 6 | $hover-color: darken($base-color, 20%); 7 | 8 | $bar-positive1: #49D02EB2; 9 | $bar-positive2: #49D02E80; 10 | $bar-positive3: #49D02E33; 11 | $bar-neutral: #D8D8D8; 12 | $bar-negative3: #FF751A33; 13 | $bar-negative2: #FF751A80; 14 | $bar-negative1: #FF751AB2; 15 | 16 | $font-family-1: Helvetica, sans-serif; 17 | $font-family-2: Helvetica, sans-serif; 18 | $font-family-icons: FontAwesome; 19 | 20 | $label-rating-positive: $bar-positive1; 21 | $label-rating-negative: $bar-negative1; 22 | $rating-star-rated: $dark-grey; 23 | $rating-star-active: $base-color; 24 | $rating-star-base: #e3e3e3; 25 | $rating-average-label: $dark-grey; 26 | $argument-list-header-text-transform: uppercase; 27 | $argument-list-header-color: $black; 28 | $barometer-background-color: $light-grey; 29 | $barometer-bottom-splitter-color: $dark-grey; 30 | 31 | $discussion-title-color: $text-color; 32 | $list-entry-title-color: $text-color; 33 | 34 | @import '../base/main.scss'; 35 | 36 | #brabbl-widget, 37 | .ReactModal__Overlay { 38 | h1 { 39 | line-height: 16px; 40 | } 41 | .fullform-body select { 42 | padding: 5px !important; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/css/themes/eyp.scss: -------------------------------------------------------------------------------- 1 | @import '../base/colors.scss'; 2 | 3 | $base-color: #1E4E8A; 4 | $text-color: $black; 5 | $link-color: $dark-grey; 6 | $hover-color: darken($base-color, 20%); 7 | 8 | $bar-positive1: #49D02EB2; 9 | $bar-positive2: #49D02E80; 10 | $bar-positive3: #49D02E33; 11 | $bar-neutral: #D8D8D8; 12 | $bar-negative3: #FF751A33; 13 | $bar-negative2: #FF751A80; 14 | $bar-negative1: #FF751AB2; 15 | 16 | $font-family-1: Roboto, sans-serif; 17 | $font-family-2: Roboto, sans-serif; 18 | $font-family-icons: FontAwesome; 19 | 20 | $label-rating-positive: $bar-positive1; 21 | $label-rating-negative: $bar-negative1; 22 | $rating-star-rated: $dark-grey; 23 | $rating-star-active: $base-color; 24 | $rating-star-base: #e3e3e3; 25 | $rating-average-label: $dark-grey; 26 | $argument-list-header-text-transform: uppercase; 27 | $argument-list-header-color: $black; 28 | $barometer-background-color: $light-grey; 29 | $barometer-bottom-splitter-color: $dark-grey; 30 | 31 | $discussion-title-color: $text-color; 32 | $list-entry-title-color: $text-color; 33 | 34 | @import '../base/main.scss'; 35 | 36 | #brabbl-widget, 37 | .ReactModal__Overlay { 38 | h1 { 39 | line-height: 16px; 40 | } 41 | .fullform-body select { 42 | padding: 5px !important; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/css/themes/jugendinfo.scss: -------------------------------------------------------------------------------- 1 | @import '../base/colors.scss'; 2 | 3 | $base-color: #05636C; 4 | $text-color: darken($base-color, 98%); 5 | $link-color: $dark-grey; 6 | $hover-color: $dark-grey; 7 | 8 | $bar-positive1: #49D02EB2; 9 | $bar-positive2: #49D02E80; 10 | $bar-positive3: #49D02E33; 11 | $bar-neutral: #D8D8D8; 12 | $bar-negative3: #FF751A33; 13 | $bar-negative2: #FF751A80; 14 | $bar-negative1: #FF751AB2; 15 | 16 | $font-family-1: Tahoma, Calibri, Verdana, Arial, sans-serif; 17 | $font-family-2: Tahoma, Calibri, Verdana, Arial, sans-serif; 18 | $font-family-icons: FontAwesome; 19 | 20 | $label-rating-positive: $bar-positive1; 21 | $label-rating-negative: $bar-negative1; 22 | $rating-star-rated: $base-color; 23 | $rating-star-active: $hover-color; 24 | $rating-star-base: #e3e3e3; 25 | $rating-average-label: $base-color; 26 | $argument-list-header-text-transform: None; 27 | $argument-list-header-color: $dark-grey; 28 | $barometer-background-color: $medium-grey; 29 | $barometer-bottom-splitter-color: $dark-grey; 30 | 31 | $discussion-title-color: $text-color; 32 | $list-entry-title-color: $text-color; 33 | 34 | @import '../base/main.scss'; 35 | -------------------------------------------------------------------------------- /src/css/themes/main.scss: -------------------------------------------------------------------------------- 1 | .spinner { 2 | text-align: center; 3 | position: relative; 4 | top: 20px; 5 | font-size: 60px; 6 | line-height: 16.8px; 7 | color: #EAEAEA; 8 | } 9 | -------------------------------------------------------------------------------- /src/css/themes/vorwaerts.scss: -------------------------------------------------------------------------------- 1 | @import '../base/colors.scss'; 2 | 3 | $base-color: $red; 4 | $text-color: $black; 5 | $link-color: $dark-grey; 6 | $hover-color: darken($base-color, 20%); 7 | 8 | $bar-positive1: #49D02EB2; 9 | $bar-positive2: #49D02E80; 10 | $bar-positive3: #49D02E33; 11 | $bar-neutral: #D8D8D8; 12 | $bar-negative3: #FF751A33; 13 | $bar-negative2: #FF751A80; 14 | $bar-negative1: #FF751AB2; 15 | 16 | 17 | $font-family-1: Georgia, serif; 18 | $font-family-2: Arial, sans-serif; 19 | $font-family-icons: FontAwesome; 20 | 21 | $label-rating-positive: $bar-positive1; 22 | $label-rating-negative: $bar-negative1; 23 | $rating-star-rated: $dark-grey; 24 | $rating-star-active: $base-color; 25 | $rating-star-base: #e3e3e3; 26 | $rating-average-label: $dark-grey; 27 | $argument-list-header-text-transform: uppercase; 28 | $argument-list-header-color: black; 29 | $barometer-background-color: $light-grey; 30 | $barometer-bottom-splitter-color: $dark-grey; 31 | 32 | $discussion-title-color: $text-color; 33 | $list-entry-title-color: $text-color; 34 | 35 | @import '../base/main.scss'; 36 | -------------------------------------------------------------------------------- /src/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redblame315/brabbl-frontend/100f4b761f450273395c62ccfb033123b80f5ae4/src/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /src/fonts/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/fonts/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /src/fonts/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/fonts/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /src/fonts/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /src/fonts/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/fonts/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | -------------------------------------------------------------------------------- /src/fonts/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /src/fonts/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /src/fonts/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /src/fonts/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.4.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | -------------------------------------------------------------------------------- /src/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redblame315/brabbl-frontend/100f4b761f450273395c62ccfb033123b80f5ae4/src/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redblame315/brabbl-frontend/100f4b761f450273395c62ccfb033123b80f5ae4/src/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redblame315/brabbl-frontend/100f4b761f450273395c62ccfb033123b80f5ae4/src/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redblame315/brabbl-frontend/100f4b761f450273395c62ccfb033123b80f5ae4/src/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /src/img/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redblame315/brabbl-frontend/100f4b761f450273395c62ccfb033123b80f5ae4/src/img/.keep -------------------------------------------------------------------------------- /src/img/Checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redblame315/brabbl-frontend/100f4b761f450273395c62ccfb033123b80f5ae4/src/img/Checkmark.png -------------------------------------------------------------------------------- /src/img/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redblame315/brabbl-frontend/100f4b761f450273395c62ccfb033123b80f5ae4/src/img/arrow.png -------------------------------------------------------------------------------- /src/js/actions/async.js: -------------------------------------------------------------------------------- 1 | import API from '../api'; 2 | import { undiscussion_list } from '../reducers/discussion'; 3 | import { processBootstrap, processDiscussionList, processUnDiscussionList, updateDiscussion, showArgument, 4 | fetchTrans, loading, setShouldSkipUpdate, setAutoUpdateLoading } from './app'; 5 | 6 | 7 | export function bootstrapApp(articleId=null, isSilent=false) { 8 | return (dispatch, getState) => { 9 | if(!isSilent) { 10 | dispatch(loading(true)) 11 | } 12 | dispatch(setAutoUpdateLoading(true)) 13 | console.log("bootstrap"); 14 | API.bootstrap(articleId).then(resp => { 15 | console.log(resp); 16 | const { shouldSkipUpdate } = getState().app; 17 | if(!shouldSkipUpdate) { 18 | dispatch(processBootstrap(resp)) 19 | } else { 20 | dispatch(setShouldSkipUpdate(false)) 21 | } 22 | 23 | if(articleId != null) 24 | API.get_undiscussion_list().then(resp=>{dispatch(processUnDiscussionList(resp))}) 25 | }) 26 | } 27 | } 28 | 29 | export function getDiscussionList(url) { 30 | return dispatch => API.get_discussion_list(url).then(resp => dispatch(processDiscussionList(resp))); 31 | } 32 | 33 | export function getUnDiscussionList() { 34 | return dispatch => API.get_undiscussion_list().then(resp => dispatch(processUnDiscussionList(resp))); 35 | } 36 | 37 | export function reloadDiscussion(articleId) { 38 | return (dispatch, getState) => 39 | API.get_discussion(articleId || window.brabbl.articleId) 40 | .then(resp => { 41 | console.log("reaload_discussion"); 42 | const { isAutoUpdateFetchInProgress } = getState().app; 43 | if(isAutoUpdateFetchInProgress) { 44 | dispatch(setShouldSkipUpdate(true)) 45 | } 46 | dispatch(updateDiscussion(resp)) 47 | // console.log("call_get_undiscussion"); 48 | // return API.get_undiscussion_list(); 49 | }); 50 | // .then(resp=>{ 51 | // console.log("undiscussion_list_resp"); 52 | // console.log(resp); 53 | // dispatch(undiscussion_list(resp.undiscussion_list)) 54 | // }); 55 | } 56 | 57 | export function fetchArgument(id) { 58 | return dispatch => API.get_argument(id).then(resp => dispatch(showArgument(resp))); 59 | } 60 | 61 | export function loadTrans() { 62 | return dispatch => API.get_trans().then(resp => dispatch(fetchTrans(resp))); 63 | } -------------------------------------------------------------------------------- /src/js/components/__tests__/app.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount, render } from 'enzyme'; 3 | import App from '../app'; 4 | import configureStore from 'redux-mock-store'; 5 | import { Provider } from 'react-redux'; 6 | const mockStore = configureStore([]); 7 | import { initialState } from '../__mocks__/app.mock'; 8 | 9 | describe("App View", () => { 10 | test('renders the spinner if no discussion(s) could be fetched', () => { 11 | initialState.app.view = 'detail'; 12 | initialState.app.loading = true; 13 | const store = mockStore(initialState); 14 | 15 | const wrapper = render( 16 | 17 | 18 | ); 19 | expect(wrapper.find('.fa-spinner')).toHaveLength(1) 20 | }); 21 | 22 | test('renders the list-view if requested', () => { 23 | let state = { ...initialState }; 24 | state.app = { ...state.app, view: 'list', loading: false }; 25 | const store = mockStore(state); 26 | 27 | const wrapper = render( 28 | 29 | 30 | ); 31 | expect(wrapper.hasClass('discussion-list-widget')).toEqual(true) 32 | }); 33 | }) -------------------------------------------------------------------------------- /src/js/components/argument/index.js: -------------------------------------------------------------------------------- 1 | import ArgumentButtons from './argument-buttons'; 2 | import ArgumentList from './argument-list'; 3 | import Argument from './argument'; 4 | import StarRating from './starrating'; 5 | 6 | export { ArgumentButtons, ArgumentList, Argument, StarRating }; 7 | -------------------------------------------------------------------------------- /src/js/components/avatar-container.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Avatar from 'react-avatar'; 4 | import moment from 'moment'; 5 | 6 | class AvatarContainer extends React.Component { 7 | 8 | render() { 9 | let user = this.props.user; 10 | if (!user) return null; 11 | let avatar_props = { 12 | src: user.image ? user.image.small : null, 13 | name: user.display_name, 14 | size: this.props.size || 38, 15 | className: 'avatar', 16 | }; 17 | let hideInfo = this.props.hide_info 18 | let dateonly = this.props.dateonly 19 | if (user.linked && user.linked.length > 0 && !user.image.small) { 20 | let social_props = user.linked[0]; 21 | avatar_props[social_props[0].replace('-oauth2', '') + 'Id'] = social_props[2]; 22 | } 23 | let formatedDate = moment(this.props.date).format(this.props.dateFormat || "DD.MM.YYYY"); 24 | let containerStyle = "avatar-container " + (this.props.containerStyle || "") 25 | return ( 26 |
27 | { 28 | !dateonly && 29 | 33 | } 34 | 35 | { 36 | !hideInfo && 37 |
38 | { 39 | !dateonly && 40 | {user.display_name} 41 | } 42 | { 43 | this.props.date && 44 | 45 | {formatedDate} 46 | 47 | } 48 |
49 | } 50 |
51 | ); 52 | } 53 | } 54 | 55 | AvatarContainer.propTypes = { 56 | user: PropTypes.object, 57 | size: PropTypes.number, 58 | date: PropTypes.string, 59 | dateFormat: PropTypes.string 60 | }; 61 | 62 | export default AvatarContainer; 63 | -------------------------------------------------------------------------------- /src/js/components/common/atoms/CAutoHeightAnimator/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class CAutoHeightAnimator extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.prevHeight = 0; 8 | } 9 | componentDidMount() { 10 | let elWrap = document.getElementById(this.props.divToAnimate) 11 | let wrect = elWrap.getBoundingClientRect(); 12 | this.prevHeight = wrect.height + 10; 13 | } 14 | 15 | animate = () => { 16 | // get the current height 17 | let eAnimator = document.getElementById(this.props.id) 18 | let elWrap = document.getElementById(this.props.divToAnimate) 19 | let prevHeight = this.prevHeight 20 | 21 | // Set height:auto and get the new height 22 | let rect = elWrap.getBoundingClientRect(); 23 | let newHeight = rect.height + 10; 24 | 25 | eAnimator.style.height = prevHeight + 'px' 26 | 27 | // Start Animation in next Animation frame from original height to new height 28 | // Reveal the contents via opacity animation 29 | requestAnimationFrame(() => { 30 | eAnimator.style.height = newHeight + 'px' 31 | eAnimator.style.transitionDelay = this.props.transitionDelay 32 | }) 33 | setTimeout(function() { eAnimator.style.height = 'auto'; }, 500); 34 | this.prevHeight = newHeight 35 | } 36 | render () { 37 | return
38 | {this.props.children} 39 |
40 | 41 | } 42 | } 43 | 44 | export default CAutoHeightAnimator -------------------------------------------------------------------------------- /src/js/components/common/atoms/CButton/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class CButton extends Component { 5 | onClick = () => { 6 | if(!this.props.isLoading) { 7 | if(this.props.onClick) this.props.onClick(); 8 | if(this.props.isFilePicker) { 9 | this.upload.click(); 10 | } 11 | } 12 | 13 | } 14 | onFileChange = (e) => { 15 | if(this.props.onFileChange) { 16 | this.props.onFileChange(e); 17 | } 18 | } 19 | getClassByType=(type) => { 20 | if(type == 'delete') 21 | return 'cw-button-container-delete' 22 | if(type == 'cancel') 23 | return 'cw-button-container-cancel' 24 | if(type == 'secondary') 25 | return 'cw-button-container-secondary' 26 | return 'cw-button-container' 27 | } 28 | render () { 29 | const props = this.props; 30 | return 40 | } 41 | } 42 | 43 | CButton.propTypes = { 44 | isFilePicker: PropTypes.bool, 45 | fileAccept: PropTypes.string, 46 | onClick: PropTypes.func, 47 | onFileChange: PropTypes.func, 48 | isLoading: PropTypes.func 49 | }; 50 | 51 | 52 | export default CButton -------------------------------------------------------------------------------- /src/js/components/common/atoms/CCircularProgress/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | function CircularProgressWithLabel(props) { 5 | return ( 6 | 7 | {Math.round(props.value)}% 8 | 9 | ); 10 | } 11 | 12 | CircularProgressWithLabel.propTypes = { 13 | /** 14 | * The value of the progress indicator for the determinate variant. 15 | * Value between 0 and 100. 16 | */ 17 | value: PropTypes.number.isRequired, 18 | }; 19 | 20 | export default CircularProgressWithLabel -------------------------------------------------------------------------------- /src/js/components/common/atoms/CCollapsable/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class CCollapsable extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | collapsed: !props.opened 9 | } 10 | } 11 | onCollapse = () => { 12 | this.setState({collapsed: !this.state.collapsed}) 13 | } 14 | render () { 15 | const props = this.props; 16 | return
17 | {this.onCollapse()}} className={"collapsable-section-title"}> 18 |

19 | {this.props.title} 22 |

23 |
24 |
25 | {props.children} 26 |
27 | 28 |
29 | 30 | } 31 | } 32 | 33 | CCollapsable.propTypes = { 34 | title: PropTypes.string, 35 | opened: PropTypes.bool 36 | }; 37 | 38 | 39 | export default CCollapsable -------------------------------------------------------------------------------- /src/js/components/common/atoms/CImageLoader/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const _loaded = {}; 4 | 5 | class CImageLoader extends React.Component { 6 | 7 | //initial state: image loaded stage 8 | state = { 9 | loaded: _loaded[this.props.src] 10 | }; 11 | 12 | //define our loading and loaded image classes 13 | static defaultProps = { 14 | className: "", 15 | loadingClassName: "img-loading", 16 | loadedClassName: "img-loaded" 17 | }; 18 | 19 | //image onLoad handler to update state to loaded 20 | onLoad = () => { 21 | _loaded[this.props.src] = true; 22 | this.setState(() => ({ loaded: true })); 23 | }; 24 | 25 | 26 | render() { 27 | 28 | let { className, loadedClassName, loadingClassName, ...props } = this.props; 29 | 30 | className = `${className} ${this.state.loaded 31 | ? loadedClassName 32 | : loadingClassName}`; 33 | 34 | return ; 39 | } 40 | } 41 | 42 | export default CImageLoader; -------------------------------------------------------------------------------- /src/js/components/common/atoms/CMarkupParser/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types' 3 | import i18next from 'i18next'; 4 | 5 | class CMarkupParser extends Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | renderParagraphs = () => { 11 | let html = this.props.htmlString; 12 | if (!html) return []; 13 | let paragraphs = html.split("\n\n"); 14 | return paragraphs.map(paragraph => { 15 | return this.renderParagraph(paragraph) 16 | }) 17 | } 18 | renderParagraph = (paragraph) => { 19 | return

{this.renderTexts(paragraph)}

20 | } 21 | renderBoldText = (text) => { 22 | return {this.renderNormalText(text)} 23 | } 24 | renderNormalText = (text) => { 25 | if(!text) return [] 26 | let normalTexts = text.split('\n') 27 | if(normalTexts.length > 1) { 28 | return normalTexts.map((normalText, index) => { 29 | if(normalTexts.length > 1 && index < normalTexts.length - 1) { 30 | return {normalText}
31 | } else { 32 | return normalText 33 | } 34 | }) 35 | } else { 36 | return text 37 | } 38 | 39 | } 40 | renderTexts = (paragraph) => { 41 | let texts = paragraph.split("**"); 42 | let boldTexts = texts.map((text, index) => { 43 | if (index > 0 && index < texts.length - 1) { 44 | return this.renderBoldText(text) 45 | } else { 46 | return this.renderNormalText(text) 47 | } 48 | }) 49 | return boldTexts 50 | } 51 | 52 | render (){ 53 | return
54 | {this.renderParagraphs()} 55 |
56 | } 57 | } 58 | 59 | CMarkupParser.propTypes = { 60 | htmlString: PropTypes.string 61 | } 62 | 63 | export default CMarkupParser -------------------------------------------------------------------------------- /src/js/components/common/atoms/CPagination/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Pagination from 'rc-pagination' 3 | const localeInfo = { 4 | 5 | } 6 | class CPagination extends Component { 7 | 8 | render () { 9 | let prevIcon = {'<'} 10 | let nextIcon = {'>'} 11 | const props = this.props; 12 | return
13 | 18 |
19 | 20 | } 21 | } 22 | 23 | export default CPagination -------------------------------------------------------------------------------- /src/js/components/common/atoms/index.js: -------------------------------------------------------------------------------- 1 | import CRemovableList from './CRemovableList' 2 | import CMarkupParser from './CMarkupParser' 3 | import CButton from './CButton' 4 | import CCircularProgress from './CCircularProgress' 5 | import CCollapsable from './CCollapsable' 6 | import CImageLoader from './CImageLoader' 7 | import CPagination from './CPagination' 8 | import CAutoHeightAnimator from './CAutoHeightAnimator' 9 | 10 | export { 11 | CRemovableList, 12 | CMarkupParser, 13 | CButton, 14 | CCircularProgress, 15 | CCollapsable, 16 | CImageLoader, 17 | CPagination, 18 | CAutoHeightAnimator 19 | } -------------------------------------------------------------------------------- /src/js/components/common/custom-widgets/CKEditor.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import forms from '../newforms/newforms'; 3 | 4 | var object = require('isomorph/object') 5 | var React = require('react') 6 | import { CKEditor } from '@ckeditor/ckeditor5-react'; 7 | import ClassicEditor from '@ckeditor/ckeditor5-build-classic'; 8 | 9 | var Widget = forms.Widget 10 | 11 | var CKEditorWidget = Widget.extend({ 12 | constructor: function CKEditor(kwargs) { 13 | if (!(this instanceof CKEditor)) { return new CKEditor(kwargs) } 14 | // Ensure we have something in attrs 15 | kwargs = object.extend({attrs: null}, kwargs) 16 | // Provide default 'cols' and 'rows' attributes 17 | kwargs.attrs = object.extend({rows: '3', cols: '40'}, kwargs.attrs) 18 | Widget.call(this, kwargs) 19 | } 20 | }) 21 | 22 | CKEditorWidget.prototype.render = function(name, value, kwargs) { 23 | kwargs = object.extend({}, kwargs) 24 | if (value === null) { 25 | value = '' 26 | } 27 | var finalAttrs = this.buildAttrs(kwargs.attrs, {name: name}) 28 | var valueAttr = 'data' 29 | finalAttrs[valueAttr] = value 30 | //return React.createElement('textarea', finalAttrs) 31 | return { 37 | editor.removeAttr("title"); 38 | // You can store the "editor" and use when it is needed. 39 | } } 40 | {...finalAttrs} 41 | onChange={ ( event, editor ) => { 42 | const data = editor.getData(); 43 | const e = { 44 | target: { 45 | name: finalAttrs.name, 46 | description: finalAttrs.description, 47 | getAttribute: (name) => { 48 | return null 49 | }, 50 | value: data 51 | }, 52 | } 53 | finalAttrs.onChange(e) 54 | 55 | } } 56 | onBlur={ ( event, editor ) => { 57 | } } 58 | onFocus={ ( event, editor ) => { 59 | } } 60 | 61 | /> 62 | } 63 | 64 | export default CKEditorWidget 65 | -------------------------------------------------------------------------------- /src/js/components/common/index.js: -------------------------------------------------------------------------------- 1 | import forms from './newforms/newforms'; 2 | 3 | export { forms }; 4 | -------------------------------------------------------------------------------- /src/js/components/common/newforms/components/FormRow.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react') 4 | 5 | var BoundField = require('../BoundField') 6 | var ProgressMixin = require('./ProgressMixin') 7 | var PropTypes = require('prop-types') 8 | 9 | /** 10 | * Renders a "row" in a form. This can contain manually provided contents, or 11 | * if a BoundField is given, it will be used to display a field's label, widget, 12 | * error message(s), help text and async pending indicator. 13 | */ 14 | class FormRow extends React.Component { 15 | 16 | constructor(props) { 17 | super(props); 18 | this.mixins = [ProgressMixin]; 19 | } 20 | 21 | 22 | render() { 23 | var attrs = {} 24 | if (this.props.className) { 25 | attrs.className = this.props.className 26 | } 27 | if (this.props.hidden) { 28 | attrs.style = {display: 'none'} 29 | } 30 | // If content was given, use it 31 | if (this.props.content) { 32 | return {this.props.content} 33 | } 34 | // Otherwise render a BoundField 35 | var bf = this.props.bf 36 | var isPending = bf.isPending() 37 | return 38 | {bf.label && bf.labelTag()} {bf.render()} 39 | {isPending && ' '} 40 | {isPending && this.renderProgress()} 41 | {bf.errors().render()} 42 | {bf.helpText && ' '} 43 | {bf.helpTextTag()} 44 | 45 | } 46 | } 47 | 48 | FormRow.propTypes = { 49 | bf: PropTypes.instanceOf(BoundField) 50 | , className: PropTypes.string 51 | , component: PropTypes.any 52 | , content: PropTypes.any 53 | , hidden: PropTypes.bool 54 | , __all__(props) { 55 | if (!props.bf && !props.content) { 56 | return new Error( 57 | 'Invalid props supplied to `FormRow`, either `bf` or `content` ' + 58 | 'must be specified.' 59 | ) 60 | } 61 | if (props.bf && props.content) { 62 | return new Error( 63 | 'Both `bf` and `content` props were passed to `FormRow` - `bf` ' + 64 | 'will be ignored.' 65 | ) 66 | } 67 | } 68 | }; 69 | 70 | FormRow.defaultProps = { 71 | component: 'div' 72 | } 73 | 74 | module.exports = FormRow -------------------------------------------------------------------------------- /src/js/components/common/newforms/components/ProgressMixin.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var is = require('isomorph/is') 4 | var React = require('react') 5 | var PropTypes = require('prop-types') 6 | 7 | var ProgressMixin = { 8 | propTypes: { 9 | progress: PropTypes.any // Component or function to render async progress 10 | }, 11 | 12 | renderProgress() { 13 | if (!this.props.progress) { 14 | return Validating... 15 | } 16 | if (is.Function(this.props.progress)) { 17 | return this.props.progress() 18 | } 19 | return 20 | } 21 | } 22 | 23 | module.exports = ProgressMixin -------------------------------------------------------------------------------- /src/js/components/common/newforms/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | /** Default maximum number of forms in a formset, to prevent memory exhaustion. */ 5 | FORMSET_DEFAULT_MAX_NUM: 1000 6 | /** Default minimum number of forms in a formset. */ 7 | , FORMSET_DEFAULT_MIN_NUM: 0 8 | /** Property under which non-field-specific errors are stored. */ 9 | , NON_FIELD_ERRORS: '__all__' 10 | } -------------------------------------------------------------------------------- /src/js/components/common/newforms/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | browser: typeof window != 'undefined' 5 | } -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/BaseTemporalField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var is = require('isomorph/is') 4 | var object = require('isomorph/object') 5 | var time = require('isomorph/time') 6 | 7 | var formats = require('../formats') 8 | var locales = require('../locales') 9 | 10 | var Field = require('../Field') 11 | 12 | var {ValidationError} = require('validators') 13 | var {strip} = require('../util') 14 | 15 | /** 16 | * Base field for fields which validate that their input is a date or time. 17 | * @constructor 18 | * @extends {Field} 19 | * @param {Object=} kwargs 20 | */ 21 | var BaseTemporalField = Field.extend({ 22 | inputFormatType: '' 23 | , constructor: function BaseTemporalField(kwargs) { 24 | kwargs = object.extend({inputFormats: null}, kwargs) 25 | Field.call(this, kwargs) 26 | this.inputFormats = kwargs.inputFormats 27 | } 28 | }) 29 | 30 | /** 31 | * Validates that its input is a valid date or time. 32 | * @param {(string|Date)} value user input. 33 | * @return {Date} 34 | * @throws {ValidationError} if the input is invalid. 35 | */ 36 | BaseTemporalField.prototype.toJavaScript = function(value) { 37 | if (!is.Date(value)) { 38 | value = strip(value) 39 | } 40 | if (is.String(value)) { 41 | if (this.inputFormats === null) { 42 | this.inputFormats = formats.getFormat(this.inputFormatType) 43 | } 44 | for (var i = 0, l = this.inputFormats.length; i < l; i++) { 45 | try { 46 | return this.strpdate(value, this.inputFormats[i]) 47 | } 48 | catch (e) { 49 | // pass 50 | } 51 | } 52 | } 53 | throw ValidationError(this.errorMessages.invalid, {code: 'invalid'}) 54 | } 55 | 56 | /** 57 | * Creates a Date from the given input if it's valid based on a format. 58 | * @param {string} value 59 | * @param {string} format 60 | * @return {Date} 61 | */ 62 | BaseTemporalField.prototype.strpdate = function(value, format) { 63 | return time.strpdate(value, format, locales.getDefaultLocale()) 64 | } 65 | 66 | BaseTemporalField.prototype._hasChanged = function(initial, data) { 67 | try { 68 | data = this.toJavaScript(data) 69 | } 70 | catch (e) { 71 | if (!(e instanceof ValidationError)) { throw e } 72 | return true 73 | } 74 | initial = this.toJavaScript(initial) 75 | if (!!initial && !!data) { 76 | return initial.getTime() !== data.getTime() 77 | } 78 | else { 79 | return initial !== data 80 | } 81 | } 82 | 83 | 84 | module.exports = BaseTemporalField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/BooleanField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var is = require('isomorph/is') 4 | 5 | var CheckboxInput = require('../widgets/CheckboxInput') 6 | var Field = require('../Field') 7 | 8 | var {ValidationError} = require('validators') 9 | 10 | /** 11 | * Normalises its input to a Boolean primitive. 12 | * @constructor 13 | * @extends {Field} 14 | * @param {Object=} kwargs 15 | */ 16 | var BooleanField = Field.extend({ 17 | widget: CheckboxInput 18 | 19 | , constructor: function BooleanField(kwargs) { 20 | if (!(this instanceof BooleanField)) { return new BooleanField(kwargs) } 21 | Field.call(this, kwargs) 22 | } 23 | }) 24 | 25 | BooleanField.prototype.toJavaScript = function(value) { 26 | // Explicitly check for a 'false' string, which is what a hidden field will 27 | // submit for false. Also check for '0', since this is what RadioSelect will 28 | // provide. Because Boolean('anything') == true, we don't need to handle that 29 | // explicitly. 30 | if (is.String(value) && (value.toLowerCase() == 'false' || value == '0')) { 31 | value = false 32 | } 33 | else { 34 | value = Boolean(value) 35 | } 36 | value = Field.prototype.toJavaScript.call(this, value) 37 | if (!value && this.required) { 38 | throw ValidationError(this.errorMessages.required, {code: 'required'}) 39 | } 40 | return value 41 | } 42 | 43 | BooleanField.prototype._hasChanged = function(initial, data) { 44 | // Sometimes data or initial could be null or '' which should be the same 45 | // thing as false. 46 | if (initial === 'false') { 47 | // showHiddenInitial may have transformed false to 'false' 48 | initial = false 49 | } 50 | return (Boolean(initial) != Boolean(data)) 51 | } 52 | 53 | module.exports = BooleanField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/CharField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var object = require('isomorph/object') 4 | 5 | var Field = require('../Field') 6 | var PasswordInput = require('../widgets/PasswordInput') 7 | var TextInput = require('../widgets/TextInput') 8 | 9 | var {MinLengthValidator, MaxLengthValidator} = require('validators') 10 | 11 | /** 12 | * Validates that its input is a valid String. 13 | * @constructor 14 | * @extends {Field} 15 | * @param {Object=} kwargs 16 | */ 17 | var CharField = Field.extend({ 18 | constructor: function CharField(kwargs) { 19 | if (!(this instanceof CharField)) { return new CharField(kwargs) } 20 | kwargs = object.extend({maxLength: null, minLength: null}, kwargs) 21 | this.maxLength = kwargs.maxLength 22 | this.minLength = kwargs.minLength 23 | Field.call(this, kwargs) 24 | if (this.minLength !== null) { 25 | this.validators.push(MinLengthValidator(this.minLength)) 26 | } 27 | if (this.maxLength !== null) { 28 | this.validators.push(MaxLengthValidator(this.maxLength)) 29 | } 30 | } 31 | }) 32 | 33 | /** 34 | * @return {string} 35 | */ 36 | CharField.prototype.toJavaScript = function(value) { 37 | if (this.isEmptyValue(value)) { 38 | return '' 39 | } 40 | return ''+value 41 | } 42 | 43 | /** 44 | * If this field is configured to enforce a maximum length, adds a suitable 45 | * maxLength attribute to text input fields. 46 | * @param {Widget} widget the widget being used to render this field's value. 47 | * @return {Object} additional attributes which should be added to the widget. 48 | */ 49 | CharField.prototype.getWidgetAttrs = function(widget) { 50 | var attrs = Field.prototype.getWidgetAttrs.call(this, widget) 51 | if (this.maxLength !== null && (widget instanceof TextInput || 52 | widget instanceof PasswordInput)) { 53 | attrs.maxLength = ''+this.maxLength 54 | } 55 | return attrs 56 | } 57 | 58 | module.exports = CharField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/ChoiceField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var is = require('isomorph/is') 4 | var object = require('isomorph/object') 5 | 6 | var Field = require('../Field') 7 | var Select = require('../widgets/Select') 8 | 9 | var {ValidationError} = require('validators') 10 | var {normaliseChoices} = require('../util') 11 | 12 | /** 13 | * Validates that its input is one of a valid list of choices. 14 | * @constructor 15 | * @extends {Field} 16 | * @param {Object=} kwargs 17 | */ 18 | var ChoiceField = Field.extend({ 19 | widget: Select 20 | , defaultErrorMessages: { 21 | invalidChoice: 'Select a valid choice. {value} is not one of the available choices.' 22 | } 23 | 24 | , constructor: function ChoiceField(kwargs) { 25 | if (!(this instanceof ChoiceField)) { return new ChoiceField(kwargs) } 26 | kwargs = object.extend({choices: []}, kwargs) 27 | Field.call(this, kwargs) 28 | this.setChoices(kwargs.choices) 29 | } 30 | }) 31 | 32 | ChoiceField.prototype.choices = function() { return this._choices } 33 | ChoiceField.prototype.setChoices = function(choices) { 34 | // Setting choices also sets the choices on the widget 35 | this._choices = this.widget.choices = normaliseChoices(choices) 36 | } 37 | 38 | ChoiceField.prototype.toJavaScript = function(value) { 39 | if (this.isEmptyValue(value)) { 40 | return '' 41 | } 42 | return ''+value 43 | } 44 | 45 | /** 46 | * Validates that the given value is in this field's choices. 47 | */ 48 | ChoiceField.prototype.validate = function(value) { 49 | Field.prototype.validate.call(this, value) 50 | if (value && !this.validValue(value)) { 51 | throw ValidationError(this.errorMessages.invalidChoice, { 52 | code: 'invalidChoice' 53 | , params: {value: value} 54 | }) 55 | } 56 | } 57 | 58 | /** 59 | * Checks to see if the provided value is a valid choice. 60 | * @param {string} value the value to be validated. 61 | */ 62 | ChoiceField.prototype.validValue = function(value) { 63 | var choices = this.choices() 64 | for (var i = 0, l = choices.length; i < l; i++) { 65 | if (is.Array(choices[i][1])) { 66 | // This is an optgroup, so look inside the group for options 67 | var optgroupChoices = choices[i][1] 68 | for (var j = 0, m = optgroupChoices.length; j < m; j++) { 69 | if (value === ''+optgroupChoices[j][0]) { 70 | return true 71 | } 72 | } 73 | } 74 | else if (value === ''+choices[i][0]) { 75 | return true 76 | } 77 | } 78 | return false 79 | } 80 | 81 | module.exports = ChoiceField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/ComboField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var object = require('isomorph/object') 4 | 5 | var Field = require('../Field') 6 | 7 | /** 8 | * A Field whose clean() method calls multiple Field clean() methods. 9 | * @constructor 10 | * @extends {Field} 11 | * @param {Object=} kwargs 12 | */ 13 | var ComboField = Field.extend({ 14 | constructor: function ComboField(kwargs) { 15 | if (!(this instanceof ComboField)) { return new ComboField(kwargs) } 16 | kwargs = object.extend({fields: []}, kwargs) 17 | Field.call(this, kwargs) 18 | // Set required to False on the individual fields, because the required 19 | // validation will be handled by ComboField, not by those individual fields. 20 | for (var i = 0, l = kwargs.fields.length; i < l; i++) { 21 | kwargs.fields[i].required = false 22 | } 23 | this.fields = kwargs.fields 24 | } 25 | }) 26 | 27 | ComboField.prototype.clean = function(value) { 28 | Field.prototype.clean.call(this, value) 29 | for (var i = 0, l = this.fields.length; i < l; i++) { 30 | value = this.fields[i].clean(value) 31 | } 32 | return value 33 | } 34 | 35 | module.exports = ComboField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/DateField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var BaseTemporalField = require('./BaseTemporalField') 4 | var DateInput = require('../widgets/DateInput') 5 | 6 | /** 7 | * Validates that its input is a date. 8 | * @constructor 9 | * @extends {BaseTemporalField} 10 | * @param {Object=} kwargs 11 | */ 12 | var DateField = BaseTemporalField.extend({ 13 | widget: DateInput 14 | , inputFormatType: 'DATE_INPUT_FORMATS' 15 | , defaultErrorMessages: { 16 | invalid: 'Enter a valid date.' 17 | } 18 | 19 | , constructor: function DateField(kwargs) { 20 | if (!(this instanceof DateField)) { return new DateField(kwargs) } 21 | BaseTemporalField.call(this, kwargs) 22 | } 23 | }) 24 | 25 | /** 26 | * Validates that the input can be converted to a date. 27 | * @param {?(string|Date)} value user input. 28 | * @return {?Date} a with its year, month and day attributes set, or null for 29 | * empty values when they are allowed. 30 | * @throws {ValidationError} if the input is invalid. 31 | */ 32 | DateField.prototype.toJavaScript = function(value) { 33 | if (this.isEmptyValue(value)) { 34 | return null 35 | } 36 | if (value instanceof Date) { 37 | return new Date(value.getFullYear(), value.getMonth(), value.getDate()) 38 | } 39 | return BaseTemporalField.prototype.toJavaScript.call(this, value) 40 | } 41 | 42 | module.exports = DateField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/DateTimeField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var is = require('isomorph/is') 4 | 5 | var BaseTemporalField = require('./BaseTemporalField') 6 | var DateTimeInput = require('../widgets/DateTimeInput') 7 | 8 | var {ValidationError} = require('validators') 9 | 10 | /** 11 | * Validates that its input is a date/time. 12 | * @constructor 13 | * @extends {BaseTemporalField} 14 | * @param {Object=} kwargs 15 | */ 16 | var DateTimeField = BaseTemporalField.extend({ 17 | widget: DateTimeInput 18 | , inputFormatType: 'DATETIME_INPUT_FORMATS' 19 | , defaultErrorMessages: { 20 | invalid: 'Enter a valid date/time.' 21 | } 22 | 23 | , constructor: function DateTimeField(kwargs) { 24 | if (!(this instanceof DateTimeField)) { return new DateTimeField(kwargs) } 25 | BaseTemporalField.call(this, kwargs) 26 | } 27 | }) 28 | 29 | /** 30 | * @param {?(string|Date|Array.)} value user input. 31 | * @return {?Date} 32 | * @throws {ValidationError} if the input is invalid. 33 | */ 34 | DateTimeField.prototype.toJavaScript = function(value) { 35 | if (this.isEmptyValue(value)) { 36 | return null 37 | } 38 | if (value instanceof Date) { 39 | return value 40 | } 41 | if (is.Array(value)) { 42 | // Input comes from a SplitDateTimeWidget, for example, so it's two 43 | // components: date and time. 44 | if (value.length != 2) { 45 | throw ValidationError(this.errorMessages.invalid, {code: 'invalid'}) 46 | } 47 | if (this.isEmptyValue(value[0]) && this.isEmptyValue(value[1])) { 48 | return null 49 | } 50 | value = value.join(' ') 51 | } 52 | return BaseTemporalField.prototype.toJavaScript.call(this, value) 53 | } 54 | 55 | 56 | module.exports = DateTimeField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/EmailField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('../util') 4 | 5 | var CharField = require('./CharField') 6 | var EmailInput = require('../widgets/EmailInput') 7 | 8 | var {validateEmail} = require('validators') 9 | 10 | /** 11 | * Validates that its input appears to be a valid e-mail address. 12 | * @constructor 13 | * @extends {CharField} 14 | * @param {Object=} kwargs 15 | */ 16 | var EmailField = CharField.extend({ 17 | widget: EmailInput 18 | , defaultValidators: [validateEmail] 19 | 20 | , constructor: function EmailField(kwargs) { 21 | if (!(this instanceof EmailField)) { return new EmailField(kwargs) } 22 | CharField.call(this, kwargs) 23 | } 24 | }) 25 | 26 | EmailField.prototype.clean = function(value) { 27 | value = util.strip(this.toJavaScript(value)) 28 | return CharField.prototype.clean.call(this, value) 29 | } 30 | 31 | 32 | module.exports = EmailField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/FilePathField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var object = require('isomorph/object') 4 | 5 | var ChoiceField = require('./ChoiceField') 6 | 7 | /** 8 | * Allows choosing from files inside a certain directory. 9 | * @constructor 10 | * @extends {ChoiceField} 11 | * @param {string} path 12 | * @param {Object=} kwargs 13 | */ 14 | var FilePathField = ChoiceField.extend({ 15 | constructor: function FilePathField(path, kwargs) { 16 | if (!(this instanceof FilePathField)) { return new FilePathField(path, kwargs) } 17 | kwargs = object.extend({ 18 | match: null, recursive: false, required: true, widget: null, 19 | label: null, initial: null, helpText: null, 20 | allowFiles: true, allowFolders: false 21 | }, kwargs) 22 | 23 | this.path = path 24 | this.match = object.pop(kwargs, 'match') 25 | this.recursive = object.pop(kwargs, 'recursive') 26 | this.allowFiles = object.pop(kwargs, 'allowFiles') 27 | this.allowFolders = object.pop(kwargs, 'allowFolders') 28 | delete kwargs.match 29 | delete kwargs.recursive 30 | 31 | kwargs.choices = [] 32 | ChoiceField.call(this, kwargs) 33 | 34 | if (this.required) { 35 | this.setChoices([]) 36 | } 37 | else { 38 | this.setChoices([['', '---------']]) 39 | } 40 | 41 | if (this.match !== null) { 42 | this.matchRE = new RegExp(this.match) 43 | } 44 | 45 | // TODO Plug in file paths when running on the server 46 | 47 | this.widget.choices = this.choices() 48 | } 49 | }) 50 | 51 | module.exports = FilePathField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/GenericIPAddressField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var object = require('isomorph/object') 4 | var validators = require('validators') 5 | 6 | var CharField = require('./CharField') 7 | 8 | var cleanIPv6Address = validators.ipv6.cleanIPv6Address 9 | 10 | var GenericIPAddressField = CharField.extend({ 11 | constructor: function GenericIPAddressField(kwargs) { 12 | if (!(this instanceof GenericIPAddressField)) { return new GenericIPAddressField(kwargs) } 13 | kwargs = object.extend({protocol: 'both', unpackIPv4: false}, kwargs) 14 | this.unpackIPv4 = kwargs.unpackIPv4 15 | this.defaultValidators = 16 | validators.ipAddressValidators(kwargs.protocol, kwargs.unpackIPv4).validators 17 | CharField.call(this, kwargs) 18 | } 19 | }) 20 | 21 | GenericIPAddressField.prototype.toJavaScript = function(value) { 22 | if (!value) { 23 | return '' 24 | } 25 | if (value && value.indexOf(':') != -1) { 26 | return cleanIPv6Address(value, {unpackIPv4: this.unpackIPv4}) 27 | } 28 | return value 29 | } 30 | 31 | 32 | module.exports = GenericIPAddressField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/IPAddressField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var CharField = require('./CharField') 4 | 5 | var {validateIPv4Address} = require('validators') 6 | 7 | /** 8 | * Validates that its input is a valid IPv4 address. 9 | * @constructor 10 | * @extends {CharField} 11 | * @param {Object=} kwargs 12 | * @deprecated in favour of GenericIPAddressField 13 | */ 14 | var IPAddressField = CharField.extend({ 15 | defaultValidators: [validateIPv4Address] 16 | 17 | , constructor: function IPAddressField(kwargs) { 18 | if (!(this instanceof IPAddressField)) { return new IPAddressField(kwargs) } 19 | CharField.call(this, kwargs) 20 | } 21 | }) 22 | 23 | module.exports = IPAddressField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/ImageField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var FileField = require('./FileField') 4 | 5 | /** 6 | * Validates that its input is a valid uploaded image. 7 | * @constructor 8 | * @extends {Field} 9 | * @param {Object=} kwargs 10 | */ 11 | var ImageField = FileField.extend({ 12 | defaultErrorMessages: { 13 | invalidImage: 'Upload a valid image. The file you uploaded was either not an image or a corrupted image.' 14 | } 15 | 16 | , constructor: function ImageField(kwargs) { 17 | if (!(this instanceof ImageField)) { return new ImageField(kwargs) } 18 | FileField.call(this, kwargs) 19 | } 20 | }) 21 | 22 | /** 23 | * Checks that the file-upload field data contains a valid image. 24 | */ 25 | ImageField.prototype.toJavaScript = function(data, initial) { 26 | var f = FileField.prototype.toJavaScript.call(this, data, initial) 27 | if (f === null) { 28 | return null 29 | } 30 | 31 | // TODO Plug in image processing code when running on the server 32 | 33 | return f 34 | } 35 | 36 | ImageField.prototype.getWidgetAttrs = function(widget) { 37 | var attrs = FileField.prototype.getWidgetAttrs.call(this, widget) 38 | attrs.accept = 'image/*' 39 | return attrs 40 | } 41 | 42 | 43 | module.exports = ImageField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/IntegerField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var object = require('isomorph/object') 4 | var Field = require('../Field') 5 | var NumberInput = require('../widgets/NumberInput') 6 | 7 | var {MaxValueValidator, MinValueValidator, ValidationError} = require('validators') 8 | 9 | /** 10 | * Validates that its input is a valid integer. 11 | * @constructor 12 | * @extends {Field} 13 | * @param {Object=} kwargs 14 | */ 15 | var IntegerField = Field.extend({ 16 | widget: NumberInput 17 | , defaultErrorMessages: { 18 | invalid: 'Enter a whole number.' 19 | } 20 | 21 | , constructor: function IntegerField(kwargs) { 22 | if (!(this instanceof IntegerField)) { return new IntegerField(kwargs) } 23 | kwargs = object.extend({maxValue: null, minValue: null}, kwargs) 24 | this.maxValue = kwargs.maxValue 25 | this.minValue = kwargs.minValue 26 | Field.call(this, kwargs) 27 | 28 | if (this.minValue !== null) { 29 | this.validators.push(MinValueValidator(this.minValue)) 30 | } 31 | if (this.maxValue !== null) { 32 | this.validators.push(MaxValueValidator(this.maxValue)) 33 | } 34 | } 35 | }) 36 | 37 | /** 38 | * Validates that Number() can be called on the input with a result that isn't 39 | * NaN and doesn't contain any decimal points. 40 | * @param {*} value user input. 41 | * @return {?number} the result of Number(), or null for empty values. 42 | * @throws {ValidationError} if the input is invalid. 43 | */ 44 | IntegerField.prototype.toJavaScript = function(value) { 45 | value = Field.prototype.toJavaScript.call(this, value) 46 | if (this.isEmptyValue(value)) { 47 | return null 48 | } 49 | value = Number(value) 50 | if (isNaN(value) || value.toString().indexOf('.') != -1) { 51 | throw ValidationError(this.errorMessages.invalid, {code: 'invalid'}) 52 | } 53 | return value 54 | } 55 | 56 | IntegerField.prototype.getWidgetAttrs = function(widget) { 57 | var attrs = Field.prototype.getWidgetAttrs.call(this, widget) 58 | if (this.minValue !== null && !object.hasOwn(widget.attrs, 'min')) { 59 | object.setDefault(attrs, 'min', this.minValue) 60 | } 61 | if (this.maxValue !== null && !object.hasOwn(widget.attrs, 'max')) { 62 | object.setDefault(attrs, 'max', this.maxValue) 63 | } 64 | return attrs 65 | } 66 | 67 | module.exports = IntegerField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/NullBooleanField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var BooleanField = require('./BooleanField') 4 | var NullBooleanSelect = require('../widgets/NullBooleanSelect') 5 | 6 | /** 7 | * A field whose valid values are null, true and false. 8 | * Invalid values are cleaned to null. 9 | * @constructor 10 | * @extends {BooleanField} 11 | * @param {Object=} kwargs 12 | */ 13 | var NullBooleanField = BooleanField.extend({ 14 | widget: NullBooleanSelect 15 | 16 | , constructor: function NullBooleanField(kwargs) { 17 | if (!(this instanceof NullBooleanField)) { return new NullBooleanField(kwargs) } 18 | BooleanField.call(this, kwargs) 19 | } 20 | }) 21 | 22 | NullBooleanField.prototype.toJavaScript = function(value) { 23 | // Explicitly checks for the string 'True' and 'False', which is what a 24 | // hidden field will submit for true and false, and for '1' and '0', which 25 | // is what a RadioField will submit. Unlike the BooleanField we also need 26 | // to check for true, because we are not using Boolean() function. 27 | if (value === true || value == 'True' || value == 'true' || value == '1') { 28 | return true 29 | } 30 | else if (value === false || value == 'False' || value == 'false' || value == '0') { 31 | return false 32 | } 33 | return null 34 | } 35 | 36 | NullBooleanField.prototype.validate = function(value) {} 37 | 38 | NullBooleanField.prototype._hasChanged = function(initial, data) { 39 | // null (unknown) and false (No) are not the same 40 | if (initial !== null) { 41 | initial = Boolean(initial) 42 | } 43 | if (data !== null) { 44 | data = Boolean(data) 45 | } 46 | return initial != data 47 | } 48 | 49 | module.exports = NullBooleanField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/RegexField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var is = require('isomorph/is') 4 | 5 | var CharField = require('./CharField') 6 | 7 | var {RegexValidator} = require('validators') 8 | 9 | /** 10 | * Validates that its input matches a given regular expression. 11 | * @constructor 12 | * @extends {CharField} 13 | * @param {(RegExp|string)} regex 14 | * @param {Object=} kwargs 15 | */ 16 | var RegexField = CharField.extend({ 17 | constructor: function RegexField(regex, kwargs) { 18 | if (!(this instanceof RegexField)) { return new RegexField(regex, kwargs) } 19 | CharField.call(this, kwargs) 20 | if (is.String(regex)) { 21 | regex = new RegExp(regex) 22 | } 23 | this.regex = regex 24 | this.validators.push(RegexValidator({regex: this.regex})) 25 | } 26 | }) 27 | 28 | module.exports = RegexField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/SlugField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var validators = require('validators') 4 | 5 | var CharField = require('./CharField') 6 | 7 | var {strip} = require('../util') 8 | 9 | /** 10 | * Validates that its input is a valid slug. 11 | * @constructor 12 | * @extends {CharField} 13 | * @param {Object=} kwargs 14 | */ 15 | var SlugField = CharField.extend({ 16 | defaultValidators: [validators.validateSlug] 17 | , constructor: function SlugField(kwargs) { 18 | if (!(this instanceof SlugField)) { return new SlugField(kwargs) } 19 | CharField.call(this, kwargs) 20 | } 21 | }) 22 | 23 | SlugField.prototype.clean = function(value) { 24 | value = strip(this.toJavaScript(value)) 25 | return CharField.prototype.clean.call(this, value) 26 | } 27 | 28 | module.exports = SlugField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/TimeField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var time = require('isomorph/time') 4 | 5 | var locales = require('../locales') 6 | 7 | var BaseTemporalField = require('./BaseTemporalField') 8 | var TimeInput = require('../widgets/TimeInput') 9 | 10 | /** 11 | * Validates that its input is a time. 12 | * @constructor 13 | * @extends {BaseTemporalField} 14 | * @param {Object=} kwargs 15 | */ 16 | var TimeField = BaseTemporalField.extend({ 17 | widget: TimeInput 18 | , inputFormatType: 'TIME_INPUT_FORMATS' 19 | , defaultErrorMessages: { 20 | invalid: 'Enter a valid time.' 21 | } 22 | 23 | , constructor: function TimeField(kwargs) { 24 | if (!(this instanceof TimeField)) { return new TimeField(kwargs) } 25 | BaseTemporalField.call(this, kwargs) 26 | } 27 | }) 28 | 29 | /** 30 | * Validates that the input can be converted to a time. 31 | * @param {?(string|Date)} value user input. 32 | * @return {?Date} a Date with its hour, minute and second attributes set, or 33 | * null for empty values when they are allowed. 34 | * @throws {ValidationError} if the input is invalid. 35 | */ 36 | TimeField.prototype.toJavaScript = function(value) { 37 | if (this.isEmptyValue(value)) { 38 | return null 39 | } 40 | if (value instanceof Date) { 41 | return new Date(1900, 0, 1, value.getHours(), value.getMinutes(), value.getSeconds()) 42 | } 43 | return BaseTemporalField.prototype.toJavaScript.call(this, value) 44 | } 45 | 46 | /** 47 | * Creates a Date representing a time from the given input if it's valid based 48 | * on the format. 49 | * @param {string} value 50 | * @param {string} format 51 | * @return {Date} 52 | */ 53 | TimeField.prototype.strpdate = function(value, format) { 54 | var t = time.strptime(value, format, locales.getDefaultLocale()) 55 | return new Date(1900, 0, 1, t[3], t[4], t[5]) 56 | } 57 | 58 | module.exports = TimeField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/TypedChoiceField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var object = require('isomorph/object') 4 | 5 | var ChoiceField = require('./ChoiceField') 6 | 7 | var {ValidationError} = require('validators') 8 | 9 | /** 10 | * A ChoiceField which returns a value coerced by some provided function. 11 | * @constructor 12 | * @extends {ChoiceField} 13 | * @param {Object=} kwargs 14 | */ 15 | var TypedChoiceField = ChoiceField.extend({ 16 | constructor: function TypedChoiceField(kwargs) { 17 | if (!(this instanceof TypedChoiceField)) { return new TypedChoiceField(kwargs) } 18 | kwargs = object.extend({ 19 | coerce: function(val) { return val }, emptyValue: '' 20 | }, kwargs) 21 | this.coerce = object.pop(kwargs, 'coerce') 22 | this.emptyValue = object.pop(kwargs, 'emptyValue') 23 | ChoiceField.call(this, kwargs) 24 | } 25 | }) 26 | 27 | /** 28 | * Validate that the value can be coerced to the right type (if not empty). 29 | */ 30 | TypedChoiceField.prototype._coerce = function(value) { 31 | if (value === this.emptyValue || this.isEmptyValue(value)) { 32 | return this.emptyValue 33 | } 34 | try { 35 | value = this.coerce(value) 36 | } 37 | catch (e) { 38 | throw ValidationError(this.errorMessages.invalidChoice, { 39 | code: 'invalidChoice' 40 | , params: {value: value} 41 | }) 42 | } 43 | return value 44 | } 45 | 46 | TypedChoiceField.prototype.clean = function(value) { 47 | value = ChoiceField.prototype.clean.call(this, value) 48 | return this._coerce(value) 49 | } 50 | 51 | 52 | module.exports = TypedChoiceField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/TypedMultipleChoiceField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var is = require('isomorph/is') 4 | var object = require('isomorph/object') 5 | var MultipleChoiceField = require('./MultipleChoiceField') 6 | 7 | var {ValidationError} = require('validators') 8 | 9 | /** 10 | * A MultipleChoiceField which returns values coerced by some provided function. 11 | * @constructor 12 | * @extends {MultipleChoiceField} 13 | * @param {Object=} kwargs 14 | */ 15 | var TypedMultipleChoiceField = MultipleChoiceField.extend({ 16 | constructor: function TypedMultipleChoiceField(kwargs) { 17 | if (!(this instanceof TypedMultipleChoiceField)) { return new TypedMultipleChoiceField(kwargs) } 18 | kwargs = object.extend({ 19 | coerce: function(val) { return val }, emptyValue: [] 20 | }, kwargs) 21 | this.coerce = object.pop(kwargs, 'coerce') 22 | this.emptyValue = object.pop(kwargs, 'emptyValue') 23 | MultipleChoiceField.call(this, kwargs) 24 | } 25 | }) 26 | 27 | TypedMultipleChoiceField.prototype._coerce = function(value) { 28 | if (value === this.emptyValue || this.isEmptyValue(value) || 29 | (is.Array(value) && !value.length)) { 30 | return this.emptyValue 31 | } 32 | var newValue = [] 33 | for (var i = 0, l = value.length; i < l; i++) { 34 | try { 35 | newValue.push(this.coerce(value[i])) 36 | } 37 | catch (e) { 38 | throw ValidationError(this.errorMessages.invalidChoice, { 39 | code: 'invalidChoice' 40 | , params: {value: value[i]} 41 | }) 42 | } 43 | } 44 | return newValue 45 | } 46 | 47 | TypedMultipleChoiceField.prototype.clean = function(value) { 48 | value = MultipleChoiceField.prototype.clean.call(this, value) 49 | return this._coerce(value) 50 | } 51 | 52 | TypedMultipleChoiceField.prototype.validate = function(value) { 53 | if (value !== this.emptyValue || (is.Array(value) && value.length)) { 54 | MultipleChoiceField.prototype.validate.call(this, value) 55 | } 56 | else if (this.required) { 57 | throw ValidationError(this.errorMessages.required, {code: 'required'}) 58 | } 59 | } 60 | 61 | module.exports = TypedMultipleChoiceField -------------------------------------------------------------------------------- /src/js/components/common/newforms/fields/URLField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var url = require('isomorph/url') 4 | 5 | var CharField = require('./CharField') 6 | var URLInput = require('../widgets/URLInput') 7 | 8 | var {URLValidator} = require('validators') 9 | var {strip} = require('../util') 10 | 11 | /** 12 | * Validates that its input appears to be a valid URL. 13 | * @constructor 14 | * @extends {CharField} 15 | * @param {Object=} kwargs 16 | */ 17 | var URLField = CharField.extend({ 18 | widget: URLInput 19 | , defaultErrorMessages: { 20 | invalid: 'Enter a valid URL.' 21 | } 22 | , defaultValidators: [URLValidator()] 23 | 24 | , constructor: function URLField(kwargs) { 25 | if (!(this instanceof URLField)) { return new URLField(kwargs) } 26 | CharField.call(this, kwargs) 27 | } 28 | }) 29 | 30 | URLField.prototype.toJavaScript = function(value) { 31 | if (value) { 32 | var urlFields = url.parseUri(value) 33 | if (!urlFields.protocol) { 34 | // If no URL protocol given, assume http:// 35 | urlFields.protocol = 'http' 36 | } 37 | if (!urlFields.path) { 38 | // The path portion may need to be added before query params 39 | urlFields.path = '/' 40 | } 41 | value = url.makeUri(urlFields) 42 | } 43 | return CharField.prototype.toJavaScript.call(this, value) 44 | } 45 | 46 | URLField.prototype.clean = function(value) { 47 | value = strip(this.toJavaScript(value)) 48 | return CharField.prototype.clean.call(this, value) 49 | } 50 | 51 | module.exports = URLField -------------------------------------------------------------------------------- /src/js/components/common/newforms/formats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var object = require('isomorph/object') 4 | 5 | var locales = require('./locales') 6 | 7 | /** 8 | * Standard input formats which will always be accepted. 9 | */ 10 | var ISO_INPUT_FORMATS = { 11 | 'DATE_INPUT_FORMATS': ['%Y-%m-%d'] 12 | , 'TIME_INPUT_FORMATS': ['%H:%M:%S', '%H:%M'] 13 | , 'DATETIME_INPUT_FORMATS': [ 14 | '%Y-%m-%d %H:%M:%S' 15 | , '%Y-%m-%d %H:%M' 16 | , '%Y-%m-%d' 17 | ] 18 | } 19 | 20 | var formatCache = {} 21 | 22 | /** 23 | * Gets all acceptable formats of a certain type (e.g. DATE_INPUT_FORMATS) for a 24 | * particular language code. All date/time formats will have the applicable ISO 25 | * formats added as lowest-precedence. 26 | * If an unknown language code is given, the default locale's formats will be 27 | * used instead. 28 | * If the locale doesn't have configuration for the format type, only the ISO 29 | * formats will be returned. 30 | * @param {string} formatType 31 | * @param {string=} lang language code - if not given, the default locale's 32 | * formats will be returned. 33 | * @return {Array.} a list of formats 34 | */ 35 | function getFormat(formatType, lang) { 36 | if (!lang) { 37 | lang = locales.getDefaultLocale() 38 | } 39 | var cacheKey = formatType + ':' + lang 40 | if (!object.hasOwn(formatCache, cacheKey)) { 41 | var langLocales = locales.getLocales(lang) 42 | var localeFormats = [] 43 | for (var i = 0, l = langLocales.length; i < l; i++) { 44 | var locale = langLocales[i] 45 | if (object.hasOwn(locale, formatType)) { 46 | // Copy locale-specific formats, as we may be adding to them 47 | localeFormats = locale[formatType].slice() 48 | break 49 | } 50 | } 51 | if (object.hasOwn(ISO_INPUT_FORMATS, formatType)) { 52 | var isoFormats = ISO_INPUT_FORMATS[formatType] 53 | for (var j = 0, m = isoFormats.length; j < m; j++) { 54 | var isoFormat = isoFormats[j] 55 | if (localeFormats.indexOf(isoFormat) == -1) { 56 | localeFormats.push(isoFormat) 57 | } 58 | } 59 | } 60 | formatCache[cacheKey] = localeFormats 61 | } 62 | return formatCache[cacheKey] 63 | } 64 | 65 | module.exports = { 66 | getFormat: getFormat 67 | } 68 | -------------------------------------------------------------------------------- /src/js/components/common/newforms/forms/isFormAsync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var is = require('isomorph/is') 4 | 5 | function isFormAsync(constructor) { 6 | var proto = constructor.prototype 7 | if (proto.clean.length == 1) { return true } 8 | var fieldNames = Object.keys(proto.baseFields) 9 | for (var i = 0, l = fieldNames.length; i < l ; i++) { 10 | var customClean = proto._getCustomClean(fieldNames[i]) 11 | if (is.Function(customClean) && customClean.length == 1) { 12 | return true 13 | } 14 | } 15 | return false 16 | } 17 | 18 | module.exports = isFormAsync -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/CheckboxInput.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var is = require('isomorph/is') 4 | var object = require('isomorph/object') 5 | var React = require('react') 6 | 7 | var Widget = require('../Widget') 8 | 9 | function defaultCheckTest(value) { 10 | return (value !== false && value != null && value !== '') 11 | } 12 | 13 | /** 14 | * An HTML widget. 15 | * @constructor 16 | * @extends {Widget} 17 | * @param {Object=} kwargs 18 | */ 19 | var CheckboxInput = Widget.extend({ 20 | constructor: function CheckboxInput(kwargs) { 21 | if (!(this instanceof Widget)) { return new CheckboxInput(kwargs) } 22 | kwargs = object.extend({checkTest: defaultCheckTest}, kwargs) 23 | Widget.call(this, kwargs) 24 | this.checkTest = kwargs.checkTest 25 | } 26 | , validation: {onChange: true} 27 | }) 28 | 29 | CheckboxInput.prototype.render = function(name, value, kwargs) { 30 | kwargs = object.extend({}, kwargs) 31 | var finalAttrs = this.buildAttrs(kwargs.attrs, {type: 'checkbox', 32 | name: name}) 33 | if (value !== '' && value !== true && value !== false && value !== null && 34 | value !== undefined) { 35 | // Only add the value attribute if value is non-empty 36 | finalAttrs.value = value 37 | } 38 | var checkedAttr = (kwargs.controlled ? 'checked' : 'defaultChecked') 39 | finalAttrs[checkedAttr] = this.checkTest(value) 40 | return React.createElement('input', finalAttrs) 41 | } 42 | 43 | CheckboxInput.prototype.valueFromData = function(data, files, name) { 44 | if (typeof data[name] == 'undefined') { 45 | // A missing value means False because HTML form submission does not 46 | // send results for unselected checkboxes. 47 | return false 48 | } 49 | var value = data[name] 50 | var values = {'true': true, 'false': false} 51 | // Translate true and false strings to boolean values 52 | if (is.String(value)) { 53 | value = object.get(values, value.toLowerCase(), value) 54 | } 55 | return !!value 56 | } 57 | 58 | module.exports = CheckboxInput -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/CheckboxSelectMultiple.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var CheckboxFieldRenderer = require('./renderers/CheckboxFieldRenderer') 4 | var RendererMixin = require('./renderers/RendererMixin') 5 | var SelectMultiple = require('./SelectMultiple') 6 | 7 | /** 8 | * Multiple selections represented as a list of widgets. 9 | * @constructor 10 | * @extends {SelectMultiple} 11 | * @param {Object=} kwargs 12 | */ 13 | var CheckboxSelectMultiple = SelectMultiple.extend({ 14 | __mixins__: [RendererMixin] 15 | , constructor: function(kwargs) { 16 | if (!(this instanceof CheckboxSelectMultiple)) { return new CheckboxSelectMultiple(kwargs) } 17 | RendererMixin.call(this, kwargs) 18 | SelectMultiple.call(this, kwargs) 19 | } 20 | , renderer: CheckboxFieldRenderer 21 | , _emptyValue: [] 22 | }) 23 | 24 | module.exports = CheckboxSelectMultiple -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/DateInput.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var DateTimeBaseInput = require('./DateTimeBaseInput') 4 | 5 | /** 6 | * @constructor 7 | * @extends {DateTimeBaseInput} 8 | * @param {Object=} kwargs 9 | */ 10 | var DateInput = DateTimeBaseInput.extend({ 11 | formatType: 'DATE_INPUT_FORMATS' 12 | , constructor: function DateInput(kwargs) { 13 | if (!(this instanceof DateInput)) { return new DateInput(kwargs) } 14 | DateTimeBaseInput.call(this, kwargs) 15 | } 16 | }) 17 | 18 | module.exports = DateInput -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/DateTimeBaseInput.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var is = require('isomorph/is') 4 | var object = require('isomorph/object') 5 | var time = require('isomorph/time') 6 | 7 | var formats = require('../formats') 8 | var locales = require('../locales') 9 | var TextInput = require('./TextInput') 10 | 11 | /** 12 | * A which, if given a Date object to display, formats it as 13 | * an appropriate date/time String. 14 | * @constructor 15 | * @extends {TextInput} 16 | * @param {Object=} kwargs 17 | */ 18 | var DateTimeBaseInput = TextInput.extend({ 19 | formatType: '' 20 | , constructor: function DateTimeBaseInput(kwargs) { 21 | kwargs = object.extend({format: null}, kwargs) 22 | TextInput.call(this, kwargs) 23 | this.format = kwargs.format 24 | } 25 | }) 26 | 27 | DateTimeBaseInput.prototype._formatValue = function(value) { 28 | if (is.Date(value)) { 29 | if (this.format === null) { 30 | this.format = formats.getFormat(this.formatType)[0] 31 | } 32 | return time.strftime(value, this.format, locales.getDefaultLocale()) 33 | } 34 | return value 35 | } 36 | 37 | module.exports = DateTimeBaseInput -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/DateTimeInput.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var DateTimeBaseInput = require('./DateTimeBaseInput') 4 | 5 | /** 6 | * @constructor 7 | * @extends {DateTimeBaseInput} 8 | * @param {Object=} kwargs 9 | */ 10 | var DateTimeInput = DateTimeBaseInput.extend({ 11 | formatType: 'DATETIME_INPUT_FORMATS' 12 | , constructor: function DateTimeInput(kwargs) { 13 | if (!(this instanceof DateTimeInput)) { return new DateTimeInput(kwargs) } 14 | DateTimeBaseInput.call(this, kwargs) 15 | } 16 | }) 17 | 18 | module.exports = DateTimeInput -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/EmailInput.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var TextInput = require('./TextInput') 4 | 5 | /** 6 | * An HTML widget. 7 | * @constructor 8 | * @extends {TextInput} 9 | * @param {Object=} kwargs 10 | */ 11 | var EmailInput = TextInput.extend({ 12 | constructor: function EmailInput(kwargs) { 13 | if (!(this instanceof EmailInput)) { return new EmailInput(kwargs) } 14 | TextInput.call(this, kwargs) 15 | } 16 | , inputType: 'email' 17 | }) 18 | 19 | module.exports = EmailInput -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/FileInput.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var object = require('isomorph/object') 4 | 5 | var Input = require('./Input') 6 | 7 | var env =require('../env') 8 | 9 | /** 10 | * An HTML widget. 11 | * @constructor 12 | * @extends {Input} 13 | * @param {Object=} kwargs 14 | */ 15 | var FileInput = Input.extend({ 16 | constructor: function FileInput(kwargs) { 17 | if (!(this instanceof FileInput)) { return new FileInput(kwargs) } 18 | Input.call(this, kwargs) 19 | } 20 | , inputType: 'file' 21 | , needsMultipartForm: true 22 | , validation: {onChange: true} 23 | , isValueSettable: false 24 | }) 25 | 26 | FileInput.prototype.render = function(name, value, kwargs) { 27 | return Input.prototype.render.call(this, name, null, kwargs) 28 | } 29 | 30 | /** 31 | * On the client, files will be populated with File objects from the input's 32 | * FileList when supported, otherwise its value will be in data as a fallback. 33 | */ 34 | FileInput.prototype.valueFromData = function(data, files, name) { 35 | var dataSource = files 36 | if (env.browser && !(name in files)) { 37 | dataSource = data 38 | } 39 | return object.get(dataSource, name, null) 40 | } 41 | 42 | module.exports = FileInput -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/HiddenInput.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Input = require('./Input') 4 | 5 | /** 6 | * An HTML widget. 7 | * @constructor 8 | * @extends {Input} 9 | * @param {Object=} kwargs 10 | */ 11 | var HiddenInput = Input.extend({ 12 | constructor: function HiddenInput(kwargs) { 13 | if (!(this instanceof HiddenInput)) { return new HiddenInput(kwargs) } 14 | Input.call(this, kwargs) 15 | } 16 | , inputType: 'hidden' 17 | , isHidden: true 18 | }) 19 | 20 | module.exports = HiddenInput -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/Input.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var object = require('isomorph/object') 4 | var React = require('react') 5 | 6 | var Widget = require('../Widget') 7 | 8 | /** 9 | * An HTML widget. 10 | * @constructor 11 | * @extends {Widget} 12 | * @param {Object=} kwargs 13 | */ 14 | var Input = Widget.extend({ 15 | constructor: function Input(kwargs) { 16 | if (!(this instanceof Input)) { return new Input(kwargs) } 17 | Widget.call(this, kwargs) 18 | } 19 | /** The type attribute of this input - subclasses must define it. */ 20 | , inputType: null 21 | }) 22 | 23 | Input.prototype._formatValue = function(value) { 24 | return value 25 | } 26 | 27 | Input.prototype.render = function(name, value, kwargs) { 28 | kwargs = object.extend({attrs: null}, kwargs) 29 | if (value === null) { 30 | value = '' 31 | } 32 | var finalAttrs = this.buildAttrs(kwargs.attrs, {type: this.inputType, 33 | name: name}) 34 | // Hidden inputs can be made controlled inputs by default, as the user 35 | // can't directly interact with them. 36 | var valueAttr = (kwargs.controlled || this.isHidden ? 'value' : 'defaultValue') 37 | if (!(valueAttr == 'defaultValue' && value === '')) { 38 | finalAttrs[valueAttr] = (value !== '' ? ''+this._formatValue(value) : value) 39 | } 40 | return React.createElement('input', finalAttrs) 41 | } 42 | 43 | module.exports = Input -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/MultipleHiddenInput.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var object = require('isomorph/object') 4 | var React = require('react') 5 | 6 | var HiddenInput = require('./HiddenInput') 7 | 8 | /** 9 | * A widget that handles for fields that have a list of 10 | * values. 11 | * @constructor 12 | * @extends {HiddenInput} 13 | * @param {Object=} kwargs 14 | */ 15 | var MultipleHiddenInput = HiddenInput.extend({ 16 | constructor: function MultipleHiddenInput(kwargs) { 17 | if (!(this instanceof MultipleHiddenInput)) { return new MultipleHiddenInput(kwargs) } 18 | HiddenInput.call(this, kwargs) 19 | } 20 | }) 21 | 22 | MultipleHiddenInput.prototype.render = function(name, value, kwargs) { 23 | kwargs = object.extend({attrs: null}, kwargs) 24 | if (value === null) { 25 | value = [] 26 | } 27 | var finalAttrs = this.buildAttrs(kwargs.attrs, {type: this.inputType, 28 | name: name}) 29 | var id = object.get(finalAttrs, 'id', null) 30 | var key = object.get(finalAttrs, 'key', null) 31 | var inputs = [] 32 | for (var i = 0, l = value.length; i < l; i++) { 33 | var inputAttrs = object.extend({}, finalAttrs, {value: value[i]}) 34 | // Add numeric index suffixes to attributes which should be unique 35 | if (id) { 36 | inputAttrs.id = id + '_' + i 37 | } 38 | if (key) { 39 | inputAttrs.key = id + '_' + i 40 | } 41 | inputs.push(React.createElement('input', inputAttrs)) 42 | } 43 | return React.createElement('div', null, inputs) 44 | } 45 | 46 | MultipleHiddenInput.prototype.valueFromData = function(data, files, name) { 47 | if (typeof data[name] != 'undefined') { 48 | return [].concat(data[name]) 49 | } 50 | return null 51 | } 52 | 53 | module.exports = MultipleHiddenInput -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/NullBooleanSelect.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Select = require('./Select') 4 | 5 | /** 6 | * A widget. 7 | * @constructor 8 | * @extends {TextInput} 9 | * @param {Object=} kwargs 10 | */ 11 | var NumberInput = TextInput.extend({ 12 | constructor: function NumberInput(kwargs) { 13 | if (!(this instanceof NumberInput)) { return new NumberInput(kwargs) } 14 | TextInput.call(this, kwargs) 15 | } 16 | , inputType: 'number' 17 | }) 18 | 19 | module.exports = NumberInput -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/PasswordInput.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var object = require('isomorph/object') 4 | 5 | var env = require('../env') 6 | var TextInput = require('./TextInput') 7 | 8 | /** 9 | * An HTML widget. 10 | * @constructor 11 | * @extends {TextInput} 12 | * @param {Object=} kwargs 13 | */ 14 | var PasswordInput = TextInput.extend({ 15 | constructor: function PasswordInput(kwargs) { 16 | if (!(this instanceof PasswordInput)) { return new PasswordInput(kwargs) } 17 | kwargs = object.extend({renderValue: false}, kwargs) 18 | TextInput.call(this, kwargs) 19 | this.renderValue = kwargs.renderValue 20 | } 21 | , inputType: 'password' 22 | }) 23 | 24 | PasswordInput.prototype.render = function(name, value, kwargs) { 25 | if (!env.browser && !this.renderValue) { 26 | value = '' 27 | } 28 | return TextInput.prototype.render.call(this, name, value, kwargs) 29 | } 30 | 31 | module.exports = PasswordInput -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/RadioSelect.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RadioFieldRenderer = require('./renderers/RadioFieldRenderer') 4 | var RendererMixin = require('./renderers/RendererMixin') 5 | var Select = require('./Select') 6 | 7 | /** 8 | * Renders a single select as a list of elements. 9 | * @constructor 10 | * @extends {Select} 11 | * @param {Object=} kwargs 12 | */ 13 | var RadioSelect = Select.extend({ 14 | __mixins__: [RendererMixin] 15 | , constructor: function(kwargs) { 16 | if (!(this instanceof RadioSelect)) { return new RadioSelect(kwargs) } 17 | RendererMixin.call(this, kwargs) 18 | Select.call(this, kwargs) 19 | } 20 | , renderer: RadioFieldRenderer 21 | , _emptyValue: '' 22 | }) 23 | 24 | module.exports = RadioSelect -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/SelectMultiple.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var is = require('isomorph/is') 4 | var object = require('isomorph/object') 5 | var React = require('react') 6 | 7 | var Select = require('./Select') 8 | 9 | /** 10 | * An HTML element which allows multiple selections. 32 | */ 33 | SelectMultiple.prototype.render = function(name, selectedValues, kwargs) { 34 | kwargs = object.extend({choices: []}, kwargs) 35 | if (selectedValues === null) { 36 | selectedValues = [] 37 | } 38 | if (!is.Array(selectedValues)) { 39 | selectedValues = [selectedValues] 40 | } 41 | var finalAttrs = this.buildAttrs(kwargs.attrs, {name: name, 42 | multiple: 'multiple'}) 43 | var options = this.renderOptions(kwargs.choices) 44 | var valueAttr = (kwargs.controlled ? 'value' : 'defaultValue') 45 | finalAttrs[valueAttr] = selectedValues 46 | return React.createElement('select', finalAttrs, options) 47 | } 48 | 49 | /** 50 | * Retrieves values for this widget from the given data. 51 | * @param {Object} data form data. 52 | * @param {Object} files file data. 53 | * @param {string} name the field name to be used to retrieve data. 54 | * @return {Array} values for this widget, or null if no values were provided. 55 | */ 56 | SelectMultiple.prototype.valueFromData = function(data, files, name) { 57 | if (object.hasOwn(data, name) && data[name] != null) { 58 | return [].concat(data[name]) 59 | } 60 | return null 61 | } 62 | 63 | module.exports = SelectMultiple 64 | -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/SplitDateTimeWidget.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var object = require('isomorph/object') 4 | 5 | var DateInput = require('./DateInput') 6 | var MultiWidget = require('./MultiWidget') 7 | var TimeInput = require('./TimeInput') 8 | 9 | /** 10 | * Splits Date input into two elements. 11 | * @constructor 12 | * @extends {MultiWidget} 13 | * @param {Object=} kwargs 14 | */ 15 | var SplitDateTimeWidget = MultiWidget.extend({ 16 | constructor: function SplitDateTimeWidget(kwargs) { 17 | if (!(this instanceof SplitDateTimeWidget)) { return new SplitDateTimeWidget(kwargs) } 18 | kwargs = object.extend({dateFormat: null, timeFormat: null}, kwargs) 19 | var widgets = [ 20 | DateInput({attrs: kwargs.attrs, format: kwargs.dateFormat}) 21 | , TimeInput({attrs: kwargs.attrs, format: kwargs.timeFormat}) 22 | ] 23 | MultiWidget.call(this, widgets, kwargs.attrs) 24 | } 25 | }) 26 | 27 | SplitDateTimeWidget.prototype.decompress = function(value) { 28 | if (value) { 29 | return [ 30 | new Date(value.getFullYear(), value.getMonth(), value.getDate()) 31 | , new Date(1900, 0, 1, value.getHours(), value.getMinutes(), value.getSeconds()) 32 | ] 33 | } 34 | return [null, null] 35 | } 36 | 37 | module.exports = SplitDateTimeWidget -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/SplitHiddenDateTimeWidget.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SplitDateTimeWidget = require('./SplitDateTimeWidget') 4 | 5 | /** 6 | * Splits Date input into two elements. 7 | * @constructor 8 | * @extends {SplitDateTimeWidget} 9 | * @param {Object=} kwargs 10 | */ 11 | var SplitHiddenDateTimeWidget = SplitDateTimeWidget.extend({ 12 | constructor: function SplitHiddenDateTimeWidget(kwargs) { 13 | if (!(this instanceof SplitHiddenDateTimeWidget)) { return new SplitHiddenDateTimeWidget(kwargs) } 14 | SplitDateTimeWidget.call(this, kwargs) 15 | for (var i = 0, l = this.widgets.length; i < l; i++) { 16 | this.widgets[i].inputType = 'hidden' 17 | this.widgets[i].isHidden = true 18 | } 19 | } 20 | , isHidden: true 21 | }) 22 | 23 | module.exports = SplitHiddenDateTimeWidget -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/SubWidget.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Concur = require('Concur') 4 | var object = require('isomorph/object') 5 | 6 | /** 7 | * Some widgets are made of multiple HTML elements -- namely, RadioSelect. 8 | * This represents the "inner" HTML element of a widget. 9 | * @constructor 10 | */ 11 | var SubWidget = Concur.extend({ 12 | constructor: function SubWidget(parentWidget, name, value, kwargs) { 13 | if (!(this instanceof SubWidget)) { 14 | return new SubWidget(parentWidget, name, value, kwargs) 15 | } 16 | this.parentWidget = parentWidget 17 | this.name = name 18 | this.value = value 19 | kwargs = object.extend({attrs: null, choices: []}, kwargs) 20 | this.attrs = kwargs.attrs 21 | this.choices = kwargs.choices 22 | } 23 | }) 24 | 25 | SubWidget.prototype.render = function() { 26 | var kwargs = {attrs: this.attrs} 27 | if (this.choices.length) { 28 | kwargs.choices = this.choices 29 | } 30 | return this.parentWidget.render(this.name, this.value, kwargs) 31 | } 32 | 33 | module.exports = SubWidget -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/TextInput.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var object = require('isomorph/object') 4 | 5 | var Input = require('./Input') 6 | 7 | /** 8 | * An HTML widget. 9 | * @constructor 10 | * @extends {Input} 11 | * @param {Object=} kwargs 12 | */ 13 | var TextInput = Input.extend({ 14 | constructor: function TextInput(kwargs) { 15 | if (!(this instanceof TextInput)) { return new TextInput(kwargs) } 16 | kwargs = object.extend({attrs: null}, kwargs) 17 | if (kwargs.attrs != null) { 18 | this.inputType = object.pop(kwargs.attrs, 'type', this.inputType) 19 | } 20 | Input.call(this, kwargs) 21 | } 22 | , inputType: 'text' 23 | }) 24 | 25 | module.exports = TextInput -------------------------------------------------------------------------------- /src/js/components/common/newforms/widgets/Textarea.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var object = require('isomorph/object') 4 | var React = require('react') 5 | 6 | var Widget = require('../Widget') 7 | 8 | /** 9 | * An HTML