├── .gitattributes ├── .meteor ├── .finished-upgraders ├── .gitignore ├── .id ├── packages ├── platforms ├── release └── versions ├── README.md ├── client ├── Routes.jsx ├── extern │ ├── Autosuggest.js │ ├── ReactRouter.js │ ├── codeEditor │ │ ├── bundle.js │ │ ├── codemirror.css │ │ ├── style.css │ │ └── theme.css │ ├── jquery.gridster.css │ ├── react-autocomplete │ │ └── style.css │ ├── react-autosuggest │ │ └── style.css │ ├── react-grid-layout.min.js │ ├── react-hotkeys.js │ └── react-typeahead.js └── features │ ├── AddMetric.jsx │ ├── AddRecord.jsx │ ├── Dashboard.jsx │ ├── Metric.jsx │ ├── MetricOverview.jsx │ ├── RecordsOverview.jsx │ ├── UI.jsx │ └── Util.js ├── lib ├── extern │ └── sugar.js └── metric.js ├── metric.css ├── metric.html ├── packages └── metric-stuff │ ├── .npm │ └── package │ │ ├── .gitignore │ │ ├── README │ │ └── npm-shrinkwrap.json │ ├── clientLibs.js │ ├── computeFunctionAnalyser.js │ ├── computeFunctionHelpers.js │ └── package.js ├── server └── metric.js └── tests └── mocha ├── client └── sampleClientTest.js └── server └── sampleServerTest.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1g26bwz7i8hgb1oenaso 8 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-platform 8 | autopublish 9 | insecure 10 | semantic:ui-css 11 | reactjs:react 12 | metric-stuff 13 | matb33:collection-hooks 14 | mike:mocha 15 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.1.0.2 2 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | amplify@1.0.0 2 | autopublish@1.0.3 3 | autoupdate@1.2.1 4 | base64@1.0.3 5 | binary-heap@1.0.3 6 | blaze@2.1.2 7 | blaze-tools@1.0.3 8 | boilerplate-generator@1.0.3 9 | callback-hook@1.0.3 10 | check@1.0.5 11 | coffeescript@1.0.6 12 | ddp@1.1.0 13 | deps@1.0.7 14 | ejson@1.0.6 15 | fastclick@1.0.3 16 | geojson-utils@1.0.3 17 | html-tools@1.0.4 18 | htmljs@1.0.4 19 | http@1.1.0 20 | id-map@1.0.3 21 | insecure@1.0.3 22 | jquery@1.11.3_2 23 | json@1.0.3 24 | launch-screen@1.0.2 25 | less@1.0.14 26 | livedata@1.0.13 27 | logging@1.0.7 28 | matb33:collection-hooks@0.7.13 29 | meteor@1.1.6 30 | meteor-platform@1.2.2 31 | meteorhacks:inject-initial@1.0.2 32 | metric-stuff@0.0.1 33 | mike:mocha@0.5.4 34 | minifiers@1.1.5 35 | minimongo@1.0.8 36 | mobile-status-bar@1.0.3 37 | mongo@1.1.0 38 | observe-sequence@1.0.6 39 | ordered-dict@1.0.3 40 | package-version-parser@3.0.3 41 | practicalmeteor:chai@1.9.2_3 42 | practicalmeteor:loglevel@1.1.0_3 43 | random@1.0.3 44 | reactive-dict@1.1.0 45 | reactive-var@1.0.5 46 | reactjs:react@0.2.3 47 | reload@1.1.3 48 | retry@1.0.3 49 | routepolicy@1.0.5 50 | sanjo:long-running-child-process@1.0.3 51 | sanjo:meteor-files-helpers@1.1.0_4 52 | sanjo:meteor-version@1.0.0 53 | semantic:ui-css@1.12.2 54 | session@1.1.0 55 | spacebars@1.0.6 56 | spacebars-compiler@1.0.6 57 | templating@1.1.1 58 | tracker@1.0.7 59 | ui@1.0.6 60 | underscore@1.0.3 61 | url@1.0.4 62 | velocity:chokidar@0.12.6_1 63 | velocity:core@0.6.1 64 | velocity:html-reporter@0.5.3 65 | velocity:meteor-internals@1.1.0_7 66 | velocity:shim@0.1.0 67 | webapp@1.2.0 68 | webapp-hashing@1.0.3 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | {metric} 2 | ======== 3 | 4 | A post-mortem -- [Metric: Building a quantitative self app in Meteor and React](http://liamz.co/2015/08/metric-building-a-quantitative-self-app-in-meteor-and-react/) 5 | 6 | **Status:** "core functionality complete", working towards [Alpha](https://github.com/liamzebedee/metric/milestones/Alpha) - [Progress through Tumblr screenshots](http://liamz.tumblr.com/tagged/metric) 7 | 8 | !["Dashboard" Screenshot as of 26/05/2015](https://41.media.tumblr.com/3e2f50eaf28999fbe9b7e116e2ab4c89/tumblr_noy93v35L11trskuwo2_1280.png) 9 | !["Add Record" Screenshot as of 17/05/2015](https://40.media.tumblr.com/a93b26f29eeac4e596c3b51775a1a61c/tumblr_nog6cpNW5H1trskuwo2_1280.png) 10 | !["Add Metric" Screenshot as of 17/05/2015](https://40.media.tumblr.com/002a9dffaf2285b7a40668d85ae19af8/tumblr_nog6cpNW5H1trskuwo1_1280.png) 11 | 12 | What is **{metric}** (or more correctly, what am I building it to become): 13 | - An app I'm using to keep track of different metrics for self-improvement in a relational manner (Health tracking is dependent on metrics relating to my Diabetes, exercise, eating habits, Happiness is related to my self-actualization, social belonging, etc.) 14 | - A modern take on what spreadsheets are supposed to do - take data and compute things. We use Web tech, JavaScript and libraries instead of MACROS and plugins. We use objects categorised and stored in databases rather than tables (2D arrays) to represent data. 15 | - An experiment, and my new life's work. 16 | 17 | Key points: 18 | - unlike Excel, we don't do tables 19 | - we do smart dashboards! 20 | - we do metrics and records 21 | - metrics are dynamically computed functions written in JavaScript, a record is just a JSON object 22 | - it's all built with web tech, it's real-time using Meteor and React 23 | - as a result of its client-server architecture, it can be hosted and accessed from multiple clients, and the metric computation thread is separated from the UI 24 | - unlike Excel, we also can handle and do natural arithmetic on dates and hours/mins/seconds 25 | 26 | ## Install 27 | 1. Install [Meteor](https://www.meteor.com/) 28 | 2. Clone the project 29 | 3. Run `meteor` and open [localhost:8000](http://localhost:8000) 30 | 31 | Later when functional I'll bundle it up as an single-file app. 32 | 33 | ## License 34 | Copyright Liam Edwards-Playne 2015. Licensed under [Creative Commons Attribution-Sharealike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/) and GPL v3. 35 | 36 | 37 | ## Development practices 38 | I admit this project does not adhere to several development practices -- the reason for this is that I am trying to develop it as quickly as possible and since I don't envision anyone else maintaining this, there's no point going the extra 20% until later. Nonetheless, I've commented all areas where things could definitely be improved. 39 | 40 | ## Ideas 41 | While developing any project, I always have too many ideas and never end up building it. So in the spirit of MVP, here's a dream list of features: 42 | - hotkey nav for menu 43 | - markdown rendering for text inputs in records, and a basic editor too 44 | - data visualisation using d3/graph.js 45 | - editable HTML React-based view for metrics 46 | - install NPM packages 47 | - natural language search interface 48 | - reactive visualisation of metric inputs à la LightTable 49 | - SQL query interface for record overview 50 | - integration/stealing design from [Jupyter](http://jupyter.org), [Personal API hacks](https://news.ycombinator.com/item?id=5799706) 51 | - metrics retrieving data from external services (Fitbit) 52 | - import/export data to CSV based on the category schema 53 | - replacing the text-based code-editor with a visual frontend, using sliders, controls, live visual update of react component view 54 | 55 | ## Thanks 56 | This uses these projects: 57 | - [Meteor](http://meteor.com) 58 | - [React.js](http://facebook.github.io/react/), React-Router 59 | - [Semantic UI](http://semantic-ui.com) 60 | - [classNames](https://github.com/JedWatson/classnames) by @JedWatson 61 | - [javascript-editor](https://github.com/maxogden/javascript-editor) by @maxogden, and subsequently Esprima and CodeMirror 62 | - [sugar.js](http://sugarjs.com/) - date parsing and such 63 | - [ReactGridLayout](https://github.com/STRML/react-grid-layout) by @STRML - dashboard <3 64 | -------------------------------------------------------------------------------- /client/Routes.jsx: -------------------------------------------------------------------------------- 1 | Meteor.startup(function(){ 2 | 3 | RouteHandler = ReactRouter.RouteHandler; 4 | TransitionGroup = React.addons.CSSTransitionGroup; 5 | 6 | App = ReactMeteor.createClass({ 7 | contextTypes: { 8 | router: React.PropTypes.func.isRequired 9 | }, 10 | 11 | startMeteorSubscriptions: function(){ 12 | Meteor.subscribe('categories'); 13 | }, 14 | 15 | render: function() { 16 | var name = this.context.router.getCurrentPath(); 17 | return ( 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 |
26 |
27 |
28 |
29 | ); 30 | } 31 | }); 32 | 33 | DefaultRoute = ReactRouter.DefaultRoute; 34 | Route = ReactRouter.Route; 35 | 36 | routes = ( 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | 49 | ReactRouter.run(routes, function (Handler) { 50 | React.render(, document.getElementById('root')); 51 | }); 52 | 53 | 54 | 55 | }); -------------------------------------------------------------------------------- /client/extern/Autosuggest.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("React")):"function"==typeof define&&define.amd?define(["React"],t):"object"==typeof exports?exports.Autosuggest=t(require("React")):e.Autosuggest=t(e.React)}(this,function(e){return function(e){function t(s){if(n[s])return n[s].exports;var u=n[s]={exports:{},id:s,loaded:!1};return e[s].call(u.exports,u,u.exports,t),u.loaded=!0,u.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function s(e){return e&&e.__esModule?e:{"default":e}}function u(e,t){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e)){var n=[],s=!0,u=!1,o=void 0;try{for(var i,r=e[Symbol.iterator]();!(s=(i=r.next()).done)&&(n.push(i.value),!t||n.length!==t);s=!0);}catch(l){u=!0,o=l}finally{try{!s&&r["return"]&&r["return"]()}finally{if(u)throw o}}return n}throw new TypeError("Invalid attempt to destructure non-iterable instance")}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var r=Object.assign||function(e){for(var t=1;t0&&"undefined"!=typeof e[0].suggestions}},{key:"setSuggestionsState",value:function(e){this.resetSectionIterator(e),this.setState({suggestions:e,focusedSectionIndex:null,focusedSuggestionIndex:null,valueBeforeUpDown:null})}},{key:"suggestionsExist",value:function(e){return this.isMultipleSections(e)?e.some(function(e){return e.suggestions.length>0}):null!==e&&e.length>0}},{key:"showSuggestions",value:function(e){var t=this,n=e.toLowerCase();this.lastSuggestionsInputValue=e,this.props.showWhen(e)?this.cache[n]?this.setSuggestionsState(this.cache[n]):this.suggestionsFn(e,function(s,u){if(t.lastSuggestionsInputValue===e){if(s)throw s;t.suggestionsExist(u)||(u=null),t.cache[n]=u,t.setSuggestionsState(u)}}):this.setSuggestionsState(null)}},{key:"suggestionIsFocused",value:function(){return null!==this.state.focusedSuggestionIndex}},{key:"getSuggestion",value:function(e,t){return this.isMultipleSections(this.state.suggestions)?this.state.suggestions[e].suggestions[t]:this.state.suggestions[t]}},{key:"getFocusedSuggestion",value:function(){return this.suggestionIsFocused()?this.getSuggestion(this.state.focusedSectionIndex,this.state.focusedSuggestionIndex):null}},{key:"getSuggestionValue",value:function(e,t){var n=this.getSuggestion(e,t);if("object"==typeof n){if(this.props.suggestionValue)return this.props.suggestionValue(n);throw new Error("When is an object, you must implement the suggestionValue() function to specify how to set input's value when suggestion selected.")}return n.toString()}},{key:"onSuggestionUnfocused",value:function(){var e=this.getFocusedSuggestion();null===e||this.justUnfocused||(this.props.onSuggestionUnfocused(e),this.justUnfocused=!0)}},{key:"onSuggestionFocused",value:function(e,t){this.onSuggestionUnfocused();var n=this.getSuggestion(e,t);this.props.onSuggestionFocused(n),this.justUnfocused=!1}},{key:"focusOnSuggestionUsingKeyboard",value:function(e){var t=u(e,2),n=t[0],s=t[1],o={focusedSectionIndex:n,focusedSuggestionIndex:s,value:null===s?this.state.valueBeforeUpDown:this.getSuggestionValue(n,s)};null===this.state.valueBeforeUpDown&&(o.valueBeforeUpDown=this.state.value),null===s?this.onSuggestionUnfocused():this.onSuggestionFocused(n,s),this.onChange(o.value),this.setState(o)}},{key:"onSuggestionSelected",value:function(e){var t=this.getFocusedSuggestion();this.props.onSuggestionUnfocused(t),this.props.onSuggestionSelected(t,e)}},{key:"onInputChange",value:function(e){var t=e.target.value;this.onSuggestionUnfocused(),this.onChange(t),this.setState({value:t,valueBeforeUpDown:null}),this.showSuggestions(t)}},{key:"onInputKeyDown",value:function(e){var t=void 0;switch(e.keyCode){case 13:null!==this.state.valueBeforeUpDown&&this.suggestionIsFocused()&&this.onSuggestionSelected(e),this.setSuggestionsState(null);break;case 27:t={suggestions:null,focusedSectionIndex:null,focusedSuggestionIndex:null,valueBeforeUpDown:null},null!==this.state.valueBeforeUpDown?t.value=this.state.valueBeforeUpDown:null===this.state.suggestions&&(t.value=""),this.onSuggestionUnfocused(),"string"==typeof t.value&&t.value!==this.state.value&&this.onChange(t.value),this.setState(t);break;case 38:null===this.state.suggestions?this.showSuggestions(this.state.value):this.focusOnSuggestionUsingKeyboard(S["default"].prev([this.state.focusedSectionIndex,this.state.focusedSuggestionIndex])),e.preventDefault();break;case 40:null===this.state.suggestions?this.showSuggestions(this.state.value):this.focusOnSuggestionUsingKeyboard(S["default"].next([this.state.focusedSectionIndex,this.state.focusedSuggestionIndex]))}}},{key:"onInputBlur",value:function(){this.onSuggestionUnfocused(),this.justClickedOnSuggestion||this.onBlur(),this.setSuggestionsState(null)}},{key:"isSuggestionFocused",value:function(e,t){return e===this.state.focusedSectionIndex&&t===this.state.focusedSuggestionIndex}},{key:"onSuggestionMouseEnter",value:function(e,t){this.isSuggestionFocused(e,t)||this.onSuggestionFocused(e,t),this.setState({focusedSectionIndex:e,focusedSuggestionIndex:t})}},{key:"onSuggestionMouseLeave",value:function(e,t){this.isSuggestionFocused(e,t)&&this.onSuggestionUnfocused(),this.setState({focusedSectionIndex:null,focusedSuggestionIndex:null})}},{key:"onSuggestionMouseDown",value:function(e,t,n){var s=this,u=this.getSuggestionValue(e,t);this.justClickedOnSuggestion=!0,this.onSuggestionSelected(n),this.onChange(u),this.setState({value:u,suggestions:null,focusedSectionIndex:null,focusedSuggestionIndex:null,valueBeforeUpDown:null},function(){setTimeout(function(){g.findDOMNode(s.refs.input).focus(),s.justClickedOnSuggestion=!1})})}},{key:"getSuggestionId",value:function(e,t){return null===t?null:"react-autosuggest-"+this.id+"-suggestion-"+(null===e?"":e)+"-"+t}},{key:"renderSuggestionContent",value:function(e){if(this.props.suggestionRenderer)return this.props.suggestionRenderer(e,this.state.valueBeforeUpDown||this.state.value);if("object"==typeof e)throw new Error("When is an object, you must implement the suggestionRenderer() function to specify how to render it.");return e.toString()}},{key:"renderSuggestionsList",value:function(e,t){var n=this;return e.map(function(e,s){var u=p["default"]({"react-autosuggest__suggestion":!0,"react-autosuggest__suggestion--focused":t===n.state.focusedSectionIndex&&s===n.state.focusedSuggestionIndex}),o="suggestion-"+(null===t?"":t)+"-"+s;return c["default"].createElement("li",{id:n.getSuggestionId(t,s),className:u,role:"option",key:o,onMouseEnter:function(){return n.onSuggestionMouseEnter(t,s)},onMouseLeave:function(){return n.onSuggestionMouseLeave(t,s)},onMouseDown:function(e){return n.onSuggestionMouseDown(t,s,e)}},n.renderSuggestionContent(e))})}},{key:"renderSuggestions",value:function(){var e=this;return""===this.state.value||null===this.state.suggestions?null:this.isMultipleSections(this.state.suggestions)?c["default"].createElement("div",{id:"react-autosuggest-"+this.id,className:"react-autosuggest__suggestions",role:"listbox"},this.state.suggestions.map(function(t,n){var s=t.sectionName?c["default"].createElement("div",{className:"react-autosuggest__suggestions-section-name"},t.sectionName):null;return 0===t.suggestions.length?null:c["default"].createElement("div",{className:"react-autosuggest__suggestions-section",key:"section-"+n},s,c["default"].createElement("ul",{className:"react-autosuggest__suggestions-section-suggestions"},e.renderSuggestionsList(t.suggestions,n)))})):c["default"].createElement("ul",{id:"react-autosuggest-"+this.id,className:"react-autosuggest__suggestions",role:"listbox"},this.renderSuggestionsList(this.state.suggestions,null))}},{key:"render",value:function(){var e=this.getSuggestionId(this.state.focusedSectionIndex,this.state.focusedSuggestionIndex);return c["default"].createElement("div",{className:"react-autosuggest"},c["default"].createElement("input",r({},this.props.inputAttributes,{type:"text",value:this.state.value,autoComplete:"off",role:"combobox","aria-autocomplete":"list","aria-owns":"react-autosuggest-"+this.id,"aria-expanded":null!==this.state.suggestions,"aria-activedescendant":e,ref:"input",onChange:this.onInputChange.bind(this),onKeyDown:this.onInputKeyDown.bind(this),onBlur:this.onInputBlur.bind(this)})),this.renderSuggestions())}}],[{key:"propTypes",value:{suggestions:g.PropTypes.func.isRequired,suggestionRenderer:g.PropTypes.func,suggestionValue:g.PropTypes.func,showWhen:g.PropTypes.func,onSuggestionSelected:g.PropTypes.func,onSuggestionFocused:g.PropTypes.func,onSuggestionUnfocused:g.PropTypes.func,inputAttributes:g.PropTypes.object},enumerable:!0},{key:"defaultProps",value:{showWhen:function(e){return e.trim().length>0},onSuggestionSelected:function(){},onSuggestionFocused:function(){},onSuggestionUnfocused:function(){},inputAttributes:{}},enumerable:!0}]),t}(g.Component);t["default"]=m,e.exports=t["default"]},function(t,n,s){t.exports=e},function(e,t,n){var s=n(3);e.exports=function(e,t,n){function u(){var g=s()-l;t>g&&g>0?o=setTimeout(u,t-g):(o=null,n||(a=e.apply(r,i),o||(r=i=null)))}var o,i,r,l,a;return null==t&&(t=100),function(){r=this,i=arguments,l=s();var g=n&&!o;return o||(o=setTimeout(u,t)),g&&(a=e.apply(r,i),r=i=null),a}}},function(e,t,n){function s(){return(new Date).getTime()}e.exports=Date.now||s},function(e,t,n){/*! 2 | Copyright (c) 2015 Jed Watson. 3 | Licensed under the MIT License (MIT), see 4 | http://jedwatson.github.io/classnames 5 | */ 6 | function s(){"use strict";for(var e="",t=0;t=0&&0===a[e];)e--;return-1===e?null:e}function r(e){var t=s(e,2),n=t[0],u=t[1];return g?null===u||u===a[n]-1?(n=o(n),null===n?[null,null]:[n,0]):[n,u+1]:0===a||u===a-1?[null,null]:null===u?[null,0]:[null,u+1]}function l(e){var t=s(e,2),n=t[0],u=t[1];return g?null===u||0===u?(n=i(n),null===n?[null,null]:[n,a[n]-1]):[n,u-1]:0===a||0===u?[null,null]:null===u?[null,a-1]:[null,u-1]}Object.defineProperty(t,"__esModule",{value:!0});var a=void 0,g=void 0;t["default"]={setData:u,next:r,prev:l},e.exports=t["default"]}])}); -------------------------------------------------------------------------------- /client/extern/codeEditor/codemirror.css: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .CodeMirror { 4 | /* Set height, width, borders, and global font properties here */ 5 | font-family:'Source Code Pro', 'Monaco', 'Consolas', monospace; 6 | line-height:1.5em !important; 7 | height:100%; 8 | font-size:20px; 9 | } 10 | .CodeMirror-scroll { 11 | /* Set scrolling behaviour here */ 12 | overflow: hidden; 13 | } 14 | 15 | /* PADDING */ 16 | 17 | .CodeMirror-lines { 18 | padding: 4px 0; /* Vertical padding around content */ 19 | } 20 | .CodeMirror pre { 21 | padding: 0 4px; /* Horizontal padding of content */ 22 | } 23 | 24 | .CodeMirror-scrollbar-filler { 25 | background-color: white; /* The little square between H and V scrollbars */ 26 | } 27 | 28 | /* GUTTER */ 29 | 30 | .CodeMirror-gutters { 31 | border-right: 1px solid #ddd; 32 | background-color: #f7f7f7; 33 | } 34 | .CodeMirror-linenumbers {} 35 | .CodeMirror-linenumber { 36 | padding: 0 3px 0 5px; 37 | min-width: 20px; 38 | text-align: right; 39 | color: #999; 40 | } 41 | 42 | /* CURSOR */ 43 | 44 | .CodeMirror div.CodeMirror-cursor { 45 | border-left: 1px solid black; 46 | } 47 | /* Shown when moving in bi-directional text */ 48 | .CodeMirror div.CodeMirror-secondarycursor { 49 | border-left: 1px solid silver; 50 | } 51 | .CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor { 52 | width: auto; 53 | border: 0; 54 | background: transparent; 55 | background: rgba(0, 200, 0, .4); 56 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#6600c800, endColorstr=#4c00c800); 57 | } 58 | /* Kludge to turn off filter in ie9+, which also accepts rgba */ 59 | .CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor:not(#nonsense_id) { 60 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 61 | } 62 | /* Can style cursor different in overwrite (non-insert) mode */ 63 | .CodeMirror div.CodeMirror-cursor.CodeMirror-overwrite {} 64 | 65 | /* DEFAULT THEME */ 66 | 67 | .cm-s-default .cm-keyword {color: #708;} 68 | .cm-s-default .cm-atom {color: #219;} 69 | .cm-s-default .cm-number {color: #164;} 70 | .cm-s-default .cm-def {color: #00f;} 71 | .cm-s-default .cm-variable {color: black;} 72 | .cm-s-default .cm-variable-2 {color: #05a;} 73 | .cm-s-default .cm-variable-3 {color: #085;} 74 | .cm-s-default .cm-property {color: black;} 75 | .cm-s-default .cm-operator {color: black;} 76 | .cm-s-default .cm-comment {color: #a50;} 77 | .cm-s-default .cm-string {color: #a11;} 78 | .cm-s-default .cm-string-2 {color: #f50;} 79 | .cm-s-default .cm-meta {color: #555;} 80 | .cm-s-default .cm-error {color: #f00;} 81 | .cm-s-default .cm-qualifier {color: #555;} 82 | .cm-s-default .cm-builtin {color: #30a;} 83 | .cm-s-default .cm-bracket {color: #997;} 84 | .cm-s-default .cm-tag {color: #170;} 85 | .cm-s-default .cm-attribute {color: #00c;} 86 | .cm-s-default .cm-header {color: blue;} 87 | .cm-s-default .cm-quote {color: #090;} 88 | .cm-s-default .cm-hr {color: #999;} 89 | .cm-s-default .cm-link {color: #00c;} 90 | 91 | .cm-negative {color: #d44;} 92 | .cm-positive {color: #292;} 93 | .cm-header, .cm-strong {font-weight: bold;} 94 | .cm-em {font-style: italic;} 95 | .cm-link {text-decoration: underline;} 96 | 97 | .cm-invalidchar {color: #f00;} 98 | 99 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} 100 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} 101 | 102 | /* STOP */ 103 | 104 | /* The rest of this file contains styles related to the mechanics of 105 | the editor. You probably shouldn't touch them. */ 106 | 107 | .CodeMirror { 108 | line-height: 1; 109 | position: relative; 110 | overflow: hidden; 111 | } 112 | 113 | .CodeMirror-scroll { 114 | /* 30px is the magic margin used to hide the element's real scrollbars */ 115 | /* See overflow: hidden in .CodeMirror, and the paddings in .CodeMirror-sizer */ 116 | margin-bottom: -30px; margin-right: -30px; 117 | padding-bottom: 30px; padding-right: 30px; 118 | height: 100%; 119 | outline: none; /* Prevent dragging from highlighting the element */ 120 | position: relative; 121 | } 122 | .CodeMirror-sizer { 123 | position: relative; 124 | } 125 | 126 | /* The fake, visible scrollbars. Used to force redraw during scrolling 127 | before actuall scrolling happens, thus preventing shaking and 128 | flickering artifacts. */ 129 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler { 130 | position: absolute; 131 | z-index: 6; 132 | display: none; 133 | } 134 | .CodeMirror-vscrollbar { 135 | right: 0; top: 0; 136 | overflow-x: hidden; 137 | overflow-y: scroll; 138 | } 139 | .CodeMirror-hscrollbar { 140 | bottom: 0; left: 0; 141 | overflow-y: hidden; 142 | overflow-x: scroll; 143 | } 144 | .CodeMirror-scrollbar-filler { 145 | right: 0; bottom: 0; 146 | z-index: 6; 147 | } 148 | 149 | .CodeMirror-gutters { 150 | position: absolute; left: 0; top: 0; 151 | height: 100%; 152 | padding-bottom: 30px; 153 | z-index: 3; 154 | } 155 | .CodeMirror-gutter { 156 | height: 100%; 157 | display: inline-block; 158 | /* Hack to make IE7 behave */ 159 | *zoom:1; 160 | *display:inline; 161 | } 162 | .CodeMirror-gutter-elt { 163 | position: absolute; 164 | cursor: default; 165 | z-index: 4; 166 | } 167 | 168 | .CodeMirror-lines { 169 | cursor: text; 170 | } 171 | .CodeMirror pre { 172 | /* Reset some styles that the rest of the page might have set */ 173 | -moz-border-radius: 0; -webkit-border-radius: 0; -o-border-radius: 0; border-radius: 0; 174 | border-width: 0; 175 | background: transparent; 176 | font-family: inherit; 177 | font-size: inherit; 178 | margin: 0; 179 | white-space: pre; 180 | word-wrap: normal; 181 | line-height: inherit; 182 | color: inherit; 183 | z-index: 2; 184 | position: relative; 185 | overflow: visible; 186 | } 187 | .CodeMirror-wrap pre { 188 | word-wrap: break-word; 189 | white-space: pre-wrap; 190 | word-break: normal; 191 | } 192 | .CodeMirror-linebackground { 193 | position: absolute; 194 | left: 0; right: 0; top: 0; bottom: 0; 195 | z-index: 0; 196 | } 197 | 198 | .CodeMirror-linewidget { 199 | position: relative; 200 | z-index: 2; 201 | overflow: auto; 202 | } 203 | 204 | .CodeMirror-widget { 205 | display: inline-block; 206 | } 207 | 208 | .CodeMirror-wrap .CodeMirror-scroll { 209 | overflow-x: hidden; 210 | } 211 | 212 | .CodeMirror-measure { 213 | position: absolute; 214 | width: 100%; height: 0px; 215 | overflow: hidden; 216 | visibility: hidden; 217 | } 218 | .CodeMirror-measure pre { position: static; } 219 | 220 | .CodeMirror div.CodeMirror-cursor { 221 | position: absolute; 222 | visibility: hidden; 223 | border-right: none; 224 | width: 0; 225 | } 226 | .CodeMirror-focused div.CodeMirror-cursor { 227 | visibility: visible; 228 | } 229 | 230 | .CodeMirror-selected { background: #d9d9d9; } 231 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 232 | 233 | .cm-searching { 234 | background: #ffa; 235 | background: rgba(255, 255, 0, .4); 236 | } 237 | 238 | /* IE7 hack to prevent it from returning funny offsetTops on the spans */ 239 | .CodeMirror span { *vertical-align: text-bottom; } 240 | 241 | @media print { 242 | /* Hide the cursor when printing */ 243 | .CodeMirror div.CodeMirror-cursor { 244 | visibility: hidden; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /client/extern/codeEditor/style.css: -------------------------------------------------------------------------------- 1 | #code-editor { height: 100%; } 2 | 3 | .code-editor-left, .code-editor-right { 4 | height: 100%; overflow: visible; display: block; float: left; position: relative; width: 100%; 5 | 6 | } 7 | .code-editor-right { background: #eee; } 8 | /* codemirror */ 9 | .CodeMirror { font-size:20px; } 10 | 11 | @media all and (max-width: 800px) { .CodeMirror { font-size: 14px !important; } } 12 | 13 | #code-editor .CodeMirror .errorLine { background: rgba(255,0,0,0.25); } 14 | 15 | .CodeMirror-scroll { overflow: auto; } -------------------------------------------------------------------------------- /client/extern/codeEditor/theme.css: -------------------------------------------------------------------------------- 1 | .cm-s-mistakes .cm-keyword {color: darkblue;} 2 | .cm-s-mistakes .cm-atom {color: darkred;} 3 | .cm-s-mistakes .cm-number {color: darkred;} 4 | .cm-s-mistakes .cm-def {color: #00f;} 5 | .cm-s-mistakes .cm-variable {color: #111;} 6 | .cm-s-mistakes .cm-variable-2 {color: #05a;} 7 | .cm-s-mistakes .cm-variable-3 {color: #085;} 8 | .cm-s-mistakes .cm-property {color: #111;} 9 | .cm-s-mistakes .cm-operator {color: #111;} 10 | .cm-s-mistakes .cm-comment {color: darkgreen;} 11 | .cm-s-mistakes .cm-string {color: darkmagenta;} 12 | .cm-s-mistakes .cm-string-2 {color: darkmagenta;} 13 | .cm-s-mistakes .cm-meta {color: #555;} 14 | .cm-s-mistakes .cm-error {color: #f00;} 15 | .cm-s-mistakes .cm-qualifier {color: #555;} 16 | .cm-s-mistakes .cm-builtin {color: #30a;} 17 | .cm-s-mistakes .cm-bracket {color: #997;} 18 | .cm-s-mistakes .cm-tag {color: #170;} 19 | .cm-s-mistakes .cm-attribute {color: #00c;} 20 | .cm-s-mistakes .cm-header {color: blue;} 21 | .cm-s-mistakes .cm-quote {color: #090;} 22 | .cm-s-mistakes .cm-hr {color: #999;} 23 | .cm-s-mistakes .cm-link {color: #00c;} 24 | 25 | .cm-negative {color: #d44;} 26 | .cm-positive {color: #292;} 27 | .cm-header, .cm-strong {font-weight: bold;} 28 | .cm-em {font-style: italic;} 29 | .cm-link {text-decoration: underline;} 30 | -------------------------------------------------------------------------------- /client/extern/jquery.gridster.css: -------------------------------------------------------------------------------- 1 | .react-grid-layout { 2 | position: relative; 3 | transition: height 200ms ease; 4 | } 5 | .react-grid-item { 6 | transition: all 200ms ease; 7 | transition-property: left, top; 8 | } 9 | .react-grid-item.cssTransforms { 10 | transition-property: transform; 11 | } 12 | .react-grid-item.resizing { 13 | z-index: 1; 14 | } 15 | .react-grid-placeholder { 16 | background: red; 17 | opacity: 0.2; 18 | transition-duration: 100ms; 19 | z-index: 2; 20 | -webkit-user-select: none; 21 | -moz-user-select: none; 22 | -ms-user-select: none; 23 | -o-user-select: none; 24 | user-select: none; 25 | } 26 | .react-grid-item.react-draggable-dragging { 27 | transition: none; 28 | z-index: 3; 29 | } 30 | .react-draggable { 31 | position: relative; 32 | } 33 | .react-draggable-active { 34 | -webkit-user-select: none; 35 | -moz-user-select: none; 36 | -ms-user-select: none; 37 | -o-user-select: none; 38 | user-select: none; 39 | } 40 | .react-grid-resize-handle { 41 | position: absolute; 42 | opacity: 0; 43 | width: 20px; 44 | height: 20px; 45 | line-height: 28px; 46 | font-size: 20px; 47 | text-align: right; 48 | cursor: se-resize; 49 | } 50 | .react-grid-item:hover .react-grid-resize-handle { 51 | opacity: 1; 52 | } 53 | 54 | 55 | 56 | 57 | .react-grid-layout { 58 | } 59 | .columns { 60 | -moz-columns: 120px; 61 | -webkit-columns: 120px; 62 | columns: 120px; 63 | } 64 | 65 | 66 | 67 | .react-grid-item:not(.react-grid-placeholder) { 68 | margin: 0 0 0 0; 69 | } 70 | 71 | 72 | 73 | 74 | .react-grid-item.resizing { 75 | opacity: 0.9; 76 | } 77 | .react-grid-item.static { 78 | background: #cce; 79 | } 80 | .react-grid-item .text { 81 | font-size: 24px; 82 | text-align: center; 83 | position: absolute; 84 | top: 0; 85 | bottom: 0; 86 | left: 0; 87 | right: 0; 88 | margin: auto; 89 | height: 24px; 90 | } 91 | .react-grid-item .minMax { 92 | font-size: 12px; 93 | } 94 | .react-grid-item .add { 95 | cursor: pointer; 96 | } 97 | 98 | 99 | 100 | 101 | .react-resizable { 102 | position: relative; 103 | } 104 | .react-draggable-active { 105 | -webkit-user-select: none; 106 | -moz-user-select: none; 107 | -ms-user-select: none; 108 | -o-user-select: none; 109 | user-select: none; 110 | } 111 | .react-resizable-handle.react-draggable { 112 | position: absolute; 113 | width: 20px; 114 | height: 20px; 115 | background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4='); 116 | background-position: bottom right; 117 | padding: 0 3px 3px 0; 118 | background-repeat: no-repeat; 119 | background-origin: content-box; 120 | box-sizing: border-box; 121 | cursor: se-resize; 122 | } -------------------------------------------------------------------------------- /client/extern/react-autocomplete/style.css: -------------------------------------------------------------------------------- 1 | .rf-combobox { 2 | display: inline-block; 3 | position: relative; 4 | } 5 | 6 | .rf-combobox-list { 7 | display: none; 8 | position: absolute; 9 | z-index: 1; 10 | border: 1px solid #aaa; 11 | background: #fff; 12 | top: 100%; 13 | padding: 5px 0px; 14 | max-height: 400px; 15 | overflow: auto; 16 | font-size: 12px; 17 | font-family: sans-serif; 18 | width: 100%; 19 | -moz-box-sizing: border-box; 20 | -ms-box-sizing: border-box; 21 | box-sizing: border-box; 22 | } 23 | 24 | .rf-combobox-is-open .rf-combobox-list { 25 | display: block; 26 | } 27 | 28 | .rf-combobox-option:focus { 29 | outline: 0; 30 | color: white; 31 | background: hsl(200, 50%, 50%); 32 | } 33 | 34 | .rf-combobox-input { 35 | padding-right: 20px; 36 | width: 100%; 37 | -moz-box-sizing: border-box; 38 | -ms-box-sizing: border-box; 39 | box-sizing: border-box; 40 | } 41 | 42 | .rf-combobox-button { 43 | display: inline-block; 44 | position: absolute; 45 | cursor: default; 46 | outline: none; 47 | top: 2px; 48 | right: 6px; 49 | font-size: 14px; 50 | cursor: default; 51 | } 52 | 53 | .rf-combobox-button:active { 54 | color: #4095BF; 55 | } 56 | 57 | .rf-combobox-option { 58 | display: block; 59 | padding: 2px 16px; 60 | cursor: default; 61 | } 62 | 63 | .rf-combobox-selected:before { 64 | content: '✓'; 65 | position: absolute; 66 | left: 4px; 67 | } 68 | -------------------------------------------------------------------------------- /client/extern/react-autosuggest/style.css: -------------------------------------------------------------------------------- 1 | .react-autosuggest { 2 | position: relative; 3 | } 4 | .react-autosuggest input { 5 | width: 240px; 6 | height: 30px; 7 | padding: 10px 20px; 8 | font-size: 18px; 9 | border: 1px solid #aaaaaa; 10 | border-radius: 4px; 11 | } 12 | .react-autosuggest ul { 13 | list-style: none; 14 | margin-top: 0; 15 | padding-left: 0; 16 | } 17 | .react-autosuggest li { 18 | margin-left: 0; 19 | } 20 | .react-autosuggest input[aria-expanded="true"] { 21 | border-bottom-left-radius: 0; 22 | border-bottom-right-radius: 0; 23 | } 24 | .react-autosuggest input:focus { 25 | outline: none; 26 | } 27 | .react-autosuggest__suggestions { 28 | display: block; 29 | position:absolute; top: 29px; left:0; 30 | width: 100%; 31 | min-width: 280px; 32 | border: 1px solid #aaaaaa; 33 | background-color: #fff; 34 | font-size: 18px; 35 | border-bottom-left-radius: 4px; 36 | border-bottom-right-radius: 4px; 37 | z-index: 2; 38 | } 39 | .react-autosuggest__suggestions-section:first-child .react-autosuggest__suggestions-section-name { 40 | border-top: 0; 41 | } 42 | .react-autosuggest__suggestions-section-name { 43 | padding: 10px 0 0 10px; 44 | font-size: 12px; 45 | text-transform: uppercase; 46 | color: #777; 47 | border-top: 1px dashed #ccc; 48 | } 49 | .react-autosuggest__suggestion { 50 | cursor: pointer; 51 | padding: 10px 20px; 52 | } 53 | .react-autosuggest__suggestion--focused { 54 | background-color: #ddd; 55 | } -------------------------------------------------------------------------------- /client/extern/react-grid-layout.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("React")):"function"==typeof define&&define.amd?define(["React"],e):"object"==typeof exports?exports.ReactGridLayout=e(require("React")):t.ReactGridLayout=e(t.React)}(this,function(t){return function(t){function e(n){if(r[n])return r[n].exports;var o=r[n]={exports:{},id:n,loaded:!1};return t[n].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var r={};return e.m=t,e.c=r,e.p="",e(0)}([function(t,e,r){"use strict";t.exports=r(5),t.exports.Responsive=r(12)},function(e,r,n){e.exports=t},function(t,e,r){"use strict";var n=r(7),o=t.exports={bottom:function(t){for(var e,r=0,n=0,o=t.length;o>n;n++)e=t[n].y+t[n].h,e>r&&(r=e);return r},clone:function(t){return n({},t)},collides:function(t,e){return t===e?!1:t.x+t.w<=e.x?!1:t.x>=e.x+e.w?!1:t.y+t.h<=e.y?!1:t.y>=e.y+e.h?!1:!0},compact:function(t,e){for(var r=o.getStatics(t),n=[],s=o.sortLayoutItemsByRowCol(t),i=0,a=s.length;a>i;i++){var p=s[i];p["static"]||(p=o.compactItem(r,p,e),r.push(p)),n[t.indexOf(p)]=p,delete p.moved}return n},compactItem:function(t,e,r){if(r)for(;e.y>0&&!o.getFirstCollision(t,e);)e.y--;for(var n;n=o.getFirstCollision(t,e);)e.y=n.y+n.h;return e},correctBounds:function(t,e){for(var r=o.getStatics(t),n=0,s=t.length;s>n;n++){var i=t[n];if(i.x+i.w>e.cols&&(i.x=e.cols-i.w),i.x<0&&(i.x=0,i.w=e.cols),i["static"])for(;o.getFirstCollision(r,i);)i.y++;else r.push(i)}return t},getLayoutItem:function(t,e){e=""+e;for(var r=0,n=t.length;n>r;r++)if(""+t[r].i===e)return t[r]},getFirstCollision:function(t,e){for(var r=0,n=t.length;n>r;r++)if(o.collides(t[r],e))return t[r]},getAllCollisions:function(t,e){for(var r=[],n=0,s=t.length;s>n;n++)o.collides(t[n],e)&&r.push(t[n]);return r},getStatics:function(t){for(var e=[],r=0,n=t.length;n>r;r++)t[r]["static"]&&e.push(t[r]);return e},moveElement:function(t,e,r,n,s){if(e["static"])return t;if(e.y===n&&e.x===r)return t;var i=e.y>n;void 0!==r&&(e.x=r),void 0!==n&&(e.y=n),e.moved=!0;var a=o.sortLayoutItemsByRowCol(t);i&&(a=a.reverse());for(var p=o.getAllCollisions(a,e),h=0,l=p.length;l>h;h++){var c=p[h];c.moved||e.y>c.y&&e.y-c.y>c.h/4||(t=c["static"]?o.moveElementAwayFromCollision(t,c,e,s):o.moveElementAwayFromCollision(t,e,c,s))}return t},moveElementAwayFromCollision:function(t,e,r,n){if(n){var s={x:r.x,y:r.y,w:r.w,h:r.h};if(s.y=Math.max(e.y-r.h,0),!o.getFirstCollision(t,s))return o.moveElement(t,r,void 0,s.y)}return o.moveElement(t,r,void 0,r.y+1)},perc:function(t){return 100*t+"%"},setTransform:function(t,e){var r=(""+e[0]).replace(/(\d)$/,"$1px"),n=(""+e[1]).replace(/(\d)$/,"$1px");return t.transform="translate("+r+","+n+")",t.WebkitTransform="translate("+r+","+n+")",t.MozTransform="translate("+r+","+n+")",t.msTransform="translate("+r+","+n+")",t.OTransform="translate("+r+","+n+")",t},sortLayoutItemsByRowCol:function(t){return[].concat(t).sort(function(t,e){return t.y>e.y||t.y===e.y&&t.x>e.x?1:-1})},synchronizeLayoutWithChildren:function(t,e,r,s){Array.isArray(e)||(e=[e]),t=t||[];for(var i=[],a=0,p=e.length;p>a;a++){var h=e[a],l=o.getLayoutItem(t,h.key);if(l)l.i=""+l.i,i.push(l);else{var c=h.props._grid;c?(o.validateLayout([c],"ReactGridLayout.child"),i.push(s?n({},c,{y:Math.min(o.bottom(i),c.y),i:h.key}):n({},c,{y:c.y,i:h.key}))):i.push({w:1,h:1,x:0,y:o.bottom(i),i:h.key})}}return i=o.correctBounds(i,{cols:r}),i=o.compact(i,s)},validateLayout:function(t,e){e=e||"Layout";var r=["x","y","w","h"];if(!Array.isArray(t))throw new Error(e+" must be an array!");for(var n=0,o=t.length;o>n;n++){for(var s=0;s=0||Object.prototype.hasOwnProperty.call(t,n)&&(r[n]=t[n]);return r},o=Object.assign||function(t){for(var e=1;et.w||t.minW>t.maxW)&&n("minW",t)},maxW:function(t,e,r){o.PropTypes.number.apply(this,arguments),(t.maxWt.h||t.minH>t.maxH)&&n("minH",t)},maxH:function(t,e,r){o.PropTypes.number.apply(this,arguments),(t.maxH=0||Object.prototype.hasOwnProperty.call(t,n)&&(r[n]=t[n]);return r},o=Object.assign||function(t){for(var e=1;es;s++){var a=r[s];e>t[a]&&(n=a)}return n},getColsFromBreakpoint:function(t,e){if(!e[t])throw new Error("ResponsiveReactGridLayout: `cols` entry for breakpoint "+t+" is missing!");return e[t]},findOrGenerateResponsiveLayout:function(t,e,r,s,i,a){if(t[r])return t[r];for(var p=t[s],h=o.sortBreakpoints(e),l=h.slice(h.indexOf(r)),c=0,u=l.length;u>c;c++){var f=l[c];if(t[f]){p=t[f];break}}return p=JSON.parse(JSON.stringify(p||[])),n.compact(n.correctBounds(p,{cols:i}),a)},sortBreakpoints:function(t){var e=Object.keys(t);return e.sort(function(e,r){return t[e]-t[r]})}}},function(t,e,r){function n(t){return null===t||void 0===t}function o(t){return t&&"object"==typeof t&&"number"==typeof t.length?"function"!=typeof t.copy||"function"!=typeof t.slice?!1:t.length>0&&"number"!=typeof t[0]?!1:!0:!1}function s(t,e,r){var s,l;if(n(t)||n(e))return!1;if(t.prototype!==e.prototype)return!1;if(p(t))return p(e)?(t=i.call(t),e=i.call(e),h(t,e,r)):!1;if(o(t)){if(!o(e))return!1;if(t.length!==e.length)return!1;for(s=0;s=0;s--)if(c[s]!=u[s])return!1;for(s=c.length-1;s>=0;s--)if(l=c[s],!h(t[l],e[l],r))return!1;return typeof t==typeof e}var i=Array.prototype.slice,a=r(16),p=r(15),h=t.exports=function(t,e,r){return r||(r={}),t===e?!0:t instanceof Date&&e instanceof Date?t.getTime()===e.getTime():"object"!=typeof t&&"object"!=typeof e?r.strict?t===e:t==e:s(t,e,r)}},function(t,e,r){function n(t){return"[object Arguments]"==Object.prototype.toString.call(t)}function o(t){return t&&"object"==typeof t&&"number"==typeof t.length&&Object.prototype.hasOwnProperty.call(t,"callee")&&!Object.prototype.propertyIsEnumerable.call(t,"callee")||!1}var s="[object Arguments]"==function(){return Object.prototype.toString.call(arguments)}();e=t.exports=s?n:o,e.supported=n,e.unsupported=o},function(t,e,r){function n(t){var e=[];for(var r in t)e.push(r);return e}e=t.exports="function"==typeof Object.keys?Object.keys:n,e.shim=n},function(t,e,r){"use strict";function n(t){return{element:t.getDOMNode(),position:{top:(t._pendingState||t.state).clientY,left:(t._pendingState||t.state).clientX}}}function o(t){return"both"===t.props.axis||"y"===t.props.axis}function s(t){return"both"===t.props.axis||"x"===t.props.axis}function i(t){return"function"==typeof t||"[object Function]"===Object.prototype.toString.call(t)}function a(t,e){for(var r=0,n=(t.length,null);n=t[r];r++)if(e.apply(e,[n,r,t]))return n}function p(t,e){var r=a(["matches","webkitMatchesSelector","mozMatchesSelector","msMatchesSelector","oMatchesSelector"],function(e){return i(t[e])});return t[r].call(t,e)}function h(t){var e=(""+t.left).replace(/(\d)$/,"$1px"),r=(""+t.top).replace(/(\d)$/,"$1px");return t.transform="translate("+e+","+r+")",t.WebkitTransform="translate("+e+","+r+")",t.OTransform="translate("+e+","+r+")",t.msTransform="translate("+e+","+r+")",t.MozTransform="translate("+e+","+r+")",delete t.left,delete t.top,t}function l(t){var e=t.touches&&t.touches[0]||t;return{clientX:e.clientX,clientY:e.clientY}}function c(t,e,r){t&&(t.attachEvent?t.attachEvent("on"+e,r):t.addEventListener?t.addEventListener(e,r,!0):t["on"+e]=r)}function u(t,e,r){t&&(t.detachEvent?t.detachEvent("on"+e,r):t.removeEventListener?t.removeEventListener(e,r,!0):t["on"+e]=null)}var f=r(1),y=r(10),d=r(20),m=r(4);if("undefined"==typeof window)var g=!1;else var g="ontouchstart"in window||"onmsgesturechange"in window;var v=function(){var t={touch:{start:"touchstart",move:"touchmove",end:"touchend"},mouse:{start:"mousedown",move:"mousemove",end:"mouseup"}};return t[g?"touch":"mouse"]}();t.exports=f.createClass({displayName:"Draggable",mixins:[y],propTypes:{axis:f.PropTypes.oneOf(["both","x","y"]),handle:f.PropTypes.string,cancel:f.PropTypes.string,grid:f.PropTypes.arrayOf(f.PropTypes.number),start:f.PropTypes.object,moveOnStartChange:f.PropTypes.bool,useCSSTransforms:f.PropTypes.bool,zIndex:f.PropTypes.number,onStart:f.PropTypes.func,onDrag:f.PropTypes.func,onStop:f.PropTypes.func,onMouseDown:f.PropTypes.func},componentWillUnmount:function(){u(window,v.move,this.handleDrag),u(window,v.end,this.handleDragEnd)},componentWillReceiveProps:function(t){t.moveOnStartChange&&this.setState({clientX:t.start.x,clientY:t.start.y})},getDefaultProps:function(){return{axis:"both",handle:null,cancel:null,grid:null,start:{x:0,y:0},moveOnStartChange:!1,useCSSTransforms:!1,zIndex:0/0,onStart:d,onDrag:d,onStop:d,onMouseDown:d}},getInitialState:function(){return{dragging:!1,startX:0,startY:0,offsetX:0,offsetY:0,clientX:this.props.start.x,clientY:this.props.start.y}},handleDragStart:function(t){if(this.props.onMouseDown(t),"number"!=typeof t.button||0===t.button){{this.getDOMNode()}if(!(this.props.handle&&!p(t.target,this.props.handle)||this.props.cancel&&p(t.target,this.props.cancel))){var e=l(t);this.setState({dragging:!0,offsetX:parseInt(e.clientX,10),offsetY:parseInt(e.clientY,10),startX:parseInt(this.state.clientX,10)||0,startY:parseInt(this.state.clientY,10)||0}),document.body.className+=" react-draggable-active",this.props.onStart(t,n(this)),c(window,v.move,this.handleDrag),c(window,v.end,this.handleDragEnd)}}},handleDragEnd:function(t){this.state.dragging&&(this.setState({dragging:!1}),document.body.className=document.body.className.replace(" react-draggable-active",""),this.props.onStop(t,n(this)),u(window,v.move,this.handleDrag),u(window,v.end,this.handleDragEnd))},handleDrag:function(t){var e=l(t),r=this.state.startX+(e.clientX-this.state.offsetX),o=this.state.startY+(e.clientY-this.state.offsetY);if(Array.isArray(this.props.grid)){var s=r=this.props.grid[0]?parseInt(this.state.clientX,10)+this.props.grid[0]*s:parseInt(this.state.clientX,10),o=Math.abs(o-parseInt(this.state.clientY,10))>=this.props.grid[1]?parseInt(this.state.clientY,10)+this.props.grid[1]*i:parseInt(this.state.clientY,10)}Array.isArray(this.props.minConstraints)&&(r=Math.max(this.props.minConstraints[0],r),o=Math.max(this.props.minConstraints[1],o)),Array.isArray(this.props.maxConstraints)&&(r=Math.min(this.props.maxConstraints[0],r),o=Math.min(this.props.maxConstraints[1],o)),this.setState({clientX:r,clientY:o}),this.props.onDrag(t,n(this))},render:function(){var t={top:o(this)?this.state.clientY:this.state.startY,left:s(this)?this.state.clientX:this.state.startX};return this.props.useCSSTransforms&&(t=h(t)),this.state.dragging&&!isNaN(this.props.zIndex)&&(t.zIndex=this.props.zIndex),m(f.Children.only(this.props.children),{style:t,className:"react-draggable"+(this.state.dragging?" react-draggable-dragging":""),onMouseDown:this.handleDragStart,onTouchStart:function(t){return t.preventDefault(),this.handleDragStart.apply(this,arguments)}.bind(this),onMouseUp:this.handleDragEnd,onTouchEnd:this.handleDragEnd})}})},function(t,e,r){"use strict";{var n=function(t,e){var r={};for(var n in t)e.indexOf(n)>=0||Object.prototype.hasOwnProperty.call(t,n)&&(r[n]=t[n]);return r},o=Object.assign||function(t){for(var e=1;elah' // The rendered string 118 | // , index: 2 // The index of the element in `arr` 119 | // , original: 'blah' // The original element in `arr` 120 | // }] 121 | // 122 | // `opts` is an optional argument bag. Details: 123 | // 124 | // opts = { 125 | // // string to put before a matching character 126 | // pre: '' 127 | // 128 | // // string to put after matching character 129 | // , post: '' 130 | // 131 | // // Optional function. Input is an element from the passed in 132 | // // `arr`, output should be the string to test `pattern` against. 133 | // // In this example, if `arr = [{crying: 'koala'}]` we would return 134 | // // 'koala'. 135 | // , extract: function(arg) { return arg.crying; } 136 | // } 137 | fuzzy.filter = function(pattern, arr, opts) { 138 | opts = opts || {}; 139 | return arr 140 | .reduce(function(prev, element, idx, arr) { 141 | var str = element; 142 | if(opts.extract) { 143 | str = opts.extract(element); 144 | } 145 | var rendered = fuzzy.match(pattern, str, opts); 146 | if(rendered != null) { 147 | prev[prev.length] = { 148 | string: rendered.rendered 149 | , score: rendered.score 150 | , index: idx 151 | , original: element 152 | }; 153 | } 154 | return prev; 155 | }, []) 156 | 157 | // Sort by score. Browsers are inconsistent wrt stable/unstable 158 | // sorting, so force stable by using the index in the case of tie. 159 | // See http://ofb.net/~sethml/is-sort-stable.html 160 | .sort(function(a,b) { 161 | var compare = b.score - a.score; 162 | if(compare) return compare; 163 | return a.index - b.index; 164 | }); 165 | }; 166 | 167 | 168 | }()); 169 | 170 | 171 | },{}],3:[function(require,module,exports){ 172 | /** 173 | * PolyFills make me sad 174 | */ 175 | var KeyEvent = KeyEvent || {}; 176 | KeyEvent.DOM_VK_UP = KeyEvent.DOM_VK_UP || 38; 177 | KeyEvent.DOM_VK_DOWN = KeyEvent.DOM_VK_DOWN || 40; 178 | KeyEvent.DOM_VK_BACK_SPACE = KeyEvent.DOM_VK_BACK_SPACE || 8; 179 | KeyEvent.DOM_VK_RETURN = KeyEvent.DOM_VK_RETURN || 13; 180 | KeyEvent.DOM_VK_ENTER = KeyEvent.DOM_VK_ENTER || 14; 181 | KeyEvent.DOM_VK_ESCAPE = KeyEvent.DOM_VK_ESCAPE || 27; 182 | KeyEvent.DOM_VK_TAB = KeyEvent.DOM_VK_TAB || 9; 183 | 184 | module.exports = KeyEvent; 185 | 186 | 187 | 188 | },{}],4:[function(require,module,exports){ 189 | var Typeahead = require('./typeahead'); 190 | var Tokenizer = require('./tokenizer'); 191 | 192 | module.exports = { 193 | Typeahead: Typeahead, 194 | Tokenizer: Tokenizer 195 | }; 196 | 197 | 198 | 199 | },{"./tokenizer":5,"./typeahead":7}],5:[function(require,module,exports){ 200 | /** 201 | * @jsx React.DOM 202 | */ 203 | 204 | var React = window.React || require('react'); 205 | var Token = require('./token'); 206 | var KeyEvent = require('../keyevent'); 207 | var Typeahead = require('../typeahead'); 208 | var classNames = require('classnames'); 209 | 210 | /** 211 | * A typeahead that, when an option is selected, instead of simply filling 212 | * the text entry widget, prepends a renderable "token", that may be deleted 213 | * by pressing backspace on the beginning of the line with the keyboard. 214 | */ 215 | var TypeaheadTokenizer = React.createClass({displayName: "TypeaheadTokenizer", 216 | propTypes: { 217 | name: React.PropTypes.string, 218 | options: React.PropTypes.array, 219 | customClasses: React.PropTypes.object, 220 | allowCustomValues: React.PropTypes.number, 221 | defaultSelected: React.PropTypes.array, 222 | defaultValue: React.PropTypes.string, 223 | placeholder: React.PropTypes.string, 224 | onTokenRemove: React.PropTypes.func, 225 | onTokenAdd: React.PropTypes.func, 226 | filterOption: React.PropTypes.func, 227 | maxVisible: React.PropTypes.number 228 | }, 229 | 230 | getInitialState: function() { 231 | return { 232 | // We need to copy this to avoid incorrect sharing 233 | // of state across instances (e.g., via getDefaultProps()) 234 | selected: this.props.defaultSelected.slice(0) 235 | }; 236 | }, 237 | 238 | getDefaultProps: function() { 239 | return { 240 | options: [], 241 | defaultSelected: [], 242 | customClasses: {}, 243 | allowCustomValues: 0, 244 | defaultValue: "", 245 | placeholder: "", 246 | onTokenAdd: function() {}, 247 | onTokenRemove: function() {} 248 | }; 249 | }, 250 | 251 | // TODO: Support initialized tokens 252 | // 253 | _renderTokens: function() { 254 | var tokenClasses = {}; 255 | tokenClasses[this.props.customClasses.token] = !!this.props.customClasses.token; 256 | var classList = classNames(tokenClasses); 257 | var result = this.state.selected.map(function(selected) { 258 | return ( 259 | React.createElement(Token, {key: selected, className: classList, 260 | onRemove: this._removeTokenForValue, 261 | name: this.props.name}, 262 | selected 263 | ) 264 | ); 265 | }, this); 266 | return result; 267 | }, 268 | 269 | _getOptionsForTypeahead: function() { 270 | // return this.props.options without this.selected 271 | return this.props.options; 272 | }, 273 | 274 | _onKeyDown: function(event) { 275 | // We only care about intercepting backspaces 276 | if (event.keyCode !== KeyEvent.DOM_VK_BACK_SPACE) { 277 | return; 278 | } 279 | 280 | // No tokens 281 | if (!this.state.selected.length) { 282 | return; 283 | } 284 | 285 | // Remove token ONLY when bksp pressed at beginning of line 286 | // without a selection 287 | var entry = this.refs.typeahead.refs.entry.getDOMNode(); 288 | if (entry.selectionStart == entry.selectionEnd && 289 | entry.selectionStart == 0) { 290 | this._removeTokenForValue( 291 | this.state.selected[this.state.selected.length - 1]); 292 | event.preventDefault(); 293 | } 294 | }, 295 | 296 | _removeTokenForValue: function(value) { 297 | var index = this.state.selected.indexOf(value); 298 | if (index == -1) { 299 | return; 300 | } 301 | 302 | this.state.selected.splice(index, 1); 303 | this.setState({selected: this.state.selected}); 304 | this.props.onTokenRemove(this.state.selected); 305 | return; 306 | }, 307 | 308 | _addTokenForValue: function(value) { 309 | if (this.state.selected.indexOf(value) != -1) { 310 | return; 311 | } 312 | this.state.selected.push(value); 313 | this.setState({selected: this.state.selected}); 314 | this.refs.typeahead.setEntryText(""); 315 | this.props.onTokenAdd(this.state.selected); 316 | }, 317 | 318 | render: function() { 319 | var classes = {}; 320 | classes[this.props.customClasses.typeahead] = !!this.props.customClasses.typeahead; 321 | var classList = classNames(classes); 322 | return ( 323 | React.createElement("div", {className: "typeahead-tokenizer"}, 324 | this._renderTokens(), 325 | React.createElement(Typeahead, {ref: "typeahead", 326 | className: classList, 327 | placeholder: this.props.placeholder, 328 | allowCustomValues: this.props.allowCustomValues, 329 | customClasses: this.props.customClasses, 330 | options: this._getOptionsForTypeahead(), 331 | defaultValue: this.props.defaultValue, 332 | maxVisible: this.props.maxVisible, 333 | onOptionSelected: this._addTokenForValue, 334 | onKeyDown: this._onKeyDown, 335 | filterOption: this.props.filterOption}) 336 | ) 337 | ); 338 | } 339 | }); 340 | 341 | module.exports = TypeaheadTokenizer; 342 | 343 | 344 | 345 | },{"../keyevent":3,"../typeahead":7,"./token":6,"classnames":1,"react":"react"}],6:[function(require,module,exports){ 346 | /** 347 | * @jsx React.DOM 348 | */ 349 | 350 | var React = window.React || require('react'); 351 | var classNames = require('classnames'); 352 | 353 | /** 354 | * Encapsulates the rendering of an option that has been "selected" in a 355 | * TypeaheadTokenizer 356 | */ 357 | var Token = React.createClass({displayName: "Token", 358 | propTypes: { 359 | className: React.PropTypes.string, 360 | name: React.PropTypes.string, 361 | children: React.PropTypes.string, 362 | onRemove: React.PropTypes.func 363 | }, 364 | 365 | render: function() { 366 | var className = classNames([ 367 | "typeahead-token", 368 | this.props.className 369 | ]); 370 | 371 | return ( 372 | React.createElement("div", {className: className}, 373 | this._renderHiddenInput(), 374 | this.props.children, 375 | this._renderCloseButton() 376 | ) 377 | ); 378 | }, 379 | 380 | _renderHiddenInput: function() { 381 | // If no name was set, don't create a hidden input 382 | if (!this.props.name) { 383 | return null; 384 | } 385 | 386 | return ( 387 | React.createElement("input", { 388 | type: "hidden", 389 | name: this.props.name + '[]', 390 | value: this.props.children} 391 | ) 392 | ); 393 | }, 394 | 395 | _renderCloseButton: function() { 396 | if (!this.props.onRemove) { 397 | return ""; 398 | } 399 | return ( 400 | React.createElement("a", {className: "typeahead-token-close", href: "#", onClick: function(event) { 401 | this.props.onRemove(this.props.children); 402 | event.preventDefault(); 403 | }.bind(this)}, "×") 404 | ); 405 | } 406 | }); 407 | 408 | module.exports = Token; 409 | 410 | 411 | 412 | },{"classnames":1,"react":"react"}],7:[function(require,module,exports){ 413 | /** 414 | * @jsx React.DOM 415 | */ 416 | 417 | var React = window.React || require('react/addons'); 418 | var TypeaheadSelector = require('./selector'); 419 | var KeyEvent = require('../keyevent'); 420 | var fuzzy = require('fuzzy'); 421 | var classNames = require('classnames'); 422 | 423 | /** 424 | * A "typeahead", an auto-completing text input 425 | * 426 | * Renders an text input that shows options nearby that you can use the 427 | * keyboard or mouse to select. Requires CSS for MASSIVE DAMAGE. 428 | */ 429 | var Typeahead = React.createClass({displayName: "Typeahead", 430 | propTypes: { 431 | name: React.PropTypes.string, 432 | customClasses: React.PropTypes.object, 433 | maxVisible: React.PropTypes.number, 434 | options: React.PropTypes.array, 435 | allowCustomValues: React.PropTypes.number, 436 | defaultValue: React.PropTypes.string, 437 | placeholder: React.PropTypes.string, 438 | onOptionSelected: React.PropTypes.func, 439 | onKeyDown: React.PropTypes.func, 440 | filterOption: React.PropTypes.func 441 | }, 442 | 443 | getDefaultProps: function() { 444 | return { 445 | options: [], 446 | customClasses: {}, 447 | allowCustomValues: 0, 448 | defaultValue: "", 449 | placeholder: "", 450 | onOptionSelected: function(option) {}, 451 | onKeyDown: function(event) {}, 452 | filterOption: null 453 | }; 454 | }, 455 | 456 | getInitialState: function() { 457 | return { 458 | // The currently visible set of options 459 | visible: this.getOptionsForValue(this.props.defaultValue, this.props.options), 460 | 461 | // This should be called something else, "entryValue" 462 | entryValue: this.props.defaultValue, 463 | 464 | // A valid typeahead value 465 | selection: null 466 | }; 467 | }, 468 | 469 | getOptionsForValue: function(value, options) { 470 | var result; 471 | if (this.props.filterOption) { 472 | result = options.filter((function(o) { return this.props.filterOption(value, o); }).bind(this)); 473 | } else { 474 | result = fuzzy.filter(value, options).map(function(res) { 475 | return res.string; 476 | }); 477 | } 478 | if (this.props.maxVisible) { 479 | result = result.slice(0, this.props.maxVisible); 480 | } 481 | return result; 482 | }, 483 | 484 | setEntryText: function(value) { 485 | this.refs.entry.getDOMNode().value = value; 486 | this._onTextEntryUpdated(); 487 | }, 488 | 489 | _hasCustomValue: function() { 490 | if (this.props.allowCustomValues > 0 && 491 | this.state.entryValue.length >= this.props.allowCustomValues && 492 | this.state.visible.indexOf(this.state.entryValue) < 0) { 493 | return true; 494 | } 495 | return false; 496 | }, 497 | 498 | _getCustomValue: function() { 499 | if (this._hasCustomValue()) { 500 | return this.state.entryValue; 501 | } 502 | return null 503 | }, 504 | 505 | _renderIncrementalSearchResults: function() { 506 | // Nothing has been entered into the textbox 507 | if (!this.state.entryValue) { 508 | return ""; 509 | } 510 | 511 | // Something was just selected 512 | if (this.state.selection) { 513 | return ""; 514 | } 515 | 516 | // There are no typeahead / autocomplete suggestions 517 | if (!this.state.visible.length && !(this.props.allowCustomValues > 0)) { 518 | return ""; 519 | } 520 | 521 | if (this._hasCustomValue()) { 522 | return ( 523 | React.createElement(TypeaheadSelector, { 524 | ref: "sel", options: this.state.visible, 525 | customValue: this.state.entryValue, 526 | onOptionSelected: this._onOptionSelected, 527 | customClasses: this.props.customClasses}) 528 | ); 529 | } 530 | 531 | return ( 532 | React.createElement(TypeaheadSelector, { 533 | ref: "sel", options: this.state.visible, 534 | onOptionSelected: this._onOptionSelected, 535 | customClasses: this.props.customClasses}) 536 | ); 537 | }, 538 | 539 | _onOptionSelected: function(option, event) { 540 | var nEntry = this.refs.entry.getDOMNode(); 541 | nEntry.focus(); 542 | nEntry.value = option; 543 | this.setState({visible: this.getOptionsForValue(option, this.props.options), 544 | selection: option, 545 | entryValue: option}); 546 | return this.props.onOptionSelected(option, event); 547 | }, 548 | 549 | _onTextEntryUpdated: function() { 550 | var value = this.refs.entry.getDOMNode().value; 551 | this.setState({visible: this.getOptionsForValue(value, this.props.options), 552 | selection: null, 553 | entryValue: value}); 554 | }, 555 | 556 | _onEnter: function(event) { 557 | if (!this.refs.sel.state.selection) { 558 | return this.props.onKeyDown(event); 559 | } 560 | return this._onOptionSelected(this.refs.sel.state.selection, event); 561 | }, 562 | 563 | _onEscape: function() { 564 | this.refs.sel.setSelectionIndex(null) 565 | }, 566 | 567 | _onTab: function(event) { 568 | var option = this.refs.sel.state.selection ? 569 | this.refs.sel.state.selection : (this.state.visible.length > 0 ? this.state.visible[0] : null); 570 | 571 | if (option === null && this._hasCustomValue()) { 572 | option = this._getCustomValue(); 573 | } 574 | 575 | if (option !== null) { 576 | return this._onOptionSelected(option, event); 577 | } 578 | }, 579 | 580 | eventMap: function(event) { 581 | var events = {}; 582 | 583 | events[KeyEvent.DOM_VK_UP] = this.refs.sel.navUp; 584 | events[KeyEvent.DOM_VK_DOWN] = this.refs.sel.navDown; 585 | events[KeyEvent.DOM_VK_RETURN] = events[KeyEvent.DOM_VK_ENTER] = this._onEnter; 586 | events[KeyEvent.DOM_VK_ESCAPE] = this._onEscape; 587 | events[KeyEvent.DOM_VK_TAB] = this._onTab; 588 | 589 | return events; 590 | }, 591 | 592 | _onKeyDown: function(event) { 593 | // If there are no visible elements, don't perform selector navigation. 594 | // Just pass this up to the upstream onKeydown handler 595 | if (!this.refs.sel) { 596 | return this.props.onKeyDown(event); 597 | } 598 | 599 | var handler = this.eventMap()[event.keyCode]; 600 | 601 | if (handler) { 602 | handler(event); 603 | } else { 604 | return this.props.onKeyDown(event); 605 | } 606 | // Don't propagate the keystroke back to the DOM/browser 607 | event.preventDefault(); 608 | }, 609 | 610 | componentWillReceiveProps: function(nextProps) { 611 | this.setState({ 612 | visible: this.getOptionsForValue(this.state.entryValue, nextProps.options) 613 | }); 614 | }, 615 | 616 | render: function() { 617 | var inputClasses = {} 618 | inputClasses[this.props.customClasses.input] = !!this.props.customClasses.input; 619 | var inputClassList = classNames(inputClasses); 620 | 621 | var classes = { 622 | typeahead: true 623 | } 624 | classes[this.props.className] = !!this.props.className; 625 | var classList = classNames(classes); 626 | 627 | return ( 628 | React.createElement("div", {className: classList}, 629 | this._renderHiddenInput(), 630 | React.createElement("input", {ref: "entry", type: "text", 631 | placeholder: this.props.placeholder, 632 | className: inputClassList, 633 | value: this.state.entryValue, 634 | defaultValue: this.props.defaultValue, 635 | onChange: this._onTextEntryUpdated, onKeyDown: this._onKeyDown}), 636 | this._renderIncrementalSearchResults() 637 | ) 638 | ); 639 | }, 640 | 641 | _renderHiddenInput: function() { 642 | if (!this.props.name) { 643 | return null; 644 | } 645 | 646 | return ( 647 | React.createElement("input", { 648 | type: "hidden", 649 | name: this.props.name, 650 | value: this.state.selection} 651 | ) 652 | ); 653 | } 654 | }); 655 | 656 | module.exports = Typeahead; 657 | 658 | 659 | 660 | },{"../keyevent":3,"./selector":9,"classnames":1,"fuzzy":2,"react/addons":"react/addons"}],8:[function(require,module,exports){ 661 | /** 662 | * @jsx React.DOM 663 | */ 664 | 665 | var React = window.React || require('react/addons'); 666 | var classNames = require('classnames'); 667 | 668 | /** 669 | * A single option within the TypeaheadSelector 670 | */ 671 | var TypeaheadOption = React.createClass({displayName: "TypeaheadOption", 672 | propTypes: { 673 | customClasses: React.PropTypes.object, 674 | customValue: React.PropTypes.string, 675 | onClick: React.PropTypes.func, 676 | children: React.PropTypes.string, 677 | hover: React.PropTypes.bool 678 | }, 679 | 680 | getDefaultProps: function() { 681 | return { 682 | customClasses: {}, 683 | onClick: function(event) { 684 | event.preventDefault(); 685 | } 686 | }; 687 | }, 688 | 689 | getInitialState: function() { 690 | return {}; 691 | }, 692 | 693 | render: function() { 694 | var classes = {}; 695 | classes[this.props.customClasses.hover || "hover"] = !!this.props.hover; 696 | classes[this.props.customClasses.listItem] = !!this.props.customClasses.listItem; 697 | 698 | if (this.props.customValue) { 699 | classes[this.props.customClasses.customAdd] = !!this.props.customClasses.customAdd; 700 | } 701 | 702 | var classList = classNames(classes); 703 | 704 | return ( 705 | React.createElement("li", {className: classList, onClick: this._onClick}, 706 | React.createElement("a", {href: "javascript: void 0;", className: this._getClasses(), ref: "anchor"}, 707 | this.props.children 708 | ) 709 | ) 710 | ); 711 | }, 712 | 713 | _getClasses: function() { 714 | var classes = { 715 | "typeahead-option": true, 716 | }; 717 | classes[this.props.customClasses.listAnchor] = !!this.props.customClasses.listAnchor; 718 | 719 | return classNames(classes); 720 | }, 721 | 722 | _onClick: function(event) { 723 | event.preventDefault(); 724 | return this.props.onClick(event); 725 | } 726 | }); 727 | 728 | 729 | module.exports = TypeaheadOption; 730 | 731 | 732 | 733 | },{"classnames":1,"react/addons":"react/addons"}],9:[function(require,module,exports){ 734 | /** 735 | * @jsx React.DOM 736 | */ 737 | 738 | var React = window.React || require('react/addons'); 739 | var TypeaheadOption = require('./option'); 740 | var classNames = require('classnames'); 741 | 742 | /** 743 | * Container for the options rendered as part of the autocompletion process 744 | * of the typeahead 745 | */ 746 | var TypeaheadSelector = React.createClass({displayName: "TypeaheadSelector", 747 | propTypes: { 748 | options: React.PropTypes.array, 749 | customClasses: React.PropTypes.object, 750 | customValue: React.PropTypes.string, 751 | selectionIndex: React.PropTypes.number, 752 | onOptionSelected: React.PropTypes.func 753 | }, 754 | 755 | getDefaultProps: function() { 756 | return { 757 | selectionIndex: null, 758 | customClasses: {}, 759 | customValue: null, 760 | onOptionSelected: function(option) { } 761 | }; 762 | }, 763 | 764 | getInitialState: function() { 765 | return { 766 | selectionIndex: this.props.selectionIndex, 767 | selection: this.getSelectionForIndex(this.props.selectionIndex) 768 | }; 769 | }, 770 | 771 | render: function() { 772 | var classes = { 773 | "typeahead-selector": true 774 | }; 775 | classes[this.props.customClasses.results] = this.props.customClasses.results; 776 | var classList = classNames(classes); 777 | 778 | var results = []; 779 | // CustomValue should be added to top of results list with different class name 780 | if (this.props.customValue !== null) { 781 | 782 | results.push( 783 | React.createElement(TypeaheadOption, {ref: this.props.customValue, key: this.props.customValue, 784 | hover: this.state.selectionIndex === results.length, 785 | customClasses: this.props.customClasses, 786 | customValue: this.props.customValue, 787 | onClick: this._onClick.bind(this, this.props.customValue)}, 788 | this.props.customValue 789 | )); 790 | } 791 | 792 | this.props.options.forEach(function(result, i) { 793 | results.push ( 794 | React.createElement(TypeaheadOption, {ref: result, key: result, 795 | hover: this.state.selectionIndex === results.length, 796 | customClasses: this.props.customClasses, 797 | onClick: this._onClick.bind(this, result)}, 798 | result 799 | ) 800 | ); 801 | }, this); 802 | 803 | 804 | return React.createElement("ul", {className: classList}, results ); 805 | }, 806 | 807 | setSelectionIndex: function(index) { 808 | this.setState({ 809 | selectionIndex: index, 810 | selection: this.getSelectionForIndex(index), 811 | }); 812 | }, 813 | 814 | getSelectionForIndex: function(index) { 815 | if (index === null) { 816 | return null; 817 | } 818 | if (index === 0 && this.props.customValue !== null) { 819 | return this.props.customValue; 820 | } 821 | 822 | if (this.props.customValue !== null) { 823 | index -= 1; 824 | } 825 | 826 | return this.props.options[index]; 827 | }, 828 | 829 | _onClick: function(result, event) { 830 | return this.props.onOptionSelected(result, event); 831 | }, 832 | 833 | _nav: function(delta) { 834 | if (!this.props.options && this.props.customValue === null) { 835 | return; 836 | } 837 | var newIndex = this.state.selectionIndex === null ? (delta == 1 ? 0 : delta) : this.state.selectionIndex + delta; 838 | var length = this.props.options.length; 839 | if (this.props.customValue !== null) { 840 | length += 1; 841 | } 842 | 843 | if (newIndex < 0) { 844 | newIndex += length; 845 | } else if (newIndex >= length) { 846 | newIndex -= length; 847 | } 848 | 849 | var newSelection = this.getSelectionForIndex(newIndex); 850 | this.setState({selectionIndex: newIndex, 851 | selection: newSelection}); 852 | }, 853 | 854 | navDown: function() { 855 | this._nav(1); 856 | }, 857 | 858 | navUp: function() { 859 | this._nav(-1); 860 | } 861 | 862 | }); 863 | 864 | module.exports = TypeaheadSelector; 865 | 866 | 867 | 868 | },{"./option":8,"classnames":1,"react/addons":"react/addons"}]},{},[4])(4) 869 | }); -------------------------------------------------------------------------------- /client/features/AddMetric.jsx: -------------------------------------------------------------------------------- 1 | Meteor.startup(function(){ 2 | 3 | AddMetric = ReactMeteor.createClass({ 4 | mixins: [React.addons.LinkedStateMixin], 5 | 6 | onEditorValidation: function(newVal, noErrors) { 7 | this.setState({ 8 | computeFunctionString: newVal, 9 | computeFunctionValid: noErrors 10 | }); 11 | }, 12 | 13 | contextTypes: { 14 | router: React.PropTypes.func.isRequired 15 | }, 16 | 17 | getInitialProps: function() { 18 | return { 19 | name: this.props.name || "", 20 | categoryText: this.props.categoryText || "", 21 | computeFunctionString: this.props.computeFunctionString || "// Metrics('/Category/Metric name')\n// Records('/Category path/goes here/').get()\n\ 22 | // the return value is your output\n", 23 | 24 | newMetric: this.props.newMetric && true 25 | }; 26 | }, 27 | 28 | getInitialState: function() { 29 | var state = { 30 | resetKey: (+new Date()), // oh I'm naughty 31 | 32 | newMetric: this.props.newMetric, 33 | runtimeError: null, 34 | 35 | name: this.props.name, 36 | categoryText: this.props.categoryText, 37 | computeFunctionString: this.props.computeFunctionString, 38 | computeFunctionValid: true 39 | }; 40 | return state; 41 | }, 42 | 43 | clearForm: function() { 44 | this.replaceState(this.getInitialState()); 45 | }, 46 | 47 | changeCategory: function(category) { 48 | this.setState({ categoryText: category }); 49 | }, 50 | 51 | componentWillReceiveProps: function(nextProps){ 52 | var state = { 53 | name: nextProps.name, 54 | categoryText: nextProps.categoryText, 55 | computeFunctionString: nextProps.computeFunctionString 56 | }; 57 | this.setState(state); 58 | console.log('ello'); 59 | }, 60 | 61 | submitForm: function() { 62 | var self = this; 63 | self.setState({ runtimeError: null }); 64 | Meteor.call('upsertMetric', this.state.name, this.state.categoryText, this.state.computeFunctionString, function (error, result) { 65 | if(error) { 66 | self.setState({ runtimeError: error }); 67 | } 68 | else self.clearForm(); // don't clear until server is good 69 | }); 70 | }, 71 | 72 | render: function() { 73 | var noValidationErrors = 74 | this.state.computeFunctionValid 75 | && this.state.name != "" 76 | && this.state.categoryText != ""; 77 | 78 | // if(this.state.metricError) { 79 | // var errorbox = (
80 | // 81 | // {this.state.metricError} 82 | // 83 | //
); 84 | // } 85 | 86 | var runtimeError; 87 | if(this.state.runtimeError != null) { 88 | runtimeError =
89 | 90 |
91 | {this.state.runtimeError.reason} 92 |
93 |
{this.state.runtimeError.details}
94 |
; 95 | } 96 | 97 | return ( 98 |
99 |
100 | 101 |

{this.state.newMetric ? 'Add' : 'Edit'} Metric

102 | 103 | 104 | 105 |
106 | 107 |
108 | 109 |
110 |
111 |
112 | 113 |
114 | 115 |
116 |
117 |
118 | 119 | 120 | 121 |
122 |
123 | 124 |
125 | 126 |
127 | 128 |
129 |

Compute function

130 | Docs 131 | 132 | {runtimeError} 133 | 134 | 135 | 137 |
138 |
139 | ); 140 | } 141 | }); 142 | 143 | 144 | }); -------------------------------------------------------------------------------- /client/features/AddRecord.jsx: -------------------------------------------------------------------------------- 1 | Meteor.startup(function(){ 2 | 3 | AddRecord = ReactMeteor.createClass({ 4 | contextTypes: { 5 | router: React.PropTypes.func.isRequired 6 | }, 7 | 8 | mixins: [React.addons.LinkedStateMixin], 9 | 10 | getInitialState: function() { 11 | var state = { 12 | resetKey: (+new Date()), // oh I'm naughty 13 | 14 | editingSchema: false, 15 | 16 | categoryText: "", 17 | 18 | defaultFields: { 19 | "string": { 20 | value: "" 21 | }, 22 | "number": { 23 | value: 6.5, 24 | step: 0.5, 25 | min: 2, 26 | max: 30 27 | }, 28 | "Date": { 29 | value: new Date() 30 | }, 31 | "boolean": { 32 | value: false 33 | } 34 | }, 35 | 36 | fields: [ 37 | { fieldName: "Time", value: new Date } 38 | ] 39 | 40 | }; 41 | return state; 42 | }, 43 | 44 | updateField: function(fieldIndex, value) { 45 | newState = this.state.fields; 46 | newState[fieldIndex].value = value; 47 | this.setState({ fields: newState }); 48 | }, 49 | 50 | clearForm: function() { 51 | // don't reset category text 52 | var categoryText = Util.clone(this.state.categoryText); 53 | this.replaceState(this.getInitialState()); 54 | this.changeCategory(categoryText); 55 | }, 56 | 57 | getSchema: function() { 58 | var copyOfStateFields = Util.clone(this.state.fields); 59 | return copyOfStateFields; 60 | }, 61 | 62 | submitForm: function() { 63 | var copyOfStateFields = Util.clone(this.state.fields); 64 | var fields = {}; 65 | var schema = []; 66 | 67 | 68 | // // separate fields from schema data 69 | for (var i = 0, field; field = copyOfStateFields[i]; i++) { 70 | // fields 71 | var name = field.fieldName; 72 | fields[name] = field.value; 73 | 74 | // schema 75 | schema.push(field); 76 | } 77 | 78 | Categories.setSchema(this.state.categoryText, schema); 79 | Records.addRecord(this.state.categoryText, null, fields); 80 | 81 | this.clearForm(); 82 | }, 83 | 84 | changeEditingSchemaStatus: function() { 85 | if(!this.state.editingSchema && this.state.categoryText != '') { 86 | Categories.setSchema(this.state.categoryText, this.getSchema()); 87 | } 88 | this.setState({ editingSchema: !this.state.editingSchema }); 89 | }, 90 | 91 | addField: function() { 92 | var fields = this.state.fields; 93 | var i = fields.length + 1; 94 | fields.push($.extend({ fieldName: "New field #"+i }, this.state.defaultFields["number"])); 95 | this.setState({fields: fields}); 96 | }, 97 | 98 | changeFieldType: function(fieldName, newType) { 99 | var fields = this.state.fields; 100 | for (var i = 0, field; field = this.state.fields[i]; i++) { 101 | if(field.fieldName == fieldName) { 102 | this.state.fields[i] = $.extend(this.state.defaultFields[newType], { fieldName: fieldName }); 103 | } 104 | }; 105 | this.setState({fields: fields}); 106 | }, 107 | 108 | changeCategory: function(category) { 109 | var newState = {}; 110 | newState.categoryText = category; 111 | try { 112 | newState.fields = Categories.findCategoryByPath(category).schema; 113 | } catch(ex) { 114 | newState.fields = [ 115 | { fieldName: "Time", value: new Date } 116 | ]; 117 | } 118 | this.setState(newState); 119 | }, 120 | 121 | renameField: function(fieldName, event) { 122 | var newFieldName = event.target.value; 123 | var fields = this.state.fields; 124 | fields.forEach(function(field){ 125 | if(field.fieldName == fieldName) { 126 | field.fieldName = newFieldName; 127 | } 128 | }); 129 | this.setState({fields: fields}); 130 | }, 131 | 132 | render: function() { 133 | fieldsView = []; 134 | 135 | for (var i = 0, field; field = this.state.fields[i]; i++) { 136 | var fieldName = field.fieldName; 137 | var fieldView = null; 138 | var fieldType = Util.getObjectType(field.value); 139 | switch(fieldType) { 140 | case 'string': 141 | fieldView = (); 142 | break; 143 | case 'number': 144 | fieldView = (); 145 | break; 146 | case 'boolean': 147 | fieldView = (); 148 | break; 149 | case 'Date': 150 | fieldView = (); 151 | break; 152 | default: 153 | console.log("Error: field "+"'"+fieldName+"'"+" type isn't recognised "+(typeof field.value)); 154 | } 155 | 156 | var typeCol, controlsCol; 157 | if(this.state.editingSchema) { 158 | typeCol = ; 159 | controlsCol = ( 160 | 161 | 162 | 163 | 164 | ); 165 | } 166 | 167 | var fieldNameView; 168 | 169 | if(this.state.editingSchema) { 170 | fieldNameView = (); 171 | } else { 172 | fieldNameView = ({fieldName}); 173 | } 174 | 175 | fieldsView.push(( 176 | 177 | {fieldNameView} 178 | {fieldView} 179 | {typeCol} 180 | {controlsCol} 181 | 182 | )); 183 | }; 184 | 185 | var editingTypeHeader, editingControlsHeader; 186 | if(this.state.editingSchema) { 187 | editingTypeHeader = Type; 188 | editingControlsHeader = Controls; 189 | } 190 | 191 | var noValidationErrors = 192 | this.state.fields.length > 0 193 | && this.state.categoryText != ""; 194 | 195 | return ( 196 |
197 |
198 | 199 |

Add Record

200 | 201 | 202 | 203 |
204 | 205 |
206 | 207 |
208 |
209 |
210 | 211 | 212 |
213 |
214 | 215 |
216 |

Fields

217 | 218 | 219 | 220 | 221 | 222 |
223 | 224 | 225 | 226 | 227 | 228 | 229 | {editingTypeHeader} 230 | {editingControlsHeader} 231 | 232 | 233 | {fieldsView} 234 |
NameValue
235 | 236 |

237 | 238 | 239 | 240 |
241 |
242 |
243 | ); 244 | } 245 | }); 246 | 247 | 248 | AddRecord.FieldTypeSelector = ReactMeteor.createClass({ 249 | componentDidMount: function() { 250 | var _this = this; 251 | $(React.findDOMNode(this.refs.dropdown)).dropdown({ 252 | onChange: _this.onChange 253 | }); 254 | $(React.findDOMNode(this.refs.text)).html(React.renderToString(this.getViewForType(this.state.selectedType))); 255 | }, 256 | 257 | onChange: function(value, text, $choice) { 258 | this.setState({ selectedType: value }); 259 | this.props.onFieldTypeChange(this.state.selectedType); 260 | }, 261 | 262 | getHumanTypeForJSType: function(jsType) { 263 | // this data shouldn't be in state, but eh. 264 | // TODO 265 | var humanType = ''; 266 | this.state.types.forEach(function(type){ 267 | if(type.jsType === jsType) { 268 | humanType = type.name; 269 | return; 270 | } 271 | }); 272 | return humanType; 273 | }, 274 | 275 | getViewForType: function(jsType) { 276 | var view = ''; 277 | this.state.types.forEach(function(type){ 278 | if(type.jsType === jsType) { 279 | view = type.view; 280 | return; 281 | } 282 | }); 283 | return view; 284 | }, 285 | 286 | getInitialState: function() { 287 | var state = { 288 | selectedType: this.props.fieldType, 289 | types: [ 290 | { 291 | name: 'Number', 292 | icon: 'calculator', 293 | jsType: 'number' 294 | }, 295 | { 296 | name: 'Dates and times', 297 | icon: 'calendar', 298 | jsType: 'Date' 299 | }, 300 | { 301 | name: 'Text', 302 | icon: 'font', 303 | jsType: 'string' 304 | }, 305 | { 306 | name: 'Checkbox', 307 | icon: 'toggle off', 308 | jsType: 'boolean' 309 | } 310 | ] 311 | }; 312 | 313 | // add views 314 | state.types.forEach(function(type) { 315 | type.view = (
{type.name}
); 316 | }); 317 | 318 | 319 | return state; 320 | }, 321 | 322 | render: function() { 323 | // fieldType={fieldType} onFieldTypeChange={this.changeFieldType} 324 | var typesView = []; 325 | this.state.types.forEach(function(type){ typesView.push(type.view); }) 326 | 327 | return ( 328 |
329 | 330 |
331 | 332 |
333 | {typesView} 334 |
335 |
336 | ); 337 | } 338 | }); 339 | 340 | 341 | 342 | 343 | 344 | 345 | }); -------------------------------------------------------------------------------- /client/features/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | Meteor.startup(function(){ 2 | 3 | 4 | Dashboard = ReactMeteor.createClass({ 5 | REACT_GRID_LAYOUT_MARGIN_LEFT: 10, 6 | 7 | mixins: [React.PureRenderMixin], 8 | 9 | contextTypes: { 10 | router: React.PropTypes.func.isRequired 11 | }, 12 | 13 | componentDidMount: function() { 14 | var ev = document.createEvent('Event'); 15 | ev.initEvent('resize', true, true); 16 | window.dispatchEvent(ev); 17 | }, 18 | 19 | startMeteorSubscriptions: function() { 20 | Meteor.subscribe("metrics"); 21 | Meteor.subscribe("categories"); 22 | }, 23 | 24 | getMeteorState: function() { 25 | var meteorState = { 26 | categories: [] 27 | }; 28 | var categories = Categories.find().fetch(); 29 | categories.forEach(function(category){ 30 | cat = { 31 | path: category.path, 32 | _id: category._id, 33 | metrics: [] 34 | }; 35 | cat.metrics = Metrics.find({ categoryId: category._id }).fetch(); 36 | if(cat.metrics.length == 0) { return; } 37 | meteorState.categories.push(cat); 38 | }); 39 | return meteorState; 40 | }, 41 | 42 | getInitialState: function() { 43 | var state = { 44 | categories: [] 45 | }; 46 | return state; 47 | }, 48 | 49 | generateLayout: function(categoriesArray) { 50 | var layout = []; 51 | 52 | categoriesArray.forEach(function(category, i){ 53 | var MAX_METRICS_PER_ROW = 3; 54 | var MIN_ROWS = 1; 55 | var catViewWidth = Math.min(category.metrics.length, MAX_METRICS_PER_ROW); // max 3 wide 56 | var catViewHeight = Math.ceil(category.metrics.length / 3) 57 | 58 | layout[i] = { 59 | x: i * 2 % 12, 60 | y: i * 2 % 12, 61 | w: catViewWidth, 62 | h: catViewHeight, 63 | i: i 64 | }; 65 | }); 66 | 67 | return layout; 68 | }, 69 | 70 | render: function() { 71 | var layout = this.generateLayout(this.state.categories); 72 | reactGridLayoutOptions = { 73 | items: this.state.categories.length, 74 | cols: 4, 75 | isResizable: false, 76 | isDraggable: true, 77 | rowHeight: 180, 78 | autoSize: true 79 | }; 80 | 81 | if(layout.length != 0) { 82 | var cards = []; 83 | 84 | var self = this; 85 | this.state.categories.forEach(function(category, i){ 86 | metricsView = []; 87 | category.metrics.forEach(function(metric, i){ 88 | metricsView.push( 89 |
90 | 91 |
92 | ); 93 | }); 94 | 95 | cards.push( 96 |
97 |
98 |

99 | 100 |
101 | {metricsView} 102 |
103 |
104 |
105 | ); 106 | }); 107 | 108 | var reactGridLayout = ( 109 | 110 | {cards} 111 | ); 112 | } 113 | 114 | return ( 115 |
116 |
117 |
118 |

{Date.create().format('{Weekday}')} {Date.create().format('{ord} {Month}')}

119 |
120 | 121 | {reactGridLayout} 122 |
123 | 124 |
125 | ); 126 | } 127 | }); 128 | 129 | 130 | 131 | // [ 132 | // { 133 | // path: ["Life"], 134 | // metrics: [ 135 | // { name: "Satisfaction", computeValue: 80 } 136 | // ] 137 | // }, 138 | // { 139 | // path: ["Life", "Health"], 140 | // metrics: [ 141 | // { name: "Diabetes", computeValue: 10.3 }, 142 | // { name: "Sleep", computeValue: 8 }, 143 | // { name: "Exercise", computeValue: 0.86 } 144 | // ] 145 | // }, 146 | // { 147 | // path: ["Life", "Social"], 148 | // metrics: [ 149 | // { name: "Communications", computeValue: 0.509 }, 150 | // { name: "Family", computeValue: true }, 151 | // { name: "Friends", computeValue: true }, 152 | // { name: "Romance", computeValue: true } 153 | // ] 154 | // }, 155 | // { 156 | // path: ["Life", "Me"], 157 | // metrics: [ 158 | // { name: "Commitment", computeValue: 0.85 }, 159 | // { name: "Self-esteem/image", computeValue: 0.79 }, 160 | // { name: "Opportunities", computeValue: 13 }, 161 | // { name: "Risk-taking", computeValue: "More!" } 162 | // ] 163 | // } 164 | // ] 165 | // meteorState.categories.push({ 166 | // path: ["Life", "Social"], 167 | // _id: 42, 168 | // metrics: [ 169 | // { name: "Communications", computeValue: 0.509, _id: "ex" }, 170 | // { name: "Family", computeValue: true, _id: "ex" }, 171 | // { name: "Friends", computeValue: false, _id: "ex" }, 172 | // { name: "Romance", computeValue: true, _id: "ex" } 173 | // ] 174 | // }); 175 | 176 | 177 | }); -------------------------------------------------------------------------------- /client/features/Metric.jsx: -------------------------------------------------------------------------------- 1 | Meteor.startup(function(){ 2 | 3 | UI = ReactMeteor.createClass({ 4 | render: function() { 5 | return (
); 6 | } 7 | }); 8 | 9 | UI.Metric = ReactMeteor.createClass({ 10 | getDefaultProps: function() { 11 | return { 12 | computeResult: 42, 13 | categoryPath: ['Path', 'to', 'Category'], 14 | name: "Metric name", 15 | _id: 42 16 | }; 17 | }, 18 | 19 | getInitialState: function(){ 20 | return { showControls: false }; 21 | }, 22 | 23 | onMouseOver: function(){ 24 | this.setState({showControls: true}); 25 | }, 26 | 27 | onMouseOut: function(){ 28 | this.setState({showControls: false}); 29 | }, 30 | 31 | render: function() { 32 | var pathAsString = this.props.categoryPath.join('/'); 33 | 34 | var resultView; 35 | var resultIsText = false; 36 | var result = this.props.computeResult; 37 | switch(Util.getObjectType(result)) { 38 | case 'string': 39 | resultIsText = true; 40 | resultView = result; 41 | break; 42 | 43 | case 'number': 44 | var isPercentage = result.between(0, 1, true); 45 | resultView = isPercentage ? 46 | (result*100).toFixed(1)+'%' : 47 | result.toFixed(2); 48 | break; 49 | 50 | case 'boolean': 51 | if(result) { 52 | resultView = ; 53 | } else { 54 | resultView = ; 55 | } 56 | break; 57 | 58 | case 'Date': 59 | resultView = result.long(); // e.g. July 22, 2012 1:55pm 60 | break; 61 | 62 | case null: 63 | resultView = ; 64 | break; 65 | 66 | default: 67 | resultView = result.toString(); 68 | } 69 | 70 | // if(this.state.showControls) { 71 | // var controls =
72 | //
73 | //
; 74 | // } 75 | 76 | var self = this; 77 | 78 | return ( 79 |
80 |

81 | {this.props.name} 82 |

83 |
84 | {resultView} 85 |
86 |
87 | ); 88 | } 89 | }); 90 | 91 | 92 | 93 | 94 | }); -------------------------------------------------------------------------------- /client/features/MetricOverview.jsx: -------------------------------------------------------------------------------- 1 | Meteor.startup(function(){ 2 | 3 | MetricOverview = ReactMeteor.createClass({ 4 | contextTypes: { 5 | router: React.PropTypes.func.isRequired 6 | }, 7 | 8 | getInitialState: function() { 9 | return { 10 | metric: null, 11 | deps: null 12 | }; 13 | }, 14 | 15 | startMeteorSubscriptions: function() { 16 | Meteor.subscribe("metrics"); 17 | }, 18 | 19 | getMeteorState: function() { 20 | var self = this; 21 | return { metric: Metrics.findOne(self.getMetricId()) }; 22 | }, 23 | 24 | getMetricId: function(){ return this.context.router.getCurrentParams().id; }, 25 | 26 | recomputeMetric: function(){ 27 | Meteor.call('recomputeMetric', this.getMetricId()); 28 | }, 29 | 30 | remove: function(){ 31 | Metrics.remove(this.state.metric._id); 32 | }, 33 | 34 | render: function() { 35 | if(this.state.metric) { 36 | var categoryPath = Categories.findOne(this.state.metric.categoryId).path; 37 | var loadedMetricView = ; 38 | var deps = JSON.stringify({ metrics: this.state.metric.metricDependencies, records: this.state.metric.recordDependencies }); 39 | } 40 | return ( 41 |
42 |

Metric Overview

43 | 44 | 45 | 46 | 47 | {this.state.metric ? 48 |
49 |
50 | 51 |
52 | 53 |
{deps}
54 |
55 | : "" } 56 |
57 | 58 | {loadedMetricView}
59 | 60 | ); 61 | } 62 | }); 63 | 64 | 65 | 66 | }); -------------------------------------------------------------------------------- /client/features/RecordsOverview.jsx: -------------------------------------------------------------------------------- 1 | Meteor.startup(function(){ 2 | 3 | RecordsOverview = ReactMeteor.createClass({ 4 | MAX_RECORD_COUNT: 100, 5 | 6 | contextTypes: { 7 | router: React.PropTypes.func.isRequired 8 | }, 9 | 10 | getInitialState: function() { 11 | return { 12 | meteorLoaded: false, 13 | category: { schema: [], path: [] }, 14 | records: [] 15 | }; 16 | }, 17 | 18 | getMeteorState: function() { 19 | var self = this; 20 | var category = Categories.findOne(this.getCategoryId()); 21 | if(category == null) return; // TODO error 22 | var records = Records.find({ categoryId: category._id }, { limit: self.MAX_RECORD_COUNT }).fetch(); 23 | return { records: records, category: category, meteorLoaded: true }; 24 | }, 25 | 26 | startMeteorSubscriptions: function() { 27 | Meteor.subscribe("categories"); 28 | Meteor.subscribe("records"); 29 | }, 30 | 31 | getCategoryId: function(){ return this.context.router.getCurrentParams().id; }, 32 | 33 | removeRecord: function(i) { 34 | Records.remove(this.state.records[i]._id); 35 | }, 36 | 37 | render: function() { 38 | var self = this; 39 | 40 | header = []; 41 | this.state.category.schema.forEach(function(field, i){ 42 | header.push({field.fieldName}); 43 | }); 44 | header.push(Controls); 45 | bodyRows = []; 46 | this.state.records.forEach(function(record, i){ 47 | var fields = []; 48 | for(var field in record.fields) { 49 | var val = record.fields[field]; 50 | fields.push({val}); 51 | } 52 | fields.push(); 53 | bodyRows.push({fields}); 54 | }); 55 | 56 | return ( 57 |
58 |

{this.state.meteorLoaded ? : ''}

59 |

{this.state.records.length} records

60 | 61 | 62 | {header} 63 | 64 | 65 | {bodyRows} 66 | 67 |
68 | 69 |
70 | ); 71 | } 72 | }); 73 | 74 | 75 | 76 | }); -------------------------------------------------------------------------------- /client/features/UI.jsx: -------------------------------------------------------------------------------- 1 | // Main UI 2 | Meteor.startup(function(){ 3 | 4 | Link = ReactRouter.Link; 5 | 6 | LoadingIndicator = ReactMeteor.createClass({ 7 | getDefaultProps: function(){ 8 | loading: true 9 | }, 10 | 11 | render: function() { 12 | return this.props.loading ?
:
; 13 | } 14 | }); 15 | 16 | Icon = ReactMeteor.createClass({ 17 | render: function() { 18 | return ; 19 | } 20 | }); 21 | 22 | CategoryBreadcrumb = ReactMeteor.createClass({ 23 | mixins: [ReactRouter.Navigation], 24 | 25 | navigateToCategory: function(categoryPath, indexClicked) { 26 | var pathUpTo = Util.clone(categoryPath).splice(0, indexClicked+1).join('/'); 27 | var categoryId = Categories.findCategoryByPath(pathUpTo, null)._id; 28 | this.transitionTo('records-overview', {id: categoryId}); 29 | }, 30 | 31 | render: function() { 32 | return ; 33 | } 34 | }); 35 | 36 | Breadcrumb = ReactMeteor.createClass({ 37 | onItemClick: function(i){ 38 | this.props.onItemClick(this.props.items, i); 39 | }, 40 | 41 | render: function() { 42 | var stuff = []; 43 | var self = this; 44 | this.props.items.forEach(function(item, i){ 45 | var lastItem = (i == self.props.items.length - 1); 46 | stuff.push(
{item}
); 47 | if(!lastItem) stuff.push(); 48 | }); 49 | 50 | return ( 51 |
52 | {stuff} 53 |
54 | ); 55 | } 56 | }); 57 | 58 | UI.Menu = ReactMeteor.createClass({ 59 | mixins: [ReactRouter.Navigation], 60 | 61 | navigateToDash: function(){ 62 | this.transitionTo('dashboard'); 63 | }, 64 | 65 | render: function() { 66 | return ( 67 | 91 | ); 92 | } 93 | }); 94 | 95 | UI.Segment = ReactMeteor.createClass({ 96 | render: function() { 97 | var cn = "ui segment " + Util.classNames({'hide': this.props.hidden}); 98 | 99 | return ( 100 |
101 |

{this.props.title}

102 | {this.props.children} 103 |
104 | ); 105 | } 106 | }); 107 | 108 | UI.Row = ReactMeteor.createClass({ 109 | render: function() { 110 | var cn = "ui row " + Util.classNames({'hide': this.props.hidden}); 111 | 112 | return ( 113 |
114 | {this.props.children} 115 |
116 | ); 117 | } 118 | }); 119 | 120 | UI.Columns = ReactMeteor.createClass({ 121 | render: function() { 122 | var cn = "columns " + Util.classNames({'hide': this.props.hidden}); 123 | 124 | return ( 125 |
126 | {this.props.children} 127 |
128 | ); 129 | } 130 | }); 131 | 132 | // 133 | // 134 | // 135 | // 136 | // 137 | // 138 | // 139 | // 140 | 141 | UI.JSONDateTime = ReactMeteor.createClass({ 142 | getInitialState: function() { 143 | return { value: this.props.value, name: this.props.name, isRequired: true }; 144 | }, 145 | 146 | updateParentValue: function(event) { 147 | var val = event.target.value; 148 | this.props.updateValue(val); 149 | }, 150 | 151 | render: function() { 152 | return ( 153 |
154 | 155 |
156 | 157 |
158 |
159 | ); 160 | } 161 | }); 162 | 163 | UI.JSONNumber = ReactMeteor.createClass({ 164 | getInitialState: function() { 165 | return { fieldValue: this.props.value, name: this.props.name, isRequired: true }; 166 | }, 167 | 168 | updateParentValue: function(event) { 169 | var val = parseFloat(event.target.value, 10); 170 | this.props.updateValue(val); 171 | }, 172 | 173 | render: function() { 174 | return ( 175 |
176 |
177 | 178 |
179 |
180 | ); 181 | } 182 | }); 183 | 184 | UI.JSONString = ReactMeteor.createClass({ 185 | mixins: [React.addons.LinkedStateMixin], 186 | 187 | getInitialState: function() { 188 | return { value: this.props.value, name: this.props.name, isRequired: true }; 189 | }, 190 | 191 | updateParentValue: function(event) { 192 | var val = event.target.value; 193 | this.props.updateValue(val); 194 | }, 195 | 196 | render: function() { 197 | return ( 198 |
199 |