├── .gitignore ├── .sake └── Sakefile.js ├── LICENSE ├── README.md ├── Sakefile ├── package.json └── src ├── blueprints.coffee ├── events.coffee ├── index.coffee ├── mediator.coffee ├── services ├── command.coffee ├── index.coffee ├── menu.coffee ├── navigation.coffee └── page.coffee ├── templates ├── command-bar.pug ├── controls │ ├── checkbox.pug │ ├── date-range-picker.pug │ ├── numeric.pug │ └── switch.pug ├── dynamic-table.pug ├── filter-facets.pug ├── graphics │ ├── chart.pug │ └── counter.pug ├── hanzo-dynamic-table.pug ├── hanzo-static-table.pug ├── login.pug ├── main.pug ├── menu.pug ├── modal.pug ├── table-row.pug └── table.pug ├── utils.coffee ├── vendor └── baremetrics-calendar │ └── calendar.coffee └── views ├── command-bar.coffee ├── controls ├── checkbox.coffee ├── date-range-picker.coffee ├── index.coffee ├── numeric.coffee └── switch.coffee ├── dynamic-table.coffee ├── dynamic.coffee ├── filter-facets.coffee ├── graphics ├── chart.coffee ├── counter.coffee ├── d3.coffee ├── index.coffee └── model.coffee ├── hanzo-dynamic-table.coffee ├── hanzo-static-table.coffee ├── index.coffee ├── login.coffee ├── main.coffee ├── menu.coffee ├── middleware └── index.coffee ├── modal.coffee ├── table-row.coffee └── table.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | coverage/ 4 | node_modules/ 5 | npm-debug.log 6 | *.map 7 | -------------------------------------------------------------------------------- /.sake/Sakefile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | use('sake-bundle'); 4 | 5 | use('sake-linked'); 6 | 7 | use('sake-outdated'); 8 | 9 | use('sake-publish'); 10 | 11 | use('sake-test'); 12 | 13 | use('sake-version'); 14 | 15 | task('clean', 'clean project', function () { 16 | return exec('rm -rf lib'); 17 | }); 18 | 19 | task('build', 'build project', function () { 20 | return bundle.write({ 21 | cache: false, 22 | entry: 'src/index.coffee', 23 | format: 'es', 24 | compilers: { 25 | coffee: { 26 | version: 1 27 | } 28 | } 29 | }); 30 | }); 31 | 32 | task('watch', 'watch project for changes and rebuild', function () { 33 | return watch('src/*', function () { 34 | return invoke('build'); 35 | }); 36 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | Copyright (c) 2016-present, Hanzo, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name Hanzo nor the names of its contributors may be used to 17 | endorse or promote products derived from this software without specific 18 | prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Daisho 2 | 3 | [![npm][npm-img]][npm-url] 4 | [![build][build-img]][build-url] 5 | [![dependencies][dependencies-img]][dependencies-url] 6 | [![downloads][downloads-img]][downloads-url] 7 | [![license][license-img]][license-url] 8 | [![chat][chat-img]][chat-url] 9 | 10 | > Modular dashboard framework. 11 | 12 | ## License 13 | [BSD][license-url] 14 | 15 | [examples]: https://github.com/hanzo-io/daisho/blob/master/test/test.coffee 16 | 17 | [build-img]: https://img.shields.io/travis/hanzo-io/daisho.svg 18 | [build-url]: https://travis-ci.org/hanzo-io/daisho 19 | [chat-img]: https://badges.gitter.im/join-chat.svg 20 | [chat-url]: https://gitter.im/hanzo-io/chat 21 | [coverage-img]: https://coveralls.io/repos/hanzo-io/daisho/badge.svg?branch=master&service=github 22 | [coverage-url]: https://coveralls.io/github/hanzo-io/daisho?branch=master 23 | [dependencies-img]: https://david-dm.org/hanzo-io/daisho.svg 24 | [dependencies-url]: https://david-dm.org/hanzo-io/daisho 25 | [downloads-img]: https://img.shields.io/npm/dm/daisho.svg 26 | [downloads-url]: http://badge.fury.io/js/daisho 27 | [license-img]: https://img.shields.io/npm/l/daisho.svg 28 | [license-url]: https://github.com/hanzo-io/daisho/blob/master/LICENSE 29 | [npm-img]: https://img.shields.io/npm/v/daisho.svg 30 | [npm-url]: https://www.npmjs.com/package/daisho 31 | -------------------------------------------------------------------------------- /Sakefile: -------------------------------------------------------------------------------- 1 | use 'sake-bundle' 2 | use 'sake-linked' 3 | use 'sake-outdated' 4 | use 'sake-publish' 5 | use 'sake-test' 6 | use 'sake-version' 7 | 8 | task 'clean', 'clean project', -> 9 | exec 'rm -rf lib' 10 | 11 | task 'build', 'build project', -> 12 | bundle.write 13 | cache: false 14 | entry: 'src/index.coffee' 15 | format: 'es' 16 | compilers: 17 | coffee: 18 | version: 1 19 | 20 | task 'watch', 'watch project for changes and rebuild', -> 21 | watch 'src/*', -> invoke 'build' 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daisho", 3 | "version": "1.2.72", 4 | "description": "Modular dashboard framework", 5 | "main": "lib/daisho.js", 6 | "module": "lib/daisho.mjs", 7 | "jsnext:main": "lib/daisho.mjs", 8 | "files": [ 9 | "lib/", 10 | "src/" 11 | ], 12 | "scripts": { 13 | "build": "sake build", 14 | "prepublishOnly": "sake build", 15 | "pretest": "sake build", 16 | "test": "sake test" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/hanzo-io/daisho.git" 21 | }, 22 | "keywords": [ 23 | "dash", 24 | "dashboard", 25 | "ecommerce", 26 | "hanzo", 27 | "modular" 28 | ], 29 | "author": "Hanzo, Inc ", 30 | "license": "BSD-3-Clause", 31 | "bugs": { 32 | "url": "https://github.com/hanzo-io/daisho/issues" 33 | }, 34 | "homepage": "https://github.com/hanzo-io/daisho#readme", 35 | "devDependencies": { 36 | "chai": "^4.2.0", 37 | "chai-as-promised": "^7.1.1", 38 | "co-mocha": "^1.2.2", 39 | "codecov.io": "^0.1.6", 40 | "coffee-coverage": "^3.0.0", 41 | "coffeescript": "^2", 42 | "coveralls": "^3.0.2", 43 | "es5-shim": "^4.5.12", 44 | "istanbul": "^0.4.5", 45 | "mocha": "^5.2.0", 46 | "mz": "^2.7.0", 47 | "postmortem": "^0.2.6", 48 | "sake-bundle": "^0.6.14", 49 | "sake-cli": "^0.7.2", 50 | "sake-linked": "^0.1.2", 51 | "sake-outdated": "^0.4.4", 52 | "sake-publish": "^0.1.17", 53 | "sake-test": "^0.2.1", 54 | "sake-version": "^0.1.19" 55 | }, 56 | "dependencies": { 57 | "akasha": "^0.1.10", 58 | "broken": "^0.1.13", 59 | "d3-array": "1.2.4", 60 | "d3-axis": "1.0.12", 61 | "d3-interpolate": "1.3.2", 62 | "d3-selection": "1.3.2", 63 | "d3-shape": "1.2.2", 64 | "d3-svg-legend": "2.25.6", 65 | "d3-tip": "0.9.1", 66 | "d3-transition": "1.1.3", 67 | "el-controls": "1.2.44", 68 | "el.js": "^2.1.37", 69 | "es-is": "^3.3.10", 70 | "es-raf": "^3.3.1", 71 | "es-selectize": "^0.12.10", 72 | "es-tween": "^0.1.0", 73 | "hanzo.js": "^2.5.4", 74 | "moment-timezone": "^0.5.23", 75 | "randomcolor": "^0.5.3", 76 | "referential": "0.5.23", 77 | "riot-observable": "^3.0.0", 78 | "shop.js-util": "^0.0.7" 79 | } 80 | } -------------------------------------------------------------------------------- /src/blueprints.coffee: -------------------------------------------------------------------------------- 1 | import isFunction from 'es-is/function' 2 | 3 | sp = (u) -> 4 | (x) -> 5 | if isFunction u 6 | url = u x 7 | else 8 | url = u 9 | 10 | if @storeId? 11 | "/store/#{@storeId}" + url 12 | else 13 | url 14 | 15 | byId = (name) -> 16 | switch name 17 | when 'coupon' 18 | sp (x) -> "/coupon/#{x.code ? x}" 19 | # when 'collection' 20 | # sp (x) -> "/collection/#{x.slug ? x}" 21 | when 'product' 22 | sp (x) -> "/product/#{x.id ? x.slug ? x}" 23 | # when 'variant' 24 | # sp (x) -> "/variant/#{x.id ? x.sku ? x}" 25 | when 'user' 26 | sp (x) -> "/user/#{x.id ? x.email ? x}" 27 | # when 'site' 28 | # (x) -> "/site/#{x.id ? x.name ? x}" 29 | else 30 | (x) -> "/#{name}/#{x.id ? x}" 31 | 32 | statusOk = (res) -> res.status is 200 33 | statusCreated = (res) -> res.status is 201 34 | statusNoContent = (res) -> res.status is 204 35 | 36 | # Complete RESTful API available with secret key, so all methods are 37 | # exposed in server environment. 38 | createBlueprint = (name) -> 39 | endpoint = "/#{name}" 40 | 41 | url = byId name 42 | 43 | list: 44 | url: endpoint 45 | method: 'GET' 46 | get: 47 | url: url 48 | method: 'GET' 49 | expects: statusOk 50 | create: 51 | url: endpoint 52 | method: 'POST' 53 | expects: statusCreated 54 | update: 55 | url: url 56 | method: 'PATCH' 57 | expects: statusOk 58 | delete: 59 | url: url 60 | method: 'DELETE' 61 | expects: statusNoContent 62 | 63 | blueprints = 64 | oauth: 65 | auth: 66 | # expects: statusOk 67 | method: 'POST' 68 | url: '/auth' 69 | 70 | account: 71 | organization: 72 | method: 'GET' 73 | url: '/_/account/organizations' 74 | 75 | dashv2: 76 | login: 77 | method: 'POST' 78 | url: '/dashv2/login' 79 | 80 | counter: 81 | search: 82 | method: 'POST' 83 | url: '/counter' 84 | 85 | library: 86 | daisho: 87 | method: 'GET' 88 | url: '/library/daisho' 89 | 90 | models = [ 91 | 'order' 92 | 'note' 93 | 'product' 94 | 'subscriber' 95 | 'user' 96 | 'wallet' 97 | 'tokentransaction' 98 | ] 99 | 100 | for model in models 101 | do (model) -> 102 | blueprints[model] = createBlueprint model 103 | 104 | blueprints.note.search = 105 | method: 'POST' 106 | url: '/search/note' 107 | 108 | blueprints.user.orders = 109 | method: 'GET' 110 | url: (x) -> "/user/#{x.id ? x}/orders" 111 | expects: statusOk 112 | 113 | blueprints.user.transactions = 114 | method: 'GET' 115 | url: (x) -> "/user/#{x.id ? x}/transactions" 116 | expects: statusOk 117 | 118 | blueprints.user.tokentransactions = 119 | method: 'GET' 120 | url: (x) -> "/user/#{x.id ? x}/tokentransactions" 121 | expects: statusOk 122 | 123 | blueprints.user.referrals = 124 | method: 'GET' 125 | url: (x) -> "/user/#{x.id ? x}/referrals" 126 | expects: statusOk 127 | 128 | blueprints.user.referrers = 129 | method: 'GET' 130 | url: (x) -> "/user/#{x.id ? x}/referrers" 131 | expects: statusOk 132 | 133 | blueprints.user.resetPassword = 134 | method: 'GET' 135 | url: (x) -> "/user/#{x.id ? x}/password/reset" 136 | expects: statusOk 137 | 138 | blueprints.user.wallet = 139 | method: 'GET' 140 | url: (x) -> "/user/#{x.id ? x}/wallet" 141 | expects: statusOk 142 | 143 | blueprints.transaction = 144 | create: 145 | url: '/transaction' 146 | method: 'POST' 147 | expects: statusCreated 148 | 149 | blueprints.library.daisho = 150 | url: '/library/daisho' 151 | method: 'POST' 152 | expects: statusOk 153 | 154 | blueprints.order.sendOrderConfirmation = 155 | method: 'GET' 156 | url: (x) -> "/order/#{x.id ? x}/sendorderconfirmation" 157 | expects: statusNoContent 158 | 159 | blueprints.order.sendRefundConfirmation = 160 | method: 'GET' 161 | url: (x) -> "/order/#{x.id ? x}/sendrefundconfirmation" 162 | expects: statusNoContent 163 | 164 | blueprints.order.sendFulfillmentConfirmation = 165 | method: 'GET' 166 | url: (x) -> "/order/#{x.id ? x}/sendfulfillmentconfirmation" 167 | expects: statusNoContent 168 | 169 | blueprints.order.payments = 170 | method: 'GET' 171 | url: (x) -> "/order/#{x.id ? x}/payments" 172 | expects: statusOk 173 | 174 | blueprints.organization = 175 | get: 176 | url: (x) -> "/c/organization/#{x.id ? x}" 177 | method: 'GET' 178 | expects: statusOk 179 | update: 180 | url: (x) -> "/c/organization/#{x.id ? x}" 181 | method: 'PATCH' 182 | expects: statusOk 183 | 184 | export default blueprints 185 | -------------------------------------------------------------------------------- /src/events.coffee: -------------------------------------------------------------------------------- 1 | Events = 2 | Change: 3 | 'change' 4 | ChangeSuccess: 5 | 'change.success' 6 | ChangeFailed: 7 | 'change.failed' 8 | 9 | Login: 10 | 'daisho.login' 11 | LoginSuccess: 12 | 'daisho.login.success' 13 | LoginFailed: 14 | 'daisho.login.failed' 15 | 16 | Logout: 17 | 'daisho.logout' 18 | 19 | Refresh: 20 | 'daisho.refresh' 21 | ForceRefresh: 22 | 'daisho.refresh.force' 23 | 24 | export default Events 25 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | import El from 'el.js' 2 | import Hanzo from 'hanzo.js' 3 | import Tween from 'es-tween' 4 | import {raf} from 'es-raf' 5 | import akasha from 'akasha' 6 | 7 | import Events from './events' 8 | import Services from './services' 9 | import Views from './views' 10 | import blueprints from './blueprints' 11 | import mediator from './mediator' 12 | import utils from './utils' 13 | import currencies from 'shop.js-util/src/data/currencies' 14 | 15 | animate = (time) -> 16 | raf animate 17 | Tween.update time 18 | 19 | raf animate 20 | 21 | reservedTags = {} 22 | 23 | # Monkey patch el.js so all registration can be validated 24 | El.Views.Form.register = El.Views.View.register = -> 25 | if reservedTags[@tag] 26 | throw new Error "#{@tag} is reserved:", reservedTags[@tag] 27 | r = new @ 28 | @tag = r.tag 29 | reservedTags[@tag] = @ 30 | return r 31 | 32 | Views.register() 33 | 34 | export default class Daisho 35 | @El: El 36 | @Views: Views 37 | @Graphics: Views.Graphics 38 | @Services: Services 39 | @Events: Events 40 | @mediator: mediator 41 | @utils: utils 42 | @currencies: currencies 43 | 44 | client: null 45 | data: null 46 | settings: null 47 | modules: null 48 | debug: false 49 | services: null 50 | utils: Daisho.utils 51 | currencies: currencies 52 | countries: [] 53 | 54 | constructor: (url, modules, @data, @settings, debug = false) -> 55 | @client = new Hanzo.Api 56 | debug: debug 57 | endpoint: url 58 | 59 | @debug = debug 60 | 61 | @services = 62 | menu: new Services.Menu @, debug 63 | page: new Services.Page @, debug 64 | command: new Services.Command @, debug 65 | navigation: new Services.Navigation @, debug 66 | 67 | @services.page.mount = => 68 | @mount.apply @, arguments 69 | @services.page.update = => 70 | @update.apply @, arguments 71 | 72 | @client.addBlueprints k,v for k,v of blueprints 73 | @modules = modules 74 | 75 | lastChecked = akasha.get 'lastChecked' 76 | countries = akasha.get 'countries' 77 | @countries.push.apply @countries, countries 78 | Daisho.countries = @countries 79 | 80 | lastChecked = utils.date.renderDate(new Date(), utils.date.rfc3339) 81 | 82 | @client.library.daisho( 83 | hasCountries: !!countries && countries.length != 0 84 | lastChecked: utils.date.renderDate(lastChecked || '2000-01-01', utils.date.rfc3339) 85 | ).then (res) => 86 | if res.countries? 87 | @countries.length = 0 88 | @countries.push.apply @countries, res.countries 89 | 90 | akasha.set 'countries', @countries 91 | akasha.set 'lastChecked', lastChecked 92 | 93 | @data.set 'countries', @countries 94 | @scheduleUpdate() 95 | .catch (err) => 96 | console.log 'Could not load countries data.', err 97 | 98 | start: -> 99 | modules = @modules 100 | 101 | for k, module of modules 102 | if typeof module == 'string' 103 | # do something 104 | else 105 | new module @, @services.page, @services.menu, @services.command, @services.navigation 106 | 107 | @services.page.start() 108 | 109 | mount: (tag, opts = {}) -> 110 | isHTML = tag instanceof HTMLElement 111 | if isHTML 112 | tagName = tag.tagName.toLowerCase() 113 | else 114 | tagName = tag 115 | 116 | if !opts.client 117 | opts.client = @client 118 | 119 | if !opts.data 120 | if !@data.get tagName 121 | @data.set tagName, {} 122 | opts.data = @data.ref tagName 123 | 124 | if !opts.parentData 125 | opts.parentData = @data 126 | 127 | if !opts.services 128 | opts.settings = @settings 129 | 130 | if !opts.services 131 | opts.services = @services 132 | 133 | if !opts.util 134 | opts.utils = @utils 135 | 136 | if !opts.currencies 137 | opts.currencies = @currencies 138 | 139 | if !opts.countries 140 | opts.countries = @countries 141 | 142 | if !opts.mediator 143 | opts.mediator = Daisho.mediator 144 | 145 | if !opts.daisho 146 | opts.daisho = @ 147 | 148 | if typeof tag == 'string' 149 | El.mount tag, opts 150 | else if isHTML 151 | El.mount tag, tagName, opts 152 | 153 | scheduleUpdate: -> 154 | El.scheduleUpdate.apply El, arguments 155 | -------------------------------------------------------------------------------- /src/mediator.coffee: -------------------------------------------------------------------------------- 1 | import observable from 'riot-observable' 2 | 3 | export default observable {} 4 | -------------------------------------------------------------------------------- /src/services/command.coffee: -------------------------------------------------------------------------------- 1 | class CommandService 2 | commands: null 3 | daisho: null 4 | 5 | constructor: (@daisho, @debug) -> 6 | @commands = {} 7 | 8 | register: (command, hint, fn) -> 9 | if @commands[command] 10 | console.log '---CMD SERVICE---\nCollision for ' + name 11 | 12 | @commands[command] = 13 | command: command 14 | hint: hint 15 | fn: fn 16 | 17 | find: (command) -> 18 | found = [] 19 | for cmd, opts of @commands 20 | if opts.command.indexOf(command)== 0 21 | found.push opts 22 | 23 | found.sort (a,b) -> 24 | nameA = a.command.toLowerCase() 25 | nameB = b.command.toLowerCase() 26 | if (nameA < nameB) 27 | return -1 28 | if (nameA > nameB) 29 | return 1 30 | return 0 31 | 32 | found 33 | 34 | execute: (command, args) -> 35 | for i, arg of args 36 | # strip quotes 37 | if arg[0] == '"' 38 | args[i] = args[i].substr 1 39 | if arg.substr(-1) == '"' 40 | args[i] = args[i].slice 0, -1 41 | cmd = @commands[command] 42 | if !cmd 43 | console.log '---COMMAND SERVICE---\n' + command + ' not registered' 44 | cmd.fn.apply @, args 45 | 46 | export default CommandService 47 | -------------------------------------------------------------------------------- /src/services/index.coffee: -------------------------------------------------------------------------------- 1 | import Menu from './menu' 2 | import Page from './page' 3 | import Command from './command' 4 | import Navigation from './navigation' 5 | 6 | export default Services = 7 | Menu: Menu 8 | Page: Page 9 | Command: Command 10 | Navigation: Navigation 11 | -------------------------------------------------------------------------------- /src/services/menu.coffee: -------------------------------------------------------------------------------- 1 | import isFunction from 'es-is/function' 2 | 3 | 4 | class MenuService 5 | menu: null 6 | menuHash: null 7 | initFn: null 8 | daisho: null 9 | debug: false 10 | 11 | constructor: (@daisho, @debug) -> 12 | @menu = [] 13 | @menuHash = {} 14 | 15 | register: (name, opts) -> 16 | if @menuHash[name] 17 | console.log '---MENU SERVICE---\nCollision for ' + name 18 | 19 | action = @run name 20 | @menuHash[name] = 21 | name: name 22 | action: action 23 | icon: opts.icon 24 | fn: if isFunction opts then opts else opts.action 25 | @menu.push @menuHash[name] 26 | 27 | run: (name) -> 28 | return => 29 | data = @menuHash[name] 30 | if !data.action && @debug 31 | console.log '---MENU SERVICE---\n' + name + ' not registered' 32 | data.fn() 33 | 34 | export default MenuService 35 | -------------------------------------------------------------------------------- /src/services/navigation.coffee: -------------------------------------------------------------------------------- 1 | # encode key value pairs 2 | import utils from '../utils' 3 | 4 | class NavService 5 | constructor: (@daisho, @debug)-> 6 | 7 | replaceState: (id, opts)-> 8 | url = utils.nav.encode id, opts 9 | 10 | history.replaceState JSON.stringify( 11 | id: id 12 | opts: opts 13 | ), id, url 14 | 15 | pushState: (id, opts)-> 16 | url = utils.nav.encode id, opts 17 | 18 | history.pushState JSON.stringify( 19 | id: id 20 | opts: opts 21 | ), id, url 22 | 23 | getState: ()-> 24 | json = null 25 | try 26 | json = JSON.parse history?.state 27 | catch e 28 | console.log '---NAV SERVICE---\ncould not parse history state' 29 | 30 | return json 31 | 32 | onPopState: (cb)-> 33 | json = null 34 | window.onpopstate = (event)-> 35 | try 36 | json = JSON.parse event.state 37 | catch e 38 | console.log '---NAV SERVICE---\ncould not parse history state' 39 | 40 | cb json 41 | 42 | export default NavService 43 | 44 | -------------------------------------------------------------------------------- /src/services/page.coffee: -------------------------------------------------------------------------------- 1 | import utils from '../utils' 2 | 3 | # PageService manages the page life cycle (doesn't do rendering) 4 | class PageService 5 | cache: null 6 | daisho: null 7 | debug: false 8 | current: null 9 | initState: null 10 | pushState: true 11 | # replaceState overwrites pushstate 12 | replaceState: false 13 | 14 | # Create the service 15 | constructor: (@daisho, @debug)-> 16 | # Initialize the page cache 17 | @cache = {} 18 | 19 | mount: -> 20 | update: -> 21 | 22 | has: (id) -> 23 | return @cache[id]? 24 | 25 | # Register a page and its life cycle callbacks, then store the page in the 26 | # cache. 27 | register: (id, enterFn, startFn, stopFn) -> 28 | if @cache[id] 29 | console.log '---PAGE SERVICE---\nCollision for ' + id 30 | 31 | if enterFn && startFn && stopFn 32 | opts = 33 | enterFn: enterFn 34 | startFn: startFn 35 | stopFn: stopFn 36 | else if enterFn 37 | opts = enterFn 38 | 39 | @cache[id] = 40 | id: id 41 | enter: opts.enterFn 42 | start: opts.startFn 43 | stop: opts.stopFn 44 | root: null 45 | opts: null 46 | state: {} 47 | 48 | # set the initial state 49 | if !@initState? 50 | @initState = @daisho.services.navigation.getState() 51 | if !@initState? 52 | [id2, opts] = utils.nav.decode window.location.pathname.substr 1 53 | 54 | id = id2 if id2 55 | 56 | @initState = 57 | id: id 58 | opts: opts 59 | @replaceState = true 60 | else 61 | @pushState = false 62 | 63 | # Show a registered page from the cache 64 | show: (id, opts = {}) -> 65 | page = @cache[id] 66 | page.opts = opts if opts? 67 | opts = page.opts 68 | 69 | if !page? 70 | console.log '---PAGE SERVICE---\n' + id + ' not registered' 71 | 72 | if @current? 73 | @current.root = @current.stop.call page.state, @ 74 | 75 | if !page.root 76 | page.root = page.enter.call page.state, @, opts 77 | page.root = page.start.call page.state, @, opts 78 | if @debug 79 | console.log '---PAGE SERVICE---\nDone serving page ' + id 80 | else 81 | page.root = page.start.call page.state, @, opts 82 | if @debug 83 | console.log '---PAGE SERVICE---\nDone serving page ' + id 84 | 85 | # replace state 86 | if @replaceState 87 | # overwrite push state 88 | @pushState = true 89 | @replaceState = false 90 | 91 | @daisho.services.navigation.replaceState(id, opts) 92 | # don't push state 93 | else if !@pushState 94 | @pushState = true 95 | @replaceState = false 96 | # push state 97 | else 98 | @daisho.services.navigation.pushState(id, opts) 99 | @initState = null 100 | 101 | @daisho.scheduleUpdate() 102 | 103 | @current = page 104 | @current 105 | 106 | start: ()-> 107 | @daisho.services.navigation.onPopState (state)=> 108 | @pushState = false 109 | @initState = state 110 | @show state.id, state.opts 111 | 112 | @show @initState.id, @initState.opts 113 | 114 | export default PageService 115 | -------------------------------------------------------------------------------- /src/templates/command-bar.pug: -------------------------------------------------------------------------------- 1 | input(id='{ input.name }', 2 | name='{ name || input.name }', 3 | type='{ type }', 4 | onchange='{ change }', 5 | onblur='{ change }', 6 | riot-value='{ input.ref.get(input.name) }', 7 | placeholder='{ placeholder }', 8 | autocomplete='{ autoComplete }', 9 | class='{invalid: errorMessage, valid: valid}') 10 | .commands-found 11 | .command-found(each='{ found() }', onclick='{ parent.pick(command) }') 12 | .command-text 13 | | /{ command } 14 | .command-hint(if='{ hint }') 15 | | { hint } 16 | #{ 'yield' } 17 | -------------------------------------------------------------------------------- /src/templates/controls/checkbox.pug: -------------------------------------------------------------------------------- 1 | input(id='{ input.name }',name='{ name || input.name }',type='checkbox',onchange='{ change }',onblur='{ change }',checked='{ input.ref.get(input.name) }',class='{invalid: errorMessage, valid: valid}') 2 | label.label(for='{ name || input.name }') { label } 3 | #{ 'yield' } 4 | 5 | -------------------------------------------------------------------------------- /src/templates/controls/date-range-picker.pug: -------------------------------------------------------------------------------- 1 | .daterange.daterange--double 2 | -------------------------------------------------------------------------------- /src/templates/controls/numeric.pug: -------------------------------------------------------------------------------- 1 | #{ 'yield' }(from='input') 2 | input( 3 | id='{ getId() }' 4 | name='{ getName() }' 5 | type='{ type }' 6 | onchange='{ change }' 7 | onblur='{ change }' 8 | riot-value='{ input.ref.get(input.name) }' 9 | class='{invalid: errorMessage, valid: valid, labeled: label}' 10 | autocomplete='{ autocomplete }' 11 | autofocus='{ autofocus }' 12 | disabled='{ disabled }' 13 | maxlength='{ maxlength }' 14 | readonly='{ readonly }' 15 | placeholder='{ placeholder }' 16 | ) 17 | #{ 'yield' }(from='label') 18 | .label( 19 | class='{ active: numericValue() || numericValue() == 0 || placeholder }' 20 | if='{ label }' 21 | ) 22 | | { label } 23 | #{ 'yield' }(from='error') 24 | .error(if='{ errorMessage }') 25 | | { errorMessage } 26 | #{ 'yield' }(from='instructions') 27 | .helper(if='{ instructions && !errorMessage }') 28 | | { instructions } 29 | #{ 'yield' } 30 | -------------------------------------------------------------------------------- /src/templates/controls/switch.pug: -------------------------------------------------------------------------------- 1 | input(id='{ input.name }',name='{ name || input.name }',type='checkbox',onchange='{ change }',onblur='{ change }',checked='{ input.ref.get(input.name) }',class='{invalid: errorMessage, valid: valid}') 2 | label.switch(for='{ name || input.name }') 3 | .switch-slider 4 | label.label(for='{ name || input.name }') { label } 5 | #{ 'yield' } 6 | -------------------------------------------------------------------------------- /src/templates/dynamic-table.pug: -------------------------------------------------------------------------------- 1 | #{ 'yield' }(from='table') 2 | daisho-table.table( 3 | table-columns='{ tableColumns }' 4 | table-field='{ tableField }' 5 | ) 6 | #{ 'yield' } 7 | #{ 'yield' }(from='table-load-more') 8 | div.m-card.table-load-more 9 | .button( 10 | class='{ loading: loading }' 11 | onclick='{ loadMore }' 12 | if='{ hasMore }' 13 | disabled='{ loading }' 14 | ) 15 | | { loading ? 'Loading...' : hasMore() ? ('Next ' + moreCount() + ' Results') : 'No More Results' } 16 | -------------------------------------------------------------------------------- /src/templates/filter-facets.pug: -------------------------------------------------------------------------------- 1 | .filter-query 2 | text.input(bind='query', label='Search') 3 | button.reset(onclick='{ reset }') Reset 4 | button.submit(onclick='{ search }') Search 5 | 6 | .filter-between 7 | strong Created Between 8 | .controls 9 | date-range-picker-control(bind='filter', after="{ parentData.get('orgs')[parentData.get('activeOrg')].createdAt }") 10 | 11 | .filter-facet( 12 | each='{ r, k in data.get("options.string") }' 13 | if='{ !hasResult(k) }') 14 | .string-facet-label { parent.facetNames[k] || createName(k) } 15 | .string-facet 16 | checkbox.checkbox.string-facet-option( 17 | each='{ o, k2 in r }' 18 | bind='{ "options.string." + k + "." + k2 }' 19 | label='{ k2 == k ? "yes" : k2 }') 20 | 21 | .filter-facet( 22 | each='{ r, k in data.get("options.range") }' 23 | if='{ !hasResult(k) }') 24 | .range-facet-label { parent.facetNames[k] || createName(k) } 25 | .range-facet(if='{ parent.facetCurrency[k] }') 26 | currency.input(bind='{ "options.range." + k + ".from" }', label='From') 27 | currency.input(bind='{ "options.range." + k + ".to" }', label='To') 28 | .range-facet(if='{ !parent.facetCurrency[k] }') 29 | numeric.input(bind='{ "options.range." + k + ".from" }', label='From') 30 | numeric.input(bind='{ "options.range." + k + ".to" }', label='To') 31 | 32 | .filter-facet(each='{ r, k in data.get("results") }', if='{ !loading }') 33 | .string-facet-label(if='{ parent.isStringFacet(r) }') { parent.facetNames[r[0].Name] || createName(r[0].Name) } 34 | .string-facet(if='{ parent.isStringFacet(r) }') 35 | checkbox.checkbox.string-facet-option( 36 | each='{ o, k in r }' 37 | bind='{ "options.string." + o.Name + "." + o.Value }' 38 | label='{ o.Value == o.Name ? "yes" : o.Value }') 39 | .range-facet-label(if='{ parent.isRangeFacet(r) }') { parent.facetNames[r[0].Name] || createName(r[0].Name) } 40 | .range-facet(if='{ parent.isRangeFacet(r) && parent.facetCurrency[r[0].Name] }') 41 | currency.input(bind='{ "options.range." + r[0].Name + ".from" }', label='From') 42 | currency.input(bind='{ "options.range." + r[0].Name + ".to" }', label='To') 43 | .range-facet(if='{ parent.isRangeFacet(r) && !parent.facetCurrency[r[0].Name] }') 44 | numeric.input(bind='{ "options.range." + r[0].Name + ".from" }', label='From') 45 | numeric.input(bind='{ "options.range." + r[0].Name + ".to" }', label='To') 46 | -------------------------------------------------------------------------------- /src/templates/graphics/chart.pug: -------------------------------------------------------------------------------- 1 | svg 2 | -------------------------------------------------------------------------------- /src/templates/graphics/counter.pug: -------------------------------------------------------------------------------- 1 | .counter-display 2 | .counter-numbers 3 | .counter-number 4 | | { getNumber(0) } 5 | .counter-divider(if="{ data.get('1') }") 6 | | / 7 | .counter-number(if="{ data.get('1') }") 8 | | { getNumber(1) } 9 | .counter-names 10 | .counter-name 11 | | { data.get('0.fmt.x')(data.get('0.xs.0')) || ' ' } 12 | .counter-name(if="{ data.get('1') }") 13 | | { data.get('1.fmt.x')(data.get('1.xs.0')) || ' ' } 14 | .counter-label(if="{ !data.get('1') }") 15 | | { data.get('0.series') || ' ' } 16 | .counter-label(if="{ data.get('1') }") 17 | | { data.get('0.series') || ' ' } 18 | .counter-divider(if="{ data.get('1') }") 19 | | vs 20 | .counter-label(if="{ data.get('1') }") 21 | | { data.get('1.series') || ' ' } 22 | 23 | -------------------------------------------------------------------------------- /src/templates/hanzo-dynamic-table.pug: -------------------------------------------------------------------------------- 1 | #{ 'yield' }(from='table-header') 2 | .header.content.m-card 3 | div 4 | h2 { data.get(countField) || 'No' } { name } { data.get(countField) ? '' : 'Found' } 5 | div.filter 6 | daisho-filter-facets( 7 | data='{ data.ref("facets") }' 8 | facet-currency='{ facetCurrency }' 9 | class='{ open: openFilter }' 10 | onsearch='{ onsearch }') 11 | button.submit(onclick='{ create }') Create 12 | button( 13 | class='{ open: openFilter }' 14 | onclick='{ toggleFilterMenu }') Filter 15 | 16 | #{ 'yield' }(from='table-body') 17 | .tables(if='{ data.get(countField) }') 18 | daisho-table.m-card.table( 19 | table-columns='{ headers }' 20 | table-field='{ resultsField }') 21 | #{ 'yield' } 22 | div.m-card.table-load-more 23 | .button( 24 | class='{ loading: loading }' 25 | onclick='{ loadMore }' 26 | if='{ hasMore }' 27 | disabled='{ loading }' 28 | ) 29 | | { loading ? 'Loading...' : hasMore() ? ('Next ' + moreCount() + ' Results') : 'No More Results' } 30 | -------------------------------------------------------------------------------- /src/templates/hanzo-static-table.pug: -------------------------------------------------------------------------------- 1 | #{ 'yield' }(from='table-body') 2 | p(if='{ !data.get(countField) }') No { name } Found 3 | .tables(if='{ data.get(countField) }') 4 | daisho-table.table( 5 | table-columns='{ headers }' 6 | table-field='{ resultsField }') 7 | #{ 'yield' } 8 | 9 | -------------------------------------------------------------------------------- /src/templates/login.pug: -------------------------------------------------------------------------------- 1 | #{ 'yield' } 2 | text.input(lookup='account.email', placeholder='Email') 3 | text.input(type='password', lookup='account.password', placeholder='Password') 4 | .error.message(if='{ error }') 5 | | { error } 6 | div(onclick='{ submit }',class='{ disabled: disabled, button: true, submit: true }') 7 | | Login 8 | -------------------------------------------------------------------------------- /src/templates/main.pug: -------------------------------------------------------------------------------- 1 | #{ 'yield' } 2 | header 3 | #org-switcher 4 | select-control.input(select-options='{ orgs }', lookup='activeOrg') 5 | 6 | daisho-menu#menu 7 | 8 | #page 9 | -------------------------------------------------------------------------------- /src/templates/menu.pug: -------------------------------------------------------------------------------- 1 | ul 2 | li(each='{ items() }', onclick='{ action }') 3 | i.menu-icon(class='{ icon }') 4 | .menu-name { name } 5 | -------------------------------------------------------------------------------- /src/templates/modal.pug: -------------------------------------------------------------------------------- 1 | .modal-content 2 | #{ 'yield' } 3 | -------------------------------------------------------------------------------- /src/templates/table-row.pug: -------------------------------------------------------------------------------- 1 | #{ 'yield' } 2 | -------------------------------------------------------------------------------- /src/templates/table.pug: -------------------------------------------------------------------------------- 1 | .table-header 2 | .table-cell(each='{ h, k in tableColumns }', onclick='{ typeof h.onclick == "string" ? parent[h.onclick](h) : h.onclick(h) }') { h.name } 3 | checkbox(bind='allItems', if='{ h.checkbox }') 4 | .asc-desc(if='{ data.get("sort") == h.field }') 5 | .asc(if='{ data.get("asc") }') 6 | .desc(if='{ !data.get("asc") }') 7 | daisho-table-row.table-row(each='{ item, k in data.get(tableField) }', data='{ parent.data.ref(parent.tableField ? parent.tableField + "." + k : k) }') 8 | #{ 'yield' } 9 | -------------------------------------------------------------------------------- /src/utils.coffee: -------------------------------------------------------------------------------- 1 | import moment from 'moment-timezone' 2 | import analytics from 'shop.js-util/src/analytics' 3 | 4 | import { 5 | requiresPostalCode 6 | requiresState 7 | } from 'shop.js-util/src/country' 8 | 9 | import { 10 | luhnCheck 11 | cardFromNumber 12 | cardType 13 | restrictNumeric 14 | } from 'shop.js-util/src/card' 15 | 16 | import { 17 | isZeroDecimal 18 | isCrypto 19 | renderUpdatedUICurrency 20 | renderUICurrencyFromJSON 21 | renderJSONCurrencyFromUI 22 | } from 'shop.js-util/src/currency' 23 | 24 | import { 25 | rfc3339 26 | mmddyyyy 27 | yyyymmdd 28 | ddmmyyyy 29 | renderDate 30 | renderUIDate 31 | renderJSONDate 32 | } from 'shop.js-util/src/dates' 33 | 34 | import { 35 | getLanguage 36 | } from 'shop.js-util/src/language' 37 | 38 | import { 39 | renderCryptoQR 40 | } from 'shop.js-util/src/qrcodes' 41 | 42 | import { 43 | getQueries 44 | getReferrer 45 | getMCIds 46 | } from 'shop.js-util/src/uri' 47 | 48 | export default utils = 49 | analytics: analytics 50 | 51 | country: 52 | requiresPostalCode: requiresPostalCode 53 | requiresState: requiresState 54 | 55 | card: 56 | luhnCheck: luhnCheck 57 | cardFromNumber: cardFromNumber 58 | cardType: cardType 59 | restrictNumeric: restrictNumeric 60 | 61 | currency: 62 | isZeroDecimal: isZeroDecimal 63 | isCrypto: isCrypto 64 | 65 | renderUpdatedUICurrency: renderUpdatedUICurrency 66 | renderUICurrencyFromJSON: renderUICurrencyFromJSON 67 | renderJSONCurrencyFromUI: renderJSONCurrencyFromUI 68 | 69 | renderCurrency: renderUICurrencyFromJSON 70 | 71 | date: 72 | rfc3339: rfc3339 73 | mmddyyyy: mmddyyyy 74 | yyyymmdd: yyyymmdd 75 | ddmmyyyy: ddmmyyyy 76 | 77 | renderDate: renderDate 78 | renderUIDate: renderUIDate 79 | renderJSONDate: renderJSONDate 80 | moment: moment 81 | 82 | language: 83 | getLanguage: getLanguage 84 | 85 | qrcode: 86 | renderCryptoQR: renderCryptoQR 87 | 88 | uri: 89 | getQueries: getQueries 90 | getReferrer: getReferrer 91 | getMCIds: getMCIds 92 | 93 | # alias incase you don't remember 94 | utils.time = utils.date 95 | 96 | utils.nav = 97 | encode: (id, opts) -> 98 | str = '/' + id + '/' 99 | if !opts? 100 | return str 101 | 102 | if typeof opts == 'string' 103 | if opts != '' 104 | return str + opts + '/' 105 | 106 | return str 107 | 108 | for k, v of opts 109 | str += k + ':' + v + '/' 110 | 111 | return str 112 | 113 | decode: (str) -> 114 | opts = {} 115 | 116 | parts = str.split '/' 117 | id = parts.shift() 118 | 119 | for k, v of parts 120 | if v == '' 121 | continue 122 | 123 | vs = v.split ':' 124 | 125 | if vs.length == 1 126 | return [id, vs[0]] 127 | 128 | opts[k] = v 129 | 130 | return [id, opts] 131 | 132 | 133 | -------------------------------------------------------------------------------- /src/vendor/baremetrics-calendar/calendar.coffee: -------------------------------------------------------------------------------- 1 | # Design by Chris Meeks 2 | # Code by Tyler van der Hoeven 3 | # https://github.com/Baremetrics/calendar 4 | import moment from 'moment-timezone' 5 | 6 | Calendar = (settings) -> 7 | self = this 8 | @settings = settings 9 | @calIsOpen = false 10 | @presetIsOpen = false 11 | @sameDayRange = settings.same_day_range or false 12 | @element = settings.element or $('.daterange') 13 | @selected = null 14 | @type = if @element.hasClass('daterange--single') then 'single' else 'double' 15 | @required = if settings.required == false then false else true 16 | @format = settings.format or {} 17 | @format.input = settings.format and settings.format.input or 'MMMM D, YYYY' 18 | @format.preset = settings.format and settings.format.preset or 'll' 19 | @format.jump_month = settings.format and settings.format.jump_month or 'MMMM' 20 | @format.jump_year = settings.format and settings.format.jump_year or 'YYYY' 21 | @placeholder = settings.placeholder or @format.input 22 | @days_array = if settings.days_array and settings.days_array.length == 7 then settings.days_array else moment.weekdaysMin() 23 | @orig_start_date = null 24 | @orig_end_date = null 25 | @orig_current_date = null 26 | @earliest_date = if settings.earliest_date then moment(settings.earliest_date) else moment('1900-01-01') 27 | @latest_date = if settings.latest_date then moment(settings.latest_date) else moment('2900-12-31') 28 | @end_date = if settings.end_date then moment(settings.end_date) else if @type == 'double' then moment() else null 29 | @start_date = if settings.start_date then moment(settings.start_date) else if @type == 'double' then @end_date.clone().subtract(1, 'month') else null 30 | @current_date = if settings.current_date then moment(settings.current_date) else if @type == 'single' then moment() else null 31 | @presets = if settings.presets == false or @type == 'single' then false else true 32 | @callback = settings.callback or @calendarSetDates 33 | @calendarHTML @type 34 | $('.dr-presets', @element).click -> 35 | self.presetToggle() 36 | return 37 | $('.dr-list-item', @element).click -> 38 | start = $('.dr-item-aside', this).data('start') 39 | end = $('.dr-item-aside', this).data('end') 40 | self.start_date = self.calendarCheckDate(start) 41 | self.end_date = self.calendarCheckDate(end) 42 | self.calendarSetDates() 43 | self.presetToggle() 44 | self.calendarSaveDates() 45 | return 46 | $('.dr-date', @element).on 47 | 'click': -> 48 | self.calendarOpen this 49 | return 50 | 'keyup': (event) -> 51 | if event.keyCode == 9 and !self.calIsOpen and !self.start_date and !self.end_date 52 | self.calendarOpen this 53 | return 54 | 'keydown': (event) -> 55 | `var timeframe` 56 | switch event.keyCode 57 | when 9 58 | # Tab 59 | if $(self.selected).hasClass('dr-date-start') 60 | event.preventDefault() 61 | self.calendarCheckDates() 62 | self.calendarSetDates() 63 | $('.dr-date-end', self.element).trigger 'click' 64 | else 65 | self.calendarCheckDates() 66 | self.calendarSetDates() 67 | self.calendarSaveDates() 68 | self.calendarClose 'force' 69 | when 13 70 | # Enter 71 | event.preventDefault() 72 | self.calendarCheckDates() 73 | self.calendarSetDates() 74 | self.calendarSaveDates() 75 | self.calendarClose 'force' 76 | when 27 77 | # ESC 78 | self.calendarSetDates() 79 | self.calendarClose 'force' 80 | when 38 81 | # Up 82 | event.preventDefault() 83 | timeframe = 'day' 84 | if event.shiftKey 85 | timeframe = 'week' 86 | if event.metaKey 87 | timeframe = 'month' 88 | back = moment(self.current_date).subtract(1, timeframe) 89 | $(this).html back.format(self.format.input) 90 | self.current_date = back 91 | when 40 92 | # Down 93 | event.preventDefault() 94 | timeframe = 'day' 95 | if event.shiftKey 96 | timeframe = 'week' 97 | if event.metaKey 98 | timeframe = 'month' 99 | forward = moment(self.current_date).add(1, timeframe) 100 | $(this).html forward.format(self.format.input) 101 | self.current_date = forward 102 | return 103 | $('.dr-month-switcher i', @element).click -> 104 | m = $('.dr-month-switcher span', self.element).data('month') 105 | y = $('.dr-year-switcher span', self.element).data('year') 106 | this_moment = moment([ 107 | y 108 | m 109 | 1 110 | ]) 111 | back = this_moment.clone().subtract(1, 'month') 112 | forward = this_moment.clone().add(1, 'month').startOf('day') 113 | if $(this).hasClass('dr-left') 114 | self.calendarOpen self.selected, back 115 | else if $(this).hasClass('dr-right') 116 | self.calendarOpen self.selected, forward 117 | return 118 | $('.dr-year-switcher i', @element).click -> 119 | m = $('.dr-month-switcher span', self.element).data('month') 120 | y = $('.dr-year-switcher span', self.element).data('year') 121 | this_moment = moment([ 122 | y 123 | m 124 | 1 125 | ]) 126 | back = this_moment.clone().subtract(1, 'year') 127 | forward = this_moment.clone().add(1, 'year').startOf('day') 128 | if $(this).hasClass('dr-left') 129 | self.calendarOpen self.selected, back 130 | else if $(this).hasClass('dr-right') 131 | self.calendarOpen self.selected, forward 132 | return 133 | $('.dr-dates-dash', @element).click -> 134 | $('.dr-date-start', self.element).trigger 'click' 135 | return 136 | # Once you click into a selection.. this lets you click out 137 | @element.on 'click', -> 138 | document.addEventListener 'click', (f) -> 139 | contains = self.element.find(f.path[0]) 140 | if !contains.length 141 | if self.presetIsOpen 142 | self.presetToggle() 143 | if self.calIsOpen 144 | if $(self.selected).hasClass('dr-date-end') 145 | self.calendarSaveDates() 146 | self.calendarSetDates() 147 | self.calendarClose 'force' 148 | return 149 | return 150 | return 151 | 152 | selectOneDate = (other, cal, date) -> 153 | string = moment(date).format(cal.format.input) 154 | if other 155 | $('.dr-date', cal.element).not(cal.selected).html other.format(cal.format.input) 156 | $(cal.selected).html string 157 | cal.calendarOpen cal.selected 158 | if $(cal.selected).hasClass('dr-date-start') 159 | $('.dr-date-end', cal.element).trigger 'click' 160 | else 161 | cal.calendarSaveDates() 162 | cal.calendarClose 'force' 163 | return 164 | 165 | Calendar::presetToggle = -> 166 | if @presetIsOpen == false 167 | @orig_start_date = @start_date 168 | @orig_end_date = @end_date 169 | @orig_current_date = @current_date 170 | @presetIsOpen = true 171 | else if @presetIsOpen 172 | @presetIsOpen = false 173 | if @calIsOpen == true 174 | @calendarClose() 175 | $('.dr-preset-list', @element).slideToggle 200 176 | $('.dr-input', @element).toggleClass 'dr-active' 177 | $('.dr-presets', @element).toggleClass 'dr-active' 178 | @element.toggleClass 'dr-active' 179 | return 180 | 181 | Calendar::presetCreate = -> 182 | self = this 183 | ul_presets = $('') 184 | presets = if typeof self.settings.presets == 'object' then self.settings.presets else [ 185 | { 186 | label: 'Last 30 days' 187 | start: moment(self.latest_date).subtract(29, 'days') 188 | end: self.latest_date 189 | } 190 | { 191 | label: 'Last month' 192 | start: moment(self.latest_date).subtract(1, 'month').startOf('month') 193 | end: moment(self.latest_date).subtract(1, 'month').endOf('month') 194 | } 195 | { 196 | label: 'Last 3 months' 197 | start: moment(self.latest_date).subtract(3, 'month').startOf('month') 198 | end: moment(self.latest_date).subtract(1, 'month').endOf('month') 199 | } 200 | { 201 | label: 'Last 6 months' 202 | start: moment(self.latest_date).subtract(6, 'month').startOf('month') 203 | end: moment(self.latest_date).subtract(1, 'month').endOf('month') 204 | } 205 | { 206 | label: 'Last year' 207 | start: moment(self.latest_date).subtract(12, 'month').startOf('month') 208 | end: moment(self.latest_date).subtract(1, 'month').endOf('month') 209 | } 210 | { 211 | label: 'All time' 212 | start: self.earliest_date 213 | end: self.latest_date 214 | } 215 | ] 216 | if moment(self.latest_date).diff(moment(self.latest_date).startOf('month'), 'days') >= 6 and typeof self.settings.presets != 'object' 217 | presets.splice 1, 0, 218 | label: 'This month' 219 | start: moment(self.latest_date).startOf('month') 220 | end: self.latest_date 221 | $.each presets, (i, d) -> 222 | if moment(d.start).isBefore(self.earliest_date) 223 | d.start = self.earliest_date 224 | if moment(d.start).isAfter(self.latest_date) 225 | d.start = self.latest_date 226 | if moment(d.end).isBefore(self.earliest_date) 227 | d.end = self.earliest_date 228 | if moment(d.end).isAfter(self.latest_date) 229 | d.end = self.latest_date 230 | startISO = moment(d.start).toISOString() 231 | endISO = moment(d.end).toISOString() 232 | string = moment(d.start).format(self.format.preset) + ' – ' + moment(d.end).format(self.format.preset) 233 | if $('.dr-preset-list', self.element).length 234 | item = $('.dr-preset-list .dr-list-item:nth-of-type(' + i + 1 + ') .dr-item-aside', self.element) 235 | item.data 'start', startISO 236 | item.data 'end', endISO 237 | item.html string 238 | else 239 | ul_presets.append '
  • ' + d.label + '' + string + '' + '
  • ' 240 | return 241 | ul_presets 242 | 243 | Calendar::calendarSetDates = -> 244 | $('.dr-date-start', @element).html moment(@start_date).format(@format.input) 245 | $('.dr-date-end', @element).html moment(@end_date).format(@format.input) 246 | if !@start_date and !@end_date 247 | old_date = $('.dr-date', @element).html() 248 | new_date = moment(@current_date).format(@format.input) 249 | if old_date.length == 0 and !@required 250 | new_date = '' 251 | if old_date != new_date 252 | $('.dr-date', @element).html new_date 253 | return 254 | 255 | Calendar::calendarSaveDates = -> 256 | if @type == 'double' 257 | if !moment(@orig_end_date).isSame(@end_date) or !moment(@orig_start_date).isSame(@start_date) 258 | return @callback() 259 | else 260 | if !@required or !moment(@orig_current_date).isSame(@current_date) 261 | return @callback() 262 | return 263 | 264 | Calendar::calendarCheckDate = (d) -> 265 | # Today 266 | if d == 'today' or d == 'now' 267 | return if moment().isAfter(@latest_date) then @latest_date else if moment().isBefore(@earliest_date) then @earliest_date else moment() 268 | # Earliest 269 | if d == 'earliest' 270 | return @earliest_date 271 | # Latest 272 | if d == 'latest' 273 | return @latest_date 274 | # Convert string to a date if keyword ago or ahead exists 275 | if d and (/\bago\b/.test(d) or /\bahead\b/.test(d)) 276 | return @stringToDate(d) 277 | regex = /(?:\d)((?:st|nd|rd|th)?,?)/ 278 | d_array = if d then d.replace(regex, '').split(' ') else [] 279 | # Add current year if year is not included 280 | if d_array.length == 2 281 | d_array.push moment().format(@format.jump_year) 282 | d = d_array.join(' ') 283 | # Convert using settings format 284 | parsed_d = @parseDate(d) 285 | if !parsed_d.isValid() 286 | return moment(d) 287 | # occurs when parsing preset dates 288 | parsed_d 289 | 290 | Calendar::calendarCheckDates = -> 291 | startTxt = $('.dr-date-start', @element).html() 292 | endTxt = $('.dr-date-end', @element).html() 293 | c = @calendarCheckDate($(@selected).html()) 294 | s = undefined 295 | e = undefined 296 | # Modify strings via some specific keywords to create valid dates 297 | # Finally set all strings as dates 298 | if startTxt == 'ytd' or endTxt == 'ytd' 299 | # Year to date 300 | s = moment().startOf('year') 301 | e = moment().endOf('year') 302 | else 303 | s = @calendarCheckDate(startTxt) 304 | e = @calendarCheckDate(endTxt) 305 | if c.isBefore(@earliest_date) 306 | c = @earliest_date 307 | if s.isBefore(@earliest_date) 308 | s = @earliest_date 309 | if e.isBefore(@earliest_date) or e.isBefore(s) 310 | e = s.clone().add(6, 'day') 311 | if c.isAfter(@latest_date) 312 | c = @latest_date 313 | if e.isAfter(@latest_date) 314 | e = @latest_date 315 | if s.isAfter(@latest_date) or s.isAfter(e) 316 | s = e.clone().subtract(6, 'day') 317 | # Push and save if it's valid otherwise return to previous state 318 | if @type == 'double' 319 | # Is this a valid date? 320 | if s.isSame(e) and !@sameDayRange 321 | return @calendarSetDates() 322 | @start_date = if s.isValid() then s else @start_date 323 | @end_date = if e.isValid() then e else @end_date 324 | @current_date = if c.isValid() then c else @current_date 325 | return 326 | 327 | Calendar::stringToDate = (str) -> 328 | date_arr = str.split(' ') 329 | if date_arr[2] == 'ago' 330 | return moment(@current_date).subtract(date_arr[0], date_arr[1]) 331 | else if date_arr[2] == 'ahead' 332 | return moment(@current_date).add(date_arr[0], date_arr[1]) 333 | @current_date 334 | 335 | Calendar::calendarOpen = (selected, switcher) -> 336 | self = this 337 | other = undefined 338 | cal_width = $('.dr-dates', @element).innerWidth() - 8 339 | @selected = selected or @selected 340 | if @presetIsOpen == true 341 | @presetToggle() 342 | if @calIsOpen == true 343 | @calendarClose if switcher then 'switcher' else undefined 344 | else if $(@selected).html().length 345 | @orig_start_date = @start_date 346 | @orig_end_date = @end_date 347 | @orig_current_date = @current_date 348 | @calendarCheckDates() 349 | @calendarCreate switcher 350 | @calendarSetDates() 351 | next_month = moment(switcher or @current_date).add(1, 'month').startOf('month').startOf('day') 352 | past_month = moment(switcher or @current_date).subtract(1, 'month').endOf('month') 353 | next_year = moment(switcher or @current_date).add(1, 'year').startOf('month').startOf('day') 354 | past_year = moment(switcher or @current_date).subtract(1, 'year').endOf('month') 355 | this_moment = moment(switcher or @current_date) 356 | $('.dr-month-switcher span', @element).data('month', this_moment.month()).html this_moment.format(@format.jump_month) 357 | $('.dr-year-switcher span', @element).data('year', this_moment.year()).html this_moment.format(@format.jump_year) 358 | $('.dr-switcher i', @element).removeClass 'dr-disabled' 359 | if next_month.isAfter(@latest_date) 360 | $('.dr-month-switcher .dr-right', @element).addClass 'dr-disabled' 361 | if past_month.isBefore(@earliest_date) 362 | $('.dr-month-switcher .dr-left', @element).addClass 'dr-disabled' 363 | if next_year.isAfter(@latest_date) 364 | $('.dr-year-switcher .dr-right', @element).addClass 'dr-disabled' 365 | if past_year.isBefore(@earliest_date) 366 | $('.dr-year-switcher .dr-left', @element).addClass 'dr-disabled' 367 | $('.dr-day', @element).on 368 | mouseenter: -> 369 | `var selected` 370 | selected = $(this) 371 | start_date = moment(self.start_date) 372 | end_date = moment(self.end_date) 373 | current_date = moment(self.current_date) 374 | 375 | setMaybeRange = (type) -> 376 | other = undefined 377 | self.range(6 * 7).forEach (i) -> 378 | next = selected.next().data('date') 379 | prev = selected.prev().data('date') 380 | curr = selected.data('date') 381 | if !curr 382 | return false 383 | if !prev 384 | prev = curr 385 | if !next 386 | next = curr 387 | if type == 'start' 388 | if moment(next).isSame(self.end_date) or self.sameDayRange and moment(curr).isSame(self.end_date) 389 | return false 390 | if moment(curr).isAfter(self.end_date) 391 | other = other or moment(curr).add(6, 'day').startOf('day') 392 | if i > 5 or (if next then moment(next).isAfter(self.latest_date) else false) 393 | $(selected).addClass 'dr-end' 394 | other = moment(curr) 395 | return false 396 | selected = selected.next().addClass('dr-maybe') 397 | else if type == 'end' 398 | if moment(prev).isSame(self.start_date) or self.sameDayRange and moment(curr).isSame(self.start_date) 399 | return false 400 | if moment(curr).isBefore(self.start_date) 401 | other = other or moment(curr).subtract(6, 'day') 402 | if i > 5 or (if prev then moment(prev).isBefore(self.earliest_date) else false) 403 | $(selected).addClass 'dr-start' 404 | other = moment(curr) 405 | return false 406 | selected = selected.prev().addClass('dr-maybe') 407 | return 408 | return 409 | 410 | if $(self.selected).hasClass('dr-date-start') 411 | selected.addClass 'dr-hover dr-hover-before' 412 | $('.dr-start', self.element).css 413 | 'border': 'none' 414 | 'padding-left': '0.3125rem' 415 | setMaybeRange 'start' 416 | if $(self.selected).hasClass('dr-date-end') 417 | selected.addClass 'dr-hover dr-hover-after' 418 | $('.dr-end', self.element).css 419 | 'border': 'none' 420 | 'padding-right': '0.3125rem' 421 | setMaybeRange 'end' 422 | if !self.start_date and !self.end_date 423 | selected.addClass 'dr-maybe' 424 | $('.dr-selected', self.element).css 'background-color', 'transparent' 425 | return 426 | mouseleave: -> 427 | if $(this).hasClass('dr-hover-before dr-end') 428 | $(this).removeClass 'dr-end' 429 | if $(this).hasClass('dr-hover-after dr-start') 430 | $(this).removeClass 'dr-start' 431 | $(this).removeClass 'dr-hover dr-hover-before dr-hover-after' 432 | $('.dr-start, .dr-end', self.element).css 433 | 'border': '' 434 | 'padding': '' 435 | $('.dr-maybe:not(.dr-current)', self.element).removeClass 'dr-start dr-end' 436 | $('.dr-day', self.element).removeClass 'dr-maybe' 437 | $('.dr-selected', self.element).css 'background-color', '' 438 | return 439 | if /iPad|iPhone|iPod/.test(navigator.userAgent) and !window.MSStream 440 | $('.dr-day', @element).on touchstart: -> 441 | selectOneDate other, self, $(this).data('date') 442 | return 443 | $('div[contenteditable]', @element).removeAttr 'contenteditable' 444 | else 445 | $('.dr-day', @element).on mousedown: -> 446 | selectOneDate other, self, $(this).data('date') 447 | return 448 | $('.dr-calendar', @element).css('width', cal_width).slideDown 200 449 | $('.dr-input', @element).addClass 'dr-active' 450 | $(selected).addClass('dr-active').focus() 451 | @element.addClass 'dr-active' 452 | @calIsOpen = true 453 | return 454 | 455 | Calendar::calendarClose = (type) -> 456 | self = this 457 | if !@calIsOpen or @presetIsOpen or type == 'force' 458 | $('.dr-calendar', @element).slideUp 200, -> 459 | $('.dr-day', self.element).remove() 460 | return 461 | else 462 | $('.dr-day', @element).remove() 463 | if type == 'switcher' 464 | return false 465 | $('.dr-input, .dr-date', @element).removeClass 'dr-active' 466 | @element.removeClass 'dr-active' 467 | @calIsOpen = false 468 | return 469 | 470 | Calendar::calendarCreate = (switcher) -> 471 | self = this 472 | array = @calendarArray(@start_date, @end_date, @current_date, switcher) 473 | array.forEach (d, i) -> 474 | classString = 'dr-day' 475 | if d.fade 476 | classString += ' dr-fade' 477 | if d.start 478 | classString += ' dr-start' 479 | if d.end 480 | classString += ' dr-end' 481 | if d.current 482 | classString += ' dr-current' 483 | if d.selected 484 | classString += ' dr-selected' 485 | if d.outside 486 | classString += ' dr-outside' 487 | $('.dr-day-list', self.element).append '
  • ' + d.str + '
  • ' 488 | return 489 | return 490 | 491 | Calendar::calendarArray = (start, end, current, switcher) -> 492 | self = this 493 | current = moment(current or start or end).startOf('day') 494 | reference = switcher or current or start or end 495 | startRange = moment(reference).startOf('month').startOf('week') 496 | endRange = moment(startRange).add(6 * 7 - 1, 'days').endOf('day') 497 | daysInRange = [] 498 | d = moment(startRange) 499 | while d.isBefore(endRange) 500 | daysInRange.push 501 | str: +d.format('D') 502 | start: start and d.isSame(start, 'day') 503 | end: end and d.isSame(end, 'day') 504 | current: current and d.isSame(current, 'day') 505 | selected: start and end and d.isBetween(start, end) 506 | date: d.toISOString() 507 | outside: d.isBefore(self.earliest_date) or d.isAfter(self.latest_date) 508 | fade: !d.isSame(reference, 'month') 509 | d.add 1, 'd' 510 | daysInRange 511 | 512 | Calendar::calendarHTML = (type) -> 513 | ul_days_of_the_week = $('') 514 | days = @days_array.splice(moment.localeData().firstDayOfWeek()).concat(@days_array.splice(0, moment.localeData().firstDayOfWeek())) 515 | $.each days, (i, elem) -> 516 | ul_days_of_the_week.append '
  • ' + elem + '
  • ' 517 | return 518 | if type == 'double' 519 | return @element.append('
    ' + '
    ' + '
    ' + moment(@start_date).format(@format.input) + '
    ' + '' + '
    ' + moment(@end_date).format(@format.input) + '
    ' + '
    ' + (if @presets then '
    ' + '' + '' + '' + '
    ' else '') + '
    ' + '
    ' + '' + (if @presets then @presetCreate()[0].outerHTML else '') + '
    ') 520 | @element.append '
    ' + '
    ' + '
    ' + (if @settings.current_date then moment(@current_date).format(@format.input) else '') + '
    ' + '
    ' + '
    ' + '
    ' + '' + '
    ' 521 | 522 | Calendar::parseDate = (d) -> 523 | if moment.defaultZone != null and moment.hasOwnProperty('tz') 524 | moment.tz d, @format.input, moment.defaultZone.name 525 | else 526 | moment d, @format.input 527 | 528 | Calendar::range = (length) -> 529 | range = new Array(length) 530 | idx = 0 531 | while idx < length 532 | range[idx] = idx 533 | idx++ 534 | range 535 | 536 | export default Calendar 537 | -------------------------------------------------------------------------------- /src/views/command-bar.coffee: -------------------------------------------------------------------------------- 1 | import { Text } from 'el-controls' 2 | import Events from '../events' 3 | 4 | regex = /(".*?"|[^"\s]+)+(?=\s*|\s*$)/g 5 | 6 | import html from '../templates/command-bar' 7 | 8 | class CommandBar extends Text 9 | tag: 'daisho-command-bar' 10 | html: html 11 | lookup: 'search' 12 | 13 | init: -> 14 | super() 15 | 16 | @on 'mount', => 17 | $(@root).find('input').on 'keydown', (e) => 18 | @keydown.apply @, arguments 19 | 20 | @on 'update', -> 21 | # stuff 22 | 23 | keydown: (event) -> 24 | if event.which == 9 25 | cmd = @getValue event 26 | # commands start with '/' 27 | if cmd && cmd[0] == '/' 28 | # autoComplete 29 | found = @found() 30 | if found[0]? 31 | if cmd.indexOf(found[0].command) != 1 32 | $(@root).find('input').val '/' + found[0].command 33 | @change event 34 | return false 35 | else if event.which == 13 36 | @execute() 37 | return false 38 | 39 | @scheduleUpdate() 40 | 41 | return true 42 | 43 | pick: (command) -> 44 | => 45 | $(@root).find('input').val '/' + command 46 | 47 | 48 | found: -> 49 | target = $(@root).find('input')[0] 50 | return [] if !target 51 | 52 | cmd = @getValue(target: target) 53 | # commands start with '/' 54 | if cmd && cmd[0] == '/' 55 | args = cmd.match(regex).map (str)-> 56 | return str.trim() 57 | 58 | @services.command.find args[0].substr 1 59 | else 60 | [] 61 | 62 | execute: -> 63 | $el = $(@root).find('input') 64 | target = $el[0] 65 | return false if !target 66 | 67 | cmd = @getValue(target: target) 68 | # commands start with '/' 69 | if cmd && cmd[0] == '/' 70 | args = cmd.match(regex).map (str)-> 71 | return str.trim() 72 | 73 | $el.val '' 74 | @change(target: $el) 75 | 76 | try 77 | @services.command.execute args.shift().substr(1), args 78 | @mediator.trigger Events.ForceRefresh 79 | catch e 80 | console.log e 81 | 82 | true 83 | 84 | export default CommandBar 85 | -------------------------------------------------------------------------------- /src/views/controls/checkbox.coffee: -------------------------------------------------------------------------------- 1 | import Switch from './switch' 2 | 3 | import html from '../../templates/controls/checkbox' 4 | 5 | export default class BetterCheckbox extends Switch 6 | tag: 'checkbox' 7 | html: html 8 | getValue: (event)-> 9 | return event.target.checked 10 | 11 | BetterCheckbox.register() 12 | 13 | -------------------------------------------------------------------------------- /src/views/controls/date-range-picker.coffee: -------------------------------------------------------------------------------- 1 | import Calendar from '../../vendor/baremetrics-calendar/calendar' 2 | import Text from 'el-controls/src/controls/text' 3 | import utils from '../../utils' 4 | import html from '../../templates/controls/date-range-picker' 5 | 6 | moment = utils.date.moment 7 | 8 | export default class DateRangePicker extends Text 9 | tag: 'date-range-picker-control' 10 | html: html 11 | 12 | after: '2015-01-01' 13 | before: moment() 14 | 15 | events: 16 | updated: -> 17 | @onUpdated() 18 | 19 | mount: -> 20 | @onUpdated() 21 | 22 | init: -> super() 23 | 24 | onUpdated: -> 25 | if !@calendar 26 | filter = @data.get 'filter' 27 | self = @ 28 | @calendar = new Calendar 29 | element: $(@root).find('.daterange') 30 | earliest_date: moment @after 31 | latest_date: moment @before 32 | start_date: filter[0] 33 | end_date: filter[1] 34 | callback: -> 35 | start = utils.date.renderJSONDate @start_date 36 | end = utils.date.renderJSONDate @end_date 37 | 38 | console.log 'Start Date: ' + start + '\nEnd Date: ' + end 39 | 40 | val = [start, end] 41 | self.data.set 'filter', val 42 | 43 | self.change() 44 | self.changed val 45 | 46 | getValue: (e) -> @data.get 'filter' 47 | -------------------------------------------------------------------------------- /src/views/controls/index.coffee: -------------------------------------------------------------------------------- 1 | import Checkbox from './checkbox' 2 | import DateRangePicker from './date-range-picker' 3 | import Numeric from './numeric' 4 | import Switch from './switch' 5 | 6 | import * as ElControls from 'el-controls/src' 7 | 8 | export default Controls = 9 | Checkbox: Checkbox 10 | DateRangePicker: DateRangePicker 11 | Numeric: Numeric 12 | Switch: Switch 13 | 14 | register: -> 15 | Checkbox.register() 16 | DateRangePicker.register() 17 | Numeric.register() 18 | Switch.register() 19 | -------------------------------------------------------------------------------- /src/views/controls/numeric.coffee: -------------------------------------------------------------------------------- 1 | import Text from 'el-controls/src/controls/text' 2 | 3 | import html from '../../templates/controls/numeric' 4 | 5 | class Numeric extends Text 6 | tag: 'numeric' 7 | html: html 8 | 9 | numericValue: ()-> 10 | val = parseFloat @input.ref.get(@input.name) 11 | val = null if isNaN val 12 | 13 | return val 14 | 15 | getValue: ()-> 16 | val = super 17 | val = parseFloat val 18 | val = null if isNaN val 19 | 20 | return val 21 | 22 | export default Numeric 23 | 24 | -------------------------------------------------------------------------------- /src/views/controls/switch.coffee: -------------------------------------------------------------------------------- 1 | import Checkbox from 'el-controls/src/controls/checkbox' 2 | 3 | import html from '../../templates/controls/switch' 4 | 5 | export default class Switch extends Checkbox 6 | tag: 'switch' 7 | html: html 8 | getValue: (event)-> 9 | return event.target.checked 10 | 11 | Switch.register() 12 | -------------------------------------------------------------------------------- /src/views/dynamic-table.coffee: -------------------------------------------------------------------------------- 1 | import Dynamic from './dynamic' 2 | import html from '../templates/dynamic-table' 3 | import Events from '../events' 4 | import ref from 'referential' 5 | 6 | tables = 0 7 | 8 | export default class DynamicTable extends Dynamic 9 | tag: 'daisho-dynamic-table' 10 | html: html 11 | 12 | # table header data 13 | headers: [] 14 | 15 | # local count 16 | count: 0 17 | 18 | # amount of results to display 19 | display: 20 20 | 21 | # starting page 22 | page: 1 23 | 24 | # set to true when loading 25 | loading: false 26 | 27 | # count field name 28 | countField: 'count' 29 | 30 | # results field name 31 | resultsField: 'results' 32 | 33 | # facet results file name 34 | facetsResultsField: 'facets.results' 35 | 36 | init: -> 37 | # generate unique ids for each of the fields 38 | @countField += tables 39 | @resultsField += tables 40 | @facetsResultsField += tables 41 | 42 | tables++ 43 | 44 | super arguments... 45 | 46 | for header in @headers 47 | if !header.onclick 48 | header.onclick = 'onheader' 49 | 50 | onheader: (header) -> 51 | return (e) => @_onheader header, e 52 | 53 | # overwrite this with sort handlers and things like that 54 | # _onheader: (header, e) -> 55 | 56 | # check if there's more results 57 | hasMore: -> 58 | return @page * @display < @count 59 | 60 | moreCount: -> 61 | return Math.min @remaining(), @display 62 | 63 | remaining: -> 64 | return 0 if !@hasMore() 65 | return @count - @page * @display 66 | 67 | # trigger load more 68 | loadMore: -> 69 | if @loading || !@hasMore() 70 | return 71 | 72 | @loading = true 73 | @scheduleUpdate() 74 | 75 | if !@hasMore() 76 | return false 77 | 78 | @page++ 79 | 80 | p = @_loadMore.apply @, arguments 81 | 82 | if p?.finally 83 | p.finally => 84 | @loading = false 85 | @scheduleUpdate() 86 | else 87 | @loading = false 88 | @scheduleUpdate() 89 | 90 | # overwrite this with sort handlers and things like that 91 | # return a promise for asynchronous loading 92 | # _loadMore: -> 93 | 94 | load: -> 95 | if @loading 96 | return 97 | 98 | @loading = true 99 | @scheduleUpdate() 100 | 101 | p = @_load.apply @, arguments 102 | 103 | if p?.finally 104 | p.finally => 105 | @loading = false 106 | @scheduleUpdate() 107 | else 108 | @loading = false 109 | @scheduleUpdate() 110 | 111 | # overwrite this with custom loading 112 | # return a promise for asynchronous loading 113 | # _load: -> 114 | 115 | # call this with client results 116 | onload: (res) -> 117 | if Array.isArray res 118 | models = res 119 | res = 120 | models: models 121 | count: models.length 122 | display: models.length 123 | page: 1 124 | 125 | # TODO: investigate why Events.Change must be called manually 126 | @count = res.count 127 | @data.set @countField, res.count 128 | @data.set @resultsField, undefined 129 | @data.set @resultsField, res.models 130 | 131 | @data.set @facetsResultsField, undefined 132 | @data.set @facetsResultsField, res.facets 133 | 134 | @mediator.trigger Events.Change 135 | @scheduleUpdate() 136 | 137 | # call this with client results 138 | onloadMore: (res)-> 139 | if Array.isArray res 140 | models = res 141 | res = 142 | models: models 143 | count: models.length 144 | display: models.length 145 | page: 1 146 | 147 | # TODO: investigate why Events.Change must be called manually 148 | results = @data.get(@resultsField) ? [] 149 | 150 | @count = res.count 151 | @data.set @countField, res.count 152 | @data.set @resultsField, undefined 153 | @data.set @resultsField, results.concat res.models 154 | 155 | @data.set @facetsResultsField, undefined 156 | @data.set @facetsResultsField, res.facets 157 | 158 | @mediator.trigger Events.Change 159 | @scheduleUpdate() 160 | -------------------------------------------------------------------------------- /src/views/dynamic.coffee: -------------------------------------------------------------------------------- 1 | import El from 'el.js' 2 | import Events from '../events' 3 | 4 | # View with built-in caching for optimizing frequently updated data-driven 5 | # views. 6 | class DynamicView extends El.Form 7 | tag: 'daisho-dynamic' 8 | html: '' 9 | 10 | # data.get field for caching staleness 11 | # null means we cache on the entire data object 12 | _dataStaleField: null 13 | 14 | # last version of data cached for staleness check 15 | _dataStaleCached: '' 16 | 17 | # refresh lock 18 | _locked: false 19 | 20 | # should automatically refresh on an update call? 21 | autoRefresh: true 22 | 23 | # can be before or after 24 | refreshTiming: 'before' 25 | 26 | _p: false 27 | 28 | # add some helpers to automagically bind refresh function 29 | init: -> 30 | # make @_refresh automatically save the stale data 31 | r = @_refresh 32 | @_refresh = => 33 | if @_locked 34 | return @locked 35 | 36 | @_locked = true 37 | p = r.apply @, arguments 38 | @_locked = p 39 | if p?.then? 40 | p.then => 41 | try 42 | @_dataStaleCached = JSON.stringify @data.get @_dataStaleField 43 | catch e 44 | console.error 'could not save stale data', e 45 | @_locked = false 46 | .catch (e)-> 47 | console.error 'count not save stale data', e 48 | else 49 | try 50 | @_dataStaleCached = JSON.stringify @data.get @_dataStaleField 51 | catch e 52 | console.error 'could not save stale data' 53 | @_locked = false 54 | return p 55 | 56 | if @autoRefresh 57 | if @mediator? 58 | @mediator.on Events.Refresh, => 59 | return @refresh.apply @, arguments 60 | 61 | if @refreshTiming == 'before' 62 | @on 'update', ()=> 63 | return @refresh.apply @, arguments 64 | else 65 | @on 'updated', ()=> 66 | return @refresh.apply @, arguments 67 | 68 | # this bypasses all the staleness checks 69 | if @mediator? 70 | @mediator.on Events.ForceRefresh, => 71 | return @_refresh.apply @, arguments 72 | 73 | super() 74 | 75 | # refresh checks if something is stale 76 | refresh: -> 77 | # abort if root isn't attached to the dom 78 | if !$(@root).closest('body')[0]? 79 | return 80 | 81 | # abort if data isn't stale 82 | _dataStaleCached = JSON.stringify @data.get @_dataStaleField 83 | if _dataStaleCached == @_dataStaleCached 84 | return 85 | 86 | return @_refresh.apply @, arguments 87 | 88 | # overwrite with refresh implementation 89 | # _refresh: -> 90 | 91 | # simple page change api 92 | show: (id, opts) -> 93 | return ()=> 94 | @services.page.show id, opts 95 | 96 | 97 | export default DynamicView 98 | -------------------------------------------------------------------------------- /src/views/filter-facets.coffee: -------------------------------------------------------------------------------- 1 | import El from 'el.js' 2 | import html from '../templates/filter-facets' 3 | import Events from '../events' 4 | import utils from '../utils' 5 | 6 | moment = utils.date.moment 7 | 8 | yyyymmdd = utils.date.yyyymmdd 9 | 10 | class FilterFacets extends El.Form 11 | tag: 'daisho-filter-facets' 12 | html: html 13 | 14 | # Supply an object with the mapping of facet fields to display names 15 | # 16 | # facetNames: { 17 | # facet: 'name' 18 | # } 19 | # 20 | # facetCurrency: { 21 | # facet: true 22 | # } 23 | 24 | # Facets received from server should be under .results, 25 | # Facet configuration should be under .options 26 | # Filter query string should be set to .query 27 | # 28 | # data: { 29 | # results: 30 | # string: ... 31 | # range: ... 32 | # options: ... 33 | # query: 'string query' 34 | # } 35 | 36 | init: -> 37 | super 38 | 39 | if !@facetNames 40 | @facetNames = {} 41 | 42 | @on 'update', => 43 | ranges = @data.get 'options.range' 44 | if ranges? 45 | for k, range of ranges 46 | if range.to < range.from 47 | z = range.from 48 | @data.set 'options.range.' + k + '.from', range.to 49 | @data.set 'options.range.' + k + '.to', z 50 | @scheduleUpdate() 51 | 52 | @data.set 'filter', [moment('2015-01-01').format(yyyymmdd), moment().format(yyyymmdd)] 53 | 54 | loading: false 55 | 56 | # check if we have a facet result with the name 57 | hasResult: (name)-> 58 | for facet in @data.get 'results' 59 | if facet[0] && facet[0].Name == name 60 | if facet[0].Count? && facet[0].Count == 1 && typeof facet[0].Value != 'string' 61 | return false 62 | return true 63 | return false 64 | 65 | # Rewrite name? 66 | createName: (name)-> 67 | name = name.split(/(?=[A-Z])/).join(' ') 68 | return name.charAt(0).toUpperCase() + name.slice(1) 69 | 70 | # Get the number of records 71 | count: ()-> 72 | for facet in @data.get 'facets' 73 | if facet[0] && facet[0].Name == 'Kind' 74 | return facet[0].Count 75 | return 0 76 | 77 | # Is the facet a string checkbox? 78 | isStringFacet: (facet)-> 79 | return facet && facet[0] && typeof facet[0].Value == 'string' && facet[0].Name != 'Kind' 80 | 81 | # Is the facet a range selector? 82 | isRangeFacet: (facet)-> 83 | return !@isStringFacet(facet) && facet[0].Name != 'Kind' && facet[0].Count > 1 84 | 85 | reset: (e)-> 86 | # investigate why Events.Change must be called manually 87 | if @onreset? 88 | if @onreset(e) != false 89 | @mediator.trigger Events.Change 90 | 91 | @data.set 'options', undefined 92 | @data.set 'query', undefined 93 | @search(e) 94 | else 95 | @mediator.trigger Events.Change 96 | 97 | @data.set 'options', undefined 98 | @data.set 'query', undefined 99 | @search(e) 100 | 101 | search: (e)-> 102 | p = @onsearch(e, @data.get()) if @onsearch? 103 | if p? && p.then? 104 | @loading = true 105 | p.then => 106 | @loading = false 107 | @scheduleUpdate() 108 | if p.catch? 109 | p.catch => 110 | @loading = false 111 | @scheduleUpdate() 112 | 113 | @scheduleUpdate() 114 | return p 115 | 116 | 117 | export default FilterFacets 118 | -------------------------------------------------------------------------------- /src/views/graphics/chart.coffee: -------------------------------------------------------------------------------- 1 | import Tween from 'es-tween' 2 | import randomColor from 'randomcolor' 3 | 4 | import Dynamic from '../dynamic' 5 | import d3 from './d3' 6 | import html from '../../templates/graphics/chart' 7 | 8 | # # http://big-elephants.com/2014-06/unrolling-line-charts-d3js/ 9 | # getSmoothInterpolation = (lineFn, data) -> 10 | # (d, i, a) -> 11 | # interpolate = d3.scalelinear().domain([ 12 | # 0 13 | # 1 14 | # ]).range([ 15 | # 1 16 | # data.length + 1 17 | # ]) 18 | # (t) -> 19 | # flooredX = Math.floor(interpolate(t)) 20 | # weight = interpolate(t) - flooredX 21 | # interpolatedLine = data.slice(0, flooredX) 22 | # if flooredX > 0 and flooredX < 31 23 | # weightedLineAverage = data[flooredX].y * weight + data[flooredX - 1].y * (1 - weight) 24 | # interpolatedLine.push [interpolate(t) - 1, weightedLineAverage] 25 | # lineFn interpolatedLine 26 | 27 | # --Chart-- 28 | # A chart supports a model with many series with x/y values. 29 | class Chart extends Dynamic 30 | tag: 'daisho-graphics-chart' 31 | html: html 32 | 33 | margin: 34 | top: 40 35 | right: 40 36 | bottom: 50 37 | left: 90 38 | 39 | width: 0 40 | height: 400 41 | 42 | yMin: 10 43 | 44 | interpolationTime: 3000 45 | redrawTime: 300 46 | 47 | # SVG Bits 48 | svg: null 49 | chart: null 50 | xA: null 51 | yA: null 52 | xAxis: null 53 | yAxis: null 54 | lines: null 55 | points: null 56 | notes: null 57 | legend: null 58 | 59 | lineWidth: 3 60 | pointRadius: 6 61 | 62 | # Update? 63 | dataHash: '' 64 | 65 | colorSeed: 10 66 | colors: null 67 | 68 | # from Dynamic 69 | refreshTiming: 'after' 70 | tips: null 71 | 72 | nextColor: ()-> 73 | x = Math.sin(@_colorSeed++) * 10000 74 | return randomColor(seed: Math.floor((x - Math.floor(x)) * 1000))#.replace new RegExp('-', 'g'), '' 75 | 76 | init: -> 77 | super() 78 | 79 | @colors = [] 80 | @tips = [] 81 | 82 | @on 'mount', => 83 | @svg = svg = d3.select @root 84 | .select 'svg' 85 | 86 | @parseTime = d3.timeParse '%Y-%m-%dT%H:%M:%S%Z' 87 | 88 | @chart = chart = svg.append 'g' 89 | .attr 'transform', 'translate(' + @margin.left + ',' + @margin.top + ')' 90 | 91 | @lines = @chart.append 'g' 92 | .classed 'lines', true 93 | 94 | @points = @chart.append 'g' 95 | .classed 'points-group', true 96 | 97 | @notes = @chart.append 'g' 98 | .classed 'notes', true 99 | 100 | @xAxis = chart.append 'g' 101 | .classed 'axis', true 102 | .classed 'x-axis', true 103 | @xAxis.append 'text' 104 | 105 | @yAxis = chart.append 'g' 106 | .classed 'axis', true 107 | .classed 'y-axis', true 108 | @yAxis.append 'text' 109 | 110 | @legend = svg.append("g") 111 | .classed 'legend', true 112 | .attr 'transform', 'translate(50,30)' 113 | 114 | @xScale = d3.scaleTime() 115 | @yScale = d3.scaleLinear() 116 | 117 | _refresh: -> 118 | width = @width || $(@root).parent().width() 119 | height = @height 120 | 121 | if width <= 0 || height <= 0 122 | return 123 | 124 | @svg 125 | .attr 'width', width 126 | .attr 'height', height 127 | 128 | serieses = @data.get() 129 | return if !serieses[0] 130 | 131 | @_colorSeed = @colorSeed 132 | @colors.length = 0 133 | 134 | width -= @margin.left + @margin.right 135 | height -= @margin.top + @margin.bottom 136 | 137 | xs = [] 138 | ys = [] 139 | 140 | xScale = @xScale 141 | yScale = @yScale 142 | 143 | xScale.rangeRound [0, width] 144 | .ticks d3.timeDay.every 1 145 | yScale.rangeRound [height, 0] 146 | 147 | for i, series of serieses 148 | if series.type == 'line' || series.type == 'bar' 149 | xs = xs.concat series.xs 150 | ys = ys.concat series.ys 151 | 152 | ysBuf = ys.map serieses[0].fmt.y 153 | ysBuf.push @yMin 154 | xScale.domain d3.extent xs.map(serieses[0].fmt.x), (x)=> return @parseTime x 155 | yScale.domain d3.extent ysBuf, (y)-> return y 156 | 157 | # redraw/remove 158 | if @xA && @yA 159 | @xAxis.transition() 160 | .duration @redrawTime 161 | .call @xA.scale(xScale) 162 | @yAxis.transition() 163 | .duration @redrawTime 164 | .call @yA.scale(yScale) 165 | else 166 | @xA = d3.axisBottom(xScale).tickFormat serieses[0].axis.x.ticks 167 | @xAxis.call @xA 168 | .attr 'transform', 'translate(0,' + height + ')' 169 | .select 'text' 170 | .attr 'fill', '#000' 171 | .attr 'x', width 172 | .attr 'y', -12 173 | .attr 'dy', '0.71em' 174 | .attr 'text-anchor', 'end' 175 | .text series.axis.x.name 176 | 177 | @yA = d3.axisLeft(yScale).tickFormat serieses[0].axis.y.ticks 178 | @yAxis.call @yA 179 | .select 'text' 180 | .attr 'fill', '#000' 181 | .attr 'transform', 'rotate(-90)' 182 | .attr 'y', 6 183 | .attr 'dy', '0.71em' 184 | .attr 'text-anchor', 'end' 185 | .text series.axis.y.name 186 | 187 | @lines.selectAll '*' 188 | .attr 'opacity', 1 189 | .transition() 190 | .duration @redrawTime 191 | .attr 'opacity', 0 192 | .attr 'd', lineFn 193 | .remove() 194 | 195 | @points.selectAll '*' 196 | .attr 'opacity', 1 197 | .transition() 198 | .duration @redrawTime 199 | .attr 'opacity', 0 200 | .remove() 201 | 202 | @notes.selectAll '*' 203 | .attr 'opacity', 1 204 | .transition() 205 | .duration @redrawTime 206 | .attr 'opacity', 0 207 | .remove() 208 | 209 | notes = [] 210 | 211 | do => 212 | for tip in @tips 213 | tip.hide() 214 | 215 | @tips = [] 216 | 217 | for i, series of serieses 218 | if series.xs.length == 0 || series.ys.length == 0 219 | continue 220 | 221 | # line renderer 222 | if series.type == 'line' 223 | xys = series.xs.map (x, j)-> 224 | return [x, series.ys[j]] 225 | 226 | lineFn = d3.line() 227 | .x (d) => return xScale @parseTime(series.fmt.x(d[0] || 0)) 228 | .y (d) -> return yScale series.fmt.y(d[1] || 0) 229 | 230 | line = @lines.append 'path' 231 | .classed 'line', true 232 | .classed 'line-' + series.series, true 233 | 234 | color = @nextColor() 235 | @colors.push color 236 | line.datum xys 237 | .attr 'fill', 'none' 238 | .attr 'stroke', color 239 | .attr 'stroke-linejoin', 'round' 240 | .attr 'stroke-linecap', 'round' 241 | .attr 'stroke-width', @lineWidth 242 | .attr 'd', lineFn 243 | 244 | do (series, line, color)=> 245 | lineLength = line.node().getTotalLength() 246 | 247 | tip = d3.tip() 248 | .attr 'class', 'tip tip-' + series.series 249 | .offset [-10, 0] 250 | .html (d) -> 251 | return """ 252 |
    253 | #{ series.axis.x.name }: 254 | #{ series.tip.x(series.fmt.x(d[0] || 0)) } 255 |
    256 |
    257 | #{ series.axis.y.name }: 258 |
    #{ series.tip.y(series.fmt.y(d[1] || 0)) }
    259 |
    260 | """ 261 | 262 | @tips.push tip 263 | 264 | # line stroke tween 265 | # http://stackoverflow.com/questions/32789314/unrolling-line-in-d3js-linechart 266 | point = @points.append 'g' 267 | .classed 'points', true 268 | .classed 'points-' + series.series, true 269 | 270 | point.call tip 271 | 272 | line 273 | .attr 'stroke-dashoffset', lineLength 274 | .attr 'stroke-dasharray', lineLength + ' ' + lineLength 275 | .transition() 276 | .duration @interpolationTime 277 | .attrTween 'stroke-dashoffset', (ds)=> 278 | j = 0 279 | len = ds.length 280 | lineInterpolator = d3.interpolate lineLength, 0 281 | return (t)=> 282 | if t >= j / len && ds[j] 283 | show = false 284 | p = point.append 'circle' 285 | .classed 'point', true 286 | .classed 'point-' + series.series, true 287 | .datum ds[j] 288 | .attr 'stroke', color 289 | .attr 'stroke-width', 0 290 | .attr 'stroke-opacity', 0 291 | .attr 'fill', color 292 | .attr 'cx', (d)=> return xScale @parseTime(series.fmt.x(d[0] || 0)) 293 | .attr 'cy', (d)-> yScale series.fmt.y(d[1] || 0) 294 | .on 'mouseover', tip.show 295 | .on 'mouseout', (e)-> 296 | if !show 297 | tip.hide(e) 298 | .on 'click', (e)-> 299 | show = !show 300 | if show 301 | tip.show(e) 302 | else 303 | tip.hide(e) 304 | p 305 | .transition() 306 | .duration @redrawTime 307 | .attrTween 'r', (d)=> 308 | return d3.interpolate 0, @pointRadius 309 | j++ 310 | 311 | return lineInterpolator t 312 | 313 | # line renderer 314 | # else if series.type == 'bar' 315 | # else if series.type == 'note' 316 | # 1 == 1 317 | # do a thing 318 | 319 | # Aggregate data like legends and notes go after here 320 | notes = {} 321 | maxes = [] 322 | 323 | for series in serieses 324 | if series.type == 'notes' 325 | xs = series.xs 326 | ys = series.ys 327 | for i, x of xs 328 | if notes[x] 329 | notes[x].push ys[i] 330 | else 331 | notes[x] = [ys[i]] 332 | else 333 | xs = series.xs 334 | ys = series.ys 335 | for i, x of xs 336 | if !maxes[x]? || maxes[x] < ys[i] 337 | maxes[x] = ys[i] 338 | 339 | for x, ys of notes 340 | datum = [x, maxes[x]] 341 | 342 | do (datum, ys)=> 343 | tip = d3.tip() 344 | .attr 'class', 'tip tip-notes' 345 | .offset [-10, 0] 346 | .html (d) -> 347 | return """ 348 |
    349 | #{ serieses[0].axis.x.name }: 350 | #{ serieses[0].tip.x(series.fmt.x(d[0] || 0)) } 351 |
    352 |
    353 | Notes: 354 |
    #{ ys.join '\n' }
    355 |
    356 | """ 357 | 358 | @tips.push tip 359 | 360 | point = @notes.append 'circle' 361 | .classed 'point', true 362 | .classed 'point-notes', true 363 | 364 | point.call tip 365 | 366 | show = false 367 | point.datum datum 368 | .attr 'stroke', '#048ba8' 369 | .attr 'stroke-width', 0 370 | .attr 'stroke-opacity', 0 371 | .attr 'fill', '#048ba8' 372 | .attr 'cx', (d)=> return xScale @parseTime(serieses[0].fmt.x(d[0] || 0)) 373 | .attr 'cy', (d)-> yScale(serieses[0].fmt.y(d[1] || 0)) - 20 374 | .on 'mouseover', tip.show 375 | .on 'mouseout', tip.hide 376 | # .on 'mouseout', (e)-> 377 | # # if !show 378 | # tip.hide(e) 379 | # .on 'click', (e)-> 380 | # show = !show 381 | # if show 382 | # tip.show(e) 383 | # else 384 | # tip.hide(e) 385 | 386 | point.transition() 387 | .duration @redrawTime 388 | .attrTween 'r', (d)=> 389 | return d3.interpolate 0, @pointRadius * 1.5 390 | 391 | ordinal = d3.scaleOrdinal() 392 | .domain serieses.map((s)-> return s.series).filter (s)-> return !!s 393 | .range @colors 394 | 395 | @legend.attr 'transform', 'translate(' + width + ',' + @margin.top + ')' 396 | 397 | legendOrdinal = d3.legendColor() 398 | .shape 'path', d3.symbol().type(d3.symbolCircle).size(150)() 399 | .shapePadding 10 400 | # .cellFilter (d)-> return d.label !== 'e' 401 | .scale ordinal 402 | 403 | @legend.call legendOrdinal 404 | 405 | export default Chart 406 | -------------------------------------------------------------------------------- /src/views/graphics/counter.coffee: -------------------------------------------------------------------------------- 1 | import Tween from 'es-tween' 2 | 3 | import Dynamic from '../dynamic' 4 | import html from '../../templates/graphics/counter' 5 | 6 | # --Counter-- 7 | # A counter supports a model with 2 series. It will display the first 8 | # datapoint in each series and display a comparison in the two series case or 9 | # just a single number 10 | class Counter extends Dynamic 11 | tag: 'daisho-graphics-counter' 12 | html: html 13 | value0: 0 14 | value1: 0 15 | tween0: null 16 | tween1: null 17 | timer: 1000 18 | 19 | init: -> super() 20 | 21 | _refresh: -> 22 | data = @data 23 | self = @ 24 | if !@tween0 && data.get '0' 25 | value0 = data.get(0 + '.ys.0') 26 | if value0? && value0 != @value0 27 | @tween0 = new Tween.Tween 28 | v: @value0 29 | .to { v: value0 }, @timer 30 | .onUpdate -> 31 | self.value0 = @v 32 | #needs to be update since its already in a RAF 33 | self.update() 34 | .onComplete => 35 | @tween0 = null 36 | @value0 = value0 37 | @scheduleUpdate() 38 | .start() 39 | 40 | if !@tween1 && data.get '1' 41 | value1 = data.get(1 + '.ys.0') 42 | if value1? && value1 != @value1 43 | @tween1 = new Tween.Tween 44 | v: @value1 45 | .to { v: value1 }, @timer 46 | .onUpdate -> 47 | self.value1 = @v 48 | #needs to be update since its already in a RAF 49 | self.update() 50 | .onComplete => 51 | @tween1 = null 52 | @value1 = value1 53 | @scheduleUpdate() 54 | .start() 55 | 56 | getNumber: (index) -> 57 | if index == 0 58 | return @value0 if !@data.get(0 + '.fmt.y') 59 | @data.get(0 + '.fmt.y') @value0 60 | else 61 | return @value1 if !@data.get(1 + '.fmt.y') 62 | @data.get(1 + '.fmt.y') @value1 63 | 64 | export default Counter 65 | -------------------------------------------------------------------------------- /src/views/graphics/d3.coffee: -------------------------------------------------------------------------------- 1 | import extent from 'd3-array/src/extent' 2 | import {axisBottom} from 'd3-axis' 3 | import {axisLeft} from 'd3-axis' 4 | import {interpolate} from 'd3-interpolate' 5 | import {line} from 'd3-shape' 6 | import {scaleLinear} from 'd3-scale' 7 | import {scaleOrdinal} from 'd3-scale' 8 | import {scaleTime} from 'd3-scale' 9 | import {select} from 'd3-selection' 10 | import {symbolCircle} from 'd3-shape' 11 | import {symbol} from 'd3-shape' 12 | import {timeDay} from 'd3-time' 13 | import {timeParse} from 'd3-time-format' 14 | import {transition} from 'd3-transition' 15 | 16 | import legendColor from 'd3-svg-legend/src/color' 17 | import tip from 'd3-tip' 18 | 19 | export default { 20 | axisBottom: axisBottom 21 | axisLeft: axisLeft 22 | extent: extent 23 | interpolate: interpolate 24 | legendColor: legendColor 25 | line: line 26 | scaleLinear: scaleLinear 27 | scaleOrdinal: scaleOrdinal 28 | scaleTime: scaleTime 29 | select: select 30 | symbol: symbol 31 | symbolCircle: symbolCircle 32 | timeDay: timeDay 33 | timeParse: timeParse 34 | tip: tip 35 | transition: transition 36 | } 37 | -------------------------------------------------------------------------------- /src/views/graphics/index.coffee: -------------------------------------------------------------------------------- 1 | import Chart from './chart' 2 | import Counter from './counter' 3 | import Model from './model' 4 | 5 | export default Graphics = 6 | Model: Model 7 | Chart: Chart 8 | Counter: Counter 9 | 10 | register: -> 11 | @Chart.register() 12 | @Counter.register() 13 | -------------------------------------------------------------------------------- /src/views/graphics/model.coffee: -------------------------------------------------------------------------------- 1 | export default Model = 2 | new: -> 3 | # multi-series model 4 | return [@newSeries()] 5 | 6 | newSeries: -> 7 | series: '' # name of series 8 | type: 'line' # type of series rendering 9 | xs: [] # x values 10 | ys: [] # y values 11 | 12 | # formatting functions 13 | fmt: 14 | x: (n)-> n 15 | y: (n)-> n 16 | 17 | tip: 18 | x: (n)-> n 19 | y: (n)-> n 20 | 21 | # axis configuration 22 | axis: 23 | x: 24 | name: '' 25 | scale: null 26 | fmt: (n) -> n 27 | ticks: (n) -> 28 | n # return a d3 tick object 29 | y: 30 | name: '' 31 | scale: null 32 | fmt: (n) -> n 33 | ticks: (n) -> 34 | n # return a d3 tick object 35 | -------------------------------------------------------------------------------- /src/views/hanzo-dynamic-table.coffee: -------------------------------------------------------------------------------- 1 | import Dynamic from './dynamic-table' 2 | import html from '../templates/hanzo-dynamic-table' 3 | 4 | # helper for facets 5 | getFacets = (options) -> 6 | facets = 7 | string: [] 8 | range: [] 9 | 10 | hasFacets = false 11 | if options && options.string 12 | for k, v of options.string 13 | hasFacets = true 14 | 15 | vals = [] 16 | 17 | for k2, v2 of v 18 | if v2 19 | facets.string.push 20 | name: k 21 | value: k2 22 | 23 | if options && options.range 24 | for k, v of options.range 25 | hasFacets = true 26 | facets.range.push 27 | name: k 28 | value: 29 | start: v.from 30 | end: v.to 31 | 32 | return facets if hasFacets 33 | 34 | return 35 | 36 | export default class HanzoDynamicTable extends Dynamic 37 | tag: 'daisho-hanzo-dynamic-table' 38 | html: html 39 | 40 | # Name of item in table 41 | # name: '' 42 | 43 | # filter config 44 | configs: 45 | 'filter': [] 46 | 47 | initialized: false 48 | loading: false 49 | 50 | # a map of all the range facets that should use currency instead of numeric 51 | # for example 52 | # facetCurrency: 53 | # price: true 54 | # listPrice: true 55 | # inventoryCost: true 56 | 57 | facetCurrency: {} 58 | 59 | openFilter: false 60 | 61 | init: -> 62 | super 63 | 64 | # generate header onclick events 65 | _onheader: (header, e) -> 66 | if @data.get('sort') == header.field 67 | @data.set 'asc', !@data.get('asc') 68 | else 69 | @data.set 'asc', true 70 | @data.set 'sort', header.field 71 | @_refresh e 72 | 73 | onsearch: (e, facet) -> 74 | @load true 75 | 76 | # return the query string 77 | getFacetQuery: -> 78 | return @data.get 'facets.query' 79 | 80 | # return the facet filter options 81 | getFacetOptions: -> 82 | return @data.get 'facets.options' 83 | 84 | # load? 85 | doLoad: -> 86 | return true 87 | 88 | # overwrite with search function call and return promise 89 | # list: (opts) -> 90 | 91 | _refresh: (force, fn) -> 92 | @load force, fn 93 | 94 | _load: (force, fn) -> 95 | if @initialized && !force 96 | return 97 | 98 | if !@doLoad() 99 | return 100 | 101 | org = @daisho.akasha.get('orgs')[@daisho.akasha.get('activeOrg')] 102 | @data.set 'facets.currency', org.currency 103 | 104 | @initialized = true 105 | 106 | #default sorting 107 | if !@data.get('sort')? 108 | @data.set 'sort', 'UpdatedAt' 109 | @data.set 'asc', false 110 | 111 | # filter = @data.get 'filter' 112 | opts = 113 | sort: if @data.get('asc') then '-' + @data.get('sort') else @data.get('sort') 114 | display: @page * @display 115 | page: 1 116 | 117 | q = @getFacetQuery() 118 | opts.q = q if q 119 | 120 | options = @getFacetOptions() 121 | 122 | if facets = getFacets options 123 | opts.facets = JSON.stringify facets 124 | 125 | p = @list(opts) ? [] 126 | 127 | if p.then 128 | p.then (res) => 129 | if fn? then fn(res) else @onload(res) 130 | else 131 | if fn? then fn(p) else @onload(p) 132 | 133 | _loadMore: -> 134 | org = @daisho.akasha.get('orgs')[@daisho.akasha.get('activeOrg')] 135 | @data.set 'facets.currency', org.currency 136 | 137 | @initialized = true 138 | 139 | #default sorting 140 | if !@data.get('sort')? 141 | @data.set 'sort', 'UpdatedAt' 142 | @data.set 'asc', false 143 | 144 | # filter = @data.get 'filter' 145 | opts = 146 | sort: if @data.get('asc') then '-' + @data.get('sort') else @data.get('sort') 147 | display: @display 148 | page: @page 149 | 150 | q = @data.get 'facets.query' 151 | opts.q = q if q 152 | 153 | options = @data.get 'facets.options' 154 | 155 | if facets = getFacets options 156 | opts.facets = JSON.stringify facets 157 | 158 | p = @list(opts) ? [] 159 | 160 | if p.then 161 | p.then (res) => 162 | if fn? then fn(res) else @onloadMore(res) 163 | else 164 | if fn? then fn(p) else @onloadMore(p) 165 | 166 | toggleFilterMenu: ()-> 167 | @openFilter = !@openFilter 168 | -------------------------------------------------------------------------------- /src/views/hanzo-static-table.coffee: -------------------------------------------------------------------------------- 1 | import HanzoDynamicTable from './hanzo-dynamic-table' 2 | import html from '../templates/hanzo-static-table' 3 | 4 | # helper for facets 5 | getFacets = (options) -> 6 | facets = 7 | string: [] 8 | range: [] 9 | 10 | hasFacets = false 11 | if options && options.string 12 | for k, v of options.string 13 | hasFacets = true 14 | 15 | vals = [] 16 | 17 | for k2, v2 of v 18 | if v2 19 | facets.string.push 20 | name: k 21 | value: k2 22 | 23 | if options && options.range 24 | for k, v of options.range 25 | hasFacets = true 26 | facets.range.push 27 | name: k 28 | value: 29 | start: v.from 30 | end: v.to 31 | 32 | return facets if hasFacets 33 | 34 | return 35 | 36 | export default class HanzoStaticTable extends HanzoDynamicTable 37 | tag: 'daisho-hanzo-static-table' 38 | html: html 39 | 40 | init: -> 41 | super 42 | 43 | _onheader: -> 44 | return (e) -> return true 45 | 46 | doLoad: -> 47 | return true 48 | 49 | getFacetQuery: -> 50 | return '' 51 | 52 | -------------------------------------------------------------------------------- /src/views/index.coffee: -------------------------------------------------------------------------------- 1 | import Controls from './controls' 2 | import Graphics from './graphics' 3 | 4 | import Table from './table' 5 | import TableRow from './table-row' 6 | 7 | import CommandBar from './command-bar' 8 | import Dynamic from './dynamic' 9 | import DynamicTable from './dynamic-table' 10 | import HanzoDynamicTable from './hanzo-dynamic-table' 11 | import HanzoStaticTable from './hanzo-static-table' 12 | import Login from './login' 13 | import Main from './main' 14 | import Menu from './menu' 15 | import Modal from './modal' 16 | 17 | import FilterFacets from './filter-facets' 18 | 19 | export default Views = 20 | Controls: Controls 21 | Graphics: Graphics 22 | 23 | Table: Table 24 | TableRow: TableRow 25 | 26 | CommandBar: CommandBar 27 | Dynamic: Dynamic 28 | DynamicTable: DynamicTable 29 | HanzoDynamicTable: HanzoDynamicTable 30 | HanzoStaticTable: HanzoStaticTable 31 | Login: Login 32 | Main: Main 33 | Menu: Menu 34 | Modal: Modal 35 | 36 | FilterFacets: FilterFacets 37 | 38 | register: -> 39 | @Controls.register() 40 | @Graphics.register() 41 | 42 | @Table.register() 43 | @TableRow.register() 44 | 45 | @CommandBar.register() 46 | @Login.register() 47 | @Main.register() 48 | @Menu.register() 49 | @Modal.register() 50 | @FilterFacets.register() 51 | -------------------------------------------------------------------------------- /src/views/login.coffee: -------------------------------------------------------------------------------- 1 | import El from 'el.js' 2 | 3 | import Events from '../events' 4 | import m from '../mediator' 5 | import { 6 | isRequired, 7 | isEmail, 8 | isPassword, 9 | } from './middleware' 10 | 11 | import html from '../templates/login' 12 | 13 | 14 | class Login extends El.Form 15 | tag: 'daisho-login' 16 | html: html 17 | 18 | configs: 19 | 'account.email': [ isRequired, isEmail ] 20 | 'account.password': [ isPassword ] 21 | 22 | error: null 23 | disabled: false 24 | 25 | init: -> 26 | if !@data.get 'account' 27 | @data.set 'account', 28 | email: '' 29 | password: '' 30 | 31 | super 32 | 33 | _submit: (event) -> 34 | opts = 35 | email: @data.get 'account.email' 36 | password: @data.get 'account.password' 37 | # client_id: @data.get 'organization' 38 | # grant_type: 'password' 39 | 40 | @error = null 41 | 42 | m.trigger Events.Login 43 | @disabled = true 44 | @scheduleUpdate() 45 | 46 | @client.dashv2.login(opts).then (res) => 47 | @disabled = false 48 | @data.set 'account.password', '' 49 | @data.set 'account', res.user 50 | @data.set 'orgs', res.organizations 51 | @data.set 'activeOrg', 0 52 | m.trigger Events.LoginSuccess, res 53 | @scheduleUpdate() 54 | .catch (err) => 55 | @disabled = false 56 | @error = err.message 57 | m.trigger Events.LoginFailed, err 58 | @scheduleUpdate() 59 | 60 | export default Login 61 | -------------------------------------------------------------------------------- /src/views/main.coffee: -------------------------------------------------------------------------------- 1 | import El from 'el.js' 2 | 3 | import Events from '../events' 4 | import m from '../mediator' 5 | 6 | import html from '../templates/main' 7 | 8 | export default class Main extends El.Form 9 | tag: 'daisho-main' 10 | html: html 11 | 12 | configs: 13 | 'activeOrg': [] 14 | 'search': [] 15 | 16 | error: null 17 | orgs: null 18 | lastRoot: null 19 | 20 | init: -> 21 | # use the parent data as data, this is special case 22 | @data = @parentData 23 | 24 | super() 25 | 26 | # should have logged in by now do grab the orgs and look up active org key 27 | @orgs = {} 28 | for i, org of @data.get 'orgs' 29 | @orgs[i] = org.fullName 30 | 31 | @client.setKey @data.get('orgs')[@data.get('activeOrg')]['live-secret-key'] 32 | 33 | # when things are updated, update to latest page 34 | @on 'update', => 35 | current = @services?.page?.current?.root 36 | if current? && current != @lastRoot 37 | $el = $(current) 38 | $page = $(@root).find '#page' 39 | $page.children().detach() 40 | $page.append $el 41 | current.scheduleUpdate?() 42 | @lastRoot = current 43 | 44 | # if active org is updated, restart the app 45 | m.on Events.Change, (name, val)=> 46 | if name == 'activeOrg' 47 | @client.setKey @data.get('orgs')[val]['live-secret-key'] 48 | requestAnimationFrame -> 49 | window.location.reload() 50 | 51 | logout: -> 52 | m.trigger Events.Logout, res 53 | window.location.reload() 54 | 55 | _submit: (event) -> 56 | -------------------------------------------------------------------------------- /src/views/menu.coffee: -------------------------------------------------------------------------------- 1 | import El from 'el.js' 2 | 3 | import html from '../templates/menu' 4 | 5 | export default class Menu extends El.View 6 | tag: 'daisho-menu' 7 | html: html 8 | 9 | error: null 10 | 11 | init: -> super() 12 | 13 | items:-> 14 | return @services.menu.menu 15 | -------------------------------------------------------------------------------- /src/views/middleware/index.coffee: -------------------------------------------------------------------------------- 1 | emailRe = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ 2 | 3 | export isRequired = (value) -> 4 | return value if value? && value != '' 5 | throw new Error 'Required' 6 | 7 | export isEmail = (value) -> 8 | return value unless value? 9 | return value.toLowerCase() if emailRe.test value 10 | throw new Error 'Enter a valid email' 11 | 12 | export isPassword = (value) -> 13 | unless value? 14 | throw new Error 'Required' 15 | return value if value.length >= 6 16 | throw new Error 'Password must be atleast 6 characters long' 17 | -------------------------------------------------------------------------------- /src/views/modal.coffee: -------------------------------------------------------------------------------- 1 | import El from 'el.js' 2 | 3 | import html from '../templates/modal' 4 | 5 | export default class Modal extends El.View 6 | tag: 'daisho-modal' 7 | html: html 8 | -------------------------------------------------------------------------------- /src/views/table-row.coffee: -------------------------------------------------------------------------------- 1 | import El from 'el.js' 2 | 3 | import html from '../templates/table-row' 4 | 5 | class TableRow extends El.Form 6 | tag: 'daisho-table-row' 7 | html: html 8 | init: -> super() 9 | 10 | export default TableRow 11 | -------------------------------------------------------------------------------- /src/views/table.coffee: -------------------------------------------------------------------------------- 1 | import El from 'el.js' 2 | 3 | import html from '../templates/table' 4 | 5 | class Table extends El.View 6 | tag: 'daisho-table' 7 | html: html 8 | 9 | # tableColumns: [] 10 | # tableField: undefined 11 | 12 | init: -> super() 13 | 14 | export default Table 15 | --------------------------------------------------------------------------------