├── .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 | 
9 | 
10 | 
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 | Clear
105 |
106 | Submit
107 |
108 |
109 |
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 | Delete
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 | Clear
203 |
204 | Submit
205 |
206 |
207 |
208 |
209 |
210 | Category
211 |
212 |
213 |
214 |
215 |
216 | Fields
217 |
218 |
219 | { this.state.editingSchema ? "Finish editing" : "Edit schema"}
220 | Add new
221 |
222 |
223 |
224 |
225 |
226 |
227 | Name
228 | Value
229 | {editingTypeHeader}
230 | {editingControlsHeader}
231 |
232 |
233 | {fieldsView}
234 |
235 |
236 |
237 |
238 |
239 |
Add record
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 = ;
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 |
Recompute
45 |
Remove
46 |
47 | {this.state.metric ?
48 |
49 |
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 |
68 |
69 |
{"{metric}"}
70 |
71 |
72 |
73 | Dashboard
74 |
75 |
76 | Add metric
77 |
78 |
79 | Add record
80 |
81 |
82 |
83 |
89 |
90 |
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 | // Add new
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 |
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 |
201 | );
202 | }
203 | });
204 |
205 | UI.JSONBoolean = ReactMeteor.createClass({
206 | mixins: [React.addons.LinkedStateMixin],
207 |
208 | getInitialState: function() {
209 | return { value: this.props.value, name: this.props.name, isRequired: true };
210 | },
211 |
212 | componentDidMount: function() {
213 | var self = this;
214 | $(React.findDOMNode(this.refs.checkbox)).checkbox({
215 | onChange: self.updateParentValue
216 | }); // I love React, it's so simple, refs make so much sense
217 | },
218 |
219 | updateParentValue: function() {
220 | var oldVal = this.state.value;
221 | this.setState({ value: !oldVal });
222 | this.props.updateValue(!oldVal);
223 | console.log(!oldVal);
224 | },
225 |
226 | render: function() {
227 | // value={this.props.value}
228 | return (
229 |
234 | );
235 | }
236 | });
237 |
238 | UI.JSEditor = ReactMeteor.createClass({
239 | componentDidMount: function() {
240 | var editor = jsEditor({
241 | value: this.props.code || "",
242 | mode: "javascript",
243 | lineNumbers: true,
244 | lineWrapping: true,
245 | matchBrackets: true,
246 | indentWithTabs: true,
247 | tabSize: 2,
248 | indentUnit: 2,
249 | updateInterval: 500,
250 | dragAndDrop: false,
251 | container: document.querySelector('#code-editor')
252 | });
253 | var _this = this;
254 | editor.on('valid', function(noErrors) {
255 | // BUG in jsEditor meaning that when there are no errors it returns undefined
256 | // We set it to true here so it makes sense
257 | /*if(noErrors != false)*/ noErrors = true; // disable temp until I can customise it
258 | _this.props.onValidation(editor.getValue(), noErrors);
259 | });
260 | },
261 |
262 | render: function() {
263 | return (
264 |
267 | );
268 | }
269 | });
270 |
271 |
272 | SearchInput = ReactMeteor.createClass({
273 | mixins: [React.addons.LinkedStateMixin],
274 |
275 |
276 | componentDidMount: function(){
277 |
278 | var _this = this;
279 | $(React.findDOMNode(this.refs.search)).search({
280 | source: this.state.results,
281 | searchFields: [
282 | 'title'
283 | ],
284 | searchFullText: true,
285 | onSearchQuery: function(){
286 | _this.setState({ results: _this.props.onSearchQuery() });
287 |
288 | }
289 | });
290 | },
291 |
292 | getInitialState: function(){
293 | return {
294 | searchText: '',
295 | results: []
296 | };
297 | },
298 |
299 | render: function() {
300 | return (
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 | );
309 | }
310 | });
311 |
312 |
313 | UI.CategorySearchInput = ReactMeteor.createClass({
314 | getDefaultProps: function(){
315 | return {
316 | onSelect: function(){},
317 | onValueChange: function(){}
318 | };
319 | },
320 |
321 | searchForCategory: function(searchText, callback) {
322 | // var modifiers = 'ig';
323 | // var pattern = '[a-zA-Z\/ -]*';
324 | // var searchBits = Categories.parsePathIntoArray(searchText);
325 |
326 | // var categoriesCursor = Categories.find({
327 | // // thanks https://regex101.com/
328 | // // took a while, but I got it
329 | // path: { $all: searchBits.map(function(v){ return new RegExp(v+pattern, modifiers) }) }
330 | // }, {});
331 | // var num = categoriesCursor.count();
332 |
333 | // var results = categoriesCursor.fetch() || [];
334 |
335 | // var resultsOnlyText = [];
336 | // results.forEach(function(cat){
337 | // resultsOnlyText.push(cat.path.join('/'));
338 | // });
339 |
340 | var cats = Categories.find().fetch();
341 | var catsText = [];
342 | cats.forEach(function(cat){
343 | catsText.push(cat.path.join('/'));
344 | });
345 | var regex = new RegExp('^' + searchText, 'i');
346 | var resultsOnlyText = catsText.filter(function(cat){ return regex.test(cat) });
347 |
348 | callback(null, resultsOnlyText);
349 | },
350 |
351 | onSuggestionSelected: function(suggestion, event) {
352 | this.props.onSelect(suggestion);
353 | event.preventDefault(); // prevent form submit
354 | },
355 |
356 | onChange: function(value) { this.props.onSelect(value); },
357 |
358 | render: function() {
359 | var self = this;
360 | var inputAttributes = {
361 | value: this.props.category,
362 | onChange: self.onChange
363 | };
364 | return (
365 |
368 | );
369 | }
370 | });
371 |
372 |
373 | });
374 |
375 | // A bit of inspiration for this project has come from Douglas Adams' "Dirk Gently's Holistic Detective Agency", from a particular section of the novel where Richard MacDuff is discussing a program he created back in the 80s, which is a sort of spreadsheeting application that turns numerical data into music. Aside from the obvious facetiousness which follows the rest of the novel, this particular idea (which hasn't been executed to my knowledge) posseses a certain childlike naivety to it - "why not? we can see data, we can touch it, why can't we hear it".
376 | // And so, why not?
--------------------------------------------------------------------------------
/client/features/Util.js:
--------------------------------------------------------------------------------
1 | Util = {
2 | timestamp: function() { return new Date().getTime(); }
3 | };
4 |
5 | Util.classNames = function() {
6 | var classes = '';
7 | var arg;
8 |
9 | for (var i = 0; i < arguments.length; i++) {
10 | arg = arguments[i];
11 | if (!arg) {
12 | continue;
13 | }
14 |
15 | if ('string' === typeof arg || 'number' === typeof arg) {
16 | classes += ' ' + arg;
17 | } else if (Object.prototype.toString.call(arg) === '[object Array]') {
18 | classes += ' ' + classNames.apply(null, arg);
19 | } else if ('object' === typeof arg) {
20 | for (var key in arg) {
21 | if (!arg.hasOwnProperty(key) || !arg[key]) {
22 | continue;
23 | }
24 | classes += ' ' + key;
25 | }
26 | }
27 | }
28 | return classes.substr(1);
29 | };
30 |
31 | // Why is this language so complex...
32 | // http://stackoverflow.com/questions/1303646/check-whether-variable-is-number-or-string-in-javascript
33 | Util.isNumber = function(obj) { return !isNaN(parseFloat(obj)) };
34 |
35 | Util.getObjectType = function(obj) {
36 | var type = "";
37 | if(obj === null) return null;
38 | if(typeof obj === "object") {
39 | try { type = obj.constructor.name; } catch(ex) { console.log("can't find type of object: "+obj); }
40 | } else {
41 | type = typeof obj;
42 | }
43 | return type;
44 | };
45 |
46 | Util.escapeRegExp = function(str) {
47 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
48 | };
49 |
50 | Util.clone = function(obj) {
51 | return JSON.parse( JSON.stringify(obj) );
52 | };
53 |
54 | Number.prototype.between = function (a, b, inclusive) {
55 | var min = Math.min.apply(Math, [a,b]),
56 | max = Math.max.apply(Math, [a,b]);
57 | return inclusive ? this >= min && this <= max : this > min && this < max;
58 | };
--------------------------------------------------------------------------------
/lib/extern/sugar.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Sugar Library v1.4.1
3 | *
4 | * Freely distributable and licensed under the MIT-style license.
5 | * Copyright (c) 2014 Andrew Plummer
6 | * http://sugarjs.com/
7 | *
8 | * ---------------------------- */
9 | (function(){function aa(a){return function(){return a}}
10 | var m=Object,p=Array,q=RegExp,r=Date,s=String,t=Number,u=Math,ba="undefined"!==typeof global?global:this,v=m.prototype.toString,da=m.prototype.hasOwnProperty,ea=m.defineProperty&&m.defineProperties,fa="function"===typeof q(),ga=!("0"in new s("a")),ia={},ja=/^\[object Date|Array|String|Number|RegExp|Boolean|Arguments\]$/,w="Boolean Number String Array Date RegExp Function".split(" "),la=ka("boolean",w[0]),y=ka("number",w[1]),z=ka("string",w[2]),A=ma(w[3]),C=ma(w[4]),D=ma(w[5]),F=ma(w[6]);
11 | function ma(a){var b="Array"===a&&p.isArray||function(b,d){return(d||v.call(b))==="[object "+a+"]"};return ia[a]=b}function ka(a,b){function c(c){return G(c)?v.call(c)==="[object "+b+"]":typeof c===a}return ia[b]=c}
12 | function na(a){a.SugarMethods||(oa(a,"SugarMethods",{}),H(a,!1,!0,{extend:function(b,c,d){H(a,!1!==d,c,b)},sugarRestore:function(){return pa(this,a,arguments,function(a,c,d){oa(a,c,d.method)})},sugarRevert:function(){return pa(this,a,arguments,function(a,c,d){d.existed?oa(a,c,d.original):delete a[c]})}}))}function H(a,b,c,d){var e=b?a.prototype:a;na(a);I(d,function(d,f){var h=e[d],l=J(e,d);F(c)&&h&&(f=qa(h,f,c));!1===c&&h||oa(e,d,f);a.SugarMethods[d]={method:f,existed:l,original:h,instance:b}})}
13 | function K(a,b,c,d,e){var g={};d=z(d)?d.split(","):d;d.forEach(function(a,b){e(g,a,b)});H(a,b,c,g)}function pa(a,b,c,d){var e=0===c.length,g=L(c),f=!1;I(b.SugarMethods,function(b,c){if(e||-1!==g.indexOf(b))f=!0,d(c.instance?a.prototype:a,b,c)});return f}function qa(a,b,c){return function(d){return c.apply(this,arguments)?b.apply(this,arguments):a.apply(this,arguments)}}function oa(a,b,c){ea?m.defineProperty(a,b,{value:c,configurable:!0,enumerable:!1,writable:!0}):a[b]=c}
14 | function L(a,b,c){var d=[];c=c||0;var e;for(e=a.length;cb&&(d=1/d);return c(a*d)/d}var Ea=48,Fa=57,Ga=65296,Ha=65305,Ia=".",Ja="",Ka={},La;function Ma(){return"\t\n\x0B\f\r \u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u2028\u2029\u3000\ufeff"}function Na(a,b){var c="";for(a=a.toString();0>=1)a+=a;return c}
17 | function Oa(a,b){var c,d;c=a.replace(La,function(a){a=Ka[a];a===Ia&&(d=!0);return a});return d?parseFloat(c):parseInt(c,b||10)}function T(a,b,c,d){d=P(a).toString(d||10);d=Na("0",b-d.replace(/\.\d+/,"").length)+d;if(c||0>a)d=(0>a?"-":"+")+d;return d}function Pa(a){if(11<=a&&13>=a)return"th";switch(a%10){case 1:return"st";case 2:return"nd";case 3:return"rd";default:return"th"}}
18 | function Qa(a,b){function c(a,c){if(a||-1(f?1:2)))return Xa(a,e,b[0],f,c);d=[];L(b,function(b){if(la(b))return!1;d.push(Xa(a,e,b,f,c))});return d}function Xa(a,b,c,d,e){d&&(c%=b,0>c&&(c=b+c));return e?a.charAt(c):a[c]}function Ya(a,b){K(b,!0,!1,a,function(a,b){a[b+("equal"===b?"s":"")]=function(){return m[b].apply(null,[this].concat(L(arguments)))}})}na(m);I(w,function(a,b){na(ba[b])});var Za,$a;
21 | for($a=0;9>=$a;$a++)Za=s.fromCharCode($a+Ga),Ja+=Za,Ka[Za]=s.fromCharCode($a+Ea);Ka[","]="";Ka["\uff0e"]=Ia;Ka[Ia]=Ia;La=q("["+Ja+"\uff0e,"+Ia+"]","g");
22 | "use strict";H(m,!1,!1,{keys:function(a){var b=[];if(!G(a)&&!D(a)&&!F(a))throw new TypeError("Object required");I(a,function(a){b.push(a)});return b}});
23 | function ab(a,b,c,d){var e=a.length,g=-1==d,f=g?e-1:0;c=isNaN(c)?f:parseInt(c>>0);0>c&&(c=e+c);if(!g&&0>c||g&&c>=e)c=f;for(;g&&0<=c||!g&&cc&&(c=a.length+c);c=isNaN(c)?0:c;for(!0===d&&(e+=c);c>>0==e&&4294967295!=e)&&e>=c&&d.push(parseInt(e));d.sort().each(function(c){return b.call(a,a[c],c,a)})}function mb(a,b,c,d,e,g){var f,h,l;0g||n&&f=Ea&&c<=Fa||c>=Ga&&c<=Ha)&&(d>=Ea&&d<=Fa||d>=Ga&&d<=Ha))&&(c=Oa(a.slice(f)),d=Oa(b.slice(f)))):(e=e!==a.charAt(f),g=g!==b.charAt(f),e!==g&&0===h&&(h=e-g)),f+=1;while(null!=c&&null!=d&&c===d);return c===d?h:c-d}
36 | function Cb(a,b,c){z(a)||(a=s(a));c&&(a=a.toLowerCase());b&&(a=a.replace(b,""));return a}var Ab="AlphanumericSortOrder",xb="AlphanumericSortIgnore",yb="AlphanumericSortIgnoreCase",zb="AlphanumericSortEquivalents",Bb="AlphanumericSortNatural";H(p,!1,!0,{create:function(){var a=[];L(arguments,function(b){if(!ua(b)&&"length"in b&&("[object Arguments]"===v.call(b)||b.callee)||!ua(b)&&"length"in b&&!z(b)&&!va(b))b=p.prototype.slice.call(b,0);a=a.concat(b)});return a}});
37 | H(p,!0,!1,{find:function(a,b){ta(a);return mb(this,a,0,!1,!1,b)},findIndex:function(a,b){var c;ta(a);c=mb(this,a,0,!1,!0,b);return N(c)?-1:c}});
38 | H(p,!0,!0,{findFrom:function(a,b,c){return mb(this,a,b,c)},findIndexFrom:function(a,b,c){b=mb(this,a,b,c,!0);return N(b)?-1:b},findAll:function(a,b,c){var d=[],e;0a&&(a=0);return this.slice(0,a)},last:function(a){return N(a)?this[this.length-1]:this.slice(0>this.length-a?0:this.length-a)},
40 | from:function(a){return this.slice(a)},to:function(a){N(a)&&(a=this.length);return this.slice(0,a)},min:function(a,b){return tb(this,a,"min",b)},max:function(a,b){return tb(this,a,"max",b)},least:function(a,b){return tb(this.groupBy.apply(this,[a]),"length","min",b)},most:function(a,b){return tb(this.groupBy.apply(this,[a]),"length","max",b)},sum:function(a){a=a?this.map(a):this;return 0f?1:0)*(b?-1:1)});return c},randomize:function(){for(var a=this.concat(),b=a.length,c,d;b;)c=u.random()*b|0,d=a[--b],a[b]=a[c],a[c]=d;return a},zip:function(){var a=L(arguments);return this.map(function(b,c){return[b].concat(a.map(function(a){return c in a?a[c]:null}))})},sample:function(a){var b=this.randomize();return 0=k[1]&&c<=(k[2]||
54 | k[1])&&e.push(a)}),d=e),d=Ub(d));h?h="(?:"+d+")":(c||g.push(E),h="("+d+")");B&&(h+="?");return h});b?(b=Vb(f,e),e=["t","[\\s\\u3000]"].concat(f.timeMarker),h=a.match(/\\d\{\d,\d\}\)+\??$/),Wb(f,"(?:"+b+")[,\\s\\u3000]+?"+a,Jb.concat(g),d),Wb(f,a+"(?:[,\\s]*(?:"+e.join("|")+(h?"+":"*")+")"+b+")?",g.concat(Jb),d)):Wb(f,a,g,d)}};
55 | function Xb(a,b,c){var d,e,g=b[0],f=b[1],h=b[2];b=a[c]||a.relative;if(F(b))return b.call(a,g,f,h,c);e=a.units[8*(a.plural&&1Ba?Ba+7:Ba))}function h(){var a=B.i[k.edge];fc(function(a){if(M(k[a]))return E=a,!1},4);if("year"===E)k.e="month";else if("month"===E||"week"===E)k.e="day";n[(0>a.value?"endOf":"beginningOf")+Yb(E)]();-2===a.value&&n.reset()}function l(){var a;fc(function(b,c,d){"day"===b&&(b="date");if(M(k[b])){if(d>=wb)return n.setTime(NaN),!1;a=a||{};a[b]=k[b];
65 | delete k[b]}});a&&e(function(){n.set(a,!0)})}var n,x,ha,vb,B,k,E,wb,Ba,ra,ca;n=cc();vb=[];n.utc(d);C(a)?n.utc(a.isUTC()).setTime(a.getTime()):y(a)?n.setTime(a):G(a)?(n.set(a,!0),k=a):z(a)&&(ha=Y(b),a=hc(a),ha&&I(ha.o?[ha.o].concat(ha.g):ha.g,function(c,d){var g=a.match(d.q);if(g){B=d.locale;k=gc(g,d.to);B.o=d;k.utc&&n.utc();if(k.timestamp)return k=k.timestamp,!1;d.r&&(!z(k.month)&&(z(k.date)||Zb(ha,b)))&&(ca=k.month,k.month=k.date,k.date=ca);k.year&&2===k.t.length&&(k.year=100*R(U(cc(),"FullYear")/
66 | 100)-100*R(k.year/100)+k.year);k.month&&(k.month=B.getMonth(k.month),k.shift&&!k.unit&&(k.unit=B.units[7]));k.weekday&&k.date?delete k.weekday:k.weekday&&(k.weekday=B.getWeekday(k.weekday),k.shift&&!k.unit&&(k.unit=B.units[5]));k.day&&(ca=B.i[k.day])?(k.day=ca.value,n.reset(),x=!0):k.day&&-1<(Ba=B.getWeekday(k.day))&&(delete k.day,k.num&&k.month?(e(f),k.day=1):k.weekday=Ba);k.date&&!y(k.date)&&(k.date=$b(B,k.date));k.ampm&&k.ampm===B.ampm[1]&&12>k.hour?k.hour+=12:k.ampm===B.ampm[0]&&12===k.hour&&
67 | (k.hour=0);if("offset_hours"in k||"offset_minutes"in k)n.utc(),k.offset_minutes=k.offset_minutes||0,k.offset_minutes+=60*k.offset_hours,"-"===k.offset_sign&&(k.offset_minutes*=-1),k.minute-=k.offset_minutes;k.unit&&(x=!0,ra=ac(B,k.num),wb=B.units.indexOf(k.unit)%8,E=W.units[wb],l(),k.shift&&(ra*=(ca=B.i[k.shift])?ca.value:0),k.sign&&(ca=B.i[k.sign])&&(ra*=ca.value),M(k.weekday)&&(n.set({weekday:k.weekday},!0),delete k.weekday),k[E]=(k[E]||0)+ra);k.edge&&e(h);"-"===k.year_sign&&(k.year*=-1);fc(function(a,
68 | b,c){b=k[a];var d=b%1;d&&(k[Ob[c-1].name]=R(d*("second"===a?1E3:60)),k[a]=Q(b))},1,4);return!1}}),k?x?n.advance(k):(n._utc&&n.reset(),kc(n,k,!0,!1,c)):("now"!==a&&(n=new r(a)),d&&n.addMinutes(-n.getTimezoneOffset())),g(),n.utc(!1));return{c:n,set:k}}function lc(a){var b,c=P(a),d=c,e=0;fc(function(a,f,h){b=Q(Da(c/f.b(),1));1<=b&&(d=b,e=h)},1);return[d,e,a]}
69 | function mc(a){var b=lc(a.millisecondsFromNow());if(6===b[1]||5===b[1]&&4===b[0]&&a.daysFromNow()>=cc().daysInMonth())b[0]=P(a.monthsFromNow()),b[1]=6;return b}function nc(a,b,c){function d(a,c){var d=U(a,"Month");return Y(c).months[d+12*b]}Z(a,d,c);Z(Yb(a),d,c,1)}function Z(a,b,c,d){X[a]=function(a,g){var f=b(a,g);c&&(f=f.slice(0,c));d&&(f=f.slice(0,d).toUpperCase()+f.slice(d));return f}}
70 | function oc(a,b,c){X[a]=b;X[a+a]=function(a,c){return T(b(a,c),2)};c&&(X[a+a+a]=function(a,c){return T(b(a,c),3)},X[a+a+a+a]=function(a,c){return T(b(a,c),4)})}function pc(a){var b=a.match(/(\{\w+\})|[^{}]+/g);Qb[a]=b.map(function(a){a.replace(/\{(\w+)\}/,function(b,e){a=X[e]||e;return e});return a})}
71 | function qc(a,b,c,d){var e;if(!a.isValid())return"Invalid Date";Date[b]?b=Date[b]:F(b)&&(e=mc(a),b=b.apply(a,e.concat(Y(d))));if(!b&&c)return e=e||mc(a),0===e[1]&&(e[1]=1,e[0]=1),a=Y(d),Xb(a,e,0=b-n&&f<=h+x}
73 | function sc(a,b,c){b=new r(b);a=(new r(c)).utc(a.isUTC());23!==U(a,"Hours")&&(b=b.getTimezoneOffset(),a=a.getTimezoneOffset(),b!==a&&(c+=(a-b).minutes()));return c}
74 | function kc(a,b,c,d,e){function g(a){return M(b[a])?b[a]:b[a+"s"]}function f(a){return M(g(a))}var h;if(y(b)&&d)b={milliseconds:b};else if(y(b))return a.setTime(b),a;M(b.date)&&(b.day=b.date);fc(function(d,e,g){var l="day"===d;if(f(d)||l&&f("weekday"))return b.e=d,h=+g,!1;!c||("week"===d||l&&f("week"))||Sa(a,e.method,l?1:0)});Rb.forEach(function(c){var e=c.name;c=c.method;var h;h=g(e);N(h)||(d?("week"===e&&(h=(b.day||0)+7*h,c="Date"),h=h*d+U(a,c)):"month"===e&&f("day")&&Sa(a,"Date",15),Sa(a,c,h),
75 | d&&"month"===e&&(e=h,0>e&&(e=e%12+12),e%12!=U(a,"Month")&&Sa(a,"Date",0)))});d||(f("day")||!f("weekday"))||a.setWeekday(g("weekday"));var l;a:{switch(e){case -1:l=a>cc();break a;case 1:l=ar.create(a).getTime()-(b||0)},isBefore:function(a,b){return this.getTime()d},isLeapYear:function(){var a=U(this,"FullYear");return 0===a%4&&0!==a%100||0===a%400},
81 | daysInMonth:function(){return 32-U(new r(U(this,"FullYear"),U(this,"Month"),32),"Date")},format:function(a,b){return qc(this,a,!1,b)},relative:function(a,b){z(a)&&(b=a,a=null);return qc(this,a,!0,b)},is:function(a,b,c){var d,e;if(this.isValid()){if(z(a))switch(a=a.trim().toLowerCase(),e=this.clone().utc(c),!0){case "future"===a:return this.getTime()>cc().getTime();case "past"===a:return this.getTime()U(e,"Day");case "weekend"===a:return 0===
82 | U(e,"Day")||6===U(e,"Day");case -1<(d=W.weekdays.indexOf(a)%7):return U(e,"Day")===d;case -1<(d=W.months.indexOf(a)%12):return U(e,"Month")===d}return rc(this,a,null,b,c)}},reset:function(a){var b={},c;a=a||"hours";"date"===a&&(a="days");c=Rb.some(function(b){return a===b.name||a===b.name+"s"});b[a]=a.match(/^days?/)?1:0;return c?this.set(b,!0):this},clone:function(){var a=new r(this.getTime());a.utc(!!this._utc);return a}});
83 | H(r,!0,!0,{iso:function(){return this.toISOString()},getWeekday:r.prototype.getDay,getUTCWeekday:r.prototype.getUTCDay});function uc(a,b){function c(){return R(this*b)}function d(){return tc(arguments)[a.j](this)}function e(){return tc(arguments)[a.j](-this)}var g=a.name,f={};f[g]=c;f[g+"s"]=c;f[g+"Before"]=e;f[g+"sBefore"]=e;f[g+"Ago"]=e;f[g+"sAgo"]=e;f[g+"After"]=d;f[g+"sAfter"]=d;f[g+"FromNow"]=d;f[g+"sFromNow"]=d;t.extend(f)}H(t,!0,!0,{duration:function(a){a=Y(a);return Xb(a,lc(this),"duration")}});
84 | W=Ib=r.addLocale("en",{plural:!0,timeMarker:"at",ampm:"am,pm",months:"January,February,March,April,May,June,July,August,September,October,November,December",weekdays:"Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday",units:"millisecond:|s,second:|s,minute:|s,hour:|s,day:|s,week:|s,month:|s,year:|s",numbers:"one,two,three,four,five,six,seven,eight,nine,ten",articles:"a,an,the",tokens:"the,st|nd|rd|th,of","short":"{Month} {d}, {yyyy}","long":"{Month} {d}, {yyyy} {h}:{mm}{tt}",full:"{Weekday} {Month} {d}, {yyyy} {h}:{mm}:{ss}{tt}",
85 | past:"{num} {unit} {sign}",future:"{num} {unit} {sign}",duration:"{num} {unit}",modifiers:[{name:"sign",src:"ago|before",value:-1},{name:"sign",src:"from now|after|from|in|later",value:1},{name:"edge",src:"last day",value:-2},{name:"edge",src:"end",value:-1},{name:"edge",src:"first day|beginning",value:1},{name:"shift",src:"last",value:-1},{name:"shift",src:"the|this",value:0},{name:"shift",src:"next",value:1}],dateParse:["{month} {year}","{shift} {unit=5-7}","{0?} {date}{1}","{0?} {edge} of {shift?} {unit=4-7?}{month?}{year?}"],
86 | timeParse:"{num} {unit} {sign};{sign} {num} {unit};{0} {num}{1} {day} of {month} {year?};{weekday?} {month} {date}{1?} {year?};{date} {month} {year};{date} {month};{shift} {weekday};{shift} week {weekday};{weekday} {2?} {shift} week;{num} {unit=4-5} {sign} {day};{0?} {date}{1} of {month};{0?}{month?} {date?}{1?} of {shift} {unit=6-7}".split(";")});Ob=Rb.concat().reverse();Nb=Rb.concat();Nb.splice(2,1);
87 | K(r,!0,!0,Rb,function(a,b,c){function d(a){a/=f;var c=a%1,d=b.error||0.999;c&&P(c%1)>d&&(a=R(a));return 0>a?Aa(a):Q(a)}var e=b.name,g=Yb(e),f=b.b(),h,l;b.j="add"+g+"s";h=function(a,b){return d(this.getTime()-r.create(a,b).getTime())};l=function(a,b){return d(r.create(a,b).getTime()-this.getTime())};a[e+"sAgo"]=l;a[e+"sUntil"]=l;a[e+"sSince"]=h;a[e+"sFromNow"]=h;a[b.j]=function(a,b){var c={};c[e]=a;return this.advance(c,b)};uc(b,f);3>c&&["Last","This","Next"].forEach(function(b){a["is"+b+g]=function(){return rc(this,
88 | b+" "+e,"en")}});4>c&&(a["beginningOf"+g]=function(){var a={};switch(e){case "year":a.year=U(this,"FullYear");break;case "month":a.month=U(this,"Month");break;case "day":a.day=U(this,"Date");break;case "week":a.weekday=0}return this.set(a,!0)},a["endOf"+g]=function(){var a={hours:23,minutes:59,seconds:59,milliseconds:999};switch(e){case "year":a.month=11;a.day=31;break;case "month":a.day=this.daysInMonth();break;case "week":a.weekday=6}return this.set(a,!0)})});
89 | W.addFormat("([+-])?(\\d{4,4})[-.]?{full_month}[-.]?(\\d{1,2})?",!0,["year_sign","year","month","date"],!1,!0);W.addFormat("(\\d{1,2})[-.\\/]{full_month}(?:[-.\\/](\\d{2,4}))?",!0,["date","month","year"],!0);W.addFormat("{full_month}[-.](\\d{4,4})",!1,["month","year"]);W.addFormat("\\/Date\\((\\d+(?:[+-]\\d{4,4})?)\\)\\/",!1,["timestamp"]);W.addFormat(Vb(W),!1,Jb);Pb=W.g.slice(0,7).reverse();W.g=W.g.slice(7).concat(Pb);oc("f",function(a){return U(a,"Milliseconds")},!0);
90 | oc("s",function(a){return U(a,"Seconds")});oc("m",function(a){return U(a,"Minutes")});oc("h",function(a){return U(a,"Hours")%12||12});oc("H",function(a){return U(a,"Hours")});oc("d",function(a){return U(a,"Date")});oc("M",function(a){return U(a,"Month")+1});(function(){function a(a,c){var d=U(a,"Hours");return Y(c).ampm[Q(d/12)]||""}Z("t",a,1);Z("tt",a);Z("T",a,1,1);Z("TT",a,null,2)})();
91 | (function(){function a(a,c){var d=U(a,"Day");return Y(c).weekdays[d]}Z("dow",a,3);Z("Dow",a,3,1);Z("weekday",a);Z("Weekday",a,null,1)})();nc("mon",0,3);nc("month",0);nc("month2",1);nc("month3",2);X.ms=X.f;X.milliseconds=X.f;X.seconds=X.s;X.minutes=X.m;X.hours=X.h;X["24hr"]=X.H;X["12hr"]=X.h;X.date=X.d;X.day=X.d;X.year=X.yyyy;K(r,!0,!0,"short,long,full",function(a,b){a[b]=function(a){return qc(this,b,!1,a)}});
92 | "\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07".split("").forEach(function(a,b){9=this.start&&a.start<=this.end&&a.end>=this.start&&a.end<=this.end:a>=this.start&&a<=this.end},every:function(a,b){var c,d=this.start,e=this.end,g=e=e:f<=e;)l.push(f),b&&b(f,h),f=c(f,a),h++;return l},union:function(a){return new Range(this.starta.end?this.end:a.end)},intersect:function(a){return a.start>this.end||a.enda.start?this.start:a.start,this.endc?b:c;return vc(ab?b:a)}});[t,s,r].forEach(function(a){H(a,!1,!0,{range:function(b,c){a.create&&(b=a.create(b),c=a.create(c));return new Range(b,c)}})});
100 | H(t,!0,!0,{upto:function(a,b,c){return t.range(this,a).every(c,b)},clamp:function(a,b){return(new Range(a,b)).clamp(this)},cap:function(a){return this.clamp(void 0,a)}});H(t,!0,!0,{downto:t.prototype.upto});H(p,!1,function(a){return a instanceof Range},{create:function(a){return a.every()}});
101 | "use strict";function Ac(a,b,c,d,e){Infinity!==b&&(a.timers||(a.timers=[]),y(b)||(b=1),a.n=!1,a.timers.push(setTimeout(function(){a.n||c.apply(d,e||[])},b)))}
102 | H(Function,!0,!0,{lazy:function(a,b,c){function d(){g.lengthb;)x=Function.prototype.apply.apply(e,g.shift()),a--;Ac(d,l,function(){f=!1;h()})}};return d},throttle:function(a){return this.lazy(a,!0,1)},debounce:function(a){function b(){b.cancel();Ac(b,a,c,this,arguments)}var c=this;return b},delay:function(a){var b=
103 | L(arguments,null,1);Ac(this,a,this,this,b);return this},every:function(a){function b(){c.apply(c,d);Ac(c,a,b)}var c=this,d=arguments,d=1=c.length)&&c.splice(b,0,a)});return a.apply(this,c)}}});
105 | "use strict";function Bc(a,b,c,d,e,g){var f=a.toFixed(20),h=f.search(/\./),f=f.search(/[1-9]/),h=h-f;0h&&(e=-3,b=P(h)-9,d=c.slice(0,1));c=g?za(2,10*e):za(10,3*e);return Da(a/c,b||0).format()+d.trim()}
106 | H(t,!1,!0,{random:function(a,b){var c,d;1==arguments.length&&(b=a,a=0);c=Ca(a||0,N(b)?1:b);d=S(a||0,N(b)?1:b)+1;return Q(u.random()*(d-c)+c)}});
107 | H(t,!0,!0,{log:function(a){return u.log(this)/(a?u.log(a):1)},abbr:function(a){return Bc(this,a,"kmbt",0,4)},metric:function(a,b){return Bc(this,a,"n\u03bcm kMGTPE",4,N(b)?1:b)},bytes:function(a,b){return Bc(this,a,"kMGTPE",0,N(b)?4:b,!0)+"B"},isInteger:function(){return 0==this%1},isOdd:function(){return!isNaN(this)&&!this.isMultipleOf(2)},isEven:function(){return this.isMultipleOf(2)},isMultipleOf:function(a){return 0===this%a},format:function(a,b,c){var d,e,g,f="";N(b)&&(b=",");N(c)&&(c=".");d=
108 | (y(a)?Da(this,a||0).toFixed(S(a,0)):this.toString()).replace(/^-/,"").split(".");e=d[0];g=d[1];for(d=e.length;0this?"-":"")+f},hex:function(a){return this.pad(a||1,!1,16)},times:function(a){if(a)for(var b=0;ba||Infinity===a)throw new RangeError("Invalid number");return a}function Jc(a,b){return Na(M(b)?b:" ",a)}function Kc(a,b,c,d,e){var g;if(a.length<=b)return a.toString();d=N(d)?"...":d;switch(c){case "left":return a=e?Lc(a,b,!0):a.slice(a.length-b),d+a;case "middle":return c=Aa(b/2),g=Q(b/2),b=e?Lc(a,c):a.slice(0,c),a=e?Lc(a,g,!0):a.slice(a.length-g),b+d+a;default:return b=e?Lc(a,b):a.slice(0,b),b+d}}
121 | function Lc(a,b,c){if(c)return Lc(a.reverse(),b).reverse();c=q("(?=["+Ma()+"])");var d=0;return a.split(c).filter(function(a){d+=a.length;return d<=b}).join("")}function Mc(a,b,c){z(b)&&(b=a.indexOf(b),-1===b&&(b=c?a.length:0));return b}var Nc,Oc;H(s,!0,!1,{repeat:function(a){a=Ic(a);return Na(this,a)}});
122 | H(s,!0,function(a){return D(a)||2 /g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")},unescapeHTML:function(){return this.replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'").replace(///g,
124 | "/").replace(/&/g,"&")},encodeBase64:function(){return Nc(unescape(encodeURIComponent(this)))},decodeBase64:function(){return decodeURIComponent(escape(Oc(this)))},each:function(a,b){var c,d,e;F(a)?(b=a,a=/[\s\S]/g):a?z(a)?a=q(Ra(a),"gi"):D(a)&&(a=q(a.source,Qa(a,"g"))):a=/[\s\S]/g;c=this.match(a)||[];if(b)for(d=0,e=c.length;d]*>","gi"),"")});return a},removeTags:function(){var a=this;sa(0]*(?:\\/>|>.*?<\\/\\1>)","gi");a=a.replace(b,"")});return a},truncate:function(a,b,c){return Kc(this,a,b,c)},truncateOnWord:function(a,b,c){return Kc(this,a,b,c,!0)},pad:function(a,b){var c,d;a=Ic(a);c=S(0,a-this.length)/2;d=Q(c);c=Aa(c);return Jc(d,b)+this+Jc(c,b)},padLeft:function(a,b){a=Ic(a);return Jc(S(0,a-
129 | this.length),b)+this},padRight:function(a,b){a=Ic(a);return this+Jc(S(0,a-this.length),b)},first:function(a){N(a)&&(a=1);return this.substr(0,a)},last:function(a){N(a)&&(a=1);return this.substr(0>this.length-a?0:this.length-a)},toNumber:function(a){return Oa(this,a)},capitalize:function(a){var b;return this.toLowerCase().replace(a?/[^']/g:/^\S/,function(a){var d=a.toUpperCase(),e;e=b?a:d;b=d!==a;return e})},assign:function(){var a={};sa(arguments,function(b,c){G(b)?xa(a,b):a[c+1]=b});return this.replace(/\{([^{]+?)\}/g,
130 | function(b,c){return J(a,c)?a[c]:b})}});H(s,!0,!0,{insert:s.prototype.add});
131 | (function(a){if(ba.btoa)Nc=ba.btoa,Oc=ba.atob;else{var b=/[^A-Za-z0-9\+\/\=]/g;Nc=function(b){var d="",e,g,f,h,l,n,x=0;do e=b.charCodeAt(x++),g=b.charCodeAt(x++),f=b.charCodeAt(x++),h=e>>2,e=(e&3)<<4|g>>4,l=(g&15)<<2|f>>6,n=f&63,isNaN(g)?l=n=64:isNaN(f)&&(n=64),d=d+a.charAt(h)+a.charAt(e)+a.charAt(l)+a.charAt(n);while(x>4,g=(g&15)<<4|h>>2,f=(h&3)<<6|l,d+=s.fromCharCode(e),64!=h&&(d+=s.fromCharCode(g)),64!=l&&(d+=s.fromCharCode(f));while(n
2 | { metric }
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/packages/metric-stuff/.npm/package/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/packages/metric-stuff/.npm/package/README:
--------------------------------------------------------------------------------
1 | This directory and the files immediately inside it are automatically generated
2 | when you change this package's NPM dependencies. Commit the files in this
3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
4 | so that others run the same versions of sub-dependencies.
5 |
6 | You should NOT check in the node_modules directory that Meteor automatically
7 | creates; if you are using git, the .gitignore file tells git to ignore it.
8 |
--------------------------------------------------------------------------------
/packages/metric-stuff/.npm/package/npm-shrinkwrap.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "escodegen": {
4 | "version": "1.6.1",
5 | "dependencies": {
6 | "estraverse": {
7 | "version": "1.9.3"
8 | },
9 | "esutils": {
10 | "version": "1.1.6"
11 | },
12 | "esprima": {
13 | "version": "1.2.5"
14 | },
15 | "optionator": {
16 | "version": "0.5.0",
17 | "dependencies": {
18 | "prelude-ls": {
19 | "version": "1.1.1"
20 | },
21 | "deep-is": {
22 | "version": "0.1.3"
23 | },
24 | "wordwrap": {
25 | "version": "0.0.2"
26 | },
27 | "type-check": {
28 | "version": "0.3.1"
29 | },
30 | "levn": {
31 | "version": "0.2.5"
32 | },
33 | "fast-levenshtein": {
34 | "version": "1.0.6"
35 | }
36 | }
37 | },
38 | "source-map": {
39 | "version": "0.1.43",
40 | "dependencies": {
41 | "amdefine": {
42 | "version": "0.1.0"
43 | }
44 | }
45 | }
46 | }
47 | },
48 | "esprima": {
49 | "version": "2.2.0"
50 | },
51 | "esprima-walk": {
52 | "version": "0.1.0"
53 | },
54 | "gauss": {
55 | "version": "0.2.12"
56 | },
57 | "react-autosuggest": {
58 | "version": "1.12.2",
59 | "dependencies": {
60 | "classnames": {
61 | "version": "2.1.1"
62 | },
63 | "debounce": {
64 | "version": "1.0.0",
65 | "dependencies": {
66 | "date-now": {
67 | "version": "1.0.1"
68 | }
69 | }
70 | },
71 | "react": {
72 | "version": "0.13.3",
73 | "dependencies": {
74 | "envify": {
75 | "version": "3.4.0",
76 | "dependencies": {
77 | "through": {
78 | "version": "2.3.7"
79 | },
80 | "jstransform": {
81 | "version": "10.1.0",
82 | "dependencies": {
83 | "base62": {
84 | "version": "0.1.1"
85 | },
86 | "esprima-fb": {
87 | "version": "13001.1001.0-dev-harmony-fb"
88 | },
89 | "source-map": {
90 | "version": "0.1.31",
91 | "dependencies": {
92 | "amdefine": {
93 | "version": "0.1.0"
94 | }
95 | }
96 | }
97 | }
98 | }
99 | }
100 | }
101 | }
102 | }
103 | }
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/packages/metric-stuff/clientLibs.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liamzebedee/metric/3c587626d277cca3410e6b48562e58ee2bbe57b2/packages/metric-stuff/clientLibs.js
--------------------------------------------------------------------------------
/packages/metric-stuff/computeFunctionAnalyser.js:
--------------------------------------------------------------------------------
1 | var esprima = Npm.require('esprima');
2 | var escodegen = Npm.require('escodegen');
3 | var walk = Npm.require('esprima-walk');
4 |
5 | ComputeFunctionAnalyser = {};
6 | ComputeFunctionAnalyser.getDependencies = function(computeFunctionCodeString) {
7 | try {
8 | var ast = esprima.parse(computeFunctionCodeString, { raw: true });
9 | } catch(ex) {
10 | throw new Meteor.Error("parsing-error", "Your code has syntax errors: " + ex.toString(), ex.toString());
11 | }
12 |
13 | var dependencies = {
14 | metrics: [],
15 | records: []
16 | };
17 |
18 | walk(ast, function(node) {
19 | if(node.type == 'CallExpression' && node.callee.type == "Identifier") {
20 | var name = node.callee.name;
21 | // 0th argument is always the path
22 |
23 | if(name == 'Metrics') {
24 | // Metrics(path)
25 | var dependencyString = eval(escodegen.generate(node.arguments[0]));
26 | dependencies.metrics.push(dependencyString);
27 |
28 | } else if(name == 'Records') {
29 | // Records(category)
30 | var dependencyString = eval(escodegen.generate(node.arguments[0]));
31 | dependencies.records.push(dependencyString);
32 | }
33 | }
34 | });
35 |
36 | return dependencies;
37 | }
--------------------------------------------------------------------------------
/packages/metric-stuff/computeFunctionHelpers.js:
--------------------------------------------------------------------------------
1 | ComputeFunctionHelpers = {};
2 | ComputeFunctionHelpers.gauss = Npm.require('gauss');
--------------------------------------------------------------------------------
/packages/metric-stuff/package.js:
--------------------------------------------------------------------------------
1 | Package.describe({
2 | summary: "What this does",
3 | version: "0.0.1"
4 | });
5 |
6 | Npm.depends({
7 | "esprima": "2.2.0",
8 | "escodegen": "1.6.1",
9 | "esprima-walk": "0.1.0",
10 | "gauss": "0.2.12",
11 | "react-autosuggest": "1.12.2"
12 | });
13 |
14 | Package.on_use(function (api) {
15 | api.add_files("computeFunctionAnalyser.js", ["server"]);
16 | api.add_files("computeFunctionHelpers.js", ["server"]);
17 | api.export('ComputeFunctionAnalyser');
18 | api.export('ComputeFunctionHelpers');
19 | });
--------------------------------------------------------------------------------
/server/metric.js:
--------------------------------------------------------------------------------
1 | // Client needs this so that we can detect when the DDP (Meteor minimongo) connection has been established
2 | // autopackage is still included
3 | Meteor.publish("metrics", function () { return Metrics.find(); });
4 | Meteor.publish("categories", function() { return Categories.find(); });
5 | Meteor.publish("records", function () { return Records.find(); });
6 |
7 | function updateMetric(metricData) {
8 | // name, categoryId, data
9 | var modifier = metricData.fullReplace ? metricData.data : { $set: metricData.data };
10 | Metrics.update({
11 | name: metricData.name,
12 | categoryId: metricData.categoryId
13 | }, modifier, { upsert: true }, function(err, _id){
14 | if(err) throw new Error(err.toString());
15 | metric_id = _id;
16 | });
17 | }
18 |
19 | function metricExists(name, categoryId) {
20 | return (Metrics.findOne({
21 | name: name,
22 | categoryId: categoryId
23 | }) == null);
24 | }
25 |
26 | Meteor.methods({
27 | upsertMetric: function(name, fullCategoryPathString, computeFunctionCodeString) {
28 | // TODO check if computeFunction hasn't changed, and skip all this expensive stuff
29 | var metricData;
30 |
31 | try {
32 | var dependenciesText = ComputeFunctionAnalyser.getDependencies(computeFunctionCodeString);
33 | var dependencies = {
34 | metrics: [],
35 | records: []
36 | };
37 | dependenciesText.metrics.forEach(function(metric){
38 | dependencies.metrics.push(Metrics.findMetricByPath(metric)._id);
39 | });
40 | dependenciesText.records.forEach(function(categoryPath){
41 | dependencies.records.push(Categories.findCategoryByPath(categoryPath)._id);
42 | });
43 |
44 | metricData = {
45 | name: name,
46 | computeResult: null,
47 | compute: computeFunctionCodeString,
48 | categoryId: Categories.findOrCreateByCategoryPath(fullCategoryPathString),
49 | metricDependencies: dependencies.metrics,
50 | recordDependencies: dependencies.records,
51 | recomputing: false
52 | };
53 |
54 | } catch(ex) {
55 | throw new Meteor.Error("parsing-error", "Your code is wrong", ex.toString());
56 | }
57 |
58 | recomputeMetric(metricData);
59 | },
60 |
61 | recomputeMetric: function(id) {
62 | var metric = Metrics.findOne(id);
63 | recomputeMetric(metric);
64 | },
65 |
66 | testNewMetricCode: function(newCodeString) {
67 | var dependencies = this.getDependencies(newCodeString);
68 | var computeResult = Metrics.runComputeFunction(newCodeString);
69 | return {
70 | computeResult: computeResult,
71 | metricDependencies: dependencies.metrics,
72 | recordDependencies: dependencies.records
73 | };
74 | },
75 |
76 | getDependencies: function(codeString) {
77 | return ComputeFunctionAnalyser.getDependencies(codeString);
78 | }
79 |
80 | });
81 |
82 | // Expects a metric object from the DB
83 | function recomputeMetric(metric) {
84 | try {
85 | // Show feedback on client-side that we are recomputing
86 | if(metricExists(metric.name, metric.categoryId)) {
87 | updateMetric({
88 | name: metric.name,
89 | categoryId: metric.categoryId,
90 | fullReplace: false,
91 | data: { recomputing: true }
92 | });
93 | }
94 |
95 | metric.computeResult = Metrics.runComputeFunction(metric.compute).result;
96 | metric.recomputing = false;
97 |
98 | console.log('recompute '+metric._id+' with val: '+metric.computeResult);
99 |
100 | return updateMetric({
101 | name: metric.name,
102 | categoryId: metric.categoryId,
103 | fullReplace: true,
104 | data: metric
105 | });
106 | } catch(ex) {
107 | throw new Meteor.Error("runtime-error", "Your metric failed to run", ex.stack);
108 | }
109 | }
110 |
111 |
112 |
113 | /*
114 | When to (re)compute a metric:
115 | - when metric compute definition changes
116 | - when a metric.computeResult changes that this metric depends on
117 | - when a record is added to a category that this metric depends on
118 | */
119 |
120 | Metrics.after.insert(onMetricsInsertOrUpdate);
121 | Metrics.after.update(onMetricsInsertOrUpdate);
122 | function onMetricsInsertOrUpdate(userId, doc, fieldNames, modifier, options){
123 | // sub in new compute value
124 | var announceChangesToDependentMetrics = false;
125 | if(modifier.computeResult || modifier.compute) {
126 | announceChangesToDependentMetrics = true;
127 | }
128 |
129 | if(announceChangesToDependentMetrics) {
130 | var dependentMetrics = Metrics.find({
131 | metricDependencies: { $in: [doc._id] }
132 | });
133 | dependentMetrics.forEach(function(metric){
134 | if(metric._id == doc._id) {
135 | // Basic check. No, this is not the solution to the halting problem.
136 | // Then again... https://xkcd.com/1266/
137 | console.log("Metric "+JSON.stringify(metric)+" depends on itself and will compute in an infinite loop. Stopping to avoid harm.");
138 | return;
139 | }
140 | recomputeMetric(metric);
141 | });
142 | }
143 | }
144 |
145 | Records.after.insert(onRecordsChange)
146 | Records.after.remove(onRecordsChange)
147 | Records.after.update(onRecordsChange);
148 | function onRecordsChange(userId, doc, fieldNames, modifier, options) {
149 | var dependentMetrics = Metrics.find({
150 | recordDependencies: { $in: [doc.categoryId || modifier.categoryId] }
151 | });
152 | dependentMetrics.forEach(recomputeMetric);
153 | }
154 |
155 |
156 |
157 |
158 |
159 | // This is what the compute function gets as parameters
160 | var metricApi = {};
161 | Vector = ComputeFunctionHelpers.gauss.Vector;
162 | Collection = ComputeFunctionHelpers.gauss.Collection;
163 |
164 | /*
165 | * Why separate types of Record/RecordImpl and Metrics/MetricsImpl?
166 | * Simply because, people, and by people I mean me, will forget to insert the "new" keyword
167 | * And because JavaScript just keeps chugging along at all costs, the error won't announce itself like a dinner guest, rather quietly lurk in the background and strike at sometime when the path is not set in a Metric.
168 | */
169 | metricApi.Records = function(path) {
170 | return new metricApi.RecordsImpl(path);
171 | }
172 | metricApi.Metrics = function(path) {
173 | return new metricApi.MetricsImpl(path);
174 | }
175 |
176 | metricApi.MetricsImpl = function(path) {
177 | this.query = {};
178 | this.query.path = path;
179 | }
180 | metricApi.MetricsImpl.prototype.find = function() {
181 | var metric = MetricPrettyWrapper(Metrics.findMetricByPath(this.query.path));
182 | return metric;
183 | }
184 |
185 | function MetricPrettyWrapper(metric) {
186 | if(metric.computeResult === undefined) var self = {};
187 | else var self = metric.computeResult;
188 | self.result = metric.computeValue;
189 | self.metric = metric;
190 | return self;
191 | }
192 |
193 |
194 | metricApi.RecordsImpl = function(path) {
195 | this.path = path;
196 | this.query = {
197 | categoryId: null,
198 | // timestamp
199 | };
200 | }
201 | metricApi.RecordsImpl.prototype.since = function(sinceStr, field) {
202 | var date = Date.create(sinceStr).getTime();
203 | if(field && field.length > 0) this.query["fields." + field] = { $gte : date };
204 | else this.query.timestamp = { $gte : date };
205 | return this;
206 | }
207 | metricApi.RecordsImpl.prototype.find = function() {
208 | var category = Categories.findCategoryByPath(this.path);
209 |
210 | this.query.categoryId = category._id;
211 | console.log(JSON.stringify(this.query));
212 | return MetricRecords(Records.find(this.query).fetch());
213 | }
214 |
215 |
216 | function MetricRecords(values) {
217 | var metricRecords = new Vector(values);
218 |
219 | metricRecords.select = function(fieldName) {
220 | // var type = Util.getObjectType(fields);
221 | // if(type == 'Array') {
222 | // for (var i = 0, field; field = fields[i]; i++) {
223 |
224 | // }
225 | // }
226 | return metricRecords.map(function(record){
227 | var f = record.fields[fieldName];
228 | if(f === undefined) throw new Error("Field doesn't exist: '"+fieldName+"'");
229 | return f;
230 | });
231 | };
232 | metricRecords.average = function() {
233 | var type = Util.getObjectType(metricRecords[0]);
234 | switch(type){
235 | case "object": throw new Error("Can only average numbers and booleans"); break;
236 | case 'boolean':
237 | // custom average
238 | var sum = 0;
239 | metricRecords.foreach(function(item){ if(item) sum++; });
240 | return (sum / metricRecords.length);
241 | default: return metricRecords.mean();
242 | }
243 | };
244 | return metricRecords;
245 | }
246 |
247 | metricApi.metric = function(){
248 | this.result = null;
249 | };
250 |
251 |
252 | Metrics.runComputeFunction = function(computeFunctionCodeString) {
253 | var metric = null;
254 | try {
255 | var func = Function(
256 | 'Metrics',
257 | 'Records',
258 | 'metric',
259 | computeFunctionCodeString);
260 | metric = new func(
261 | metricApi.Metrics,
262 | metricApi.Records,
263 | metric);
264 | } catch(ex) {
265 | throw new Meteor.Error("runtime-error", "Your code failed to run when we tested it: " + ex.toString(), ex.stack);
266 | }
267 | return metric;
268 | }
--------------------------------------------------------------------------------
/tests/mocha/client/sampleClientTest.js:
--------------------------------------------------------------------------------
1 | if(typeof MochaWeb === 'undefined') return;
2 | MochaWeb.testOnly(function(){
3 | describe("Client initialization", function(){
4 | it("should have a Meteor version defined", function(){
5 | chai.assert(Meteor.release);
6 | });
7 | });
8 | });
--------------------------------------------------------------------------------
/tests/mocha/server/sampleServerTest.js:
--------------------------------------------------------------------------------
1 | if(typeof MochaWeb === 'undefined') return;
2 |
3 | // note that .to.throw is using strings for the error names, as the custom Meteor.Error class doesn't reveal the error type
4 | MochaWeb.testOnly(function(){
5 | describe("Metric", function(){
6 | this.timeout(15000);
7 |
8 | var basicMetricData = {
9 | name: 'Test metric 1',
10 | fullCategoryPathString: 'Tests/Subcategory/Place',
11 | computeFunction: "a = 1; a++; metric.value = a;"
12 | };
13 |
14 | // This one doesn't actually test
15 | // TODO fix this
16 | it("should be created successfully", function(done){
17 | var fn = function(){ Meteor.call(
18 | "upsertMetric",
19 | basicMetricData.name,
20 | basicMetricData.fullCategoryPathString,
21 | basicMetricData.computeFunction,
22 | function(error, result){
23 | try {
24 | chai.expect(error).to.not.exist;
25 |
26 | var metric = Metrics.find(metric_id);
27 | throw new Error(JSON.stringify(metric));
28 | chai.expect(metric).to.exist;
29 | var category = Categories.find(metric.categoryId);
30 | chai.expect(category).to.exist;
31 | chai.expect(category.path).to.be(basicMetricData.fullCategoryPathString.split('/'));
32 | chai.expect(metric.name).to.be(basicMetricData.name);
33 | chai.expect(metric.computeFunction).to.be(basicMetricData.computeFunction);
34 | } catch(ex) {throw ex} finally { done(); }
35 | }
36 | ); }
37 | chai.expect(fn).to.not.throw(Error);
38 | });
39 |
40 | it("should throw a syntax error", function(){
41 | var fn = function() { Meteor.call(
42 | "upsertMetric",
43 | basicMetricData.name,
44 | basicMetricData.fullCategoryPathString,
45 | "a = 1; /syntax error"
46 | ); done(); }
47 | chai.expect(fn).to.throw('[parsing-error]');
48 | });
49 |
50 | it("should throw a runtime error and not be saved to the DB", function(){
51 | var fn = function() { Meteor.call(
52 | "upsertMetric",
53 | basicMetricData.name,
54 | basicMetricData.fullCategoryPathString,
55 | "a = 1; undefinedFunction(); runtimeErrorsForMe();"
56 | ); }
57 | chai.expect(fn).to.throw('[runtime-error]');
58 | });
59 |
60 | it("should trigger a recompute of its dependencies", function(){
61 | // Meteor.call(
62 | // "upsertMetric",
63 | // basicMetricData.name,
64 | // basicMetricData.fullCategoryPathString,
65 | // "var dep = Metrics.find('/some/metric'); var dep2 = Records.find('/some/category');"
66 | // );
67 | });
68 | });
69 | });
--------------------------------------------------------------------------------