diff --git a/3.4.1 b/3.4.1 new file mode 100644 index 00000000..e69de29b diff --git a/3.4.2 b/3.4.2 new file mode 100644 index 00000000..e69de29b diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100755 index 00000000..4d67602c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +* Add visible attribute to market and default visible is true +* I18n translations for Email template +* Put Our Mission and Features into README.md +* Add CHANGELOG.md diff --git a/Gemfile b/Gemfile new file mode 100755 index 00000000..a1d1df3a --- /dev/null +++ b/Gemfile @@ -0,0 +1,101 @@ +source 'https://rubygems.org' + +gem 'rails', '~> 4.0.12' +gem 'rails-i18n' + +gem 'mysql2', '~> 0.3.21' +gem 'daemons-rails' +gem 'redis-rails' + +gem 'rotp' +gem 'json' +gem 'jbuilder' +gem 'bcrypt-ruby', '~> 3.1.2' + +gem 'doorkeeper', '~> 1.4.1' +gem 'omniauth', '~> 1.2.1' +gem 'omniauth-identity', '~> 1.1.1' + +gem 'figaro' +gem 'hashie' + +gem 'aasm', '~> 3.4.0' +gem 'amqp', '~> 1.3.0' +gem 'bunny', '~> 1.2.1' +gem 'cancancan' +gem 'enumerize' +gem 'datagrid' +gem 'acts-as-taggable-on' +gem 'kaminari' +gem 'paranoid2' +gem 'active_hash' +gem 'http_accept_language' +gem "globalize", "~> 4.0.0" +gem 'paper_trail', '~> 3.0.1' +gem 'rails-observers' +gem 'country_select', '~> 2.1.0' + +gem 'gon', '~> 5.2.0' +gem 'pusher' +gem 'eventmachine', '~> 1.0.4' +gem 'em-websocket', '~> 0.5.1' + +gem 'simple_form', '~> 3.1.0' +gem 'slim-rails' +gem 'sass-rails' +gem 'coffee-rails' +gem 'uglifier' +gem "jquery-rails" +gem "angularjs-rails" +gem 'bootstrap-sass', '~> 3.2.0.4' +gem 'bootstrap-wysihtml5-rails' +gem 'font-awesome-sass' +gem 'bourbon' +gem 'momentjs-rails' +gem 'eco' +gem 'browser', '~> 0.8.0' +gem 'rbtree' +gem 'liability-proof', '0.0.9' +gem 'whenever', '~> 0.9.2' +gem 'grape', '~> 0.7.0' +gem 'grape-entity', '~> 0.4.2' +gem 'grape-swagger', '~> 0.7.2' +gem 'rack-attack', '~> 3.0.0' +gem 'easy_table' +gem 'phonelib', '~> 0.3.5' +gem 'twilio-ruby', '~> 5.7.2' +gem 'unread', github: 'InfraexDev/unread' +gem 'carrierwave', '~> 0.10.0' +gem 'simple_captcha2', require: 'simple_captcha' +gem 'rest-client', '~> 1.6.8' + +group :development, :test do + gem 'factory_girl_rails' + gem 'faker', '~> 1.4.3' + gem 'mina' + gem 'mina-slack', github: 'InfraexDev/mina-slack' + gem 'meta_request' + gem 'better_errors' + gem 'binding_of_caller' + gem 'pry-rails' + gem 'quiet_assets' + gem 'mails_viewer' + gem 'timecop' + gem 'dotenv-rails' + gem 'rspec-rails' + gem 'byebug' +end + +group :test do + gem 'database_cleaner' + gem 'mocha', :require => false + gem 'shoulda-matchers' + gem 'capybara' + gem 'launchy' + gem 'selenium-webdriver' + gem 'poltergeist' + + # rspec-rails rely on test-unit if rails version less then 4.1.0 + # but test-unit has been removed from ruby core since 2.2.0 + gem 'test-unit' +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100755 index 00000000..6cc7311d --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,501 @@ +GIT + remote: git://github.com/PeatioCryptoExchange/mina-slack.git + revision: 5b7571a34f1979784720c0bfef555591e89ca3a7 + specs: + mina-slack (0.0.3) + mina (~> 0.3.0) + rest-client (~> 1.8.0) + +GIT + remote: git://github.com/PeatioCryptoExchange/unread.git + revision: b8fa0502e9558c93388a104bcbe20376cf43027d + specs: + unread (0.4.0) + activerecord (>= 3) + +GEM + remote: https://rubygems.org/ + specs: + aasm (3.4.0) + actionmailer (4.0.12) + actionpack (= 4.0.12) + mail (~> 2.5, >= 2.5.4) + actionpack (4.0.12) + activesupport (= 4.0.12) + builder (~> 3.1.0) + erubis (~> 2.7.0) + rack (~> 1.5.2) + rack-test (~> 0.6.2) + active_hash (1.3.0) + activesupport (>= 2.2.2) + activemodel (4.0.12) + activesupport (= 4.0.12) + builder (~> 3.1.0) + activerecord (4.0.12) + activemodel (= 4.0.12) + activerecord-deprecated_finders (~> 1.0.2) + activesupport (= 4.0.12) + arel (~> 4.0.0) + activerecord-deprecated_finders (1.0.3) + activesupport (4.0.12) + i18n (~> 0.6, >= 0.6.9) + minitest (~> 4.2) + multi_json (~> 1.3) + thread_safe (~> 0.1) + tzinfo (~> 0.3.37) + acts-as-taggable-on (3.0.1) + rails (>= 3, < 5) + addressable (2.3.5) + amq-protocol (1.9.2) + amqp (1.3.0) + amq-protocol (>= 1.9.2) + eventmachine + angularjs-rails (1.3.15) + arel (4.0.2) + atomic (1.1.99) + awesome_print (1.2.0) + axiom-types (0.1.0) + descendants_tracker (~> 0.0.3) + ice_nine (~> 0.11.0) + thread_safe (~> 0.1.3) + bcrypt-ruby (3.1.2) + better_errors (1.1.0) + coderay (>= 1.0.0) + erubis (>= 2.6.6) + binding_of_caller (0.7.2) + debug_inspector (>= 0.0.1) + bootstrap-sass (3.4.1) + sass (~> 3.2) + bootstrap-wysihtml5-rails (0.3.1.23) + railties (>= 3.0) + bourbon (3.2.3) + sass (~> 3.2) + thor + browser (0.8.0) + builder (3.1.4) + bunny (1.2.1) + amq-protocol (>= 1.9.2) + byebug (2.7.0) + columnize (~> 0.3) + debugger-linecache (~> 1.2) + callsite (0.0.11) + cancancan (1.7.1) + capybara (2.4.4) + mime-types (>= 1.16) + nokogiri (>= 1.8.5) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) + carrierwave (0.10.0) + activemodel (>= 3.2.0) + activesupport (>= 3.2.0) + json (>= 1.7) + mime-types (>= 1.16) + childprocess (0.4.0) + ffi (~> 1.0, >= 1.0.11) + chronic (0.10.2) + cliver (0.3.2) + coderay (1.1.0) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) + coffee-rails (4.0.1) + coffee-script (>= 2.2.0) + railties (>= 4.0.0, < 5.0) + coffee-script (2.2.0) + coffee-script-source + execjs + coffee-script-source (1.7.0) + columnize (0.3.6) + countries (0.9.3) + currencies (~> 0.4.2) + country_select (2.1.0) + countries (~> 0.9, >= 0.9.3) + currencies (0.4.2) + daemons (1.1.9) + daemons-rails (1.2.1) + daemons + multi_json (~> 1.0) + database_cleaner (1.2.0) + datagrid (1.0.5) + rails (>= 3.0) + debug_inspector (0.0.2) + debugger-linecache (1.2.0) + descendants_tracker (0.0.3) + diff-lcs (1.2.5) + doorkeeper (1.4.1) + railties (>= 3.1) + dotenv (0.9.0) + dotenv-rails (0.9.0) + dotenv (= 0.9.0) + easy_table (0.0.6) + actionpack (~> 4.0) + activemodel (~> 4.0) + rubytree (~> 0.8.3) + eco (1.0.0) + coffee-script + eco-source + execjs + eco-source (1.1.0.rc.1) + em-websocket (0.5.1) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0.6.0) + enumerize (0.8.0) + activesupport (>= 3.2) + equalizer (0.0.9) + erubis (2.7.0) + eventmachine (1.0.4) + execjs (2.2.1) + factory_girl (4.3.0) + activesupport (>= 3.0.0) + factory_girl_rails (4.3.0) + factory_girl (~> 4.3.0) + railties (>= 3.0.0) + faker (1.4.3) + i18n (~> 0.5) + faraday (0.14.0) + multipart-post (>= 1.2, < 3) + ffi (1.9.3) + figaro (0.7.0) + bundler (~> 1.0) + rails (>= 3, < 5) + font-awesome-sass (4.2.0) + sass (~> 3.2) + globalize (4.0.0) + activemodel (>= 4.0.0, < 5) + activerecord (>= 4.0.0, < 5) + gon (5.2.0) + actionpack (>= 2.3.0) + json + multi_json + request_store (>= 1.0.5) + grape (0.7.0) + activesupport + builder + hashie (>= 1.2.0) + multi_json (>= 1.3.2) + multi_xml (>= 0.5.2) + rack (>= 1.3.0) + rack-accept + rack-mount + virtus (>= 1.0.0) + grape-entity (0.4.2) + activesupport + multi_json (>= 1.3.2) + grape-swagger (0.7.2) + grape (>= 0.2.0) + grape-entity (>= 0.3.0) + kramdown (>= 1.3.1) + hashie (3.3.2) + hike (1.2.3) + http_accept_language (2.0.1) + http_parser.rb (0.6.0) + httpclient (2.3.4.1) + i18n (0.7.0) + ice_nine (0.11.0) + jbuilder (2.0.2) + activesupport (>= 3.0.0) + multi_json (>= 1.2.0) + jquery-datatables-rails (1.12.2) + jquery-rails + jquery-rails (3.1.0) + railties (>= 3.0, < 5.0) + thor (>= 0.14, < 2.0) + json (1.8.1) + jwt (2.1.0) + kaminari (0.15.1) + actionpack (>= 3.0.0) + activesupport (>= 3.0.0) + kramdown (1.3.3) + launchy (2.4.2) + addressable (~> 2.3) + liability-proof (0.0.9) + awesome_print + mail (2.6.3) + mime-types (>= 1.16, < 3) + mails_viewer (0.1.2) + jquery-datatables-rails + jquery-rails (>= 2.0.1) + rails (>= 3.1.0) + meta_request (0.2.8) + callsite + rack-contrib + railties + metaclass (0.0.2) + method_source (0.8.2) + mime-types (1.25.1) + mina (0.3.0) + open4 + rake + mini_portile (0.6.2) + minitest (4.7.5) + mocha (1.0.0) + metaclass (~> 0.0.1) + momentjs-rails (2.5.1) + railties (>= 3.1) + multi_json (1.10.1) + multi_xml (0.5.5) + multipart-post (2.0.0) + mysql2 (0.3.21) + nokogiri (1.8.5) + mini_portile (~> 0.6.0) + omniauth (1.2.2) + hashie (>= 1.2, < 4) + rack (~> 1.0) + omniauth-identity (1.1.1) + bcrypt-ruby (~> 3.0) + omniauth (~> 1.0) + open4 (1.3.0) + paper_trail (3.0.1) + activerecord (>= 3.0, < 5.0) + activesupport (>= 3.0, < 5.0) + paranoid2 (1.1.3) + activerecord (>= 4.0.0.rc1) + phonelib (0.3.5) + poltergeist (1.5.1) + capybara (~> 2.1) + cliver (~> 0.3.1) + multi_json (~> 1.0) + websocket-driver (>= 0.2.0) + power_assert (0.2.2) + pry (0.9.12.6) + coderay (~> 1.0) + method_source (~> 0.8) + slop (~> 3.4) + pry-rails (0.3.2) + pry (>= 0.9.10) + pusher (0.12.0) + httpclient (~> 2.3.0) + multi_json (~> 1.0) + signature (~> 0.1.6) + quiet_assets (1.0.2) + railties (>= 3.1, < 5.0) + rack (1.5.2) + rack-accept (0.4.5) + rack (>= 0.4) + rack-attack (3.0.0) + rack + rack-contrib (1.1.0) + rack (>= 0.9.1) + rack-mount (0.8.3) + rack (>= 1.0.0) + rack-test (0.6.3) + rack (>= 1.0) + rails (4.0.12) + actionmailer (= 4.0.12) + actionpack (= 4.0.12) + activerecord (= 4.0.12) + activesupport (= 4.0.12) + bundler (>= 1.3.0, < 2.0) + railties (= 4.0.12) + sprockets-rails (~> 2.0) + rails-i18n (4.0.1) + i18n (~> 0.6) + rails (~> 4.0) + rails-observers (0.1.2) + activemodel (~> 4.0) + railties (4.0.12) + actionpack (= 4.0.12) + activesupport (= 4.0.12) + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rake (10.4.2) + rbtree (0.4.2) + rdoc (4.1.1) + json (~> 1.4) + redis (3.0.7) + redis-actionpack (4.0.0) + actionpack (~> 4) + redis-rack (~> 1.5.0) + redis-store (~> 1.1.0) + redis-activesupport (4.0.0) + activesupport (~> 4) + redis-store (~> 1.1.0) + redis-rack (1.5.0) + rack (~> 1.5) + redis-store (~> 1.1.0) + redis-rails (4.0.0) + redis-actionpack (~> 4) + redis-activesupport (~> 4) + redis-store (~> 1.1.0) + redis-store (1.1.4) + redis (>= 2.2) + request_store (1.1.0) + rest-client (1.6.8) + mime-types (~> 1.16) + rdoc (>= 2.4.2) + rotp (1.6.1) + rspec-core (2.14.7) + rspec-expectations (2.14.5) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.14.5) + rspec-rails (2.14.1) + actionpack (>= 3.0) + activemodel (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 2.14.0) + rspec-expectations (~> 2.14.0) + rspec-mocks (~> 2.14.0) + rubytree (0.8.3) + json (>= 1.7.5) + structured_warnings (>= 0.1.3) + rubyzip (1.1.0) + sass (3.2.19) + sass-rails (4.0.3) + railties (>= 4.0.0, < 5.0) + sass (~> 3.2.0) + sprockets (~> 2.8, <= 2.11.0) + sprockets-rails (~> 2.0) + selenium-webdriver (2.39.0) + childprocess (>= 0.2.5) + multi_json (~> 1.0) + rubyzip (~> 1.0) + websocket (~> 1.0.4) + shoulda-matchers (2.5.0) + activesupport (>= 3.0.0) + signature (0.1.7) + simple_captcha2 (0.2.2) + rails (>= 3.1, < 4.1) + simple_form (3.1.0) + actionpack (~> 4.0) + activemodel (~> 4.0) + slim (2.0.2) + temple (~> 0.6.6) + tilt (>= 1.3.3, < 2.1) + slim-rails (2.1.0) + actionpack (>= 3.0, < 4.1) + activesupport (>= 3.0, < 4.1) + railties (>= 3.0, < 4.1) + slim (~> 2.0) + slop (3.4.7) + sprockets (2.11.0) + hike (~> 1.2) + multi_json (~> 1.0) + rack (~> 1.0) + tilt (~> 1.1, != 1.3.0) + sprockets-rails (2.2.2) + actionpack (>= 3.0) + activesupport (>= 3.0) + sprockets (>= 2.8, < 4.0) + structured_warnings (0.1.4) + temple (0.6.7) + test-unit (3.0.8) + power_assert + thor (0.19.1) + thread_safe (0.1.3) + atomic + tilt (1.4.1) + timecop (0.7.1) + twilio-ruby (5.7.2) + faraday (~> 0.9) + jwt (>= 1.5, <= 2.5) + nokogiri (>= 1.6, < 2.0) + tzinfo (0.3.42) + uglifier (2.4.0) + execjs (>= 0.3.0) + json (>= 1.8.0) + virtus (1.0.2) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0.3) + equalizer (~> 0.0.9) + websocket (1.0.7) + websocket-driver (0.5.1) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.1) + whenever (0.9.2) + activesupport (>= 2.3.4) + chronic (>= 0.6.3) + xpath (2.0.0) + nokogiri (>= 1.8.5) + +PLATFORMS + ruby + +DEPENDENCIES + aasm (~> 3.4.0) + active_hash + acts-as-taggable-on + amqp (~> 1.3.0) + angularjs-rails + bcrypt-ruby (~> 3.1.2) + better_errors + binding_of_caller + bootstrap-sass (~> 3.4.1) + bootstrap-wysihtml5-rails + bourbon + browser (~> 0.8.0) + bunny (~> 1.2.1) + byebug + cancancan + capybara + carrierwave (~> 0.10.0) + coffee-rails + country_select (~> 2.1.0) + daemons-rails + database_cleaner + datagrid + doorkeeper (~> 1.4.1) + dotenv-rails + easy_table + eco + em-websocket (~> 0.5.1) + enumerize + eventmachine (~> 1.0.4) + factory_girl_rails + faker (~> 1.4.3) + figaro + font-awesome-sass + globalize (~> 4.0.0) + gon (~> 5.2.0) + grape (~> 0.7.0) + grape-entity (~> 0.4.2) + grape-swagger (~> 0.7.2) + hashie + http_accept_language + jbuilder + jquery-rails + json + kaminari + launchy + liability-proof (= 0.0.9) + mails_viewer + meta_request + mina + mina-slack! + mocha + momentjs-rails + mysql2 (~> 0.3.21) + omniauth (~> 1.2.1) + omniauth-identity (~> 1.1.1) + paper_trail (~> 3.0.1) + paranoid2 + phonelib (~> 0.3.5) + poltergeist + pry-rails + pusher + quiet_assets + rack-attack (~> 3.0.0) + rails (~> 4.0.12) + rails-i18n + rails-observers + rbtree + redis-rails + rest-client (~> 1.6.8) + rotp + rspec-rails + sass-rails + selenium-webdriver + shoulda-matchers + simple_captcha2 + simple_form (~> 3.1.0) + slim-rails + test-unit + timecop + twilio-ruby (~> 5.7.2) + uglifier + unread! + whenever (~> 0.9.2) + +BUNDLED WITH + 1.16.1 diff --git a/Guardfile b/Guardfile new file mode 100644 index 00000000..91980140 --- /dev/null +++ b/Guardfile @@ -0,0 +1,55 @@ +# A sample Guardfile +# More info at https://github.com/guard/guard#readme + +## Uncomment and set this to only include directories you want to watch +# directories %w(app lib config test spec features) \ +# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")} + +## Note: if you are using the `directories` clause above and you are not +## watching the project directory ('.'), then you will want to move +## the Guardfile to a watched dir and symlink it back, e.g. +# +# $ mkdir config +# $ mv Guardfile config/ +# $ ln -s config/Guardfile . +# +# and, you'll have to watch "config/Guardfile" instead of "Guardfile" + +guard 'livereload' do + extensions = { + css: :css, + scss: :css, + sass: :css, + js: :js, + coffee: :js, + html: :html, + png: :png, + gif: :gif, + jpg: :jpg, + jpeg: :jpeg, + # less: :less, # uncomment if you want LESS stylesheets done in browser + } + + rails_view_exts = %w(erb haml slim) + + # file types LiveReload may optimize refresh for + compiled_exts = extensions.values.uniq + watch(%r{public/.+\.(#{compiled_exts * '|'})}) + + extensions.each do |ext, type| + watch(%r{ + (?:app|vendor) + (?:/assets/\w+/(?[^.]+) # path+base without extension + (?\.#{ext})) # matching extension (must be first encountered) + (?:\.\w+|$) # other extensions + }x) do |m| + path = m[1] + "/assets/#{path}.#{type}" + end + end + + # file needing a full reload of the page anyway + watch(%r{app/views/.+\.(#{rails_view_exts * '|'})$}) + watch(%r{app/helpers/.+\.rb}) + watch(%r{config/locales/.+\.yml}) +end diff --git a/PEATIO-INSTALLER-ONE.sh b/PEATIO-INSTALLER-ONE.sh new file mode 100755 index 00000000..3401f9fc --- /dev/null +++ b/PEATIO-INSTALLER-ONE.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Peatio-fresh-installer.sh: Crypto Currency Exchange +# Author: AlgoBasket +# Skype algobasket +# Email algobasket@gmail.com + +################################################################ +# Goals of the script: +# To install the secured crypto currency exchange +# +# Script by Algobasket. +################################################################ + +sudo apt-get -y install boxes; +sudo apt-get -y update +echo 'WELCOME TO PEATIO CRYPTOCURRENCY EXCHANGE v1.0 - DEVELOPED BY ALGOBASKET' | boxes -d diamonds -a hcvc +echo -e "\n\n" +echo -e "\033[34;7mWelcome to Peatio Crypto Exchange v1.0 - Build by Algobasket\e[0m " +echo -e "\n\n" + +rm -rf peatio +rm -rf ~/.rbenv + +sudo apt-get update +sudo apt-get upgrade +sudo apt-get -y install git-core curl zlib1g-dev build-essential \ + libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 \ + libxml2-dev libxslt1-dev libcurl4-openssl-dev \ + python-software-properties libffi-dev + +echo -e "\n\n" +echo -e "\033[34;7mInstalling Ruby Environment\e[0m" + +cd +git clone git://github.com/sstephenson/rbenv.git .rbenv +echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc +echo 'eval "$(rbenv init -)"' >> ~/.bashrc +exec $SHELL + +echo 'FIRST PART INSTALLED SUCCESSFULLY !' +echo 'NOW USE SECOND PART PEATIO-INSTALLER-TWO.sh' +echo -e "\n\n" \ No newline at end of file diff --git a/PEATIO-INSTALLER-THREE.sh b/PEATIO-INSTALLER-THREE.sh new file mode 100644 index 00000000..8054318f --- /dev/null +++ b/PEATIO-INSTALLER-THREE.sh @@ -0,0 +1,202 @@ +#!/bin/bash +# Peatio-installer-using-vagrant.sh: Crypto Currency Exchange +# Author: AlgoBasket +# Skype algobasket +# Email algobasket@gmail.com + +################################################################ +# Goals of the script: +# To install the secured crypto currency exchange +# +# Script by Algobasket. +################################################################ + +echo -e "\n\n" +echo -e "\033[34;7mInstalling Gem\e[0m" +echo "gem: --no-ri --no-rdoc" > ~/.gemrc +gem install bundler -v 1.9.2 +rbenv rehash + +echo -e "\n\n" +echo -e "\033[34;7mInstalling MYSQL\e[0m" + +sudo apt-get -y install mysql-server mysql-client libmysqlclient-dev + +echo -e "\n\n" +echo -e "\033[34;7mInstalling REDIS\e[0m" + +sudo apt install -y redis-server + +echo -e "\n\n" +echo -e "\033[34;7mInstalling RabbitMQ\e[0m" + +echo 'deb http://www.rabbitmq.com/debian/ testing main' | sudo tee /etc/apt/sources.list.d/rabbitmq.list +wget -O- https://www.rabbitmq.com/rabbitmq-release-signing-key.asc | sudo apt-key add - +sudo apt-get update +sudo apt-get -y install rabbitmq-server + +sudo rabbitmq-plugins enable rabbitmq_management +sudo service rabbitmq-server restart +wget http://localhost:15672/cli/rabbitmqadmin +chmod +x rabbitmqadmin +sudo mv rabbitmqadmin /usr/local/sbin + +echo -e "\n\n" +echo -e "\033[34;7mInstalling Bitcoin\e[0m" + +sudo add-apt-repository ppa:bitcoin/bitcoin +sudo apt-get update +sudo apt-get -y install bitcoind + +echo -e "\n\n" +echo -e "\033[34;7mConfiguring Bitcoin\e[0m" + +mkdir -p ~/.bitcoin +touch ~/.bitcoin/bitcoin.conf +cat < ~/.bitcoin/bitcoin.conf +server=1 +daemon=1 + +# If run on the test network instead of the real bitcoin network +testnet=1 + +# You must set rpcuser and rpcpassword to secure the JSON-RPC api +# Please make rpcpassword to something secure, `5gKAgrJv8CQr2CGUhjVbBFLSj29HnE6YGXvfykHJzS3k` for example. +# Listen for JSON-RPC connections on (default: 8332 or testnet: 18332) +rpcuser=testuser +rpcpassword=testpass +rpcport=18332 + +# Notify when receiving coins +walletnotify=/usr/local/sbin/rabbitmqadmin publish routing_key=peatio.deposit.coin payload='{"txid":"%s", "channel_key":"satoshi"}' +EOF + + +echo -e "\n\n" +echo -e "\033[34;7mStarting Bitcoin\e[0m" +#bitcoind + +echo -e "\n\n" +echo -e "\033[34;7mInstalling Nginx & Passenger\e[0m" +sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7 + +echo -e "\n\n" +echo -e "\033[34;7mAdd HTTPS support to APT\e[0m" +sudo apt-get install -y apt-transport-https ca-certificates + +sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger xenial main > /etc/apt/sources.list.d/passenger.list' +sudo apt-get update + +echo -e "\n\n" +echo -e "\033[34;7mInstalling nginx and passenger\e[0m" +sudo apt-get install -y nginx-extras passenger + +sudo rm /etc/nginx/passenger.conf +touch /etc/nginx/passenger.conf + +cat < /etc/nginx/passenger.conf +passenger_root /usr/lib/ruby/vendor_ruby/phusion_passenger/locations.ini; +passenger_ruby /home/deploy/.rbenv/shims/ruby; +EOF + +sudo sed -i 's+# include /etc/nginx/passenger.conf;+include /etc/nginx/passenger.conf;+g' /etc/nginx/nginx.conf + +echo -e "\n\n" +echo -e "\033[34;7mInstalling JavaScript Runtime\e[0m" + +curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - +sudo apt-get install nodejs + +echo -e "\n\n" +echo -e "\033[34;7mInstalling ImageMagick\e[0m" + +sudo apt-get -y install imagemagick gsfonts + +echo -e "\n\n" +echo -e "\033[34;7mSetup production environment variable\e[0m" + +echo "export RAILS_ENV=production" >> ~/.bashrc +source ~/.bashrc + +echo -e "\n\n" +echo -e "\033[34;7mCloning Stable Peatio Repo\e[0m" + +mkdir -p ~/peatio +cd peatio +git clone https://github.com/algobasket/PeatioCryptoExchange.git . + + +echo -e "\n\n" +echo -e "\033[34;7mInstalling dependency gems\e[0m" + +bundle install --without development test --path vendor/bundle + +echo -e "\n\n" +echo -e "\033[34;7mPrepare configure files\e[0m" + +bin/init_config + +echo -e "\n\n" +echo -e "\033[34;7mSetup Pusher\e[0m" + +sudo sed -i "s+YOUR_PUSHER_APP+594243+g" config/application.yml +sudo sed -i "s+YOUR_PUSHER_KEY+155f063acccd16c2f04d+g" config/application.yml +sudo sed -i "s+YOUR_PUSHER_SECRET+326c0ae14849b6c6bff5+g" config/application.yml + + +echo "ENTER YOUR SSH IP OR DOMAIN NAME : " sship +read sship +sudo sed -i "s+URL_HOST: localhost:3000+URL_HOST:${sship}+g" config/application.yml + +echo "USE http or https : " protocol +read protocol +sed -i "s+URL_SCHEMA: http+URL_SCHEMA: ${protocol}+g" config/application.yml + +echo -e "\n\n" +echo -e "\033[34;7mSetup bitcoind rpc endpoint\e[0m" +echo "Enter Bitcoin Username: " bitcoinusername +read bitcoinusername +sed -i "s+username+${bitcoinusername}+g" config/currencies.yml +echo "Enter Bitcoin Password: " bitcoinpass +read bitcoinpass +sed -i "s+password@+${bitcoinpass}@+g" config/currencies.yml + +echo -e "\n\n" +echo -e "\033[34;7mConfig database settings\e[0m" +echo "Enter MySQL Username: " mysqlusername +read mysqlusername +sed -i "s+username: root+username: ${mysqlusername}@+g" config/database.yml +echo "Enter MySQL Password: " mysqlpassword +read mysqlpassword +sed -i "s+password:+password: ${mysqlpassword}@+g" config/database.yml + +echo -e "\n\n" +echo -e "\033[34;7mInitialize the database and load the seed data\e[0m" +bundle exec rake db:setup + +echo -e "\n\n" +echo -e "\033[34;7mPrecompile assets\e[0m" +bundle exec rake assets:precompile + +echo -e "\n\n" +echo -e "\033[34;7mRunning Daemons\e[0m" +#bundle exec rake daemons:start + +echo -e "\n\n" +echo -e "\033[34;7mRunning Daemons\e[0m" +#TRADE_EXECUTOR=4 rake daemons:start + +echo -e "\n\n" +echo -e "\033[34;7mPassenger Setting\e[0m" +sudo rm /etc/nginx/sites-enabled/default +sudo ln -s /home/deploy/peatio/config/nginx.conf /etc/nginx/conf.d/peatio.conf +sudo service nginx restart + +echo -e "\n\n" +echo -e "\033[34;7mLiability Proof - Add this rake task to your crontab so it runs regularly\e[0m" + +RAILS_ENV=production rake solvency:liability_proof + +echo 'THANKS FOR INSTALLING PEATIO ENJOY !! CONTACT US ON SKYPE : algobasket | EMAIL : algobasket@gmail.com' | boxes -d peek -a c -s 40x11 +echo -e "\n\n" +echo 'Donate us at paypal : algobasket@gmail.com for future contribution' | boxes -d shell -p a1l2 diff --git a/PEATIO-INSTALLER-TWO.sh b/PEATIO-INSTALLER-TWO.sh new file mode 100755 index 00000000..398edee8 --- /dev/null +++ b/PEATIO-INSTALLER-TWO.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Peatio-repair-installer.sh: Crypto Currency Exchange +# Author: AlgoBasket +# Skype algobasket +# Email algobasket@gmail.com + +################################################################ +# Goals of the script: +# To install the secured crypto currency exchange +# +# Script by Algobasket. +################################################################ +echo -e "\n\n" +echo -e "\033[34;7mInstalling Ruby Build\e[0m" + +git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build +echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc +exec $SHELL & +sudo apt-get update +rbenv install --verbose 2.2.2 +rbenv global 2.2.2 + +echo 'SECOND PART INSTALLED SUCCESSFULLY !' +echo 'NOW USE THIRD PART PEATIO-INSTALLER-THREE.sh' +echo -e "\n\n" diff --git a/README.md b/README.md new file mode 100755 index 00000000..03e54f15 --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +𝗣𝗲𝗮𝘁𝗶𝗼 : 𝗧𝗵𝗲 𝗢𝗽𝗲𝗻-𝗦𝗼𝘂𝗿𝗰𝗲 𝗖𝗿𝘆𝗽𝘁𝗼 𝗖𝘂𝗿𝗿𝗲𝗻𝗰𝘆 𝗘𝘅𝗰𝗵𝗮𝗻𝗴𝗲 [version 2.0] +===================================== +![Bitcoin](http://peatio.info/images/ads/peatio-aminate.jpeg) + +Peatio is a free and open-source crypto currency exchange implementation with the Rails framework and other cutting-edge technology. + + +### Mission + +Our mission is to build the world best open-source crypto currency exchange with a high performance trading engine and safety which can be trusted and enjoyed by users.Help is greatly appreciated, feel free to submit pull-requests or open issues. + + +### Things You Should Know ### + +RUNNING AN EXCHANGE IS HARD. + +Peatio makes it easier, but running an exchange is still harder than a blog, which you can download the source code and following the guide or even a cool installer and boom!!! a fancy site is there to profit. We always prioritize security and speed higher than 1-click setup. We split Peatio to many components (processes) so it's flexible to deploy and scalable. + +SECURITY KNOWLEDGE IS A REQUIREMENT. + +* Rails knowledge +* Security knowledge +* System administration + + +### Features + +* Designed as high performance crypto currency exchange. +* Built-in high performance matching-engine. +* Built-in [Proof of Solvency](https://iwilcox.me.uk/2014/proving-bitcoin-reserves) Audit. +* Built-in ticket system for customer support. +* Usability and scalibility. +* Websocket API and high frequency trading support. +* Support multiple digital currencies (eg. Bitcoin, Litecoin, Dogecoin etc.). +* Easy customization of payment processing for both fiat and digital currencies. +* SMS and Google Two-Factor authenticaton. +* [KYC Verification](http://en.wikipedia.org/wiki/Know_your_customer). +* Powerful admin dashboard and management tools. +* Highly configurable and extendable. +* Industry standard security out of box. +* Active community behind. +* Free and open-source. +* Created and maintained by [Peatio open-source group](http://peat.io). + + +### Known Exchanges using Peatio + +* [Yunbi Exchange](https://yunbi.com) - A crypto-currency exchange funded by BitFundPE +* [One World Coin](https://oneworldcoin.com) +* [Bitspark](https://bitspark.io) - Bitcoin Exchange in Hong Kong +* [MarsX.io](https://acx.io) - Australian Cryptocurrency Exchange + +### Mobile Apps ### + +* [Boilr](https://github.com/andrefbsantos/boilr) - Cryptocurrency and bullion price alarms for Android + +### Requirements + +* Linux / Mac OSX +* Ruby 2.2.2 +* Rails 4.0+ +* Git 1.7.10+ +* Redis 2.0+ +* MySQL +* RabbitMQ + +** More details are in the [doc](doc). + + +### Getting Quick start using Installer + + ### Step - 1 + + * chmod +x PEATIO-INSTALLER-ONE.sh + * chmod +x PEATIO-INSTALLER-TWO.sh + * chmod +x PEATIO-INSTALLER-THREE.sh + + ### Step - 2 + + * ./PEATIO-INSTALLER-ONE.sh + * ./PEATIO-INSTALLER-TWO.sh + * ./PEATIO-INSTALLER-THREE.sh + + * To install manually please follow the docs + +### Getting Manual start + +* [Setup on Mac OS X](https://github.com/algobasket/PeatioCryptoExchange/blob/rebuild-peatio/doc/setup-local-osx.md) +* [Setup on Ubuntu](https://github.com/algobasket/PeatioCryptoExchange/blob/rebuild-peatio/doc/setup-local-ubuntu.md) +* [Deploy production server](https://github.com/algobasket/PeatioCryptoExchange/blob/rebuild-peatio/doc/deploy-production-server.md) +* [Setup Ethereum Server](https://github.com/algobasket/PeatioCryptoExchange/blob/rebuild-peatio/doc/eth.md) + +### API + +You can interact with Peatio through API: + +* [API v2](http://demo.peat.io/documents/api_v2?lang=en) +* [Websocket API](http://demo.peat.io/documents/websocket_api) + +Here're some API clients and/or wrappers: + +* [peatio-client-ruby](https://github.com/peatio/peatio-client-ruby) is the official ruby client of both HTTP/Websocket API. +* [peatio-client-python by JohnnyZhao](https://github.com/JohnnyZhao/peatio-client-python) is a python client written by JohnnyZhao. +* [peatio-client-python by czheo](https://github.com/JohnnyZhao/peatio-client-python) is a python wrapper similar to peatio-client-ruby written by czheo. +* [peatioJavaClient](https://github.com/classic1999/peatioJavaClient.git) is a java client written by classic1999. +* [yunbi-client-php](https://github.com/panlilu/yunbi-client-php) is a php client written by panlilu. + +### Custom Style + +Peatio front-end based Bootstrap 3.0 version and Sass, and you can custom exchange style for your mind. + +* change bootstrap default variables in `vars/_bootstrap.css.scss` +* change peatio custom default variables in `vars/_basic.css.scss` +* add your custom variables in `vars/_custom.css.scss` +* add your custom css style in `layouts/_custom.css.scss` +* add or change features style in `features/_xyz.css.scss' + +`vars/_custom.css.scss` can overwrite `vars/_basic.css.scss` defined variables +`layout/_custom.css.scss` can overwrite `layout/_basic.css.scss` and `layoputs/_header.css.scss` style + +### License + +Peatio is released under the terms of the MIT license. See [http://peatio.mit-license.org](http://peatio.mit-license.org) for more information. + + +### DONATE FOR MORE CONTRIBUTION AND SECURITY UPDATES + +**Bitcoin** address [18dr92LBJsnEihqv85zgQfQD5oqr2HcR4f](https://blockchain.info/address/18dr92LBJsnEihqv85zgQfQD5oqr2HcR4f) +**ETHEREUM** 0xE0adAeD68598bFD1Fa66326FDfb9e8137Bc47815 +**BTC CASH** qz6qn7cyvxehrl5pfmgwkf0ql3garnlc5yqr6nv0n7 +**STELLAR** GDO3ZSCEXB646XFKKCYNFXHGHNDGZMXBKQ6YA5TNCQZQVMWVMD2LJTHW +**Paypal** [https://paypal.me/algobasket](https://paypal.me/algobasket) + +# SKYPE : algobasket +# EMAIL : algobasket@gmail.com diff --git a/Rakefile b/Rakefile new file mode 100755 index 00000000..440b8a7e --- /dev/null +++ b/Rakefile @@ -0,0 +1,7 @@ +#!/usr/bin/env rake +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require File.expand_path('../config/application', __FILE__) + +Peatio::Application.load_tasks diff --git a/app/api/api_v2/auth/authenticator.rb b/app/api/api_v2/auth/authenticator.rb new file mode 100755 index 00000000..21d7a083 --- /dev/null +++ b/app/api/api_v2/auth/authenticator.rb @@ -0,0 +1,81 @@ +module APIv2 + module Auth + class Authenticator + + def initialize(request, params) + @request = request + @params = params + end + + def authenticate! + check_token! + check_tonce! + check_signature! + token + end + + def token + @token ||= APIToken.joins(:member).where(access_key: @params[:access_key]).first + end + + def check_token! + raise InvalidAccessKeyError, @params[:access_key] unless token + raise DisabledAccessKeyError, @params[:access_key] if token.member.api_disabled + raise ExpiredAccessKeyError, @params[:access_key] if token.expired? + raise OutOfScopeError unless token.in_scopes?(route_scopes) + end + + def check_signature! + if @params[:signature] != Utils.hmac_signature(token.secret_key, payload) + Rails.logger.warn "APIv2 auth failed: signature doesn't match. token: #{token.access_key} payload: #{payload}" + raise IncorrectSignatureError, @params[:signature] + end + end + + def check_tonce! + key = "api_v2:tonce:#{token.access_key}:#{tonce}" + if Utils.cache.read(key) + Rails.logger.warn "APIv2 auth failed: used tonce. token: #{token.access_key} payload: #{payload} tonce: #{tonce}" + raise TonceUsedError.new(token.access_key, tonce) + end + Utils.cache.write key, tonce, 61 # forget after 61 seconds + + now = Time.now.to_i*1000 + if tonce < now-30000 || tonce > now+30000 # within 30 seconds + Rails.logger.warn "APIv2 auth failed: invalid tonce. token: #{token.access_key} payload: #{payload} tonce: #{tonce} current timestamp: #{now}" + raise InvalidTonceError.new(tonce, now) + end + end + + def tonce + @tonce ||= @params[:tonce].to_i + end + + def payload + "#{canonical_verb}|#{APIv2::Mount::PREFIX}#{canonical_uri}|#{canonical_query}" + end + + def canonical_verb + @request.request_method + end + + def canonical_uri + @request.path_info + end + + def canonical_query + hash = @params.select {|k,v| !%w(route_info signature format).include?(k) } + URI.unescape(hash.to_param) + end + + def endpoint + @request.env['api.endpoint'] + end + + def route_scopes + endpoint.options[:route_options][:scopes] + end + + end + end +end diff --git a/app/api/api_v2/auth/middleware.rb b/app/api/api_v2/auth/middleware.rb new file mode 100755 index 00000000..2f35f433 --- /dev/null +++ b/app/api/api_v2/auth/middleware.rb @@ -0,0 +1,26 @@ +module APIv2 + module Auth + class Middleware < ::Grape::Middleware::Base + + def before + if provided? + auth = Authenticator.new(request, params) + @env['api_v2.token'] = auth.authenticate! + end + end + + def provided? + params[:access_key] && params[:tonce] && params[:signature] + end + + def request + @request ||= ::Grape::Request.new(env) + end + + def params + @params ||= request.params + end + + end + end +end diff --git a/app/api/api_v2/auth/utils.rb b/app/api/api_v2/auth/utils.rb new file mode 100755 index 00000000..924977dc --- /dev/null +++ b/app/api/api_v2/auth/utils.rb @@ -0,0 +1,26 @@ +module APIv2 + module Auth + module Utils + class <(trade, options){ options[:current_user] } do |trade, options| + if trade.ask_member_id == options[:current_user].id + trade.ask_id + elsif trade.bid_member_id == options[:current_user].id + trade.bid_id + else + nil + end + end + + end + end +end diff --git a/app/api/api_v2/errors.rb b/app/api/api_v2/errors.rb new file mode 100755 index 00000000..4cfaa692 --- /dev/null +++ b/app/api/api_v2/errors.rb @@ -0,0 +1,107 @@ +module APIv2 + + module ExceptionHandlers + + def self.included(base) + base.instance_eval do + rescue_from Grape::Exceptions::ValidationErrors do |e| + Rack::Response.new({ + error: { + code: 1001, + message: e.message + } + }.to_json, e.status) + end + end + end + + end + + class Error < Grape::Exceptions::Base + attr :code, :text + + # code: api error code defined by Peatio, errors originated from + # subclasses of Error have code start from 2000. + # text: human readable error message + # status: http status code + def initialize(opts={}) + @code = opts[:code] || 2000 + @text = opts[:text] || '' + + @status = opts[:status] || 400 + @message = {error: {code: @code, message: @text}} + end + end + + class AuthorizationError < Error + def initialize + super code: 2001, text: 'Authorization failed', status: 401 + end + end + + class CreateOrderError < Error + def initialize(e) + super code: 2002, text: "Failed to create order. Reason: #{e}", status: 400 + end + end + + class CancelOrderError < Error + def initialize(e) + super code: 2003, text: "Failed to cancel order. Reason: #{e}", status: 400 + end + end + + class OrderNotFoundError < Error + def initialize(id) + super code: 2004, text: "Order##{id} doesn't exist.", status: 404 + end + end + + class IncorrectSignatureError < Error + def initialize(signature) + super code: 2005, text: "Signature #{signature} is incorrect.", status: 401 + end + end + + class TonceUsedError < Error + def initialize(access_key, tonce) + super code: 2006, text: "The tonce #{tonce} has already been used by access key #{access_key}.", status: 401 + end + end + + class InvalidTonceError < Error + def initialize(tonce, now) + super code: 2007, text: "The tonce #{tonce} is invalid, current timestamp is #{now}.", status: 401 + end + end + + class InvalidAccessKeyError < Error + def initialize(access_key) + super code: 2008, text: "The access key #{access_key} does not exist.", status: 401 + end + end + + class DisabledAccessKeyError < Error + def initialize(access_key) + super code: 2009, text: "The access key #{access_key} is disabled.", status: 401 + end + end + + class ExpiredAccessKeyError < Error + def initialize(access_key) + super code: 2010, text: "The access key #{access_key} has expired.", status: 401 + end + end + + class OutOfScopeError < Error + def initialize + super code: 2011, text: "Requested API is out of access key scopes.", status: 401 + end + end + + class DepositByTxidNotFoundError < Error + def initialize(txid) + super code: 2012, text: "Deposit##txid=#{txid} doesn't exist.", status: 404 + end + end +end diff --git a/app/api/api_v2/helpers.rb b/app/api/api_v2/helpers.rb new file mode 100755 index 00000000..67bbe8bd --- /dev/null +++ b/app/api/api_v2/helpers.rb @@ -0,0 +1,100 @@ +module APIv2 + module Helpers + + def authenticate! + current_user or raise AuthorizationError + end + + def redis + @r ||= KlineDB.redis + end + + def current_user + @current_user ||= current_token.try(:member) + end + + def current_token + @current_token ||= env['api_v2.token'] + end + + def current_market + @current_market ||= Market.find params[:market] + end + + def time_to + params[:timestamp].present? ? Time.at(params[:timestamp]) : nil + end + + def build_order(attrs) + klass = attrs[:side] == 'sell' ? OrderAsk : OrderBid + + order = klass.new( + source: 'APIv2', + state: ::Order::WAIT, + member_id: current_user.id, + ask: current_market.base_unit, + bid: current_market.quote_unit, + currency: current_market.id, + ord_type: attrs[:ord_type] || 'limit', + price: attrs[:price], + volume: attrs[:volume], + origin_volume: attrs[:volume] + ) + end + + def create_order(attrs) + order = build_order attrs + Ordering.new(order).submit + order + rescue + Rails.logger.info "Failed to create order: #{$!}" + Rails.logger.debug order.inspect + Rails.logger.debug $!.backtrace.join("\n") + raise CreateOrderError, $! + end + + def create_orders(multi_attrs) + orders = multi_attrs.map {|attrs| build_order attrs } + Ordering.new(orders).submit + orders + rescue + Rails.logger.info "Failed to create order: #{$!}" + Rails.logger.debug $!.backtrace.join("\n") + raise CreateOrderError, $! + end + + def order_param + params[:order_by].downcase == 'asc' ? 'id asc' : 'id desc' + end + + def format_ticker(ticker) + { at: ticker[:at], + ticker: { + buy: ticker[:buy], + sell: ticker[:sell], + low: ticker[:low], + high: ticker[:high], + last: ticker[:last], + vol: ticker[:volume] + } + } + end + + def get_k_json + key = "peatio:#{params[:market]}:k:#{params[:period]}" + + if params[:timestamp] + ts = JSON.parse(redis.lindex(key, 0)).first + offset = (params[:timestamp] - ts) / 60 / params[:period] + offset = 0 if offset < 0 + + JSON.parse('[%s]' % redis.lrange(key, offset, offset + params[:limit] - 1).join(',')) + else + length = redis.llen(key) + offset = [length - params[:limit], 0].max + JSON.parse('[%s]' % redis.lrange(key, offset, -1).join(',')) + end + end + + end +end diff --git a/app/api/api_v2/k.rb b/app/api/api_v2/k.rb new file mode 100755 index 00000000..0ebae6fa --- /dev/null +++ b/app/api/api_v2/k.rb @@ -0,0 +1,40 @@ +module APIv2 + class K < Grape::API + helpers ::APIv2::NamedParams + + desc 'Get OHLC(k line) of specific market.' + params do + use :market + optional :limit, type: Integer, default: 30, values: 1..10000, desc: "Limit the number of returned data points, default to 30." + optional :period, type: Integer, default: 1, values: [1, 5, 15, 30, 60, 120, 240, 360, 720, 1440, 4320, 10080], desc: "Time period of K line, default to 1. You can choose between 1, 5, 15, 30, 60, 120, 240, 360, 720, 1440, 4320, 10080" + optional :timestamp, type: Integer, desc: "An integer represents the seconds elapsed since Unix epoch. If set, only k-line data after that time will be returned." + end + get "/k" do + get_k_json + end + + desc "Get K data with pending trades, which are the trades not included in K data yet, because there's delay between trade generated and processed by K data generator." + params do + use :market + requires :trade_id, type: Integer, desc: "The trade id of the first trade you received." + optional :limit, type: Integer, default: 30, values: 1..10000, desc: "Limit the number of returned data points, default to 30." + optional :period, type: Integer, default: 1, values: [1, 5, 15, 30, 60, 120, 240, 360, 720, 1440, 4320, 10080], desc: "Time period of K line, default to 1. You can choose between 1, 5, 15, 30, 60, 120, 240, 360, 720, 1440, 4320, 10080" + optional :timestamp, type: Integer, desc: "An integer represents the seconds elapsed since Unix epoch. If set, only k-line data after that time will be returned." + end + get "/k_with_pending_trades" do + k = get_k_json + + if params[:trade_id] > 0 + from = Time.at k.last[0] + trades = Trade.with_currency(params[:market]) + .where('created_at >= ? AND id < ?', from, params[:trade_id]) + .map(&:for_global) + + {k: k, trades: trades} + else + {k: k, trades: []} + end + end + + end +end diff --git a/app/api/api_v2/markets.rb b/app/api/api_v2/markets.rb new file mode 100755 index 00000000..7d781299 --- /dev/null +++ b/app/api/api_v2/markets.rb @@ -0,0 +1,10 @@ +module APIv2 + class Markets < Grape::API + + desc 'Get all available markets.' + get "/markets" do + present Market.all, with: APIv2::Entities::Market + end + + end +end diff --git a/app/api/api_v2/members.rb b/app/api/api_v2/members.rb new file mode 100755 index 00000000..8c572470 --- /dev/null +++ b/app/api/api_v2/members.rb @@ -0,0 +1,15 @@ +module APIv2 + class Members < Grape::API + helpers ::APIv2::NamedParams + + desc 'Get your profile and accounts info.', scopes: %w(profile) + params do + use :auth + end + get "/members/me" do + authenticate! + present current_user, with: APIv2::Entities::Member + end + + end +end diff --git a/app/api/api_v2/mount.rb b/app/api/api_v2/mount.rb new file mode 100755 index 00000000..141459e9 --- /dev/null +++ b/app/api/api_v2/mount.rb @@ -0,0 +1,43 @@ +require_relative 'errors' +require_relative 'validations' + +module APIv2 + class Mount < Grape::API + PREFIX = '/api' + + version 'v2', using: :path + + cascade false + + format :json + default_format :json + + helpers ::APIv2::Helpers + + do_not_route_options! + + use APIv2::Auth::Middleware + + include Constraints + include ExceptionHandlers + + before do + header 'Access-Control-Allow-Origin', '*' + end + + mount Markets + mount Tickers + mount Members + mount Deposits + mount Orders + mount OrderBooks + mount Trades + mount K + mount Tools + + base_path = Rails.env.production? ? "#{ENV['URL_SCHEMA']}://#{ENV['URL_HOST']}/#{PREFIX}" : PREFIX + add_swagger_documentation base_path: base_path, + mount_path: '/doc/swagger', api_version: 'v2', + hide_documentation_path: true + end +end diff --git a/app/api/api_v2/named_params.rb b/app/api/api_v2/named_params.rb new file mode 100755 index 00000000..8a2f23f0 --- /dev/null +++ b/app/api/api_v2/named_params.rb @@ -0,0 +1,35 @@ +module APIv2 + module NamedParams + extend ::Grape::API::Helpers + + params :auth do + requires :access_key, type: String, desc: "Access key." + requires :tonce, type: Integer, desc: "Tonce is an integer represents the milliseconds elapsed since Unix epoch." + requires :signature, type: String, desc: "The signature of your request payload, generated using your secret key." + end + + params :market do + requires :market, type: String, values: ::Market.all.map(&:id), desc: ::APIv2::Entities::Market.documentation[:id] + end + + params :order do + requires :side, type: String, values: %w(sell buy), desc: ::APIv2::Entities::Order.documentation[:side] + requires :volume, type: String, desc: ::APIv2::Entities::Order.documentation[:volume] + optional :price, type: String, desc: ::APIv2::Entities::Order.documentation[:price] + optional :ord_type, type: String, values: %w(limit market), desc: ::APIv2::Entities::Order.documentation[:type] + end + + params :order_id do + requires :id, type: Integer, desc: ::APIv2::Entities::Order.documentation[:id] + end + + params :trade_filters do + optional :limit, type: Integer, range: 1..1000, default: 50, desc: 'Limit the number of returned trades. Default to 50.' + optional :timestamp, type: Integer, desc: "An integer represents the seconds elapsed since Unix epoch. If set, only trades executed before the time will be returned." + optional :from, type: Integer, desc: "Trade id. If set, only trades created after the trade will be returned." + optional :to, type: Integer, desc: "Trade id. If set, only trades created before the trade will be returned." + optional :order_by, type: String, values: %w(asc desc), default: 'desc', desc: "If set, returned trades will be sorted in specific order, default to 'desc'." + end + + end +end diff --git a/app/api/api_v2/order_books.rb b/app/api/api_v2/order_books.rb new file mode 100755 index 00000000..2699645c --- /dev/null +++ b/app/api/api_v2/order_books.rb @@ -0,0 +1,33 @@ +module APIv2 + class OrderBook < Struct.new(:asks, :bids); end + + class OrderBooks < Grape::API + helpers ::APIv2::NamedParams + + desc 'Get the order book of specified market.' + params do + use :market + optional :asks_limit, type: Integer, default: 20, range: 1..200, desc: 'Limit the number of returned sell orders. Default to 20.' + optional :bids_limit, type: Integer, default: 20, range: 1..200, desc: 'Limit the number of returned buy orders. Default to 20.' + end + get "/order_book" do + asks = OrderAsk.active.with_currency(params[:market]).matching_rule.limit(params[:asks_limit]) + bids = OrderBid.active.with_currency(params[:market]).matching_rule.limit(params[:bids_limit]) + book = OrderBook.new asks, bids + present book, with: APIv2::Entities::OrderBook + end + + desc 'Get depth or specified market. Both asks and bids are sorted from highest price to lowest.' + params do + use :market + optional :limit, type: Integer, default: 300, range: 1..1000, desc: 'Limit the number of returned price levels. Default to 300.' + end + get "/depth" do + global = Global[params[:market]] + asks = global.asks[0,params[:limit]].reverse + bids = global.bids[0,params[:limit]] + {timestamp: Time.now.to_i, asks: asks, bids: bids} + end + + end +end diff --git a/app/api/api_v2/orders.rb b/app/api/api_v2/orders.rb new file mode 100755 index 00000000..09e4b196 --- /dev/null +++ b/app/api/api_v2/orders.rb @@ -0,0 +1,91 @@ +module APIv2 + class Orders < Grape::API + helpers ::APIv2::NamedParams + + before { authenticate! } + + desc 'Get your orders, results is paginated.', scopes: %w(history trade) + params do + use :auth, :market + optional :state, type: String, default: 'wait', values: Order.state.values, desc: "Filter order by state, default to 'wait' (active orders)." + optional :limit, type: Integer, default: 100, range: 1..1000, desc: "Limit the number of returned orders, default to 100." + optional :page, type: Integer, default: 1, desc: "Specify the page of paginated results." + optional :order_by, type: String, values: %w(asc desc), default: 'asc', desc: "If set, returned orders will be sorted in specific order, default to 'asc'." + end + get "/orders" do + orders = current_user.orders + .order(order_param) + .with_currency(current_market) + .with_state(params[:state]) + .page(params[:page]) + .per(params[:limit]) + + present orders, with: APIv2::Entities::Order + end + + desc 'Get information of specified order.', scopes: %w(history trade) + params do + use :auth, :order_id + end + get "/order" do + order = current_user.orders.where(id: params[:id]).first + raise OrderNotFoundError, params[:id] unless order + present order, with: APIv2::Entities::Order, type: :full + end + + desc 'Create multiple sell/buy orders.', scopes: %w(trade) + params do + use :auth, :market + requires :orders, type: Array do + use :order + end + end + post "/orders/multi" do + orders = create_orders params[:orders] + present orders, with: APIv2::Entities::Order + end + + desc 'Create a Sell/Buy order.', scopes: %w(trade) + params do + use :auth, :market, :order + end + post "/orders" do + order = create_order params + present order, with: APIv2::Entities::Order + end + + desc 'Cancel an order.', scopes: %w(trade) + params do + use :auth, :order_id + end + post "/order/delete" do + begin + order = current_user.orders.find(params[:id]) + Ordering.new(order).cancel + present order, with: APIv2::Entities::Order + rescue + raise CancelOrderError, $! + end + end + + desc 'Cancel all my orders.', scopes: %w(trade) + params do + use :auth + optional :side, type: String, values: %w(sell buy), desc: "If present, only sell orders (asks) or buy orders (bids) will be canncelled." + end + post "/orders/clear" do + begin + orders = current_user.orders.with_state(:wait) + if params[:side].present? + type = params[:side] == 'sell' ? 'OrderAsk' : 'OrderBid' + orders = orders.where(type: type) + end + orders.each {|o| Ordering.new(o).cancel } + present orders, with: APIv2::Entities::Order + rescue + raise CancelOrderError, $! + end + end + + end +end diff --git a/app/api/api_v2/tickers.rb b/app/api/api_v2/tickers.rb new file mode 100755 index 00000000..1d0a114e --- /dev/null +++ b/app/api/api_v2/tickers.rb @@ -0,0 +1,22 @@ +module APIv2 + class Tickers < Grape::API + helpers ::APIv2::NamedParams + + desc 'Get ticker of all markets.' + get "/tickers" do + Market.all.inject({}) do |h, m| + h[m.id] = format_ticker Global[m.id].ticker + h + end + end + + desc 'Get ticker of specific market.' + params do + use :market + end + get "/tickers/:market" do + format_ticker Global[params[:market]].ticker + end + + end +end diff --git a/app/api/api_v2/tools.rb b/app/api/api_v2/tools.rb new file mode 100755 index 00000000..b7b3ed44 --- /dev/null +++ b/app/api/api_v2/tools.rb @@ -0,0 +1,8 @@ +module APIv2 + class Tools < Grape::API + desc 'Get server current time, in seconds since Unix epoch.' + get "/timestamp" do + ::Time.now.to_i + end + end +end diff --git a/app/api/api_v2/trades.rb b/app/api/api_v2/trades.rb new file mode 100755 index 00000000..d3ea2773 --- /dev/null +++ b/app/api/api_v2/trades.rb @@ -0,0 +1,32 @@ +module APIv2 + class Trades < Grape::API + helpers ::APIv2::NamedParams + + desc 'Get recent trades on market, each trade is included only once. Trades are sorted in reverse creation order.' + params do + use :market, :trade_filters + end + get "/trades" do + trades = Trade.filter(params[:market], time_to, params[:from], params[:to], params[:limit], order_param) + present trades, with: APIv2::Entities::Trade + end + + desc 'Get your executed trades. Trades are sorted in reverse creation order.', scopes: %w(history) + params do + use :auth, :market, :trade_filters + end + get "/trades/my" do + authenticate! + + trades = Trade.for_member( + params[:market], current_user, + limit: params[:limit], time_to: time_to, + from: params[:from], to: params[:to], + order: order_param + ) + + present trades, with: APIv2::Entities::Trade, current_user: current_user + end + + end +end diff --git a/app/api/api_v2/validations.rb b/app/api/api_v2/validations.rb new file mode 100755 index 00000000..218a42a0 --- /dev/null +++ b/app/api/api_v2/validations.rb @@ -0,0 +1,19 @@ +module APIv2 + module Validations + class Range < ::Grape::Validations::Validator + + def initialize(attrs, options, required, scope) + @range = options + @required = required + super + end + + def validate_param!(attr_name, params) + if (params[attr_name] || @required) && !@range.cover?(params[attr_name]) + raise Grape::Exceptions::Validation, param: @scope.full_name(attr_name), message: "must be in range: #{@range}" + end + end + + end + end +end diff --git a/app/api/api_v2/websocket_protocol.rb b/app/api/api_v2/websocket_protocol.rb new file mode 100755 index 00000000..02b14f11 --- /dev/null +++ b/app/api/api_v2/websocket_protocol.rb @@ -0,0 +1,114 @@ +module APIv2 + class WebSocketProtocol + + def initialize(socket, channel, logger) + @socket = socket + @channel = channel #FIXME: amqp should not be mixed into this class + @logger = logger + end + + def challenge + @challenge = SecureRandom.urlsafe_base64(40) + send :challenge, @challenge + end + + def handle(message) + @logger.debug message + + message = JSON.parse(message) + key = message.keys.first + data = message[key] + + case key.downcase + when 'auth' + access_key = data['access_key'] + token = APIToken.where(access_key: access_key).includes(:member).first + result = verify_answer data['answer'], token + + if result + subscribe_orders + subscribe_trades token.member + send :success, {message: "Authenticated."} + else + send :error, {message: "Authentication failed."} + end + else + end + rescue + @logger.error "Error on handling message: #{$!}" + @logger.error $!.backtrace.join("\n") + end + + private + + def send(method, data) + payload = JSON.dump({method => data}) + @logger.debug payload + @socket.send payload + end + + def verify_answer(answer, token) + str = "#{token.access_key}#{@challenge}" + answer == OpenSSL::HMAC.hexdigest('SHA256', token.secret_key, str) + end + + def subscribe_orders + x = @channel.send *AMQPConfig.exchange(:orderbook) + q = @channel.queue '', auto_delete: true + q.bind(x).subscribe do |metadata, payload| + begin + payload = JSON.parse payload + send :orderbook, payload + rescue + @logger.error "Error on receiving orders: #{$!}" + @logger.error $!.backtrace.join("\n") + end + end + end + + def subscribe_trades(member) + x = @channel.send *AMQPConfig.exchange(:trade) + q = @channel.queue '', auto_delete: true + q.bind(x, arguments: {'ask_member_id' => member.id, 'bid_member_id' => member.id, 'x-match' => 'any'}) + q.subscribe(ack: true) do |metadata, payload| + begin + payload = JSON.parse payload + trade = Trade.find payload['id'] + + send :trade, serialize_trade(trade, member, metadata) + rescue + @logger.error "Error on receiving trades: #{$!}" + @logger.error $!.backtrace.join("\n") + ensure + metadata.ack + end + end + end + + def serialize_trade(trade, member, metadata) + side = trade_side(member, metadata.headers) + hash = ::APIv2::Entities::Trade.represent(trade, side: side).serializable_hash + + if [:both, :ask].include?(side) + hash[:ask] = ::APIv2::Entities::Order.represent trade.ask + end + + if [:both, :bid].include?(side) + hash[:bid] = ::APIv2::Entities::Order.represent trade.bid + end + + hash + end + + def trade_side(member, headers) + if headers['ask_member_id'] == headers['bid_member_id'] + :both + elsif headers['ask_member_id'] == member.id + :ask + else + :bid + end + end + + end +end diff --git a/app/assets/images/Peatio.png b/app/assets/images/Peatio.png new file mode 100644 index 00000000..82f66f0c Binary files /dev/null and b/app/assets/images/Peatio.png differ diff --git a/app/assets/images/banner.png b/app/assets/images/banner.png new file mode 100644 index 00000000..172c4ff7 Binary files /dev/null and b/app/assets/images/banner.png differ diff --git a/app/assets/images/bch.png b/app/assets/images/bch.png new file mode 100644 index 00000000..8b25d556 Binary files /dev/null and b/app/assets/images/bch.png differ diff --git a/app/assets/images/btc.png b/app/assets/images/btc.png new file mode 100644 index 00000000..9518ff74 Binary files /dev/null and b/app/assets/images/btc.png differ diff --git a/app/assets/images/btg.png b/app/assets/images/btg.png new file mode 100644 index 00000000..baee4117 Binary files /dev/null and b/app/assets/images/btg.png differ diff --git a/app/assets/images/burst.png b/app/assets/images/burst.png new file mode 100644 index 00000000..ceb70518 Binary files /dev/null and b/app/assets/images/burst.png differ diff --git a/app/assets/images/dash.png b/app/assets/images/dash.png new file mode 100644 index 00000000..3c7e3b73 Binary files /dev/null and b/app/assets/images/dash.png differ diff --git a/app/assets/images/dgb.png b/app/assets/images/dgb.png new file mode 100644 index 00000000..c67cf806 Binary files /dev/null and b/app/assets/images/dgb.png differ diff --git a/app/assets/images/doge.png b/app/assets/images/doge.png new file mode 100644 index 00000000..e447fea5 Binary files /dev/null and b/app/assets/images/doge.png differ diff --git a/app/assets/images/etc.png b/app/assets/images/etc.png new file mode 100644 index 00000000..f7ea1d24 Binary files /dev/null and b/app/assets/images/etc.png differ diff --git a/app/assets/images/eth.png b/app/assets/images/eth.png new file mode 100644 index 00000000..fbbb43fe Binary files /dev/null and b/app/assets/images/eth.png differ diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico new file mode 100755 index 00000000..245c8d14 Binary files /dev/null and b/app/assets/images/favicon.ico differ diff --git a/app/assets/images/kmd.png b/app/assets/images/kmd.png new file mode 100644 index 00000000..4d64b629 Binary files /dev/null and b/app/assets/images/kmd.png differ diff --git a/app/assets/images/logo.png b/app/assets/images/logo.png new file mode 100755 index 00000000..a2cf6757 Binary files /dev/null and b/app/assets/images/logo.png differ diff --git a/app/assets/images/logo2.png b/app/assets/images/logo2.png new file mode 100644 index 00000000..c70b04aa Binary files /dev/null and b/app/assets/images/logo2.png differ diff --git a/app/assets/images/lsk.png b/app/assets/images/lsk.png new file mode 100644 index 00000000..d52e9c17 Binary files /dev/null and b/app/assets/images/lsk.png differ diff --git a/app/assets/images/ltc.png b/app/assets/images/ltc.png new file mode 100644 index 00000000..80b33d1d Binary files /dev/null and b/app/assets/images/ltc.png differ diff --git a/app/assets/images/neo.png b/app/assets/images/neo.png new file mode 100644 index 00000000..beb54b17 Binary files /dev/null and b/app/assets/images/neo.png differ diff --git a/app/assets/images/peatio2.png b/app/assets/images/peatio2.png new file mode 100644 index 00000000..a1796b35 Binary files /dev/null and b/app/assets/images/peatio2.png differ diff --git a/app/assets/images/peatio3.png b/app/assets/images/peatio3.png new file mode 100644 index 00000000..54d37896 Binary files /dev/null and b/app/assets/images/peatio3.png differ diff --git a/app/assets/images/qtum.png b/app/assets/images/qtum.png new file mode 100644 index 00000000..9bee8aad Binary files /dev/null and b/app/assets/images/qtum.png differ diff --git a/app/assets/images/rails.jpg b/app/assets/images/rails.jpg new file mode 100644 index 00000000..01a902ce Binary files /dev/null and b/app/assets/images/rails.jpg differ diff --git a/app/assets/images/railss.png b/app/assets/images/railss.png new file mode 100644 index 00000000..24e41488 Binary files /dev/null and b/app/assets/images/railss.png differ diff --git a/app/assets/images/redis.png b/app/assets/images/redis.png new file mode 100644 index 00000000..8398693d Binary files /dev/null and b/app/assets/images/redis.png differ diff --git a/app/assets/images/redis2.png b/app/assets/images/redis2.png new file mode 100644 index 00000000..38c62aa5 Binary files /dev/null and b/app/assets/images/redis2.png differ diff --git a/app/assets/images/strat.png b/app/assets/images/strat.png new file mode 100644 index 00000000..59a1a3ec Binary files /dev/null and b/app/assets/images/strat.png differ diff --git a/app/assets/images/waves.png b/app/assets/images/waves.png new file mode 100644 index 00000000..77d5546f Binary files /dev/null and b/app/assets/images/waves.png differ diff --git a/app/assets/images/xrp.png b/app/assets/images/xrp.png new file mode 100644 index 00000000..60b2fc79 Binary files /dev/null and b/app/assets/images/xrp.png differ diff --git a/app/assets/images/zec.png b/app/assets/images/zec.png new file mode 100644 index 00000000..eed30e8a Binary files /dev/null and b/app/assets/images/zec.png differ diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js new file mode 100755 index 00000000..cf0fbda5 --- /dev/null +++ b/app/assets/javascripts/admin.js @@ -0,0 +1,7 @@ +//= require jquery +//= require jquery_ujs +//= require bootstrap +//= require bootstrap-wysihtml5/b3 +//= require bootstrap-datetimepicker +//= require ZeroClipboard +//= require admin/app diff --git a/app/assets/javascripts/admin/app.js.coffee b/app/assets/javascripts/admin/app.js.coffee new file mode 100755 index 00000000..21f347f6 --- /dev/null +++ b/app/assets/javascripts/admin/app.js.coffee @@ -0,0 +1,18 @@ +$ -> + $('input[name*=created_at]').datetimepicker() + + $('[data-clipboard-text], [data-clipboard-target]').each -> + zero = new ZeroClipboard($(@)) + + zero.on 'complete', -> + $(zero.htmlBridge) + .attr('title', 'done') + .tooltip('fixTitle') + .tooltip('show') + zero.on 'mouseout', -> + $(zero.htmlBridge) + .attr('title', 'copy') + .tooltip('fixTitle') + + placement = $(@).data('placement') || 'bottom' + $(zero.htmlBridge).tooltip({title: 'copy', placement: placement}) diff --git a/app/assets/javascripts/api_v2.js b/app/assets/javascripts/api_v2.js new file mode 100755 index 00000000..2860bc5d --- /dev/null +++ b/app/assets/javascripts/api_v2.js @@ -0,0 +1,35 @@ +//= require swagger-ui/lib/shred.bundle +//= require swagger-ui/lib/jquery-1.8.0.min +//= require swagger-ui/lib/jquery.slideto.min +//= require swagger-ui/lib/jquery.wiggle.min +//= require swagger-ui/lib/jquery.ba-bbq.min +//= require swagger-ui/lib/handlebars-1.0.0 +//= require swagger-ui/lib/underscore-min +//= require swagger-ui/lib/backbone-min +//= require swagger-ui/lib/swagger +//= require swagger-ui/swagger-ui +//= require swagger-ui/lib/highlight.7.3.pack +//= require bootstrap/dropdown + +$(function() { + + window.swaggerUi = new SwaggerUi({ + url: "/api/v2/doc/swagger", + dom_id: "swagger-ui-container", + supportedSubmitMethods: ['get', 'post', 'put', 'delete'], + onComplete: function(swaggerApi, swaggerUi){ + log("Loaded SwaggerUI"); + + $('pre code').each(function(i, e) { + hljs.highlightBlock(e) + }); + }, + onFailure: function(data) { + log("Unable to Load SwaggerUI"); + }, + docExpansion: "none" + }); + + window.swaggerUi.load(); + +}); diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee new file mode 100755 index 00000000..0ea782c5 --- /dev/null +++ b/app/assets/javascripts/application.js.coffee @@ -0,0 +1,77 @@ +#= require es5-shim.min +#= require es5-sham.min +#= require jquery +#= require jquery_ujs +#= require jquery-timing.min +#= require bootstrap +#= require bootstrap-switch.min +#= require scrollIt +#= require moment +#= require bignumber +#= require underscore +#= require ZeroClipboard +#= require flight.min +#= require pusher.min +#= require list +#= require jquery.mousewheel +#= require jquery-timing.min +#= require qrcode +#= require cookies.min + +#= require ./lib/notifier +#= require ./lib/pusher_connection +#= require ./lib/tiny-pubsub + +#= require highstock +#= require_tree ./highcharts/ + +#= require_tree ./helpers +#= require_tree ./component_mixin +#= require_tree ./component_data +#= require_tree ./component_ui +#= require_tree ./templates + +$ -> + BigNumber.config(ERRORS: false) + + if $('#assets-index').length + $.scrollIt + topOffset: -180 + activeClass: 'active' + + $('a.go-verify').on 'click', (e) -> + e.preventDefault() + + root = $('.tab-pane.active .root.json pre').text() + partial_tree = $('.tab-pane.active .partial-tree.json pre').text() + + if partial_tree + uri = 'http://syskall.com/proof-of-liabilities/#verify?partial_tree=' + partial_tree + '&expected_root=' + root + window.open(encodeURI(uri), '_blank') + + $('[data-clipboard-text], [data-clipboard-target]').each -> + zero = new ZeroClipboard $(@), forceHandCursor: true + + zero.on 'complete', -> + $(zero.htmlBridge) + .attr('title', gon.clipboard.done) + .tooltip('fixTitle') + .tooltip('show') + zero.on 'mouseout', -> + $(zero.htmlBridge) + .attr('title', gon.clipboard.click) + .tooltip('fixTitle') + + placement = $(@).data('placement') || 'bottom' + $(zero.htmlBridge).tooltip({title: gon.clipboard.click, placement: placement}) + + $('.qrcode-container').each (index, el) -> + $el = $(el) + new QRCode el, + text: $el.data('text') + width: $el.data('width') + height: $el.data('height') + + FlashMessageUI.attachTo('.flash-message') + SmsAuthVerifyUI.attachTo('#edit_sms_auth') + TwoFactorAuth.attachTo('.two-factor-auth-container') diff --git a/app/assets/javascripts/component_data/global.js.coffee b/app/assets/javascripts/component_data/global.js.coffee new file mode 100755 index 00000000..ea369194 --- /dev/null +++ b/app/assets/javascripts/component_data/global.js.coffee @@ -0,0 +1,108 @@ +window.GlobalData = flight.component -> + + @refreshDocumentTitle = (event, data) -> + symbol = gon.currencies[gon.market.bid.currency].symbol + price = data.last + market = [gon.market.ask.currency, gon.market.bid.currency].join("/").toUpperCase() + brand = "Peatio Exchange" + + document.title = "#{symbol}#{price} #{market} - #{brand}" + + @refreshDepth = (data) -> + asks = [] + bids = [] + [bids_sum, asks_sum] = [0, 0] + + _.each data.asks, ([price, volume]) -> + if asks.length == 0 || price < _.last(asks)[0]*100 + asks.push [parseFloat(price), asks_sum += parseFloat(volume)] + + _.each data.bids, ([price, volume]) -> + if bids.length == 0 || price > _.last(bids)[0]/100 + bids.push [parseFloat(price), bids_sum += parseFloat(volume)] + + la = _.last(asks) + lb = _.last(bids) + if la && lb + mid = (_.first(bids)[0] + _.first(asks)[0]) / 2 + offset = Math.min.apply(Math, [Math.max(mid*0.1, 1), (mid-lb[0])*0.8, (la[0]-mid)*0.8]) + else if !la? && lb + mid = _.first(bids)[0] + offset = Math.min.apply(Math, [Math.max(mid*0.1, 1), (mid-lb[0])*0.8]) + else if la && !lb? + mid = _.first(asks)[0] + offset = Math.min.apply(Math, [Math.max(mid*0.1, 1), (la[0]-mid)*0.8]) + + @trigger 'market::depth::response', + asks: asks, bids: bids, high: mid + offset, low: mid - offset + + @refreshTicker = (data) -> + unless @.last_tickers + for market, ticker of data + data[market]['buy_trend'] = data[market]['sell_trend'] = data[market]['last_trend'] = true + @.last_tickers = data + + tickers = for market, ticker of data + buy = parseFloat(ticker.buy) + sell = parseFloat(ticker.sell) + last = parseFloat(ticker.last) + last_buy = parseFloat(@.last_tickers[market].buy) + last_sell = parseFloat(@.last_tickers[market].sell) + last_last = parseFloat(@.last_tickers[market].last) + + if buy != last_buy + data[market]['buy_trend'] = ticker['buy_trend'] = (buy > last_buy) + else + ticker['buy_trend'] = @.last_tickers[market]['buy_trend'] + + if sell != last_sell + data[market]['sell_trend'] = ticker['sell_trend'] = (sell > last_sell) + else + ticker['sell_trend'] = @.last_tickers[market]['sell_trend'] + + if last != last_last + data[market]['last_trend'] = ticker['last_trend'] = (last > last_last) + else + ticker['last_trend'] = @.last_tickers[market]['last_trend'] + + if market == gon.market.id + @trigger 'market::ticker', ticker + + market: market, data: ticker + + @trigger 'market::tickers', {tickers: tickers, raw: data} + @.last_tickers = data + + @after 'initialize', -> + @on document, 'market::ticker', @refreshDocumentTitle + + global_channel = @attr.pusher.subscribe("market-global") + market_channel = @attr.pusher.subscribe("market-#{gon.market.id}-global") + + global_channel.bind 'tickers', (data) => + @refreshTicker(data) + + market_channel.bind 'update', (data) => + gon.asks = data.asks + gon.bids = data.bids + @trigger 'market::order_book::update', asks: data.asks, bids: data.bids + @refreshDepth asks: data.asks, bids: data.bids + + market_channel.bind 'trades', (data) => + @trigger 'market::trades', {trades: data.trades} + + # Initializing at bootstrap + if gon.ticker + @trigger 'market::ticker', gon.ticker + + if gon.tickers + @refreshTicker(gon.tickers) + + if gon.asks and gon.bids + @trigger 'market::order_book::update', asks: gon.asks, bids: gon.bids + @refreshDepth asks: gon.asks, bids: gon.bids + + if gon.trades # is in desc order initially + # .reverse() will modify original array! It makes gon.trades sorted + # in asc order afterwards + @trigger 'market::trades', trades: gon.trades.reverse() diff --git a/app/assets/javascripts/component_data/market.js.coffee b/app/assets/javascripts/component_data/market.js.coffee new file mode 100755 index 00000000..a5743183 --- /dev/null +++ b/app/assets/javascripts/component_data/market.js.coffee @@ -0,0 +1,148 @@ +@MarketData = flight.component -> + + @load = (event, data) -> + @trigger 'market::candlestick::request' + @reqK gon.market.id, data['x'] + + @reqK = (market, minutes, limit = 768) -> + tid = if gon.trades.length > 0 then gon.trades[0].tid else 0 + tid = @last_tid+1 if @last_tid + url = "/api/v2/k_with_pending_trades.json?market=#{market}&limit=#{limit}&period=#{minutes}&trade_id=#{tid}" + $.getJSON url, (data) => + @handleData(data, minutes) + + @checkTrend = (pre, cur) -> + # time, open, high, low, close, volume + [_, _, _, _, cur_close, _] = cur + [_, _, _, _, pre_close, _] = pre + cur_close >= pre_close # {true: up, false: down} + + @createPoint = (i, trade) -> + # if the gap between old and new point is too wide (> 100 points), stop live + # load and show hints + gap = Math.floor((trade.date-@next_ts) / (@minutes*60)) + if gap > 100 + console.log "failed to update, too wide gap." + window.clearInterval @interval + @trigger 'market::candlestick::request' + return i + + while trade.date >= @next_ts + x = @next_ts*1000 + + @last_ts = @next_ts + @next_ts = @last_ts + @minutes*60 + + [p, v] = if trade.date < @next_ts + [parseFloat(trade.price), parseFloat(trade.amount)] + else + [@points.close[i][1], 0] + + @points.close.push [x, p] + @points.candlestick.push [x, p, p, p, p] + @points.volume.push {x: x, y: v, color: if p >= @points.close[i][1] then 'rgba(0, 255, 0, 0.5)' else 'rgba(255, 0, 0, 0.5)'} + i += 1 + i + + @updatePoint = (i, trade) -> + p = parseFloat(trade.price) + v = parseFloat(trade.amount) + + @points.close[i][1] = p + + if p > @points.candlestick[i][2] + @points.candlestick[i][2] = p + else if p < @points.candlestick[i][3] + @points.candlestick[i][3] = p + @points.candlestick[i][4] = p + + @points.volume[i].y += v + @points.volume[i].color = if i > 0 && @points.close[i][1] >= @points.close[i-1][1] then 'rgba(0, 255, 0, 0.5)' else 'rgba(255, 0, 0, 0.5)' + + @refreshUpdatedAt = -> + @updated_at = Math.round(new Date().valueOf()/1000) + + @processTrades = -> + i = @points.candlestick.length - 1 + $.each @tradesCache, (ti, trade) => + if trade.tid > @last_tid + if @last_ts <= trade.date && trade.date < @next_ts + @updatePoint i, trade + else if @next_ts <= trade.date + i = @createPoint i, trade + @last_tid = trade.tid + @refreshUpdatedAt() + @tradesCache = [] + + @prepare = (k) -> + [volume, candlestick, close_price] = [[], [], []] + + for cur, i in k + [time, open, high, low, close, vol] = cur + time = time * 1000 # fixed unix timestamp for highsotck + trend = if i >= 1 then @checkTrend(k[i-1], cur) else true + + close_price.push [time, close] + candlestick.push [time, open, high, low, close] + volume.push {x: time, y: vol, color: if trend then 'rgba(0, 255, 0, 0.5)' else 'rgba(255, 0, 0, 0.5)'} + + # remove last point from result, because we'll re-calculate it later + minutes: @minutes, candlestick: candlestick.slice(0, -1), volume: volume.slice(0, -1), close: close_price.slice(0, -1) + + @handleData = (data, minutes) -> + @minutes = minutes + @tradesCache = data.trades.concat @tradesCache + + @points = @prepare data.k + @last_tid = 0 + if @points.candlestick.length > 0 + @last_ts = @points.candlestick[@points.candlestick.length-1][0]/1000 + else + @last_ts = 0 + @next_ts = @last_ts + 60*minutes + + @deliverTrades 'market::candlestick::response' + + @deliverTrades = (event) -> + @processTrades() + + # skip the first point + @trigger event, + minutes: @points.minutes + candlestick: @points.candlestick.slice(1) + close: @points.close.slice(1) + volume: @points.volume.slice(1) + + # we only need to keep the last 2 points for future calculation + @points.close = @points.close.slice(-2) + @points.candlestick = @points.candlestick.slice(-2) + @points.volume = @points.volume.slice(-2) + + @hardRefresh = (threshold) -> + ts = Math.round( new Date().valueOf()/1000 ) + + # if there's no trade received in `threshold` seconds, request server side data + if ts > @updated_at + threshold + @refreshUpdatedAt() + @reqK gon.market.id, @minutes + + @startDeliver = (event, data) -> + if @interval? + window.clearInterval @interval + + deliver = => + if @tradesCache.length > 0 + @deliverTrades 'market::candlestick::trades' + else + @hardRefresh(300) + + @interval = setInterval deliver, 999 + + @cacheTrades = (event, data) -> + @tradesCache = Array.prototype.concat @tradesCache, data.trades + + @after 'initialize', -> + @tradesCache = [] + @on document, 'market::trades', @cacheTrades + @on document, 'switch::range_switch', @load + @on document, 'market::candlestick::created', @startDeliver diff --git a/app/assets/javascripts/component_data/member.js.coffee b/app/assets/javascripts/component_data/member.js.coffee new file mode 100755 index 00000000..54d5a39e --- /dev/null +++ b/app/assets/javascripts/component_data/member.js.coffee @@ -0,0 +1,19 @@ +@MemberData = flight.component -> + @after 'initialize', -> + return if not gon.current_user + channel = @attr.pusher.subscribe("private-#{gon.current_user.sn}") + + channel.bind 'account', (data) => + gon.accounts[data.currency] = data + @trigger 'account::update', gon.accounts + + channel.bind 'order', (data) => + @trigger "order::#{data.state}", data + + channel.bind 'trade', (data) => + @trigger 'trade', data + + # Initializing at bootstrap + @trigger 'account::update', gon.accounts + @trigger 'order::wait::populate', orders: gon.my_orders if gon.my_orders + @trigger 'trade::populate', trades: gon.my_trades if gon.my_trades diff --git a/app/assets/javascripts/component_data/place_order.js.coffee b/app/assets/javascripts/component_data/place_order.js.coffee new file mode 100755 index 00000000..2bb99fa9 --- /dev/null +++ b/app/assets/javascripts/component_data/place_order.js.coffee @@ -0,0 +1,21 @@ +@PlaceOrderData = flight.component -> + + @onInput = (event, data) -> + {input: @input, known: @known, output: @output} = data.variables + @order[@input] = data.value + + return unless @order[@input] && @order[@known] + @trigger "place_order::output::#{@output}", @order + + @onReset = (event, data) -> + {input: @input, known: @known, output: @output} = data.variables + @order[@input] = @order[@output] = null + + @trigger "place_order::reset::#{@output}" + @trigger "place_order::order::updated", @order + + @after 'initialize', -> + @order = {price: null, volume: null, total: null} + + @on 'place_order::input', @onInput + @on 'place_order::reset', @onReset diff --git a/app/assets/javascripts/component_mixin/item_list.js.coffee b/app/assets/javascripts/component_mixin/item_list.js.coffee new file mode 100755 index 00000000..2559271a --- /dev/null +++ b/app/assets/javascripts/component_mixin/item_list.js.coffee @@ -0,0 +1,34 @@ +@ItemListMixin = -> + @attributes + tbody: 'table > tbody' + empty: '.empty-row' + + @checkEmpty = (event, data) -> + if @select('tbody').find('tr.order').length is 0 + @select('empty').fadeIn() + else + @select('empty').fadeOut() + + @addOrUpdateItem = (item) -> + template = @getTemplate(item) + existsItem = @select('tbody').find("tr[data-id=#{item.id}][data-kind=#{item.kind}]") + + if existsItem.length + existsItem.html template.html() + else + template.prependTo(@select('tbody')).show('slow') + + @checkEmpty() + + @removeItem = (id) -> + item = @select('tbody').find("tr[data-id=#{id}]") + item.hide 'slow', => + item.remove() + @checkEmpty() + + @populate = (event, data) -> + if not _.isEmpty(data.orders) + @addOrUpdateItem item for item in data.orders + + @checkEmpty() + diff --git a/app/assets/javascripts/component_mixin/notification.js.coffee b/app/assets/javascripts/component_mixin/notification.js.coffee new file mode 100755 index 00000000..46cc7b30 --- /dev/null +++ b/app/assets/javascripts/component_mixin/notification.js.coffee @@ -0,0 +1,4 @@ +@NotificationMixin = -> + @notify = (body, title) -> + title ||= gon.i18n.notification.title + notification = notifier.notify(title, body) diff --git a/app/assets/javascripts/component_mixin/order_input.js.coffee b/app/assets/javascripts/component_mixin/order_input.js.coffee new file mode 100755 index 00000000..0f108c22 --- /dev/null +++ b/app/assets/javascripts/component_mixin/order_input.js.coffee @@ -0,0 +1,94 @@ +@OrderInputMixin = -> + + @attributes + form: null + type: null + + @reset = -> + @text = '' + @value = null + + @rollback = -> + @$node.val @text + + @parseText = -> + text = @$node.val() + value = BigNumber(text) + + switch + when text == @text + false + when text == '' + @reset() + @trigger 'place_order::reset', variables: @attr.variables + false + when !$.isNumeric(text) + @rollback() + false + when (value.c.length - value.e - 1) > @attr.precision + @rollback() + false + else + @text = text + @value = value + true + + @roundValueToText = (v) -> + v.round(@attr.precision, BigNumber.ROUND_DOWN).toF(@attr.precision) + + @setInputValue = (v) -> + if v? + @text = @roundValueToText(v) + else + @text = '' + + @$node.val @text + + @changeOrder = (v) -> + @trigger 'place_order::input', variables: @attr.variables, value: v + + @process = (event) -> + return unless @parseText() + + if @validateRange(@value) + @changeOrder @value + else + @setInputValue @value + + @validateRange = (v) -> + if @max && v.greaterThan(@max) + @value = @max + @changeOrder @max + false + else if v.lessThan(0) + @value = null + false + else + @value = v + true + + @onInput = (e, data) -> + @$node.val @roundValueToText(data[@attr.variables.input]) + @process() + + @onMax = (e, data) -> + @max = data.max + + @onReset = (e) -> + @$node.val '' + @reset() + + @onFocus = (e) -> + @$node.focus() + + @after 'initialize', -> + @orderType = @attr.type + @text = '' + @value = null + + @on @$node, 'change paste keyup', @process + @on @attr.form, "place_order::max::#{@attr.variables.input}", @onMax + @on @attr.form, "place_order::input::#{@attr.variables.input}", @onInput + @on @attr.form, "place_order::output::#{@attr.variables.input}", @onOutput + @on @attr.form, "place_order::reset::#{@attr.variables.input}", @onReset + @on @attr.form, "place_order::focus::#{@attr.variables.input}", @onFocus diff --git a/app/assets/javascripts/component_ui/account_balance.js.coffee b/app/assets/javascripts/component_ui/account_balance.js.coffee new file mode 100755 index 00000000..541f9e96 --- /dev/null +++ b/app/assets/javascripts/component_ui/account_balance.js.coffee @@ -0,0 +1,27 @@ +@AccountBalanceUI = flight.component -> + @updateAccount = (event, data) -> + for currency, account of data + symbol = gon.currencies[currency].symbol || '' + @$node.find(".account.#{currency} span.balance").text "#{account.balance}" + @$node.find(".account.#{currency} span.locked").text "#{account.locked}" + total = (new BigNumber(account.locked)).plus(new BigNumber(account.balance)) + @$node.find(".account.#{currency} span.total").text "#{symbol}#{formatter.round total, 2}" + + @updateTotalAssets = (event, data) -> + fiatCurrency = gon.fiat_currency + symbol = gon.currencies[fiatCurrency].symbol + sum = 0 + for currency, account of data + if currency is fiatCurrency + sum += +account.balance + sum += +account.locked + else if ticker = gon.tickers["#{currency}#{fiatCurrency}"] + sum += +account.balance * +ticker.last + sum += +account.locked * +ticker.last + + @$node.find(".total-assets").text " ≈ #{symbol} #{formatter.round sum, 2}" + + @after 'initialize', -> + @on document, 'account::update', @updateAccount + @on document, 'account::update', @updateTotalAssets + diff --git a/app/assets/javascripts/component_ui/account_summary.js.coffee b/app/assets/javascripts/component_ui/account_summary.js.coffee new file mode 100755 index 00000000..84ca9f82 --- /dev/null +++ b/app/assets/javascripts/component_ui/account_summary.js.coffee @@ -0,0 +1,40 @@ +@AccountSummaryUI = flight.component -> + @attributes + total_assets: '#total_assets' + + @updateAccount = (event, data) -> + for currency, account of data + amount = (new BigNumber(account.locked)).plus(new BigNumber(account.balance)) + @$node.find("tr.#{currency} span.amount").text(formatter.round(amount, 2)) + @$node.find("tr.#{currency} span.locked").text(formatter.round(account.locked, 2)) + + @updateTotalAssets = -> + fiatCurrency = gon.fiat_currency + symbol = gon.currencies[fiatCurrency].symbol + sum = 0 + + for currency, account of @accounts + if currency is fiatCurrency + sum += +account.balance + sum += +account.locked + else if ticker = @tickers["#{currency}#{fiatCurrency}"] + sum += +account.balance * +ticker.last + sum += +account.locked * +ticker.last + + @select('total_assets').text "#{symbol}#{formatter.round sum, 2}" + + @after 'initialize', -> + @accounts = gon.accounts + @tickers = gon.tickers + + @on document, 'account::update', @updateAccount + + @on document, 'account::update', (event, data) => + @accounts = data + @updateTotalAssets() + + @on document, 'market::tickers', (event, data) => + @tickers = data.raw + @updateTotalAssets() + + diff --git a/app/assets/javascripts/component_ui/auto_window.js.coffee b/app/assets/javascripts/component_ui/auto_window.js.coffee new file mode 100755 index 00000000..7d08481c --- /dev/null +++ b/app/assets/javascripts/component_ui/auto_window.js.coffee @@ -0,0 +1,49 @@ +GUTTER = 2 # linkage to market.css.scss $gutter var +PANEL_TABLE_HEADER_HIGH = 37 +PANEL_PADDING = 8 +BORDER_WIDTH = 1 + +@AutoWindowUI = flight.component -> + @after 'initialize', -> + gutter = GUTTER + gutter_2x = GUTTER * 2 + gutter_3x = GUTTER * 3 + gutter_4x = GUTTER * 4 + gutter_5x = GUTTER * 5 + gutter_6x = GUTTER * 6 + gutter_7x = GUTTER * 7 + gutter_8x = GUTTER * 8 + gutter_9x = GUTTER * 9 + panel_table_header_high = PANEL_TABLE_HEADER_HIGH + + @$node.resize -> + navbar_h = $('.navbar').height() + BORDER_WIDTH + markets_h = $('#market_list').height() + BORDER_WIDTH + entry_h = $('#ask_entry').height() + 2*BORDER_WIDTH + depths_h = $('#depths_wrapper').height() + 2*BORDER_WIDTH + my_orders_h = $('#my_orders').height() + 2*BORDER_WIDTH + ticker_h = $('#ticker').height() + 2*BORDER_WIDTH + + # Adjust heights first. Because scrollbar may be removed after heights + # adjustment, window width will be affected. + window_h = $(@).height() + $('.content').height(window_h - navbar_h) + + $('#candlestick').height(window_h - navbar_h - gutter_3x) + + order_h = window_h - navbar_h - entry_h - depths_h - my_orders_h - ticker_h - gutter_6x - 2*BORDER_WIDTH + $('#order_book').height(order_h) + $('#order_book .panel-body-content').height(order_h - panel_table_header_high - 2*PANEL_PADDING) + + trades_h = window_h - navbar_h - markets_h - gutter_3x - BORDER_WIDTH + $('#market_trades').height(trades_h) + $('#market_trades .panel').height(trades_h - 2*BORDER_WIDTH) + $('#market_trades .panel-body-content').height(trades_h - 2*BORDER_WIDTH - panel_table_header_high - 2*PANEL_PADDING) + + # Adjust widths. + window_w = window.innerWidth + markets_w = $('#market_list').width() + order_book_w = $('#order_book').width() + $('#candlestick').width(window_w - order_book_w - markets_w - gutter_4x - 20) + + @$node.resize() diff --git a/app/assets/javascripts/component_ui/candlestick.js.coffee b/app/assets/javascripts/component_ui/candlestick.js.coffee new file mode 100755 index 00000000..add669d0 --- /dev/null +++ b/app/assets/javascripts/component_ui/candlestick.js.coffee @@ -0,0 +1,440 @@ +if gon.local is "zh-CN" + DATETIME_LABEL_FORMAT_FOR_TOOLTIP = + millisecond: ['%m月%e日, %H:%M:%S.%L', '%m月%e日, %H:%M:%S.%L', '-%H:%M:%S.%L'] + second: ['%m月%e日, %H:%M:%S', '%m月%e日, %H:%M:%S', '-%H:%M:%S'] + minute: ['%m月%e日, %H:%M', '%m月%e日, %H:%M', '-%H:%M'] + hour: ['%m月%e日, %H:%M', '%m月%e日, %H:%M', '-%H:%M'] + day: ['%m月%e日, %H:%M', '%m月%e日, %H:%M', '-%H:%M'] + week: ['%Y年%m月%e日', '%Y年%m月%e日', '-%m月%e日'] + month: ['%Y年%m月', '%Y年%m月', '-%m'] + year: ['%Y', '%Y', '-%Y'] + +DATETIME_LABEL_FORMAT = + second: '%H:%M:%S' + minute: '%H:%M' + hour: '%H:%M' + day: '%m-%d' + week: '%m-%d' + month: '%Y-%m' + year: '%Y' + +RANGE_DEFAULT = + fill: 'none', + stroke: 'none', + 'stroke-width': 0, + r: 8, + style: + color: '#333', + states: + hover: + fill: '#000', + style: + color: '#ccc' + select: + fill: '#000', + style: + color: '#eee' + +COLOR_ON = + candlestick: + color: '#990f0f' + upColor: '#000000' + lineColor: '#cc1414' + upLineColor: '#49c043' + close: + color: null + +# The trick is use invalid color code to make the line transparent +COLOR_OFF = + candlestick: + color: 'invalid' + upColor: 'invalid' + lineColor: 'invalid' + upLineColor: 'invalid' + close: + color: 'invalid' + +COLOR = { + candlestick: _.extend({}, COLOR_OFF.candlestick), + close: _.extend({}, COLOR_OFF.close) +} +INDICATOR = {MA: false, EMA: false} + +@CandlestickUI = flight.component -> + @mask = -> + @$node.find('.mask').show() + + @unmask = -> + @$node.find('.mask').hide() + + @request = -> + @mask() + + @init = (event, data) -> + @running = true + @$node.find('#candlestick_chart').highcharts()?.destroy() + + @initHighStock(data) + @trigger 'market::candlestick::created', data + + @switchType = (event, data) -> + _.extend(COLOR[key], COLOR_OFF[key]) for key, val of COLOR + _.extend(COLOR[data.x], COLOR_ON[data.x]) + + if chart = @$node.find('#candlestick_chart').highcharts() + for type, colors of COLOR + for s in chart.series + if !s.userOptions.algorithm? && (s.userOptions.id == type) + s.update(colors, false) + @trigger "switch::main_indicator_switch::init" + + @switchMainIndicator = (event, data) -> + INDICATOR[key] = false for key, val of INDICATOR + INDICATOR[data.x] = true + + if chart = @$node.find('#candlestick_chart').highcharts() + # reset all series depend on close + for s in chart.series + if s.userOptions.linkedTo == 'close' + s.setVisible(true, false) + + for indicator, visible of INDICATOR + for s in chart.series + if s.userOptions.algorithm? && (s.userOptions.algorithm == indicator) + s.setVisible(visible, false) + chart.redraw() + + @default_range = (unit) -> + 1000 * 60 * unit * 100 + + @initHighStock = (data) -> + component = @ + range = @default_range(data['minutes']) + unit = $("[data-unit=#{data['minutes']}]").text() + title = "#{gon.market.base_unit.toUpperCase()}/#{gon.market.quote_unit.toUpperCase()} - #{unit}" + + timeUnits = + millisecond: 1 + second: 1000 + minute: 60000 + hour: 3600000 + day: 24 * 3600000 + week: 7 * 24 * 3600000 + month: 31 * 24 * 3600000 + year: 31556952000 + + dataGrouping = + enabled: false + + tooltipTemplate = JST["templates/tooltip"] + + if DATETIME_LABEL_FORMAT_FOR_TOOLTIP + dataGrouping['dateTimeLabelFormats'] = DATETIME_LABEL_FORMAT_FOR_TOOLTIP + + @$node.find('#candlestick_chart').highcharts "StockChart", + chart: + events: + load: => + @unmask() + animation: true + marginTop: 95 + backgroundColor: 'rgba(0,0,0, 0.0)' + + credits: + enabled: false + + tooltip: + crosshairs: [{ + width: 0.5, + dashStyle: 'solid', + color: '#777' + }, false], + valueDecimals: gon.market.bid.fixed + borderWidth: 0 + backgroundColor: 'rgba(0,0,0,0)' + borderRadius: 2 + shadow: false + shared: true + positioner: (w, h, point) -> + chart_w = $(@chart.renderTo).width() + chart_h = $(@chart.renderTo).height() + grid_h = Math.min(20, Math.ceil(chart_h/10)) + x = Math.max(10, point.plotX-w-20) + y = Math.max(0, Math.floor(point.plotY/grid_h)*grid_h-20) + x: x, y: y + useHTML: true + formatter: -> + chart = @points[0].series.chart + series = @points[0].series + index = @points[0].point.index + key = @points[0].key + + for k, v of timeUnits + if v >= series.xAxis.closestPointRange || (v <= timeUnits.day && key % v > 0) + dateFormat = dateTimeLabelFormats = series.options.dataGrouping.dateTimeLabelFormats[k][0] + title = Highcharts.dateFormat dateFormat, key + break + + fun = (h, s) -> + h[s.options.id] = s.data[index] + h + tooltipTemplate + title: title + indicator: INDICATOR + format: (v, fixed=3) -> Highcharts.numberFormat v, fixed + points: _.reduce chart.series, fun, {} + + plotOptions: + candlestick: + turboThreshold: 0 + followPointer: true + color: '#990f0f' + upColor: '#000000' + lineColor: '#cc1414' + upLineColor: '#49c043' + dataGrouping: dataGrouping + column: + turboThreshold: 0 + dataGrouping: dataGrouping + trendline: + lineWidth: 1 + histogram: + lineWidth: 1 + tooltip: + pointFormat: + """ +
  • {series.name}: {point.y}
  • + """ + + scrollbar: + buttonArrowColor: '#333' + barBackgroundColor: '#202020' + buttonBackgroundColor: '#202020' + trackBackgroundColor: '#202020' + barBorderColor: '#2a2a2a' + buttonBorderColor: '#2a2a2a' + trackBorderColor: '#2a2a2a' + + rangeSelector: + enabled: false + + navigator: + maskFill: 'rgba(32, 32, 32, 0.6)' + outlineColor: '#333' + outlineWidth: 1 + xAxis: + dateTimeLabelFormats: DATETIME_LABEL_FORMAT + + xAxis: + type: 'datetime', + dateTimeLabelFormats: DATETIME_LABEL_FORMAT + lineColor: '#333' + tickColor: '#333' + tickWidth: 2 + range: range + events: + afterSetExtremes: (e) -> + if e.trigger == 'navigator' && e.triggerOp == 'navigator-drag' + if component.liveRange(@.chart) && !component.running + component.trigger "switch::range_switch::init" + + yAxis: [ + { + labels: + enabled: true + align: 'right' + x: 2 + y: 3 + zIndex: -7 + gridLineColor: '#222' + gridLineDashStyle: 'ShortDot' + top: "0%" + height: "70%" + lineColor: '#fff' + minRange: if gon.ticker.last then parseFloat(gon.ticker.last)/25 else null + } + { + labels: + enabled: false + top: "70%" + gridLineColor: '#000' + height: "15%" + } + { + labels: + enabled: false + top: "85%" + gridLineColor: '#000' + height: "15%" + } + ] + + series: [ + _.extend({ + id: 'candlestick' + name: gon.i18n.chart.candlestick + type: "candlestick" + data: data['candlestick'] + showInLegend: false + }, COLOR['candlestick']), + _.extend({ + id: 'close' + type: 'spline' + data: data['close'] + showInLegend: false + marker: + radius: 0 + }, COLOR['close']), + { + id: 'volume' + name: gon.i18n.chart.volume + yAxis: 1 + type: "column" + data: data['volume'] + color: '#777' + showInLegend: false + } + { + id: 'ma5' + name: 'MA5', + linkedTo: 'close', + showInLegend: true, + type: 'trendline', + algorithm: 'MA', + periods: 5 + color: '#7c9aaa' + visible: INDICATOR['MA'] + marker: + radius: 0 + } + { + id: 'ma10' + name: 'MA10' + linkedTo: 'close', + showInLegend: true, + type: 'trendline', + algorithm: 'MA', + periods: 10 + color: '#be8f53' + visible: INDICATOR['MA'] + marker: + radius: 0 + } + { + id: 'ema7' + name: 'EMA7', + linkedTo: 'close', + showInLegend: true, + type: 'trendline', + algorithm: 'EMA', + periods: 7 + color: '#7c9aaa' + visible: INDICATOR['EMA'] + marker: + radius: 0 + } + { + id: 'ema30' + name: 'EMA30', + linkedTo: 'close', + showInLegend: true, + type: 'trendline', + algorithm: 'EMA', + periods: 30 + color: '#be8f53' + visible: INDICATOR['EMA'] + marker: + radius: 0 + } + { + id: 'macd' + name : 'MACD', + linkedTo: 'close', + yAxis: 2, + showInLegend: true, + type: 'trendline', + algorithm: 'MACD' + color: '#7c9aaa' + marker: + radius: 0 + } + { + id: 'sig' + name : 'SIG', + linkedTo: 'close', + yAxis: 2, + showInLegend: true, + type: 'trendline', + algorithm: 'signalLine' + color: '#be8f53' + marker: + radius: 0 + } + { + id: 'hist' + name: 'HIST', + linkedTo: 'close', + yAxis: 2, + showInLegend: true, + type: 'histogram' + color: '#990f0f' + } + ] + + @formatPointArray = (point) -> + x: point[0], open: point[1], high: point[2], low: point[3], close: point[4] + + @createPointOnSeries = (chart, i, px, point) -> + chart.series[i].addPoint(point, true, true) + #last = chart.series[i].points[chart.series[i].points.length-1] + #console.log "Add point on #{i}: px=#{px} lastx=#{last.x}" + + @createPoint = (chart, data, i) -> + @createPointOnSeries(chart, 0, data.candlestick[i][0], data.candlestick[i]) + @createPointOnSeries(chart, 1, data.close[i][0], data.close[i]) + @createPointOnSeries(chart, 2, data.volume[i].x, data.volume[i]) + chart.redraw(true) + + @updatePointOnSeries = (chart, i, px, point) -> + if chart.series[i].points + last = chart.series[i].points[chart.series[i].points.length-1] + if px == last.x + last.update(point, false) + else + console.log "Error update on series #{i}: px=#{px} lastx=#{last.x}" + + @updatePoint = (chart, data, i) -> + @updatePointOnSeries(chart, 0, data.candlestick[i][0], @formatPointArray(data.candlestick[i])) + @updatePointOnSeries(chart, 1, data.close[i][0], data.close[i][1]) + @updatePointOnSeries(chart, 2, data.volume[i].x, data.volume[i]) + chart.redraw(true) + + @process = (chart, data) -> + for i in [0..(data.candlestick.length-1)] + current = chart.series[0].points.length - 1 + current_point = chart.series[0].points[current] + + if data.candlestick[i][0] > current_point.x + @createPoint chart, data, i + else + @updatePoint chart, data, i + + @updateByTrades = (event, data) -> + chart = @$node.find('#candlestick_chart').highcharts() + + if @liveRange(chart) + @process(chart, data) + else + @running = false + + @liveRange = (chart) -> + p1 = chart.series[0].points[ chart.series[0].points.length-1 ].x + p2 = chart.series[10].points[ chart.series[10].points.length-1 ].x + p1 == p2 + + @after 'initialize', -> + @on document, 'market::candlestick::request', @request + @on document, 'market::candlestick::response', @init + @on document, 'market::candlestick::trades', @updateByTrades + @on document, 'switch::main_indicator_switch', @switchMainIndicator + @on document, 'switch::type_switch', @switchType diff --git a/app/assets/javascripts/component_ui/depth.js.coffee b/app/assets/javascripts/component_ui/depth.js.coffee new file mode 100755 index 00000000..50f4068f --- /dev/null +++ b/app/assets/javascripts/component_ui/depth.js.coffee @@ -0,0 +1,87 @@ +@DepthUI = flight.component -> + @attributes + chart: '#depths' + + @refresh = (event, data) -> + chart = @select('chart').highcharts() + chart.series[0].setData data.bids.reverse(), false + chart.series[1].setData data.asks, false + chart.xAxis[0].setExtremes(data.low, data.high) + chart.redraw() + + @initChart = (data) -> + @select('chart').highcharts + chart: + margin: 0 + height: 100 + backgroundColor: 'rgba(0,0,0,0)' + + title: + text: '' + + credits: + enabled: false + + legend: + enabled: false + + rangeSelector: + enabled: false + + xAxis: + labels: + enabled: false + + yAxis: + min: 0 + gridLineColor: '#333' + gridLineDashStyle: 'ShortDot' + title: + text: '' + labels: + enabled: false + + tooltip: + valueDecimals: 4 + headerFormat: + """ + + + + """ + pointFormat: '' + footerFormat: '
    {series.name} #{gon.i18n.chart.price}#{gon.i18n.chart.depth}
    {point.x}{point.y}
    ' + borderWidth: 0 + backgroundColor: 'rgba(0,0,0,0)' + borderRadius: 0 + shadow: false + useHTML: true + shared: true + positioner: -> {x: 128, y: 28} + + series : [{ + name : gon.i18n.bid + type : 'area' + fillColor: 'rgba(77, 215, 16, 0.5)' + lineColor: 'rgb(77, 215, 16)' + color: 'transparent' + animation: + duration: 1000 + },{ + name: gon.i18n.ask + type: 'area' + animation: + duration: 1000 + fillColor: 'rgba(208, 0, 23, 0.3)' + lineColor: 'rgb(208, 0, 23)' + color: 'transparent' + }] + + @after 'initialize', -> + @initChart() + @on document, 'market::depth::response', @refresh + @on document, 'market::depth::fade_toggle', -> + @$node.fadeToggle() + + @on @select('close'), 'click', => + @trigger 'market::depth::fade_toggle' diff --git a/app/assets/javascripts/component_ui/flash_message.js.coffee b/app/assets/javascripts/component_ui/flash_message.js.coffee new file mode 100755 index 00000000..253efbc5 --- /dev/null +++ b/app/assets/javascripts/component_ui/flash_message.js.coffee @@ -0,0 +1,23 @@ +@FlashMessageUI = flight.component -> + + @showMeg = (data) -> + @$node.html("") + template = JST['templates/flash_message'](data) + $(template).prependTo(@$node) + + @info = (event, data) -> + data.info = true + @showMeg(data) + + @notice = (event, data) -> + data.notice = true + @showMeg(data) + + @alert = (event, data) -> + data.alert = true + @showMeg(data) + + @after 'initialize', -> + @on document, 'flash:info', @info + @on document, 'flash:notice', @notice + @on document, 'flash:alert', @alert diff --git a/app/assets/javascripts/component_ui/float.js.coffee b/app/assets/javascripts/component_ui/float.js.coffee new file mode 100755 index 00000000..7bb450b3 --- /dev/null +++ b/app/assets/javascripts/component_ui/float.js.coffee @@ -0,0 +1,21 @@ +@FloatUI = flight.component -> + @attributes + action: 'ul.nav.nav-tabs > li' + close: 'i.fa.fa-close' + + @after 'initialize', -> + @select('action').click (e) => + if @select('action').length > 1 + if @$node.hasClass('hover') and $(e.currentTarget).hasClass('active') + @select('close').click() + else + @$node.addClass('hover') + else + unless @$node.hasClass('hover') + @$node.addClass('hover') + else + @select('close').click() + + @select('close').click => + @$node.removeClass('hover') + @select('action').removeClass('active') diff --git a/app/assets/javascripts/component_ui/header.js.coffee b/app/assets/javascripts/component_ui/header.js.coffee new file mode 100755 index 00000000..3969c40f --- /dev/null +++ b/app/assets/javascripts/component_ui/header.js.coffee @@ -0,0 +1,36 @@ +@HeaderUI = flight.component -> + @attributes + vol: 'span.vol' + amount: 'span.amount' + high: 'span.high' + low: 'span.low' + change: 'span.change' + sound: 'input[name="sound-checkbox"]' + + @refresh = (event, ticker) -> + @select('vol').text("#{ticker.volume} #{gon.market.base_unit.toUpperCase()}") + @select('high').text(ticker.high) + @select('low').text(ticker.low) + + p1 = parseFloat ticker.open + p2 = parseFloat ticker.last + trend = formatter.trend(p1 <= p2) + @select('change').html("#{formatter.price_change(p1, p2)}%") + + @after 'initialize', -> + @on document, 'market::ticker', @refresh + + if Cookies.get('sound') == undefined + Cookies.set('sound', true, 30) + state = Cookies.get('sound') == 'true' ? true : false + + @select('sound').bootstrapSwitch + labelText: gon.i18n.switch.sound + state: state + handleWidth: 40 + labelWidth: 40 + onSwitchChange: (event, state) -> + Cookies.set('sound', state, 30) + + $('header .dropdown-menu').click (e) -> + e.stopPropagation() diff --git a/app/assets/javascripts/component_ui/key_bind.js.coffee b/app/assets/javascripts/component_ui/key_bind.js.coffee new file mode 100755 index 00000000..127c430b --- /dev/null +++ b/app/assets/javascripts/component_ui/key_bind.js.coffee @@ -0,0 +1,8 @@ +ESC = 27 +@KeyBindUI = flight.component -> + @after 'initialize', -> + entry = '#ask_entry' + @$node.on 'keyup', (e) -> + if e.keyCode == ESC + if entry == '#bid_entry' then entry = '#ask_entry' else entry = '#bid_entry' + $(entry).trigger 'place_order::clear' diff --git a/app/assets/javascripts/component_ui/market_switch.js.coffee b/app/assets/javascripts/component_ui/market_switch.js.coffee new file mode 100755 index 00000000..84bec42b --- /dev/null +++ b/app/assets/javascripts/component_ui/market_switch.js.coffee @@ -0,0 +1,53 @@ +window.MarketSwitchUI = flight.component -> + @attributes + table: 'tbody' + marketGroupName: '.panel-body-head thead span.name' + marketGroupItem: '.dropdown-wrapper .dropdown-menu li a' + marketsTable: '.table.markets' + + @switchMarketGroup = (event, item) -> + item = $(event.target).closest('a') + name = item.data('name') + + @select('marketGroupItem').removeClass('active') + item.addClass('active') + + @select('marketGroupName').text item.find('span').text() + @select('marketsTable').attr("class", "table table-hover markets #{name}") + + @updateMarket = (select, ticker) -> + trend = formatter.trend ticker.last_trend + + select.find('td.price') + .attr('title', ticker.last) + .html("#{formatter.ticker_price ticker.last}") + + p1 = parseFloat(ticker.open) + p2 = parseFloat(ticker.last) + trend = formatter.trend(p1 <= p2) + select.find('td.change').html("#{formatter.price_change(p1, p2)}%") + + @refresh = (event, data) -> + table = @select('table') + for ticker in data.tickers + @updateMarket table.find("tr#market-list-#{ticker.market}"), ticker.data + + table.find("tr#market-list-#{gon.market.id}").addClass 'highlight' + + @after 'initialize', -> + @on document, 'market::tickers', @refresh + @on @select('marketGroupItem'), 'click', @switchMarketGroup + + @select('table').on 'click', 'tr', (e) -> + unless e.target.nodeName == 'I' + window.location.href = window.formatter.market_url($(@).data('market')) + + @.hide_accounts = $('tr.hide') + $('.view_all_accounts').on 'click', (e) => + $el = $(e.currentTarget) + if @.hide_accounts.hasClass('hide') + $el.text($el.data('hide-text')) + @.hide_accounts.removeClass('hide') + else + $el.text($el.data('show-text')) + @.hide_accounts.addClass('hide') diff --git a/app/assets/javascripts/component_ui/market_ticker.js.coffee b/app/assets/javascripts/component_ui/market_ticker.js.coffee new file mode 100755 index 00000000..32dd273c --- /dev/null +++ b/app/assets/javascripts/component_ui/market_ticker.js.coffee @@ -0,0 +1,18 @@ +window.MarketTickerUI = flight.component -> + @attributes + askSelector: '.ask .price' + bidSelector: '.bid .price' + lastSelector: '.last .price' + priceSelector: '.price' + + @updatePrice = (selector, price, trend) -> + selector.removeClass('text-up').removeClass('text-down').addClass(formatter.trend(trend)) + selector.html(formatter.fixBid(price)) + + @refresh = (event, ticker) -> + @updatePrice @select('askSelector'), ticker.sell, ticker.sell_trend + @updatePrice @select('bidSelector'), ticker.buy, ticker.buy_trend + @updatePrice @select('lastSelector'), ticker.last, ticker.last_trend + + @after 'initialize', -> + @on document, 'market::ticker', @refresh diff --git a/app/assets/javascripts/component_ui/market_trades.js.coffee b/app/assets/javascripts/component_ui/market_trades.js.coffee new file mode 100755 index 00000000..3dde63de --- /dev/null +++ b/app/assets/javascripts/component_ui/market_trades.js.coffee @@ -0,0 +1,99 @@ +window.MarketTradesUI = flight.component -> + flight.compose.mixin @, [NotificationMixin] + + @attributes + defaultHeight: 156 + tradeSelector: 'tr' + newTradeSelector: 'tr.new' + allSelector: 'a.all' + mySelector: 'a.my' + allTableSelector: 'table.all-trades tbody' + myTableSelector: 'table.my-trades tbody' + newMarketTradeContent: 'table.all-trades tr.new div' + newMyTradeContent: 'table.my-trades tr.new div' + tradesLimit: 80 + + @showAllTrades = (event) -> + @select('mySelector').removeClass('active') + @select('allSelector').addClass('active') + @select('myTableSelector').hide() + @select('allTableSelector').show() + + @showMyTrades = (event) -> + @select('allSelector').removeClass('active') + @select('mySelector').addClass('active') + @select('allTableSelector').hide() + @select('myTableSelector').show() + + @bufferMarketTrades = (event, data) -> + @marketTrades = @marketTrades.concat data.trades + + @clearMarkers = (table) -> + table.find('tr.new').removeClass('new') + table.find('tr').slice(@attr.tradesLimit).remove() + + @notifyMyTrade = (trade) -> + market = gon.markets[trade.market] + message = gon.i18n.notification.new_trade + .replace(/%{kind}/g, gon.i18n[trade.kind]) + .replace(/%{id}/g, trade.id) + .replace(/%{price}/g, trade.price) + .replace(/%{volume}/g, trade.volume) + .replace(/%{base_unit}/g, market.base_unit.toUpperCase()) + .replace(/%{quote_unit}/g, market.quote_unit.toUpperCase()) + @notify message + + @isMine = (trade) -> + return false if @myTrades.length == 0 + + for t in @myTrades + if trade.tid == t.id + return true + if trade.tid > t.id # @myTrades is sorted reversely + return false + + @handleMarketTrades = (event, data) -> + for trade in data.trades + @marketTrades.unshift trade + trade.classes = 'new' + trade.classes += ' mine' if @isMine(trade) + el = @select('allTableSelector').prepend(JST['templates/market_trade'](trade)) + + @marketTrades = @marketTrades.slice(0, @attr.tradesLimit) + @select('newMarketTradeContent').slideDown('slow') + + setTimeout => + @clearMarkers(@select('allTableSelector')) + , 900 + + @handleMyTrades = (event, data, notify=true) -> + for trade in data.trades + if trade.market == gon.market.id + @myTrades.unshift trade + trade.classes = 'new' + + el = @select('myTableSelector').prepend(JST['templates/my_trade'](trade)) + @select('allTableSelector').find("tr#market-trade-#{trade.id}").addClass('mine') + + @notifyMyTrade(trade) if notify + + @myTrades = @myTrades.slice(0, @attr.tradesLimit) if @myTrades.length > @attr.tradesLimit + @select('newMyTradeContent').slideDown('slow') + + setTimeout => + @clearMarkers(@select('myTableSelector')) + , 900 + + @after 'initialize', -> + @marketTrades = [] + @myTrades = [] + + @on document, 'trade::populate', (event, data) => + @handleMyTrades(event, trades: data.trades.reverse(), false) + @on document, 'trade', (event, trade) => + @handleMyTrades(event, trades: [trade]) + + @on document, 'market::trades', @handleMarketTrades + + @on @select('allSelector'), 'click', @showAllTrades + @on @select('mySelector'), 'click', @showMyTrades diff --git a/app/assets/javascripts/component_ui/my_orders.js.coffee b/app/assets/javascripts/component_ui/my_orders.js.coffee new file mode 100755 index 00000000..ae4f8f36 --- /dev/null +++ b/app/assets/javascripts/component_ui/my_orders.js.coffee @@ -0,0 +1,27 @@ +@MyOrdersUI = flight.component -> + flight.compose.mixin @, [ItemListMixin] + + @getTemplate = (order) -> $(JST["templates/order_active"](order)) + + @orderHandler = (event, order) -> + return unless order.market == gon.market.id + + switch order.state + when 'wait' + @addOrUpdateItem order + when 'cancel' + @removeItem order.id + when 'done' + @removeItem order.id + + @cancelOrder = (event) -> + tr = $(event.target).parents('tr') + if confirm(formatter.t('place_order')['confirm_cancel']) + $.ajax + url: formatter.market_url gon.market.id, tr.data('id') + method: 'delete' + + @.after 'initialize', -> + @on document, 'order::wait::populate', @populate + @on document, 'order::wait order::cancel order::done', @orderHandler + @on @select('tbody'), 'click', @cancelOrder diff --git a/app/assets/javascripts/component_ui/order_book.js.coffee b/app/assets/javascripts/component_ui/order_book.js.coffee new file mode 100755 index 00000000..7b3e3ba4 --- /dev/null +++ b/app/assets/javascripts/component_ui/order_book.js.coffee @@ -0,0 +1,118 @@ +@OrderBookUI = flight.component -> + @attributes + bookLimit: 30 + askBookSel: 'table.asks' + bidBookSel: 'table.bids' + seperatorSelector: 'table.seperator' + fade_toggle_depth: '#fade_toggle_depth' + + @update = (event, data) -> + @updateOrders(@select('bidBookSel'), _.first(data.bids, @.attr.bookLimit), 'bid') + @updateOrders(@select('askBookSel'), _.first(data.asks, @.attr.bookLimit), 'ask') + + @appendRow = (book, template, data) -> + data.classes = 'new' + book.append template(data) + + @insertRow = (book, row, template, data) -> + data.classes = 'new' + row.before template(data) + + @updateRow = (row, order, index, v1, v2) -> + row.data('order', index) + return if v1.equals(v2) + + if v2.greaterThan(v1) + row.addClass('text-up') + else + row.addClass('text-down') + + row.data('volume', order[1]) + row.find('td.volume').html(formatter.mask_fixed_volume(order[1])) + row.find('td.amount').html(formatter.amount(order[1], order[0])) + + @mergeUpdate = (bid_or_ask, book, orders, template) -> + rows = book.find('tr') + + i = j = 0 + while(true) + row = rows[i] + order = orders[j] + $row = $(row) + + if row && order + p1 = new BigNumber($row.data('price')) + v1 = new BigNumber($row.data('volume')) + p2 = new BigNumber(order[0]) + v2 = new BigNumber(order[1]) + if (bid_or_ask == 'ask' && p2.lessThan(p1)) || (bid_or_ask == 'bid' && p2.greaterThan(p1)) + @insertRow(book, $row, template, + price: order[0], volume: order[1], index: j) + j += 1 + else if p1.equals(p2) + @updateRow($row, order, j, v1, v2) + i += 1 + j += 1 + else + $row.addClass 'obsolete' + i += 1 + else if row + $row.addClass 'obsolete' + i += 1 + else if order + @appendRow(book, template, + price: order[0], volume: order[1], index: j) + j += 1 + else + break + + @clearMarkers = (book) -> + book.find('tr.new').removeClass('new') + book.find('tr.text-up').removeClass('text-up') + book.find('tr.text-down').removeClass('text-down') + + obsolete = book.find('tr.obsolete') + obsolete_divs = book.find('tr.obsolete div') + obsolete_divs.slideUp 'slow', -> + obsolete.remove() + + @updateOrders = (table, orders, bid_or_ask) -> + book = @select("#{bid_or_ask}BookSel") + + @mergeUpdate bid_or_ask, book, orders, JST["templates/order_book_#{bid_or_ask}"] + + book.find("tr.new div").slideDown('slow') + setTimeout => + @clearMarkers(@select("#{bid_or_ask}BookSel")) + , 900 + + @computeDeep = (event, orders) -> + index = Number $(event.currentTarget).data('order') + orders = _.take(orders, index + 1) + + volume_fun = (memo, num) -> memo.plus(BigNumber(num[1])) + volume = _.reduce(orders, volume_fun, BigNumber(0)) + price = BigNumber(_.last(orders)[0]) + origVolume = _.last(orders)[1] + + {price: price, volume: volume, origVolume: origVolume} + + @placeOrder = (target, data) -> + @trigger target, 'place_order::input::price', data + @trigger target, 'place_order::input::volume', data + + @after 'initialize', -> + @on document, 'market::order_book::update', @update + + @on @select('fade_toggle_depth'), 'click', => + @trigger 'market::depth::fade_toggle' + + $('.asks').on 'click', 'tr', (e) => + i = $(e.target).closest('tr').data('order') + @placeOrder $('#bid_entry'), _.extend(@computeDeep(e, gon.asks), type: 'ask') + @placeOrder $('#ask_entry'), {price: BigNumber(gon.asks[i][0]), volume: BigNumber(gon.asks[i][1])} + + $('.bids').on 'click', 'tr', (e) => + i = $(e.target).closest('tr').data('order') + @placeOrder $('#ask_entry'), _.extend(@computeDeep(e, gon.bids), type: 'bid') + @placeOrder $('#bid_entry'), {price: BigNumber(gon.bids[i][0]), volume: BigNumber(gon.bids[i][1])} diff --git a/app/assets/javascripts/component_ui/order_price.js.coffee b/app/assets/javascripts/component_ui/order_price.js.coffee new file mode 100755 index 00000000..688181dd --- /dev/null +++ b/app/assets/javascripts/component_ui/order_price.js.coffee @@ -0,0 +1,32 @@ +@OrderPriceUI = flight.component -> + flight.compose.mixin @, [OrderInputMixin] + + @attributes + precision: gon.market.bid.fixed + variables: + input: 'price' + known: 'volume' + output: 'total' + + @getLastPrice = -> + Number gon.ticker.last + + @toggleAlert = (event) -> + lastPrice = @getLastPrice() + + switch + when !@value + @trigger 'place_order::price_alert::hide' + when @value > (lastPrice * 1.1) + @trigger 'place_order::price_alert::show', {label: 'price_high'} + when @value < (lastPrice * 0.9) + @trigger 'place_order::price_alert::show', {label: 'price_low'} + else + @trigger 'place_order::price_alert::hide' + + @onOutput = (event, order) -> + price = order.total.div order.volume + @$node.val price + + @after 'initialize', -> + @on 'focusout', @toggleAlert diff --git a/app/assets/javascripts/component_ui/order_total.js.coffee b/app/assets/javascripts/component_ui/order_total.js.coffee new file mode 100755 index 00000000..c3ab7b66 --- /dev/null +++ b/app/assets/javascripts/component_ui/order_total.js.coffee @@ -0,0 +1,18 @@ +@OrderTotalUI = flight.component -> + flight.compose.mixin @, [OrderInputMixin] + + @attributes + precision: gon.market.bid.fixed + variables: + input: 'total' + known: 'price' + output: 'volume' + + @onOutput = (event, order) -> + total = order.price.times order.volume + + @changeOrder @value unless @validateRange(total) + @setInputValue @value + + order.total = @value + @trigger 'place_order::order::updated', order diff --git a/app/assets/javascripts/component_ui/order_volume.js.coffee b/app/assets/javascripts/component_ui/order_volume.js.coffee new file mode 100755 index 00000000..9fdfd9f1 --- /dev/null +++ b/app/assets/javascripts/component_ui/order_volume.js.coffee @@ -0,0 +1,19 @@ +@OrderVolumeUI = flight.component -> + flight.compose.mixin @, [OrderInputMixin] + + @attributes + precision: gon.market.ask.fixed + variables: + input: 'volume' + known: 'price' + output: 'total' + + @onOutput = (event, order) -> + return if order.price.equals(0) + volume = order.total.div order.price + + @changeOrder @value unless @validateRange(volume) + @setInputValue @value + + order.volume = @value + @trigger 'place_order::order::updated', order diff --git a/app/assets/javascripts/component_ui/place_order.js.coffee b/app/assets/javascripts/component_ui/place_order.js.coffee new file mode 100755 index 00000000..6b4ffef5 --- /dev/null +++ b/app/assets/javascripts/component_ui/place_order.js.coffee @@ -0,0 +1,149 @@ +@PlaceOrderUI = flight.component -> + @attributes + formSel: 'form' + successSel: '.status-success' + infoSel: '.status-info' + dangerSel: '.status-danger' + priceAlertSel: '.hint-price-disadvantage' + positionsLabelSel: '.hint-positions' + + priceSel: 'input[id$=price]' + volumeSel: 'input[id$=volume]' + totalSel: 'input[id$=total]' + + currentBalanceSel: 'span.current-balance' + submitButton: ':submit' + + @panelType = -> + switch @$node.attr('id') + when 'bid_entry' then 'bid' + when 'ask_entry' then 'ask' + + @cleanMsg = -> + @select('successSel').text('') + @select('infoSel').text('') + @select('dangerSel').text('') + + @resetForm = (event) -> + @trigger 'place_order::reset::price' + @trigger 'place_order::reset::volume' + @trigger 'place_order::reset::total' + @priceAlertHide() + + @disableSubmit = -> + @select('submitButton').addClass('disabled').attr('disabled', 'disabled') + + @enableSubmit = -> + @select('submitButton').removeClass('disabled').removeAttr('disabled') + + @confirmDialogMsg = -> + confirmType = @select('submitButton').text() + price = @select('priceSel').val() + volume = @select('volumeSel').val() + sum = @select('totalSel').val() + """ + #{gon.i18n.place_order.confirm_submit} "#{confirmType}"? + + #{gon.i18n.place_order.price}: #{price} + #{gon.i18n.place_order.volume}: #{volume} + #{gon.i18n.place_order.sum}: #{sum} + """ + + @beforeSend = (event, jqXHR) -> + if true #confirm(@confirmDialogMsg()) + @disableSubmit() + else + jqXHR.abort() + + @handleSuccess = (event, data) -> + @cleanMsg() + @select('successSel').append(JST["templates/hint_order_success"]({msg: data.message})).show() + @resetForm(event) + window.sfx_success() + @enableSubmit() + + @handleError = (event, data) -> + @cleanMsg() + ef_class = 'shake shake-constant hover-stop' + json = JSON.parse(data.responseText) + @select('dangerSel').append(JST["templates/hint_order_warning"]({msg: json.message})).show() + .addClass(ef_class).wait(500).removeClass(ef_class) + window.sfx_warning() + @enableSubmit() + + @getBalance = -> + BigNumber( @select('currentBalanceSel').data('balance') ) + + @getLastPrice = -> + BigNumber(gon.ticker.last) + + @allIn = (event)-> + switch @panelType() + when 'ask' + @trigger 'place_order::input::price', {price: @getLastPrice()} + @trigger 'place_order::input::volume', {volume: @getBalance()} + when 'bid' + @trigger 'place_order::input::price', {price: @getLastPrice()} + @trigger 'place_order::input::total', {total: @getBalance()} + + @refreshBalance = (event, data) -> + type = @panelType() + currency = gon.market[type].currency + balance = gon.accounts[currency]?.balance || 0 + + @select('currentBalanceSel').data('balance', balance) + @select('currentBalanceSel').text(formatter.fix(type, balance)) + + @trigger 'place_order::balance::change', balance: BigNumber(balance) + @trigger "place_order::max::#{@usedInput}", max: BigNumber(balance) + + @updateAvailable = (event, order) -> + type = @panelType() + node = @select('currentBalanceSel') + + order[@usedInput] = 0 unless order[@usedInput] + available = formatter.fix type, @getBalance().minus(order[@usedInput]) + + if BigNumber(available).equals(0) + @select('positionsLabelSel').hide().text(gon.i18n.place_order["full_#{type}"]).fadeIn() + else + @select('positionsLabelSel').fadeOut().text('') + node.text(available) + + @priceAlertHide = (event) -> + @select('priceAlertSel').fadeOut -> + $(@).text('') + + @priceAlertShow = (event, data) -> + @select('priceAlertSel') + .hide().text(gon.i18n.place_order[data.label]).fadeIn() + + @clear = (e) -> + @resetForm(e) + @trigger 'place_order::focus::price' + + @after 'initialize', -> + type = @panelType() + + if type == 'ask' + @usedInput = 'volume' + else + @usedInput = 'total' + + PlaceOrderData.attachTo @$node + OrderPriceUI.attachTo @select('priceSel'), form: @$node, type: type + OrderVolumeUI.attachTo @select('volumeSel'), form: @$node, type: type + OrderTotalUI.attachTo @select('totalSel'), form: @$node, type: type + + @on 'place_order::price_alert::hide', @priceAlertHide + @on 'place_order::price_alert::show', @priceAlertShow + @on 'place_order::order::updated', @updateAvailable + @on 'place_order::clear', @clear + + @on document, 'account::update', @refreshBalance + + @on @select('formSel'), 'ajax:beforeSend', @beforeSend + @on @select('formSel'), 'ajax:success', @handleSuccess + @on @select('formSel'), 'ajax:error', @handleError + + @on @select('currentBalanceSel'), 'click', @allIn diff --git a/app/assets/javascripts/component_ui/push_button.js.coffee b/app/assets/javascripts/component_ui/push_button.js.coffee new file mode 100755 index 00000000..11be5dcc --- /dev/null +++ b/app/assets/javascripts/component_ui/push_button.js.coffee @@ -0,0 +1,10 @@ +@PushButton = flight.component -> + @attributes + buttons: '.type-toggle button' + + @setActiveButton = (event) -> + @select('buttons').removeClass('active') + $(event.target).closest('button').addClass('active') + + @after 'initialize', -> + @on @select('buttons'), 'click', @setActiveButton diff --git a/app/assets/javascripts/component_ui/range_switch.js.coffee b/app/assets/javascripts/component_ui/range_switch.js.coffee new file mode 100755 index 00000000..e69de29b diff --git a/app/assets/javascripts/component_ui/sms_auth_verify.js.coffee b/app/assets/javascripts/component_ui/sms_auth_verify.js.coffee new file mode 100755 index 00000000..72efeb6b --- /dev/null +++ b/app/assets/javascripts/component_ui/sms_auth_verify.js.coffee @@ -0,0 +1,63 @@ +@SmsAuthVerifyUI = flight.component -> + + @countDown = 0 + + @attributes + phoneNumberInput: '#token_sms_token_phone_number' + verifyCodeInput: '#token_sms_token_verify_code' + sendCodeButton: 'button[value=send_code]' + + @verifyPhoneNumber = (event, data) -> + @select('phoneNumberInput').parent().removeClass 'has-error' + + if @select('phoneNumberInput').val() is "" + @select('phoneNumberInput').parent().addClass 'has-error' + event.preventDefault() + else + setTimeout => + @countDownSendCodeButton() + , 0 + + @countDownSendCodeButton = -> + origName = @select('sendCodeButton').data('orig-name') + altName = @select('sendCodeButton').data('alt-name') + @countDown = 30 + + @select('sendCodeButton').attr('disabled', 'disabled').addClass('disabled') + countDownTimer = => + setTimeout => + if @countDown isnt 0 + @countDown-- + @select('sendCodeButton').text(altName.replace('COUNT', @countDown)) + countDownTimer() + else + @select('sendCodeButton').removeAttr('disabled').removeClass('disabled').text(origName) + , 1000 + countDownTimer() + + @beforeSend = (event, jqXHR, settings) -> + return if settings.data.match 'send_code' + + input = @select('verifyCodeInput') + input.parent().removeClass 'has-error' + if input.val() is "" + input.parent().addClass 'has-error' + jqXHR.abort() + + @handleSuccess = (event, text, status, jqXHR) -> + data = JSON.parse(text) + if data.reload + window.location.reload() + @trigger 'flash:notice', msg: data.text + + @handleError = (event, jqXHR, status, error) -> + data = JSON.parse(jqXHR.responseText) + @countDown = 0 + @trigger 'flash:alert', msg: data.text + + @after 'initialize', -> + @on @select('sendCodeButton'), 'click', @verifyPhoneNumber + @on 'ajax:beforeSend', @beforeSend + @on 'ajax:success', @handleSuccess + @on 'ajax:error', @handleError + diff --git a/app/assets/javascripts/component_ui/switch.js.coffee b/app/assets/javascripts/component_ui/switch.js.coffee new file mode 100755 index 00000000..d4eda838 --- /dev/null +++ b/app/assets/javascripts/component_ui/switch.js.coffee @@ -0,0 +1,35 @@ +@SwitchUI = flight.component -> + @attributes + switch: 'li > a' + + @getX = -> + if Cookies.get(@name()) + Cookies.get(@name()) + else + @setX(@defaultX()) + + @setX = (x) -> + Cookies.set(@name(), x) + return x + + @name = -> + @$node.attr('id') + + @defaultX = -> + @$node.data('x') + + @init = (event, data) -> + @$node.find("[data-x=#{@getX()}]").click() + + @after 'initialize', -> + @on @select('switch'), 'click', (e) => + @select('switch').removeClass('active') + $(e.currentTarget).addClass('active') + + x = $(e.currentTarget).data('x') + @setX(x) + + @trigger "switch::#{@name()}", {x: x} + + @on document, "switch::#{@name()}::init", @init + @init() diff --git a/app/assets/javascripts/component_ui/two_factor_auth.js.coffee b/app/assets/javascripts/component_ui/two_factor_auth.js.coffee new file mode 100755 index 00000000..594e1084 --- /dev/null +++ b/app/assets/javascripts/component_ui/two_factor_auth.js.coffee @@ -0,0 +1,64 @@ +@TwoFactorAuth = flight.component -> + @attributes + switchName: 'span.switch-name' + switchItem: '.dropdown-menu a' + switchItemApp: '.dropdown-menu a[data-type="app"]' + switchItemSms: '.dropdown-menu a[data-type="sms"]' + sendCodeButtonContainer: '.send-code-button' + sendCodeButton: 'button[value=send_code]' + authType: '.two_factor_auth_type' + appHint: 'span.hint.app' + smsHint: 'span.hint.sms' + chapterWrap: '.captcha-wrap' + + @setActiveItem = (event) -> + switch $(event.target).data('type') + when 'app' then @switchToApp() + when 'sms' then @switchToSms() + + @switchToApp = -> + @select('switchName').text @select('switchItemApp').text() + @select('sendCodeButtonContainer').addClass('hide') + @select('authType').val('app') + @select('smsHint').addClass('hide') + @select('appHint').removeClass('hide') + + @switchToSms = -> + @select('switchName').text @select('switchItemSms').text() + @select('sendCodeButtonContainer').removeClass('hide') + @select('authType').val('sms') + @select('smsHint').removeClass('hide') + @select('appHint').addClass('hide') + + @countDownSendCodeButton = -> + origName = @select('sendCodeButton').data('orig-name') + altName = @select('sendCodeButton').data('alt-name') + countDown = 30 + + @select('sendCodeButton').attr('disabled', 'disabled').addClass('disabled') + countDownTimer = => + setTimeout => + if countDown isnt 0 + countDown-- + @select('sendCodeButton').text(altName.replace('COUNT', countDown)) + countDownTimer() + else + @select('sendCodeButton').removeAttr('disabled').removeClass('disabled').text(origName) + , 1000 + countDownTimer() + + @sendCode = (event) -> + event.preventDefault() + + @countDownSendCodeButton() + $.get('/two_factors/sms?refresh=true') + + @checkCaptchaRequired = -> + @select('chapterWrap').load '/two_factors/app', (html) -> $(@).html(html) + + @after 'initialize', -> + @checkCaptchaRequired() + $.subscribe 'withdraw:form:submitted', => @checkCaptchaRequired() + @on @select('switchItem'), 'click', @setActiveItem + @on @select('sendCodeButton'), 'click', @sendCode + diff --git a/app/assets/javascripts/funds.js.coffee b/app/assets/javascripts/funds.js.coffee new file mode 100755 index 00000000..9ad8067e --- /dev/null +++ b/app/assets/javascripts/funds.js.coffee @@ -0,0 +1,35 @@ +#= require jquery +#= require pusher.min + +#= require ./lib/tiny-pubsub +#= require angular +#= require angular-resource +#= require ./lib/angular-ui-router +#= require ./lib/peatio_model +#= require ./lib/ajax + +#= require ./lib/pusher_connection +#= require ./lib/pusher_subscriber + +#= require ngDialog/ngDialog + +#= require_self +#= require ./funds/funds + +#= require es5-shim.min +#= require es5-sham.min +#= require jquery_ujs +#= require bootstrap +# +#= require bignumber +#= require moment +#= require ZeroClipboard +#= require underscore +#= require flight.min +#= require list +#= require qrcode + +#= require_tree ./helpers +#= require_tree ./component_mixin +#= require_tree ./component_data +#= require_tree ./component_ui diff --git a/app/assets/javascripts/funds/controllers/deposit_history_controller.js.coffee b/app/assets/javascripts/funds/controllers/deposit_history_controller.js.coffee new file mode 100755 index 00000000..ffb2a5ba --- /dev/null +++ b/app/assets/javascripts/funds/controllers/deposit_history_controller.js.coffee @@ -0,0 +1,28 @@ +app.controller 'DepositHistoryController', ($scope, $stateParams, $http) -> + ctrl = @ + $scope.predicate = '-id' + @currency = $stateParams.currency + @account = Account.findBy('currency', @currency) + @deposits = @account.deposits().slice(0, 3) + @newRecord = (deposit) -> + if deposit.aasm_state == "submitting" then true else false + + @noDeposit = -> + @deposits.length == 0 + + @refresh = -> + @deposits = @account.deposits().slice(0, 3) + $scope.$apply() + + @cancelDeposit = (deposit) -> + deposit_channel = DepositChannel.findBy('currency', deposit.currency) + $http.delete("/deposits/#{deposit_channel.resource_name}/#{deposit.id}") + .error (responseText) -> + $.publish 'flash', { message: responseText } + + @canCancel = (state) -> + ['submitting'].indexOf(state) > -1 + + do @event = -> + Deposit.bind "create update destroy", -> + ctrl.refresh() diff --git a/app/assets/javascripts/funds/controllers/deposits_controller.js.coffee b/app/assets/javascripts/funds/controllers/deposits_controller.js.coffee new file mode 100755 index 00000000..3e8cde85 --- /dev/null +++ b/app/assets/javascripts/funds/controllers/deposits_controller.js.coffee @@ -0,0 +1,54 @@ +app.controller 'DepositsController', ['$scope', '$stateParams', '$http', '$filter', '$gon', 'ngDialog', ($scope, $stateParams, $http, $filter, $gon, ngDialog) -> + @deposit = {} + $scope.currency = $stateParams.currency + $scope.current_user = current_user = $gon.current_user + $scope.name = current_user.name + $scope.fund_sources = $gon.fund_sources + $scope.account = Account.findBy('currency', $scope.currency) + $scope.deposit_channel = DepositChannel.findBy('currency', $scope.currency) + + @createDeposit = (currency) -> + depositCtrl = @ + deposit_channel = DepositChannel.findBy('currency', currency) + account = deposit_channel.account() + + data = { account_id: account.id, member_id: current_user.id, currency: currency, amount: @deposit.amount, fund_source: @deposit.fund_source } + + $('.form-submit > input').attr('disabled', 'disabled') + + $http.post("/deposits/#{deposit_channel.resource_name}", { deposit: data}) + .error (responseText) -> + $.publish 'flash', {message: responseText } + .finally -> + depositCtrl.deposit = {} + $('.form-submit > input').removeAttr('disabled') + + $scope.openFundSourceManagerPanel = -> + ngDialog.open + template: '/templates/fund_sources/bank.html' + controller: 'FundSourcesController' + className: 'ngdialog-theme-default custom-width' + data: {currency: $scope.currency} + + $scope.genAddress = (resource_name) -> + ngDialog.openConfirm + template: '/templates/shared/confirm_dialog.html' + data: {content: $filter('t')('funds.deposit_coin.confirm_gen_new_address')} + .then -> + $("a#new_address").html('...') + $("a#new_address").attr('disabled', 'disabled') + + $http.post("/deposits/#{resource_name}/gen_address", {}) + .error (responseText) -> + $.publish 'flash', {message: responseText } + .finally -> + $("a#new_address").html(I18n.t("funds.deposit_coin.new_address")) + $("a#new_address").attr('disabled', 'disabled') + + + $scope.$watch (-> $scope.account.deposit_address), -> + setTimeout(-> + $.publish 'deposit_address:create' + , 1000) + +] diff --git a/app/assets/javascripts/funds/controllers/fund_sources_controller.js.coffee b/app/assets/javascripts/funds/controllers/fund_sources_controller.js.coffee new file mode 100755 index 00000000..2c706d35 --- /dev/null +++ b/app/assets/javascripts/funds/controllers/fund_sources_controller.js.coffee @@ -0,0 +1,30 @@ +app.controller 'FundSourcesController', ['$scope', '$gon', 'fundSourceService', ($scope, $gon, fundSourceService) -> + + $scope.banks = $gon.banks + $scope.currency = currency = $scope.ngDialogData.currency + + $scope.fund_sources = -> + fundSourceService.filterBy currency:currency + + $scope.defaultFundSource = -> + fundSourceService.defaultFundSource currency:currency + + $scope.add = -> + uid = $scope.uid.trim() if angular.isString($scope.uid) + extra = $scope.extra.trim() if angular.isString($scope.extra) + + return if not uid + return if not extra + + data = uid: uid, extra: extra, currency: currency + fundSourceService.create data, -> + $scope.uid = "" + $scope.extra = "" if currency isnt $gon.fiat_currency + + $scope.remove = (fund_source) -> + fundSourceService.remove fund_source + + $scope.makeDefault = (fund_source) -> + fundSourceService.update fund_source + +] diff --git a/app/assets/javascripts/funds/controllers/withdraw_hitstory_controller.js.coffee b/app/assets/javascripts/funds/controllers/withdraw_hitstory_controller.js.coffee new file mode 100755 index 00000000..5af84726 --- /dev/null +++ b/app/assets/javascripts/funds/controllers/withdraw_hitstory_controller.js.coffee @@ -0,0 +1,28 @@ +app.controller 'WithdrawHistoryController', ($scope, $stateParams, $http) -> + ctrl = @ + $scope.predicate = '-id' + @currency = $stateParams.currency + @account = Account.findBy('currency', @currency) + @withdraws = @account.withdraws().slice(0, 3) + @newRecord = (withdraw) -> + if withdraw.aasm_state == "submitting" then true else false + + @noWithdraw = -> + @withdraws.length == 0 + + @refresh = -> + ctrl.withdraws = ctrl.account.withdraws().slice(0, 3) + $scope.$apply() + + @canCancel = (state) -> + ['submitting', 'submitted', 'accepted'].indexOf(state) > -1 + + @cancelWithdraw = (withdraw) -> + withdraw_channel = WithdrawChannel.findBy('currency', withdraw.currency) + $http.delete("/withdraws/#{withdraw_channel.resource_name}/#{withdraw.id}") + .error (responseText) -> + $.publish 'flash', { message: responseText } + + do @event = -> + Withdraw.bind "create update destroy", -> + ctrl.refresh() diff --git a/app/assets/javascripts/funds/controllers/withdraws_controller.js.coffee b/app/assets/javascripts/funds/controllers/withdraws_controller.js.coffee new file mode 100755 index 00000000..a78f0501 --- /dev/null +++ b/app/assets/javascripts/funds/controllers/withdraws_controller.js.coffee @@ -0,0 +1,99 @@ +app.controller 'WithdrawsController', ['$scope', '$stateParams', '$http', '$gon', 'fundSourceService', 'ngDialog', ($scope, $stateParams, $http, $gon, fundSourceService, ngDialog) -> + + _selectedFundSourceId = null + _selectedFundSourceIdInList = (list) -> + for fs in list + return true if fs.id is _selectedFundSourceId + return false + + $scope.currency = currency = $stateParams.currency + $scope.current_user = current_user = $gon.current_user + $scope.name = current_user.name + $scope.account = Account.findBy('currency', $scope.currency) + $scope.balance = $scope.account.balance + $scope.withdraw_channel = WithdrawChannel.findBy('currency', $scope.currency) + + $scope.selected_fund_source = (newId) -> + if angular.isDefined(newId) + _selectedFundSourceId = newId + else + _selectedFundSourceId + + $scope.fund_sources = -> + fund_sources = fundSourceService.filterBy currency:currency + # reset selected fundSource after add new one or remove previous one + if not _selectedFundSourceId or not _selectedFundSourceIdInList(fund_sources) + $scope.selected_fund_source fund_sources[0].id if fund_sources.length + fund_sources + + # set defaultFundSource as selected + defaultFundSource = fundSourceService.defaultFundSource currency:currency + if defaultFundSource + _selectedFundSourceId = defaultFundSource.id + else + fund_sources = $scope.fund_sources() + _selectedFundSourceId = fund_sources[0].id if fund_sources.length + + # set current default fundSource as selected + $scope.$watch -> + fundSourceService.defaultFundSource currency:currency + , (defaultFundSource) -> + $scope.selected_fund_source defaultFundSource.id if defaultFundSource + + @withdraw = {} + @createWithdraw = (currency) -> + withdraw_channel = WithdrawChannel.findBy('currency', currency) + account = withdraw_channel.account() + data = { withdraw: { member_id: current_user.id, currency: currency, sum: @withdraw.sum, fund_source: _selectedFundSourceId } } + + if current_user.app_activated or current_user.sms_activated + type = $('.two_factor_auth_type').val() + otp = $("#two_factor_otp").val() + + data.two_factor = { type: type, otp: otp } + data.captcha = $('#captcha').val() + data.captcha_key = $('#captcha_key').val() + + $('.form-submit > input').attr('disabled', 'disabled') + + $http.post("/withdraws/#{withdraw_channel.resource_name}", data) + .error (responseText) -> + $.publish 'flash', { message: responseText } + .finally => + @withdraw = {} + $('.form-submit > input').removeAttr('disabled') + $.publish 'withdraw:form:submitted' + + @withdrawAll = -> + @withdraw.sum = Number($scope.account.balance) + + $scope.openFundSourceManagerPanel = -> + if $scope.currency == $gon.fiat_currency + template = '/templates/fund_sources/bank.html' + className = 'ngdialog-theme-default custom-width' + else + template = '/templates/fund_sources/coin.html' + className = 'ngdialog-theme-default custom-width coin' + + ngDialog.open + template:template + controller: 'FundSourcesController' + className: className + data: {currency: $scope.currency} + + $scope.sms_and_app_activated = -> + current_user.app_activated and current_user.sms_activated + + $scope.only_app_activated = -> + current_user.app_activated and !current_user.sms_activated + + $scope.only_sms_activated = -> + current_user.sms_activated and !current_user.app_activated + + + $scope.$watch (-> $scope.currency), -> + setTimeout(-> + $.publish "two_factor_init" + , 100) + +] \ No newline at end of file diff --git a/app/assets/javascripts/funds/directives/accounts_directives.js.coffee b/app/assets/javascripts/funds/directives/accounts_directives.js.coffee new file mode 100755 index 00000000..eceb515e --- /dev/null +++ b/app/assets/javascripts/funds/directives/accounts_directives.js.coffee @@ -0,0 +1,45 @@ +app.directive 'accounts', -> + return { + restrict: 'E' + templateUrl: '/templates/funds/accounts.html' + scope: { localValue: '=accounts' } + controller: ($scope, $state) -> + ctrl = @ + @state = $state + if window.location.hash == "" + @state.transitionTo("deposits.currency", {currency: Account.first().currency}) + + $scope.accounts = Account.all() + + # Might have a better way + # #/deposits/cny + @selectedCurrency = window.location.hash.split('/')[2] || Account.first().currency + @currentAction = window.location.hash.split('/')[1] || 'deposits' + $scope.currency = @selectedCurrency + + @isSelected = (currency) -> + @selectedCurrency == currency + + @isDeposit = -> + @currentAction == 'deposits' + + @isWithdraw = -> + @currentAction == 'withdraws' + + @deposit = (account) -> + ctrl.state.transitionTo("deposits.currency", {currency: account.currency}) + ctrl.selectedCurrency = account.currency + ctrl.currentAction = "deposits" + + @withdraw = (account) -> + ctrl.state.transitionTo("withdraws.currency", {currency: account.currency}) + ctrl.selectedCurrency = account.currency + ctrl.currentAction = "withdraws" + + do @event = -> + Account.bind "create update destroy", -> + $scope.$apply() + + controllerAs: 'accountsCtrl' + } + diff --git a/app/assets/javascripts/funds/events.js.coffee b/app/assets/javascripts/funds/events.js.coffee new file mode 100755 index 00000000..9cde4233 --- /dev/null +++ b/app/assets/javascripts/funds/events.js.coffee @@ -0,0 +1,51 @@ +$(window).load -> + + # clipboard + $.subscribe 'deposit_address:create', (event, data) -> + $('[data-clipboard-text], [data-clipboard-target]').each -> + zero = new ZeroClipboard $(@), forceHandCursor: true + + zero.on 'complete', -> + $(zero.htmlBridge) + .attr('title', gon.clipboard.done) + .tooltip('fixTitle') + .tooltip('show') + zero.on 'mouseout', -> + $(zero.htmlBridge) + .attr('title', gon.clipboard.click) + .tooltip('fixTitle') + + placement = $(@).data('placement') || 'bottom' + $(zero.htmlBridge).tooltip({title: gon.clipboard.click, placement: placement}) + + # qrcode + $.subscribe 'deposit_address:create', (event, data) -> + code = if data then data else $('#deposit_address').html() + + $("#qrcode").attr('data-text', code) + $("#qrcode").attr('title', code) + $('.qrcode-container').each (index, el) -> + $el = $(el) + $("#qrcode img").remove() + $("#qrcode canvas").remove() + + new QRCode el, + text: $("#qrcode").attr('data-text') + width: $el.data('width') + height: $el.data('height') + + $.publish 'deposit_address:create' + + # flash message + $.subscribe 'flash', (event, data) -> + $('.flash-messages').show() + $('#flash-content').html(data.message) + setTimeout(-> + $('.flash-messages').hide(1000) + , 10000) + + # init the two factor auth + $.subscribe 'two_factor_init', (event, data) -> + TwoFactorAuth.attachTo('.two-factor-auth-container') + + $.publish 'two_factor_init' diff --git a/app/assets/javascripts/funds/filters/precision_filters.js.coffee b/app/assets/javascripts/funds/filters/precision_filters.js.coffee new file mode 100755 index 00000000..a70ad6b3 --- /dev/null +++ b/app/assets/javascripts/funds/filters/precision_filters.js.coffee @@ -0,0 +1,3 @@ +angular.module('precisionFilters', []).filter 'round_down', -> + (number) -> + BigNumber(number).round(5, BigNumber.ROUND_DOWN).toF(5) diff --git a/app/assets/javascripts/funds/filters/text_filters.js.coffee b/app/assets/javascripts/funds/filters/text_filters.js.coffee new file mode 100755 index 00000000..084df1c3 --- /dev/null +++ b/app/assets/javascripts/funds/filters/text_filters.js.coffee @@ -0,0 +1,6 @@ +angular.module('textFilters', []).filter 'truncate', -> + (text, size) -> + if text.length > 20 + text.slice(0, size) + '...' + else + text diff --git a/app/assets/javascripts/funds/filters/translate_filters.js.coffee b/app/assets/javascripts/funds/filters/translate_filters.js.coffee new file mode 100755 index 00000000..930f8545 --- /dev/null +++ b/app/assets/javascripts/funds/filters/translate_filters.js.coffee @@ -0,0 +1,3 @@ +angular.module('translateFilters', []).filter 't', -> + (key, args={}) -> + I18n.t(key, args) diff --git a/app/assets/javascripts/funds/funds.js.coffee b/app/assets/javascripts/funds/funds.js.coffee new file mode 100755 index 00000000..6e8701f9 --- /dev/null +++ b/app/assets/javascripts/funds/funds.js.coffee @@ -0,0 +1,22 @@ +#= require_tree ./models +#= require_tree ./filters +#= require_self +#= require_tree ./services +#= require_tree ./directives +#= require_tree ./controllers +#= require ./router +#= require ./events + +$ -> + window.pusher_subscriber = new PusherSubscriber() + +Member.initData [gon.current_user] +DepositChannel.initData gon.deposit_channels +WithdrawChannel.initData gon.withdraw_channels +Deposit.initData gon.deposits +Account.initData gon.accounts +Currency.initData gon.currencies +Withdraw.initData gon.withdraws + +window.app = app = angular.module 'funds', ["ui.router", "ngResource", "translateFilters", "textFilters", "precisionFilters", "ngDialog"] + diff --git a/app/assets/javascripts/funds/models/.gitkeep b/app/assets/javascripts/funds/models/.gitkeep new file mode 100755 index 00000000..e69de29b diff --git a/app/assets/javascripts/funds/models/account.js.coffee b/app/assets/javascripts/funds/models/account.js.coffee new file mode 100755 index 00000000..ac10972d --- /dev/null +++ b/app/assets/javascripts/funds/models/account.js.coffee @@ -0,0 +1,30 @@ +class Account extends PeatioModel.Model + @configure 'Account', 'member_id', 'currency', 'balance', 'locked', 'created_at', 'updated_at', 'in', 'out', 'deposit_address', 'name_text' + + @initData: (records) -> + PeatioModel.Ajax.disable -> + $.each records, (idx, record) -> + Account.create(record) + + deposit_channels: -> + DepositChannel.findAllBy 'currency', @currency + + withdraw_channels: -> + WithdrawChannel.findAllBy 'currency', @currency + + deposit_channel: -> + DepositChannel.findBy 'currency', @currency + + deposits: -> + _.sortBy(Deposit.findAllBy('account_id', @id), (d) -> d.id).reverse() + + withdraws: -> + _.sortBy(Withdraw.findAllBy('account_id', @id), (d) -> d.id).reverse() + + topDeposits: -> + @deposits().reverse().slice(0,3) + + topWithdraws: -> + @withdraws().reverse().slice(0,3) + +window.Account = Account diff --git a/app/assets/javascripts/funds/models/currency.js.coffee b/app/assets/javascripts/funds/models/currency.js.coffee new file mode 100755 index 00000000..dbdd7cda --- /dev/null +++ b/app/assets/javascripts/funds/models/currency.js.coffee @@ -0,0 +1,9 @@ +class Currency extends PeatioModel.Model + @configure 'Currency', 'key', 'code', 'coin', 'blockchain' + + @initData: (records) -> + PeatioModel.Ajax.disable -> + $.each records, (idx, record) -> + currency = Currency.create(record.attributes) + +window.Currency = Currency diff --git a/app/assets/javascripts/funds/models/deposit.js.coffee b/app/assets/javascripts/funds/models/deposit.js.coffee new file mode 100755 index 00000000..ecf44cf9 --- /dev/null +++ b/app/assets/javascripts/funds/models/deposit.js.coffee @@ -0,0 +1,17 @@ +class Deposit extends PeatioModel.Model + @configure 'Deposit', 'account_id', 'member_id', 'currency', 'amount', 'fee', 'fund_uid', 'fund_extra', + 'txid', 'state', 'aasm_state', 'created_at', 'updated_at', 'done_at', 'type', 'confirmations', 'is_submitting', 'blockchain_url', 'txid_desc' + + constructor: -> + super + @is_submitting = @aasm_state == "submitting" + + @initData: (records) -> + PeatioModel.Ajax.disable -> + $.each records, (idx, record) -> + Deposit.create(record) + +window.Deposit = Deposit + + + diff --git a/app/assets/javascripts/funds/models/deposit_channel.js.coffee b/app/assets/javascripts/funds/models/deposit_channel.js.coffee new file mode 100755 index 00000000..08b03918 --- /dev/null +++ b/app/assets/javascripts/funds/models/deposit_channel.js.coffee @@ -0,0 +1,13 @@ +class DepositChannel extends PeatioModel.Model + @configure 'DepositChannel', 'key', 'currency', 'min_confirm', 'max_confirm', 'bank_accounts', 'resource_name' + + @initData: (records) -> + PeatioModel.Ajax.disable -> + $.each records, (idx, record) -> + DepositChannel.create(record) + + account: -> + Account.findBy('currency', @currency) + +window.DepositChannel = DepositChannel + diff --git a/app/assets/javascripts/funds/models/member.js.coffee b/app/assets/javascripts/funds/models/member.js.coffee new file mode 100755 index 00000000..ae7e8a7b --- /dev/null +++ b/app/assets/javascripts/funds/models/member.js.coffee @@ -0,0 +1,10 @@ +class Member extends PeatioModel.Model + @configure 'Member', 'sn', 'display_name', 'created_at', 'updated_at', 'state', + 'country_code', 'phone_number', 'name', 'app_activated', 'sms_activated' + + @initData: (records) -> + PeatioModel.Ajax.disable -> + $.each records, (idx, record) -> + Member.create(record) + +window.Member = Member diff --git a/app/assets/javascripts/funds/models/withdraw.js.coffee b/app/assets/javascripts/funds/models/withdraw.js.coffee new file mode 100755 index 00000000..2a54c531 --- /dev/null +++ b/app/assets/javascripts/funds/models/withdraw.js.coffee @@ -0,0 +1,22 @@ +class Withdraw extends PeatioModel.Model + @configure 'Withdraw', 'sn', 'account_id', 'member_id', 'currency', 'amount', 'fee', 'fund_uid', 'fund_extra', + 'created_at', 'updated_at', 'done_at', 'txid', 'blockchain_url', 'aasm_state', 'sum', 'type', 'is_submitting' + + constructor: -> + super + @is_submitting = @aasm_state == "submitting" + + @initData: (records) -> + PeatioModel.Ajax.disable -> + $.each records, (idx, record) -> + Withdraw.create(record) + + afterScope: -> + "#{@pathName()}" + + pathName: -> + switch @currency + when 'cny' then 'banks' + when 'btc' then 'satoshis' + +window.Withdraw = Withdraw diff --git a/app/assets/javascripts/funds/models/withdraw_channel.js.coffee b/app/assets/javascripts/funds/models/withdraw_channel.js.coffee new file mode 100755 index 00000000..f3827a27 --- /dev/null +++ b/app/assets/javascripts/funds/models/withdraw_channel.js.coffee @@ -0,0 +1,12 @@ +class WithdrawChannel extends PeatioModel.Model + @configure 'WithdrawChannel', 'key', 'currency', 'resource_name' + + @initData: (records) -> + PeatioModel.Ajax.disable -> + $.each records, (idx, record) -> + WithdrawChannel.create(record) + + account: -> + Account.findBy('currency', @currency) + +window.WithdrawChannel = WithdrawChannel diff --git a/app/assets/javascripts/funds/router.js.coffee b/app/assets/javascripts/funds/router.js.coffee new file mode 100755 index 00000000..7dc3d783 --- /dev/null +++ b/app/assets/javascripts/funds/router.js.coffee @@ -0,0 +1,22 @@ +app.config ($stateProvider, $urlRouterProvider) -> + $stateProvider + .state('deposits', { + url: '/deposits' + templateUrl: "/templates/funds/deposits.html" + }) + .state('deposits.currency', { + url: "/:currency" + templateUrl: "/templates/funds/deposit.html" + controller: 'DepositsController' + currentAction: 'deposit' + }) + .state('withdraws', { + url: '/withdraws' + templateUrl: "/templates/funds/withdraws.html" + }) + .state('withdraws.currency', { + url: "/:currency" + templateUrl: "/templates/funds/withdraw.html" + controller: 'WithdrawsController' + currentAction: "withdraw" + }) diff --git a/app/assets/javascripts/funds/services/account_service.js.coffee b/app/assets/javascripts/funds/services/account_service.js.coffee new file mode 100755 index 00000000..e3d9d949 --- /dev/null +++ b/app/assets/javascripts/funds/services/account_service.js.coffee @@ -0,0 +1,10 @@ +app.service 'accountService', ['$filter', '$gon', ($filter, $gon) -> + + filterBy: (filter) -> + $filter('filter')($gon.accounts, filter) + + findBy: (filter) -> + result = @filterBy filter + if result.length then result[0] else null + +] diff --git a/app/assets/javascripts/funds/services/fund_source_service.js.coffee b/app/assets/javascripts/funds/services/fund_source_service.js.coffee new file mode 100755 index 00000000..f88db689 --- /dev/null +++ b/app/assets/javascripts/funds/services/fund_source_service.js.coffee @@ -0,0 +1,39 @@ +app.service 'fundSourceService', ['$filter', '$gon', '$resource', 'accountService', ($filter, $gon, $resource, accountService) -> + + resource = $resource '/fund_sources/:id', + {id: '@id'} + {update: { method: 'PUT' }} + + filterBy: (filter) -> + $filter('filter')($gon.fund_sources, filter) + + findBy: (filter) -> + result = @filterBy filter + if result.length then result[0] else null + + defaultFundSource: (filter) -> + account = accountService.findBy filter + return null if not account + @findBy id: account.default_withdraw_fund_source + + create: (data, afterCreate) -> + resource.save data, (fund_source) => + $gon.fund_sources.push fund_source + afterCreate(fund_source) if afterCreate + + update: (fund_source, afterUpdate) -> + # Change default_withdraw_fund_source immediately, + # Do not wait for server side response + account = accountService.findBy currency:fund_source.currency + return null if not account + account.default_withdraw_fund_source = fund_source.id + + resource.update id: fund_source.id, => + afterUpdate() if afterUpdate + + remove: (fund_source, afterRemove) -> + resource.remove id: fund_source.id, => + $gon.fund_sources.splice $gon.fund_sources.indexOf(fund_source), 1 + afterRemove() if afterRemove + +] \ No newline at end of file diff --git a/app/assets/javascripts/funds/services/gon.js.coffee b/app/assets/javascripts/funds/services/gon.js.coffee new file mode 100755 index 00000000..64b18b98 --- /dev/null +++ b/app/assets/javascripts/funds/services/gon.js.coffee @@ -0,0 +1 @@ +app.factory '$gon', ['$window', (win)-> win.gon] diff --git a/app/assets/javascripts/funds/services/ng_dialog.js.coffee b/app/assets/javascripts/funds/services/ng_dialog.js.coffee new file mode 100755 index 00000000..05f04903 --- /dev/null +++ b/app/assets/javascripts/funds/services/ng_dialog.js.coffee @@ -0,0 +1,7 @@ +app.config ['ngDialogProvider', (ngDialogProvider) -> + ngDialogProvider.setDefaults + closeByDocument: false + closeByEscape: false + trapFocus: false + cache: false +] diff --git a/app/assets/javascripts/helpers/.gitkeep b/app/assets/javascripts/helpers/.gitkeep new file mode 100755 index 00000000..e69de29b diff --git a/app/assets/javascripts/helpers/formatter.js.coffee b/app/assets/javascripts/helpers/formatter.js.coffee new file mode 100755 index 00000000..541dd6ce --- /dev/null +++ b/app/assets/javascripts/helpers/formatter.js.coffee @@ -0,0 +1,114 @@ +class Formatter + round: (str, fixed) -> + BigNumber(str).round(fixed, BigNumber.ROUND_HALF_UP).toF(fixed) + + fix: (type, str) -> + str = '0' unless $.isNumeric(str) + if type is 'ask' + @.round(str, gon.market.ask.fixed) + else if type is 'bid' + @.round(str, gon.market.bid.fixed) + + fixAsk: (str) -> + @.fix('ask', str) + + fixBid: (str) -> + @.fix('bid', str) + + fixPriceGroup: (str) -> + if gon.market.price_group_fixed + str = '0' unless $.isNumeric(str) + @.round(str, gon.market.price_group_fixed) + else + @fixBid(str) + + check_trend: (type) -> + if type == 'up' or type == 'buy' or type == 'bid' or type == true + true + else if type == 'down' or type == "sell" or type = 'ask' or type == false + false + else + throw "unknown trend smybol #{type}" + + market: (base, quote) -> + "#{base.toUpperCase()}/#{quote.toUpperCase()}" + + market_url: (market, order_id) -> + if order_id? + "/markets/#{market}/orders/#{order_id}" + else + "/markets/#{market}" + + trade: (ask_or_bid) -> + gon.i18n[ask_or_bid] + + short_trade: (type) -> + if type == 'buy' or type == 'bid' + gon.i18n['bid'] + else if type == "sell" or type = 'ask' + gon.i18n['ask'] + else + 'n/a' + + trade_time: (timestamp) -> + m = moment.unix(timestamp) + "#{m.format("HH:mm")}#{m.format(":ss")}" + + fulltime: (timestamp) -> + m = moment.unix(timestamp) + "#{m.format("MM/DD HH:mm")}" + + mask_price: (price) -> + price.replace(/\..*/, "$&") + + mask_fixed_price: (price) -> + @mask_price @fixPriceGroup(price) + + ticker_fill: ['', '0', '00', '000', '0000', '00000', '000000', '0000000', '00000000'] + ticker_price: (price, fillTo=6) -> + [left, right] = price.split('.') + if fill = @ticker_fill[fillTo-right.length] + "#{left}.#{right}#{fill}" + else + "#{left}.#{right.slice(0,fillTo)}" + + price_change: (p1, p2) -> + percent = if p1 + @round(100*(p2-p1)/p1, 2) + else + '0.00' + "#{if p1 > p2 then '' else '+'}#{percent}" + + long_time: (timestamp) -> + m = moment.unix(timestamp) + "#{m.format("YYYY/MM/DD HH:mm")}" + + mask_fixed_volume: (volume) -> + @.fixAsk(volume).replace(/\..*/, "$&") + + fix_ask: (volume) -> + @.fixAsk volume + + fix_bid: (price) -> + @.fixBid price + + amount: (amount, price) -> + val = (new BigNumber(amount)).times(new BigNumber(price)) + @.fixAsk(val).replace(/\..*/, "$&") + + trend: (type) -> + if @.check_trend(type) + "text-up" + else + "text-down" + + trend_icon: (type) -> + if @.check_trend(type) + "" + else + "" + + t: (key) -> + gon.i18n[key] + +window.formatter = new Formatter() diff --git a/app/assets/javascripts/highcharts/config.js.coffee b/app/assets/javascripts/highcharts/config.js.coffee new file mode 100755 index 00000000..d3509ea8 --- /dev/null +++ b/app/assets/javascripts/highcharts/config.js.coffee @@ -0,0 +1,34 @@ +Highcharts.setOptions + global: + useUTC: false + +if gon.local is "zh-CN" + Highcharts.setOptions + lang: + months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'] + shortMonths: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'] + weekdays: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'] + +render = Highcharts.RangeSelector.prototype.render + +Highcharts.RangeSelector.prototype.render = (min, max) -> + render.apply(this, [min, max]) + leftPosition = @.chart.plotLeft + topPosition = @.chart.plotTop + space = 10 + + @.zoomText.attr + x: leftPosition + 2, + y: topPosition + 15, + text: gon.i18n.chart.zoom + + leftPosition += @.zoomText.getBBox().width + 15 + + for button in @.buttons + button.attr + x: leftPosition + y: topPosition + leftPosition += button.width + space + +f = (callback) -> return +Highcharts.wrap Highcharts.Tooltip.prototype, 'hide', f diff --git a/app/assets/javascripts/highcharts/technical_indicators.js b/app/assets/javascripts/highcharts/technical_indicators.js new file mode 100755 index 00000000..53b47fd2 --- /dev/null +++ b/app/assets/javascripts/highcharts/technical_indicators.js @@ -0,0 +1,410 @@ +(function (H) { + + // create shortcuts + var defaultOptions = H.getOptions(), + defaultPlotOptions = defaultOptions.plotOptions, + seriesTypes = H.seriesTypes; + + // Trendline functionality and default options. + defaultPlotOptions.trendline = H.merge(defaultPlotOptions.line, { + + marker: { + enabled: false + }, + + tooltip: { + valueDecimals: 2 + } + }); + + seriesTypes.trendline = H.extendClass(seriesTypes.line, { + + type: 'trendline', + animate: null, + requiresSorting: false, + processData: function() { + var data; + + if (this.linkedParent) { + data = [].concat(this.linkedParent.options.data) + this.setData(this.runAlgorithm(), false); + } + + H.Series.prototype.processData.call(this); + }, + runAlgorithm: function () { + + var xData = this.linkedParent.xData, + yData = this.linkedParent.yData, + periods = this.options.periods || 100, // Set this to what default? should be defaults for each algorithm. + algorithm = this.options.algorithm || 'linear'; + + return this[algorithm](xData, yData, periods); + }, + + + /* Function that uses the calcMACD function to return the MACD line. + * + * @return : the first index of the calcMACD return, the MACD. + **/ + MACD: function (xData, yData, periods) { + + return calcMACD(xData, yData, periods)[0]; + }, + + /* Function that uses the global calcMACD. + * + * @return : the second index of the calcMACD return, the signalLine. + **/ + signalLine: function (xData, yData, periods) { + + return calcMACD(xData, yData, periods)[1]; + }, + + /* Function using the global SMA function. + * + * @return : an array of SMA data. + **/ + SMA: function (xData, yData, periods) { + + return SMA(xData, yData, periods); + }, + + MA: function (xData, yData, periods) { + + return MA(xData, yData, periods); + }, + + + /* Function using the global EMA function. + * + * @return : an array of EMA data. + **/ + EMA: function (xData, yData, periods) { + + return EMA(xData, yData, periods); + }, + + /* Function that uses the global linear function. + * + * @return : an array of EMA data + **/ + linear: function (xData, yData, periods) { + + return linear(xData, yData, periods); + } + + }); + + // Setting default options for the Histogram type. + defaultPlotOptions.histogram = H.merge(defaultPlotOptions.column, { + + borderWidth : 0, + + tooltip: { + valueDecimals: 2 + } + + }); + + + seriesTypes.histogram = H.extendClass(seriesTypes.column, { + + type: 'histogram', + animate: null, + requiresSorting: false, + processData: function() { + var data; + + if (this.linkedParent) { + data = [].concat(this.linkedParent.options.data) + this.setData(this.runAlgorithm(), false); + } + + H.Series.prototype.processData.call(this); + }, + + runAlgorithm: function () { + + var xData = this.linkedParent.xData, + yData = this.linkedParent.yData, + periods = this.options.periods || 100, // Set this to what default? should be defaults for each algorithm. + algorithm = this.options.algorithm || 'histogram'; + + return this[algorithm](xData, yData, periods); + }, + + + histogram: function (xData, yData, periods) { + + return calcMACD(xData, yData, periods)[2]; + }, + + }); + + + // Global functions. + + /* Function that calculates the MACD (Moving Average Convergance-Divergence). + * + * @param yData : array of y variables. + * @param xData : array of x variables. + * @param periods : The amount of "days" to average from. + * @return : An array with 3 arrays. (0 : macd, 1 : signalline , 2 : histogram) + **/ + function calcMACD (xData, yData, periods) { + + var chart = this, + shortPeriod = 12, + longPeriod = 26, + signalPeriod = 9, + shortEMA, + longEMA, + MACD = [], + xMACD = [], + yMACD = [], + signalLine = [], + histogram = []; + + + // Calculating the short and long EMA used when calculating the MACD + shortEMA = EMA(xData, yData, 12); + longEMA = EMA(xData, yData, 26); + + // subtract each Y value from the EMA's and create the new dataset (MACD) + for (var i = 0; i < shortEMA.length; i++) { + + if (longEMA[i][1] == null) { + + MACD.push( [xData[i] , null]); + + } else { + MACD.push( [ xData[i] , (shortEMA[i][1] - longEMA[i][1]) ] ); + } + } + + // Set the Y and X data of the MACD. This is used in calculating the signal line. + for (var i = 0; i < MACD.length; i++) { + xMACD.push(MACD[i][0]); + yMACD.push(MACD[i][1]); + } + + // Setting the signalline (Signal Line: X-day EMA of MACD line). + signalLine = EMA(xMACD, yMACD, signalPeriod); + + // Setting the MACD Histogram. In comparison to the loop with pure MACD this loop uses MACD x value not xData. + for (var i = 0; i < MACD.length; i++) { + + if (MACD[i][1] == null) { + + histogram.push( [ MACD[i][0], null ] ); + + } else { + + histogram.push( [ MACD[i][0], (MACD[i][1] - signalLine[i][1]) ] ); + + } + } + + return [MACD, signalLine, histogram]; + } + + /** + * Calculating a linear trendline. + * The idea of a trendline is to reveal a linear relationship between + * two variables, x and y, in the "y = mx + b" form. + * @param yData : array of y variables. + * @param xData : array of x variables. + * @param periods : Only here for overloading purposes. + * @return an array containing the linear trendline. + **/ + function linear (xData, yData, periods) { + + var lineData = [], + step1, + step2 = 0, + step3 = 0, + step3a = 0, + step3b = 0, + step4 = 0, + step5 = 0, + step5a = 0, + step6 = 0, + step7 = 0, + step8 = 0, + step9 = 0; + + + // Step 1: The number of data points. + step1 = xData.length; + + // Step 2: "step1" times the summation of all x-values multiplied by their corresponding y-values. + // Step 3: Sum of all x-values times the sum of all y-values. 3a and b are used for storing data. + // Step 4: "step1" times the sum of all squared x-values. + // Step 5: The squared sum of all x-values. 5a stores data. + // Step 6: Equation to calculate the slope of the regression line. + // Step 7: The sum of all y-values. + // Step 8: "step6" times the sum of all x-values (step5). + // Step 9: The equation for the y-intercept of the trendline. + for ( var i = 0; i < step1; i++) { + step2 = (step2 + (xData[i] * yData[i])); + step3a = (step3a + xData[i]); + step3b = (step3b + yData[i]); + step4 = (step4 + Math.pow(xData[i], 2)); + step5a = (step5a + xData[i]); + step7 = (step7 + yData[i]); + } + step2 = (step1 * step2); + step3 = (step3a * step3b); + step4 = (step1 * step4); + step5 = (Math.pow(step5a, 2)); + step6 = ((step2 - step3) / (step4 - step5)); + step8 = (step6 * step5a); + step9 = ((step7 - step8) / step1); + + // Step 10: Plotting the trendline. Only two points are calulated. + // The starting point. + // This point will have values equal to the first X and Y value in the original dataset. + lineData.push([xData[0] , yData[0]]); + + // Calculating the ending point. + // The point X is equal the X in the original dataset. + // The point Y is calculated using the function of a straight line and our variables found. + step10 = ( ( step6 * xData[step1 - 1] ) + step9 ); + lineData.push([ ( xData[step1 - 1] ), step10 ]); + + return lineData; + } + + function MA (xData, yData, periods) { + var maLine = [], + periodArr = [], + length = yData.length; + + for (var i = 0; i < length; i++) { + periodArr.push(yData[i]); + + if (i >= periods) { + maLine.push([xData[i] , arrayAvg(periodArr)]); + periodArr.shift(); + } + else { + maLine.push([xData[i] , null]); + } + } + + return maLine; + } + + + /* Function based on the idea of an exponential moving average. + * + * Formula: EMA = Price(t) * k + EMA(y) * (1 - k) + * t = today, y = yesterday, N = number of days in EMA, k = 2/(2N+1) + * + * @param yData : array of y variables. + * @param xData : array of x variables. + * @param periods : The amount of "days" to average from. + * @return an array containing the EMA. + **/ + function EMA (xData, yData, periods) { + var t, + y = false, + n = periods, + k = (2 / (n + 1)), + ema, // exponential moving average. + emLine = [], + periodArr = [], + length = yData.length, + pointStart = xData[0]; + + // loop through data + for (var i = 0; i < length; i++) { + + + // Add the last point to the period arr, but only if its set. + if (yData[i-1]) { + periodArr.push(yData[i]); + } + + + // 0: runs if the periodArr has enough points. + // 1: set currentvalue (today). + // 2: set last value. either by past avg or yesterdays ema. + // 3: calculate todays ema. + if (n == periodArr.length) { + + + t = yData[i]; + + if (!y) { + y = arrayAvg(periodArr); + } else { + ema = (t * k) + (y * (1 - k)); + y = ema; + } + + emLine.push([xData[i] , y]); + + // remove first value in array. + periodArr.splice(0,1); + + } else { + + emLine.push([xData[i] , null]); + } + + } + + return emLine; + } + + /* Function based on the idea of a simple moving average. + * @param yData : array of y variables. + * @param xData : array of x variables. + * @param periods : The amount of "days" to average from. + * @return an array containing the SMA. + **/ + function SMA (xData, yData, periods) { + var periodArr = [], + smLine = [], + length = yData.length, + pointStart = xData[0]; + + // Loop through the entire array. + for (var i = 0; i < length; i++) { + + // add points to the array. + periodArr.push(yData[i]); + + // 1: Check if array is "filled" else create null point in line. + // 2: Calculate average. + // 3: Remove first value. + if (periods == periodArr.length) { + + smLine.push([ xData[i] , arrayAvg(periodArr)]); + periodArr.splice(0,1); + + } else { + smLine.push([ xData[i] , null]); + } + } + return smLine; + } + + /* Function that returns average of an array's values. + * + **/ + function arrayAvg (arr) { + var sum = 0, + arrLength = arr.length, + i = arrLength; + + while (i--) { + sum = sum + arr[i]; + } + + return (sum / arrLength); + } + +}(Highcharts)); diff --git a/app/assets/javascripts/html5.js b/app/assets/javascripts/html5.js new file mode 100755 index 00000000..d72615e9 --- /dev/null +++ b/app/assets/javascripts/html5.js @@ -0,0 +1,2 @@ +//= require html5shiv +//= require respond.min diff --git a/app/assets/javascripts/lib/ajax.js.coffee b/app/assets/javascripts/lib/ajax.js.coffee new file mode 100755 index 00000000..f02b2bbd --- /dev/null +++ b/app/assets/javascripts/lib/ajax.js.coffee @@ -0,0 +1,287 @@ +PeatioModel = @PeatioModel or require('peatio_model') +$ = PeatioModel.$ +Model = PeatioModel.Model +Queue = $({}) + +Ajax = + getURL: (object) -> + if object.className? + @generateURL(object) + else + @generateURL(object, encodeURIComponent(object.id)) + + getCollectionURL: (object) -> + @generateURL(object) + + getScope: (object) -> + object.scope?() or object.scope + + getAfterScope: (object) -> + object.afterScope?() or object.afterScope + + getCollection: (object) -> + if object.url isnt object.generateURL + if typeof object.url is 'function' + object.url() + else + object.url + else if object.className? + object.className.toLowerCase() + 's' + + generateURL: (object, args...) -> + collection = Ajax.getCollection(object) or Ajax.getCollection(object.constructor) + scope = Ajax.getScope(object) or Ajax.getScope(object.constructor) + afterScope = Ajax.getAfterScope(object) or Ajax.getAfterScope(object.constructor) + args.unshift(collection) + args.unshift(scope) + args.push(afterScope) + # construct and clean url + path = args.join('/') + path = path.replace /(\/\/)/g, "/" + path = path.replace /^\/|\/$/g, "" + # handle relative urls vs those that use a host + if path.indexOf("../") isnt 0 + Model.host + "/" + path + else + path + + enabled: true + + disable: (callback) -> + if @enabled + @enabled = false + try + do callback + catch e + throw e + finally + @enabled = true + else + do callback + + queue: (request) -> + if request then Queue.queue(request) else Queue.queue() + + clearQueue: -> + @queue [] + + config: + loadMethod: 'GET' + updateMethod: 'PUT' + createMethod: 'POST' + destroyMethod: 'DELETE' + +class Base + defaults: + dataType: 'json' + processData: false + headers: {'X-Requested-With': 'XMLHttpRequest'} + + queue: Ajax.queue + + ajax: (params, defaults) -> + $.ajax @ajaxSettings(params, defaults) + + ajaxQueue: (params, defaults, record) -> + jqXHR = null + deferred = $.Deferred() + promise = deferred.promise() + return promise unless Ajax.enabled + settings = @ajaxSettings(params, defaults) + # prefer setting if exists else default is to parallelize 'GET' requests + parallel = if settings.parallel isnt undefined then settings.parallel else (settings.type is 'GET') + request = (next) -> + if record?.id? + # for existing singleton, model id may have been updated + # after request has been queued + settings.url ?= Ajax.getURL(record) + settings.data?.id = record.id + # 2 reasons not to stringify: if already a string, or if intend to have ajax processData + if typeof settings.data isnt 'string' and settings.processData isnt true + settings.data = JSON.stringify(settings.data) + jqXHR = $.ajax(settings) + .done(deferred.resolve) + .fail(deferred.reject) + .then(next, next) + if parallel + Queue.dequeue() + + promise.abort = (statusText) -> + return jqXHR.abort(statusText) if jqXHR + index = $.inArray(request, @queue()) + @queue().splice(index, 1) if index > -1 + deferred.rejectWith( + settings.context or settings, + [promise, statusText, ''] + ) + promise + + @queue request + promise + + ajaxSettings: (params, defaults) -> + $.extend({}, @defaults, defaults, params) + +class Collection extends Base + constructor: (@model) -> + + find: (id, params, options = {}) -> + record = new @model(id: id) + @ajaxQueue( + params, { + type: options.method or Ajax.config.loadMethod + url: options.url or Ajax.getURL(record) + parallel: options.parallel + } + ).done(@recordsResponse) + .fail(@failResponse) + + all: (params, options = {}) -> + @ajaxQueue( + params, { + type: options.method or Ajax.config.loadMethod + url: options.url or Ajax.getURL(@model) + parallel: options.parallel + } + ).done(@recordsResponse) + .fail(@failResponse) + + fetch: (params = {}, options = {}) -> + if id = params.id + delete params.id + @find(id, params, options).done (record) => + @model.refresh(record, options) + else + @all(params, options).done (records) => + @model.refresh(records, options) + + # Private + + recordsResponse: (data, status, xhr) => + @model.trigger('ajaxSuccess', null, status, xhr) + + failResponse: (xhr, statusText, error) => + @model.trigger('ajaxError', null, xhr, statusText, error) + +class Singleton extends Base + constructor: (@record) -> + @model = @record.constructor + + reload: (params, options = {}) -> + @ajaxQueue( + params, { + type: options.method or Ajax.config.loadMethod + url: options.url + parallel: options.parallel + }, @record + ).done(@recordResponse(options)) + .fail(@failResponse(options)) + + create: (params, options = {}) -> + @ajaxQueue( + params, { + type: options.method or Ajax.config.createMethod + contentType: 'application/json' + data: @record.toJSON() + url: options.url or Ajax.getCollectionURL(@record) + parallel: options.parallel + } + ).done(@recordResponse(options)) + .fail(@failResponse(options)) + + update: (params, options = {}) -> + @ajaxQueue( + params, { + type: options.method or Ajax.config.updateMethod + contentType: 'application/json' + data: @record.toJSON() + url: options.url + parallel: options.parallel + }, @record + ).done(@recordResponse(options)) + .fail(@failResponse(options)) + + destroy: (params, options = {}) -> + @ajaxQueue( + params, { + type: options.method or Ajax.config.destroyMethod + url: options.url + parallel: options.parallel + }, @record + ).done(@recordResponse(options)) + .fail(@failResponse(options)) + + # Private + + recordResponse: (options = {}) => + (data, status, xhr) => + + Ajax.disable => + unless PeatioModel.isBlank(data) or @record.destroyed + # ID change, need to do some shifting + if data.id and @record.id isnt data.id + @record.changeID(data.id) + # Update with latest data + @record.refresh(data) + + @record.trigger('ajaxSuccess', data, status, xhr) + options.done?.apply(@record) + + failResponse: (options = {}) => + (xhr, statusText, error) => + @record.trigger('ajaxError', xhr, statusText, error) + options.fail?.apply(@record) + +# Ajax endpoint +Model.host = '' + +GenerateURL = + include: (args...) -> + args.unshift(encodeURIComponent(@id)) + Ajax.generateURL(@, args...) + extend: (args...) -> + Ajax.generateURL(@, args...) + +Include = + ajax: -> new Singleton(this) + + generateURL: GenerateURL.include + + url: GenerateURL.include + +Extend = + ajax: -> new Collection(this) + + generateURL: GenerateURL.extend + + url: GenerateURL.extend + +Model.Ajax = + extended: -> + @fetch @ajaxFetch + @change @ajaxChange + @extend Extend + @include Include + + # Private + + ajaxFetch: -> + @ajax().fetch(arguments...) + + ajaxChange: (record, type, options = {}) -> + return if options.ajax is false + record.ajax()[type](options.ajax, options) + +Model.Ajax.Methods = + extended: -> + @extend Extend + @include Include + +# Globals +Ajax.defaults = Base::defaults +Ajax.Base = Base +Ajax.Singleton = Singleton +Ajax.Collection = Collection +PeatioModel.Ajax = Ajax +module?.exports = Ajax + diff --git a/app/assets/javascripts/lib/angular-ui-router.js b/app/assets/javascripts/lib/angular-ui-router.js new file mode 100755 index 00000000..37d2ae69 --- /dev/null +++ b/app/assets/javascripts/lib/angular-ui-router.js @@ -0,0 +1,3658 @@ +/** + * State-based routing for AngularJS + * @version v0.2.11 + * @link http://angular-ui.github.com/ + * @license MIT License, http://www.opensource.org/licenses/MIT + */ + +/* commonjs package manager support (eg componentjs) */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){ + module.exports = 'ui.router'; +} + +(function (window, angular, undefined) { +/*jshint globalstrict:true*/ +/*global angular:false*/ +'use strict'; + +var isDefined = angular.isDefined, + isFunction = angular.isFunction, + isString = angular.isString, + isObject = angular.isObject, + isArray = angular.isArray, + forEach = angular.forEach, + extend = angular.extend, + copy = angular.copy; + +function inherit(parent, extra) { + return extend(new (extend(function() {}, { prototype: parent }))(), extra); +} + +function merge(dst) { + forEach(arguments, function(obj) { + if (obj !== dst) { + forEach(obj, function(value, key) { + if (!dst.hasOwnProperty(key)) dst[key] = value; + }); + } + }); + return dst; +} + +/** + * Finds the common ancestor path between two states. + * + * @param {Object} first The first state. + * @param {Object} second The second state. + * @return {Array} Returns an array of state names in descending order, not including the root. + */ +function ancestors(first, second) { + var path = []; + + for (var n in first.path) { + if (first.path[n] !== second.path[n]) break; + path.push(first.path[n]); + } + return path; +} + +/** + * IE8-safe wrapper for `Object.keys()`. + * + * @param {Object} object A JavaScript object. + * @return {Array} Returns the keys of the object as an array. + */ +function objectKeys(object) { + if (Object.keys) { + return Object.keys(object); + } + var result = []; + + angular.forEach(object, function(val, key) { + result.push(key); + }); + return result; +} + +/** + * IE8-safe wrapper for `Array.prototype.indexOf()`. + * + * @param {Array} array A JavaScript array. + * @param {*} value A value to search the array for. + * @return {Number} Returns the array index value of `value`, or `-1` if not present. + */ +function arraySearch(array, value) { + if (Array.prototype.indexOf) { + return array.indexOf(value, Number(arguments[2]) || 0); + } + var len = array.length >>> 0, from = Number(arguments[2]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + + if (from < 0) from += len; + + for (; from < len; from++) { + if (from in array && array[from] === value) return from; + } + return -1; +} + +/** + * Merges a set of parameters with all parameters inherited between the common parents of the + * current state and a given destination state. + * + * @param {Object} currentParams The value of the current state parameters ($stateParams). + * @param {Object} newParams The set of parameters which will be composited with inherited params. + * @param {Object} $current Internal definition of object representing the current state. + * @param {Object} $to Internal definition of object representing state to transition to. + */ +function inheritParams(currentParams, newParams, $current, $to) { + var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = []; + + for (var i in parents) { + if (!parents[i].params) continue; + parentParams = objectKeys(parents[i].params); + if (!parentParams.length) continue; + + for (var j in parentParams) { + if (arraySearch(inheritList, parentParams[j]) >= 0) continue; + inheritList.push(parentParams[j]); + inherited[parentParams[j]] = currentParams[parentParams[j]]; + } + } + return extend({}, inherited, newParams); +} + +/** + * Performs a non-strict comparison of the subset of two objects, defined by a list of keys. + * + * @param {Object} a The first object. + * @param {Object} b The second object. + * @param {Array} keys The list of keys within each object to compare. If the list is empty or not specified, + * it defaults to the list of keys in `a`. + * @return {Boolean} Returns `true` if the keys match, otherwise `false`. + */ +function equalForKeys(a, b, keys) { + if (!keys) { + keys = []; + for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility + } + + for (var i=0; i + * + * + * + * + * + * + * + * + * + * + * + * + */ +angular.module('ui.router', ['ui.router.state']); + +angular.module('ui.router.compat', ['ui.router']); + +/** + * @ngdoc object + * @name ui.router.util.$resolve + * + * @requires $q + * @requires $injector + * + * @description + * Manages resolution of (acyclic) graphs of promises. + */ +$Resolve.$inject = ['$q', '$injector']; +function $Resolve( $q, $injector) { + + var VISIT_IN_PROGRESS = 1, + VISIT_DONE = 2, + NOTHING = {}, + NO_DEPENDENCIES = [], + NO_LOCALS = NOTHING, + NO_PARENT = extend($q.when(NOTHING), { $$promises: NOTHING, $$values: NOTHING }); + + + /** + * @ngdoc function + * @name ui.router.util.$resolve#study + * @methodOf ui.router.util.$resolve + * + * @description + * Studies a set of invocables that are likely to be used multiple times. + *
    +   * $resolve.study(invocables)(locals, parent, self)
    +   * 
    + * is equivalent to + *
    +   * $resolve.resolve(invocables, locals, parent, self)
    +   * 
    + * but the former is more efficient (in fact `resolve` just calls `study` + * internally). + * + * @param {object} invocables Invocable objects + * @return {function} a function to pass in locals, parent and self + */ + this.study = function (invocables) { + if (!isObject(invocables)) throw new Error("'invocables' must be an object"); + + // Perform a topological sort of invocables to build an ordered plan + var plan = [], cycle = [], visited = {}; + function visit(value, key) { + if (visited[key] === VISIT_DONE) return; + + cycle.push(key); + if (visited[key] === VISIT_IN_PROGRESS) { + cycle.splice(0, cycle.indexOf(key)); + throw new Error("Cyclic dependency: " + cycle.join(" -> ")); + } + visited[key] = VISIT_IN_PROGRESS; + + if (isString(value)) { + plan.push(key, [ function() { return $injector.get(value); }], NO_DEPENDENCIES); + } else { + var params = $injector.annotate(value); + forEach(params, function (param) { + if (param !== key && invocables.hasOwnProperty(param)) visit(invocables[param], param); + }); + plan.push(key, value, params); + } + + cycle.pop(); + visited[key] = VISIT_DONE; + } + forEach(invocables, visit); + invocables = cycle = visited = null; // plan is all that's required + + function isResolve(value) { + return isObject(value) && value.then && value.$$promises; + } + + return function (locals, parent, self) { + if (isResolve(locals) && self === undefined) { + self = parent; parent = locals; locals = null; + } + if (!locals) locals = NO_LOCALS; + else if (!isObject(locals)) { + throw new Error("'locals' must be an object"); + } + if (!parent) parent = NO_PARENT; + else if (!isResolve(parent)) { + throw new Error("'parent' must be a promise returned by $resolve.resolve()"); + } + + // To complete the overall resolution, we have to wait for the parent + // promise and for the promise for each invokable in our plan. + var resolution = $q.defer(), + result = resolution.promise, + promises = result.$$promises = {}, + values = extend({}, locals), + wait = 1 + plan.length/3, + merged = false; + + function done() { + // Merge parent values we haven't got yet and publish our own $$values + if (!--wait) { + if (!merged) merge(values, parent.$$values); + result.$$values = values; + result.$$promises = true; // keep for isResolve() + delete result.$$inheritedValues; + resolution.resolve(values); + } + } + + function fail(reason) { + result.$$failure = reason; + resolution.reject(reason); + } + + // Short-circuit if parent has already failed + if (isDefined(parent.$$failure)) { + fail(parent.$$failure); + return result; + } + + if (parent.$$inheritedValues) { + merge(values, parent.$$inheritedValues); + } + + // Merge parent values if the parent has already resolved, or merge + // parent promises and wait if the parent resolve is still in progress. + if (parent.$$values) { + merged = merge(values, parent.$$values); + result.$$inheritedValues = parent.$$values; + done(); + } else { + if (parent.$$inheritedValues) { + result.$$inheritedValues = parent.$$inheritedValues; + } + extend(promises, parent.$$promises); + parent.then(done, fail); + } + + // Process each invocable in the plan, but ignore any where a local of the same name exists. + for (var i=0, ii=plan.length; i} The template html as a string, or a promise + * for that string. + */ + this.fromUrl = function (url, params) { + if (isFunction(url)) url = url(params); + if (url == null) return null; + else return $http + .get(url, { cache: $templateCache }) + .then(function(response) { return response.data; }); + }; + + /** + * @ngdoc function + * @name ui.router.util.$templateFactory#fromProvider + * @methodOf ui.router.util.$templateFactory + * + * @description + * Creates a template by invoking an injectable provider function. + * + * @param {Function} provider Function to invoke via `$injector.invoke` + * @param {Object} params Parameters for the template. + * @param {Object} locals Locals to pass to `invoke`. Defaults to + * `{ params: params }`. + * @return {string|Promise.} The template html as a string, or a promise + * for that string. + */ + this.fromProvider = function (provider, params, locals) { + return $injector.invoke(provider, null, locals || { params: params }); + }; +} + +angular.module('ui.router.util').service('$templateFactory', $TemplateFactory); + +/** + * @ngdoc object + * @name ui.router.util.type:UrlMatcher + * + * @description + * Matches URLs against patterns and extracts named parameters from the path or the search + * part of the URL. A URL pattern consists of a path pattern, optionally followed by '?' and a list + * of search parameters. Multiple search parameter names are separated by '&'. Search parameters + * do not influence whether or not a URL is matched, but their values are passed through into + * the matched parameters returned by {@link ui.router.util.type:UrlMatcher#methods_exec exec}. + * + * Path parameter placeholders can be specified using simple colon/catch-all syntax or curly brace + * syntax, which optionally allows a regular expression for the parameter to be specified: + * + * * `':'` name - colon placeholder + * * `'*'` name - catch-all placeholder + * * `'{' name '}'` - curly placeholder + * * `'{' name ':' regexp '}'` - curly placeholder with regexp. Should the regexp itself contain + * curly braces, they must be in matched pairs or escaped with a backslash. + * + * Parameter names may contain only word characters (latin letters, digits, and underscore) and + * must be unique within the pattern (across both path and search parameters). For colon + * placeholders or curly placeholders without an explicit regexp, a path parameter matches any + * number of characters other than '/'. For catch-all placeholders the path parameter matches + * any number of characters. + * + * Examples: + * + * * `'/hello/'` - Matches only if the path is exactly '/hello/'. There is no special treatment for + * trailing slashes, and patterns have to match the entire path, not just a prefix. + * * `'/user/:id'` - Matches '/user/bob' or '/user/1234!!!' or even '/user/' but not '/user' or + * '/user/bob/details'. The second path segment will be captured as the parameter 'id'. + * * `'/user/{id}'` - Same as the previous example, but using curly brace syntax. + * * `'/user/{id:[^/]*}'` - Same as the previous example. + * * `'/user/{id:[0-9a-fA-F]{1,8}}'` - Similar to the previous example, but only matches if the id + * parameter consists of 1 to 8 hex digits. + * * `'/files/{path:.*}'` - Matches any URL starting with '/files/' and captures the rest of the + * path into the parameter 'path'. + * * `'/files/*path'` - ditto. + * + * @param {string} pattern The pattern to compile into a matcher. + * @param {Object} config A configuration object hash: + * + * * `caseInsensitive` - `true` if URL matching should be case insensitive, otherwise `false`, the default value (for backward compatibility) is `false`. + * * `strict` - `false` if matching against a URL with a trailing slash should be treated as equivalent to a URL without a trailing slash, the default value is `true`. + * + * @property {string} prefix A static prefix of this pattern. The matcher guarantees that any + * URL matching this matcher (i.e. any string for which {@link ui.router.util.type:UrlMatcher#methods_exec exec()} returns + * non-null) will start with this prefix. + * + * @property {string} source The pattern that was passed into the constructor + * + * @property {string} sourcePath The path portion of the source property + * + * @property {string} sourceSearch The search portion of the source property + * + * @property {string} regex The constructed regex that will be used to match against the url when + * it is time to determine which url will match. + * + * @returns {Object} New `UrlMatcher` object + */ +function UrlMatcher(pattern, config) { + config = angular.isObject(config) ? config : {}; + + // Find all placeholders and create a compiled pattern, using either classic or curly syntax: + // '*' name + // ':' name + // '{' name '}' + // '{' name ':' regexp '}' + // The regular expression is somewhat complicated due to the need to allow curly braces + // inside the regular expression. The placeholder regexp breaks down as follows: + // ([:*])(\w+) classic placeholder ($1 / $2) + // \{(\w+)(?:\:( ... ))?\} curly brace placeholder ($3) with optional regexp ... ($4) + // (?: ... | ... | ... )+ the regexp consists of any number of atoms, an atom being either + // [^{}\\]+ - anything other than curly braces or backslash + // \\. - a backslash escape + // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms + var placeholder = /([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, + compiled = '^', last = 0, m, + segments = this.segments = [], + params = this.params = {}; + + /** + * [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the + * default value, which may be the result of an injectable function. + */ + function $value(value) { + /*jshint validthis: true */ + return isDefined(value) ? this.type.decode(value) : $UrlMatcherFactory.$$getDefaultValue(this); + } + + function addParameter(id, type, config) { + if (!/^\w+(-+\w+)*$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'"); + if (params[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'"); + params[id] = extend({ type: type || new Type(), $value: $value }, config); + } + + function quoteRegExp(string, pattern, isOptional) { + var result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); + if (!pattern) return result; + var flag = isOptional ? '?' : ''; + return result + flag + '(' + pattern + ')' + flag; + } + + function paramConfig(param) { + if (!config.params || !config.params[param]) return {}; + var cfg = config.params[param]; + return isObject(cfg) ? cfg : { value: cfg }; + } + + this.source = pattern; + + // Split into static segments separated by path parameter placeholders. + // The number of segments is always 1 more than the number of parameters. + var id, regexp, segment, type, cfg; + + while ((m = placeholder.exec(pattern))) { + id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null + regexp = m[4] || (m[1] == '*' ? '.*' : '[^/]*'); + segment = pattern.substring(last, m.index); + type = this.$types[regexp] || new Type({ pattern: new RegExp(regexp) }); + cfg = paramConfig(id); + + if (segment.indexOf('?') >= 0) break; // we're into the search part + + compiled += quoteRegExp(segment, type.$subPattern(), isDefined(cfg.value)); + addParameter(id, type, cfg); + segments.push(segment); + last = placeholder.lastIndex; + } + segment = pattern.substring(last); + + // Find any search parameter names and remove them from the last segment + var i = segment.indexOf('?'); + + if (i >= 0) { + var search = this.sourceSearch = segment.substring(i); + segment = segment.substring(0, i); + this.sourcePath = pattern.substring(0, last + i); + + // Allow parameters to be separated by '?' as well as '&' to make concat() easier + forEach(search.substring(1).split(/[&?]/), function(key) { + addParameter(key, null, paramConfig(key)); + }); + } else { + this.sourcePath = pattern; + this.sourceSearch = ''; + } + + compiled += quoteRegExp(segment) + (config.strict === false ? '\/?' : '') + '$'; + segments.push(segment); + + this.regexp = new RegExp(compiled, config.caseInsensitive ? 'i' : undefined); + this.prefix = segments[0]; +} + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#concat + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Returns a new matcher for a pattern constructed by appending the path part and adding the + * search parameters of the specified pattern to this pattern. The current pattern is not + * modified. This can be understood as creating a pattern for URLs that are relative to (or + * suffixes of) the current pattern. + * + * @example + * The following two matchers are equivalent: + *
    + * new UrlMatcher('/user/{id}?q').concat('/details?date');
    + * new UrlMatcher('/user/{id}/details?q&date');
    + * 
    + * + * @param {string} pattern The pattern to append. + * @param {Object} config An object hash of the configuration for the matcher. + * @returns {UrlMatcher} A matcher for the concatenated pattern. + */ +UrlMatcher.prototype.concat = function (pattern, config) { + // Because order of search parameters is irrelevant, we can add our own search + // parameters to the end of the new pattern. Parse the new pattern by itself + // and then join the bits together, but it's much easier to do this on a string level. + return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch, config); +}; + +UrlMatcher.prototype.toString = function () { + return this.source; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#exec + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Tests the specified path against this matcher, and returns an object containing the captured + * parameter values, or null if the path does not match. The returned object contains the values + * of any search parameters that are mentioned in the pattern, but their value may be null if + * they are not present in `searchParams`. This means that search parameters are always treated + * as optional. + * + * @example + *
    + * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', {
    + *   x: '1', q: 'hello'
    + * });
    + * // returns { id: 'bob', q: 'hello', r: null }
    + * 
    + * + * @param {string} path The URL path to match, e.g. `$location.path()`. + * @param {Object} searchParams URL search parameters, e.g. `$location.search()`. + * @returns {Object} The captured parameter values. + */ +UrlMatcher.prototype.exec = function (path, searchParams) { + var m = this.regexp.exec(path); + if (!m) return null; + searchParams = searchParams || {}; + + var params = this.parameters(), nTotal = params.length, + nPath = this.segments.length - 1, + values = {}, i, cfg, param; + + if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'"); + + for (i = 0; i < nPath; i++) { + param = params[i]; + cfg = this.params[param]; + values[param] = cfg.$value(m[i + 1]); + } + for (/**/; i < nTotal; i++) { + param = params[i]; + cfg = this.params[param]; + values[param] = cfg.$value(searchParams[param]); + } + + return values; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#parameters + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Returns the names of all path and search parameters of this pattern in an unspecified order. + * + * @returns {Array.} An array of parameter names. Must be treated as read-only. If the + * pattern has no parameters, an empty array is returned. + */ +UrlMatcher.prototype.parameters = function (param) { + if (!isDefined(param)) return objectKeys(this.params); + return this.params[param] || null; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#validate + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Checks an object hash of parameters to validate their correctness according to the parameter + * types of this `UrlMatcher`. + * + * @param {Object} params The object hash of parameters to validate. + * @returns {boolean} Returns `true` if `params` validates, otherwise `false`. + */ +UrlMatcher.prototype.validates = function (params) { + var result = true, isOptional, cfg, self = this; + + forEach(params, function(val, key) { + if (!self.params[key]) return; + cfg = self.params[key]; + isOptional = !val && isDefined(cfg.value); + result = result && (isOptional || cfg.type.is(val)); + }); + return result; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#format + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Creates a URL that matches this pattern by substituting the specified values + * for the path and search parameters. Null values for path parameters are + * treated as empty strings. + * + * @example + *
    + * new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' });
    + * // returns '/user/bob?q=yes'
    + * 
    + * + * @param {Object} values the values to substitute for the parameters in this pattern. + * @returns {string} the formatted URL (path and optionally search part). + */ +UrlMatcher.prototype.format = function (values) { + var segments = this.segments, params = this.parameters(); + + if (!values) return segments.join('').replace('//', '/'); + + var nPath = segments.length - 1, nTotal = params.length, + result = segments[0], i, search, value, param, cfg, array; + + if (!this.validates(values)) return null; + + for (i = 0; i < nPath; i++) { + param = params[i]; + value = values[param]; + cfg = this.params[param]; + + if (!isDefined(value) && (segments[i] === '/' || segments[i + 1] === '/')) continue; + if (value != null) result += encodeURIComponent(cfg.type.encode(value)); + result += segments[i + 1]; + } + + for (/**/; i < nTotal; i++) { + param = params[i]; + value = values[param]; + if (value == null) continue; + array = isArray(value); + + if (array) { + value = value.map(encodeURIComponent).join('&' + param + '='); + } + result += (search ? '&' : '?') + param + '=' + (array ? value : encodeURIComponent(value)); + search = true; + } + return result; +}; + +UrlMatcher.prototype.$types = {}; + +/** + * @ngdoc object + * @name ui.router.util.type:Type + * + * @description + * Implements an interface to define custom parameter types that can be decoded from and encoded to + * string parameters matched in a URL. Used by {@link ui.router.util.type:UrlMatcher `UrlMatcher`} + * objects when matching or formatting URLs, or comparing or validating parameter values. + * + * See {@link ui.router.util.$urlMatcherFactory#methods_type `$urlMatcherFactory#type()`} for more + * information on registering custom types. + * + * @param {Object} config A configuration object hash that includes any method in `Type`'s public + * interface, and/or `pattern`, which should contain a custom regular expression used to match + * string parameters originating from a URL. + * + * @property {RegExp} pattern The regular expression pattern used to match values of this type when + * coming from a substring of a URL. + * + * @returns {Object} Returns a new `Type` object. + */ +function Type(config) { + extend(this, config); +} + +/** + * @ngdoc function + * @name ui.router.util.type:Type#is + * @methodOf ui.router.util.type:Type + * + * @description + * Detects whether a value is of a particular type. Accepts a native (decoded) value + * and determines whether it matches the current `Type` object. + * + * @param {*} val The value to check. + * @param {string} key Optional. If the type check is happening in the context of a specific + * {@link ui.router.util.type:UrlMatcher `UrlMatcher`} object, this is the name of the + * parameter in which `val` is stored. Can be used for meta-programming of `Type` objects. + * @returns {Boolean} Returns `true` if the value matches the type, otherwise `false`. + */ +Type.prototype.is = function(val, key) { + return true; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:Type#encode + * @methodOf ui.router.util.type:Type + * + * @description + * Encodes a custom/native type value to a string that can be embedded in a URL. Note that the + * return value does *not* need to be URL-safe (i.e. passed through `encodeURIComponent()`), it + * only needs to be a representation of `val` that has been coerced to a string. + * + * @param {*} val The value to encode. + * @param {string} key The name of the parameter in which `val` is stored. Can be used for + * meta-programming of `Type` objects. + * @returns {string} Returns a string representation of `val` that can be encoded in a URL. + */ +Type.prototype.encode = function(val, key) { + return val; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:Type#decode + * @methodOf ui.router.util.type:Type + * + * @description + * Converts a string URL parameter value to a custom/native value. + * + * @param {string} val The URL parameter value to decode. + * @param {string} key The name of the parameter in which `val` is stored. Can be used for + * meta-programming of `Type` objects. + * @returns {*} Returns a custom representation of the URL parameter value. + */ +Type.prototype.decode = function(val, key) { + return val; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:Type#equals + * @methodOf ui.router.util.type:Type + * + * @description + * Determines whether two decoded values are equivalent. + * + * @param {*} a A value to compare against. + * @param {*} b A value to compare against. + * @returns {Boolean} Returns `true` if the values are equivalent/equal, otherwise `false`. + */ +Type.prototype.equals = function(a, b) { + return a == b; +}; + +Type.prototype.$subPattern = function() { + var sub = this.pattern.toString(); + return sub.substr(1, sub.length - 2); +}; + +Type.prototype.pattern = /.*/; + +/** + * @ngdoc object + * @name ui.router.util.$urlMatcherFactory + * + * @description + * Factory for {@link ui.router.util.type:UrlMatcher `UrlMatcher`} instances. The factory + * is also available to providers under the name `$urlMatcherFactoryProvider`. + */ +function $UrlMatcherFactory() { + + var isCaseInsensitive = false, isStrictMode = true; + + var enqueue = true, typeQueue = [], injector, defaultTypes = { + int: { + decode: function(val) { + return parseInt(val, 10); + }, + is: function(val) { + if (!isDefined(val)) return false; + return this.decode(val.toString()) === val; + }, + pattern: /\d+/ + }, + bool: { + encode: function(val) { + return val ? 1 : 0; + }, + decode: function(val) { + return parseInt(val, 10) === 0 ? false : true; + }, + is: function(val) { + return val === true || val === false; + }, + pattern: /0|1/ + }, + string: { + pattern: /[^\/]*/ + }, + date: { + equals: function (a, b) { + return a.toISOString() === b.toISOString(); + }, + decode: function (val) { + return new Date(val); + }, + encode: function (val) { + return [ + val.getFullYear(), + ('0' + (val.getMonth() + 1)).slice(-2), + ('0' + val.getDate()).slice(-2) + ].join("-"); + }, + pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/ + } + }; + + function getDefaultConfig() { + return { + strict: isStrictMode, + caseInsensitive: isCaseInsensitive + }; + } + + function isInjectable(value) { + return (isFunction(value) || (isArray(value) && isFunction(value[value.length - 1]))); + } + + /** + * [Internal] Get the default value of a parameter, which may be an injectable function. + */ + $UrlMatcherFactory.$$getDefaultValue = function(config) { + if (!isInjectable(config.value)) return config.value; + if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); + return injector.invoke(config.value); + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#caseInsensitive + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Defines whether URL matching should be case sensitive (the default behavior), or not. + * + * @param {boolean} value `false` to match URL in a case sensitive manner; otherwise `true`; + */ + this.caseInsensitive = function(value) { + isCaseInsensitive = value; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#strictMode + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Defines whether URLs should match trailing slashes, or not (the default behavior). + * + * @param {boolean} value `false` to match trailing slashes in URLs, otherwise `true`. + */ + this.strictMode = function(value) { + isStrictMode = value; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#compile + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Creates a {@link ui.router.util.type:UrlMatcher `UrlMatcher`} for the specified pattern. + * + * @param {string} pattern The URL pattern. + * @param {Object} config The config object hash. + * @returns {UrlMatcher} The UrlMatcher. + */ + this.compile = function (pattern, config) { + return new UrlMatcher(pattern, extend(getDefaultConfig(), config)); + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#isMatcher + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Returns true if the specified object is a `UrlMatcher`, or false otherwise. + * + * @param {Object} object The object to perform the type check against. + * @returns {Boolean} Returns `true` if the object matches the `UrlMatcher` interface, by + * implementing all the same methods. + */ + this.isMatcher = function (o) { + if (!isObject(o)) return false; + var result = true; + + forEach(UrlMatcher.prototype, function(val, name) { + if (isFunction(val)) { + result = result && (isDefined(o[name]) && isFunction(o[name])); + } + }); + return result; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#type + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Registers a custom {@link ui.router.util.type:Type `Type`} object that can be used to + * generate URLs with typed parameters. + * + * @param {string} name The type name. + * @param {Object|Function} def The type definition. See + * {@link ui.router.util.type:Type `Type`} for information on the values accepted. + * + * @returns {Object} Returns `$urlMatcherFactoryProvider`. + * + * @example + * This is a simple example of a custom type that encodes and decodes items from an + * array, using the array index as the URL-encoded value: + * + *
    +   * var list = ['John', 'Paul', 'George', 'Ringo'];
    +   *
    +   * $urlMatcherFactoryProvider.type('listItem', {
    +   *   encode: function(item) {
    +   *     // Represent the list item in the URL using its corresponding index
    +   *     return list.indexOf(item);
    +   *   },
    +   *   decode: function(item) {
    +   *     // Look up the list item by index
    +   *     return list[parseInt(item, 10)];
    +   *   },
    +   *   is: function(item) {
    +   *     // Ensure the item is valid by checking to see that it appears
    +   *     // in the list
    +   *     return list.indexOf(item) > -1;
    +   *   }
    +   * });
    +   *
    +   * $stateProvider.state('list', {
    +   *   url: "/list/{item:listItem}",
    +   *   controller: function($scope, $stateParams) {
    +   *     console.log($stateParams.item);
    +   *   }
    +   * });
    +   *
    +   * // ...
    +   *
    +   * // Changes URL to '/list/3', logs "Ringo" to the console
    +   * $state.go('list', { item: "Ringo" });
    +   * 
    + * + * This is a more complex example of a type that relies on dependency injection to + * interact with services, and uses the parameter name from the URL to infer how to + * handle encoding and decoding parameter values: + * + *
    +   * // Defines a custom type that gets a value from a service,
    +   * // where each service gets different types of values from
    +   * // a backend API:
    +   * $urlMatcherFactoryProvider.type('dbObject', function(Users, Posts) {
    +   *
    +   *   // Matches up services to URL parameter names
    +   *   var services = {
    +   *     user: Users,
    +   *     post: Posts
    +   *   };
    +   *
    +   *   return {
    +   *     encode: function(object) {
    +   *       // Represent the object in the URL using its unique ID
    +   *       return object.id;
    +   *     },
    +   *     decode: function(value, key) {
    +   *       // Look up the object by ID, using the parameter
    +   *       // name (key) to call the correct service
    +   *       return services[key].findById(value);
    +   *     },
    +   *     is: function(object, key) {
    +   *       // Check that object is a valid dbObject
    +   *       return angular.isObject(object) && object.id && services[key];
    +   *     }
    +   *     equals: function(a, b) {
    +   *       // Check the equality of decoded objects by comparing
    +   *       // their unique IDs
    +   *       return a.id === b.id;
    +   *     }
    +   *   };
    +   * });
    +   *
    +   * // In a config() block, you can then attach URLs with
    +   * // type-annotated parameters:
    +   * $stateProvider.state('users', {
    +   *   url: "/users",
    +   *   // ...
    +   * }).state('users.item', {
    +   *   url: "/{user:dbObject}",
    +   *   controller: function($scope, $stateParams) {
    +   *     // $stateParams.user will now be an object returned from
    +   *     // the Users service
    +   *   },
    +   *   // ...
    +   * });
    +   * 
    + */ + this.type = function (name, def) { + if (!isDefined(def)) return UrlMatcher.prototype.$types[name]; + typeQueue.push({ name: name, def: def }); + if (!enqueue) flushTypeQueue(); + return this; + }; + + /* No need to document $get, since it returns this */ + this.$get = ['$injector', function ($injector) { + injector = $injector; + enqueue = false; + UrlMatcher.prototype.$types = {}; + flushTypeQueue(); + + forEach(defaultTypes, function(type, name) { + if (!UrlMatcher.prototype.$types[name]) UrlMatcher.prototype.$types[name] = new Type(type); + }); + return this; + }]; + + // To ensure proper order of operations in object configuration, and to allow internal + // types to be overridden, `flushTypeQueue()` waits until `$urlMatcherFactory` is injected + // before actually wiring up and assigning type definitions + function flushTypeQueue() { + forEach(typeQueue, function(type) { + if (UrlMatcher.prototype.$types[type.name]) { + throw new Error("A type named '" + type.name + "' has already been defined."); + } + var def = new Type(isInjectable(type.def) ? injector.invoke(type.def) : type.def); + UrlMatcher.prototype.$types[type.name] = def; + }); + } +} + +// Register as a provider so it's available to other providers +angular.module('ui.router.util').provider('$urlMatcherFactory', $UrlMatcherFactory); + +/** + * @ngdoc object + * @name ui.router.router.$urlRouterProvider + * + * @requires ui.router.util.$urlMatcherFactoryProvider + * @requires $locationProvider + * + * @description + * `$urlRouterProvider` has the responsibility of watching `$location`. + * When `$location` changes it runs through a list of rules one by one until a + * match is found. `$urlRouterProvider` is used behind the scenes anytime you specify + * a url in a state configuration. All urls are compiled into a UrlMatcher object. + * + * There are several methods on `$urlRouterProvider` that make it useful to use directly + * in your module config. + */ +$UrlRouterProvider.$inject = ['$locationProvider', '$urlMatcherFactoryProvider']; +function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { + var rules = [], otherwise = null, interceptDeferred = false, listener; + + // Returns a string that is a prefix of all strings matching the RegExp + function regExpPrefix(re) { + var prefix = /^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(re.source); + return (prefix != null) ? prefix[1].replace(/\\(.)/g, "$1") : ''; + } + + // Interpolates matched values into a String.replace()-style pattern + function interpolate(pattern, match) { + return pattern.replace(/\$(\$|\d{1,2})/, function (m, what) { + return match[what === '$' ? 0 : Number(what)]; + }); + } + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#rule + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Defines rules that are used by `$urlRouterProvider` to find matches for + * specific URLs. + * + * @example + *
    +   * var app = angular.module('app', ['ui.router.router']);
    +   *
    +   * app.config(function ($urlRouterProvider) {
    +   *   // Here's an example of how you might allow case insensitive urls
    +   *   $urlRouterProvider.rule(function ($injector, $location) {
    +   *     var path = $location.path(),
    +   *         normalized = path.toLowerCase();
    +   *
    +   *     if (path !== normalized) {
    +   *       return normalized;
    +   *     }
    +   *   });
    +   * });
    +   * 
    + * + * @param {object} rule Handler function that takes `$injector` and `$location` + * services as arguments. You can use them to return a valid path as a string. + * + * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance + */ + this.rule = function (rule) { + if (!isFunction(rule)) throw new Error("'rule' must be a function"); + rules.push(rule); + return this; + }; + + /** + * @ngdoc object + * @name ui.router.router.$urlRouterProvider#otherwise + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Defines a path that is used when an invalid route is requested. + * + * @example + *
    +   * var app = angular.module('app', ['ui.router.router']);
    +   *
    +   * app.config(function ($urlRouterProvider) {
    +   *   // if the path doesn't match any of the urls you configured
    +   *   // otherwise will take care of routing the user to the
    +   *   // specified url
    +   *   $urlRouterProvider.otherwise('/index');
    +   *
    +   *   // Example of using function rule as param
    +   *   $urlRouterProvider.otherwise(function ($injector, $location) {
    +   *     return '/a/valid/url';
    +   *   });
    +   * });
    +   * 
    + * + * @param {string|object} rule The url path you want to redirect to or a function + * rule that returns the url path. The function version is passed two params: + * `$injector` and `$location` services, and must return a url string. + * + * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance + */ + this.otherwise = function (rule) { + if (isString(rule)) { + var redirect = rule; + rule = function () { return redirect; }; + } + else if (!isFunction(rule)) throw new Error("'rule' must be a function"); + otherwise = rule; + return this; + }; + + + function handleIfMatch($injector, handler, match) { + if (!match) return false; + var result = $injector.invoke(handler, handler, { $match: match }); + return isDefined(result) ? result : true; + } + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#when + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Registers a handler for a given url matching. if handle is a string, it is + * treated as a redirect, and is interpolated according to the syntax of match + * (i.e. like `String.replace()` for `RegExp`, or like a `UrlMatcher` pattern otherwise). + * + * If the handler is a function, it is injectable. It gets invoked if `$location` + * matches. You have the option of inject the match object as `$match`. + * + * The handler can return + * + * - **falsy** to indicate that the rule didn't match after all, then `$urlRouter` + * will continue trying to find another one that matches. + * - **string** which is treated as a redirect and passed to `$location.url()` + * - **void** or any **truthy** value tells `$urlRouter` that the url was handled. + * + * @example + *
    +   * var app = angular.module('app', ['ui.router.router']);
    +   *
    +   * app.config(function ($urlRouterProvider) {
    +   *   $urlRouterProvider.when($state.url, function ($match, $stateParams) {
    +   *     if ($state.$current.navigable !== state ||
    +   *         !equalForKeys($match, $stateParams) {
    +   *      $state.transitionTo(state, $match, false);
    +   *     }
    +   *   });
    +   * });
    +   * 
    + * + * @param {string|object} what The incoming path that you want to redirect. + * @param {string|object} handler The path you want to redirect your user to. + */ + this.when = function (what, handler) { + var redirect, handlerIsString = isString(handler); + if (isString(what)) what = $urlMatcherFactory.compile(what); + + if (!handlerIsString && !isFunction(handler) && !isArray(handler)) + throw new Error("invalid 'handler' in when()"); + + var strategies = { + matcher: function (what, handler) { + if (handlerIsString) { + redirect = $urlMatcherFactory.compile(handler); + handler = ['$match', function ($match) { return redirect.format($match); }]; + } + return extend(function ($injector, $location) { + return handleIfMatch($injector, handler, what.exec($location.path(), $location.search())); + }, { + prefix: isString(what.prefix) ? what.prefix : '' + }); + }, + regex: function (what, handler) { + if (what.global || what.sticky) throw new Error("when() RegExp must not be global or sticky"); + + if (handlerIsString) { + redirect = handler; + handler = ['$match', function ($match) { return interpolate(redirect, $match); }]; + } + return extend(function ($injector, $location) { + return handleIfMatch($injector, handler, what.exec($location.path())); + }, { + prefix: regExpPrefix(what) + }); + } + }; + + var check = { matcher: $urlMatcherFactory.isMatcher(what), regex: what instanceof RegExp }; + + for (var n in check) { + if (check[n]) return this.rule(strategies[n](what, handler)); + } + + throw new Error("invalid 'what' in when()"); + }; + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#deferIntercept + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Disables (or enables) deferring location change interception. + * + * If you wish to customize the behavior of syncing the URL (for example, if you wish to + * defer a transition but maintain the current URL), call this method at configuration time. + * Then, at run time, call `$urlRouter.listen()` after you have configured your own + * `$locationChangeSuccess` event handler. + * + * @example + *
    +   * var app = angular.module('app', ['ui.router.router']);
    +   *
    +   * app.config(function ($urlRouterProvider) {
    +   *
    +   *   // Prevent $urlRouter from automatically intercepting URL changes;
    +   *   // this allows you to configure custom behavior in between
    +   *   // location changes and route synchronization:
    +   *   $urlRouterProvider.deferIntercept();
    +   *
    +   * }).run(function ($rootScope, $urlRouter, UserService) {
    +   *
    +   *   $rootScope.$on('$locationChangeSuccess', function(e) {
    +   *     // UserService is an example service for managing user state
    +   *     if (UserService.isLoggedIn()) return;
    +   *
    +   *     // Prevent $urlRouter's default handler from firing
    +   *     e.preventDefault();
    +   *
    +   *     UserService.handleLogin().then(function() {
    +   *       // Once the user has logged in, sync the current URL
    +   *       // to the router:
    +   *       $urlRouter.sync();
    +   *     });
    +   *   });
    +   *
    +   *   // Configures $urlRouter's listener *after* your custom listener
    +   *   $urlRouter.listen();
    +   * });
    +   * 
    + * + * @param {boolean} defer Indicates whether to defer location change interception. Passing + no parameter is equivalent to `true`. + */ + this.deferIntercept = function (defer) { + if (defer === undefined) defer = true; + interceptDeferred = defer; + }; + + /** + * @ngdoc object + * @name ui.router.router.$urlRouter + * + * @requires $location + * @requires $rootScope + * @requires $injector + * @requires $browser + * + * @description + * + */ + this.$get = $get; + $get.$inject = ['$location', '$rootScope', '$injector', '$browser']; + function $get( $location, $rootScope, $injector, $browser) { + + var baseHref = $browser.baseHref(), location = $location.url(); + + function appendBasePath(url, isHtml5, absolute) { + if (baseHref === '/') return url; + if (isHtml5) return baseHref.slice(0, -1) + url; + if (absolute) return baseHref.slice(1) + url; + return url; + } + + // TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree + function update(evt) { + if (evt && evt.defaultPrevented) return; + + function check(rule) { + var handled = rule($injector, $location); + + if (!handled) return false; + if (isString(handled)) $location.replace().url(handled); + return true; + } + var n = rules.length, i; + + for (i = 0; i < n; i++) { + if (check(rules[i])) return; + } + // always check otherwise last to allow dynamic updates to the set of rules + if (otherwise) check(otherwise); + } + + function listen() { + listener = listener || $rootScope.$on('$locationChangeSuccess', update); + return listener; + } + + if (!interceptDeferred) listen(); + + return { + /** + * @ngdoc function + * @name ui.router.router.$urlRouter#sync + * @methodOf ui.router.router.$urlRouter + * + * @description + * Triggers an update; the same update that happens when the address bar url changes, aka `$locationChangeSuccess`. + * This method is useful when you need to use `preventDefault()` on the `$locationChangeSuccess` event, + * perform some custom logic (route protection, auth, config, redirection, etc) and then finally proceed + * with the transition by calling `$urlRouter.sync()`. + * + * @example + *
    +       * angular.module('app', ['ui.router'])
    +       *   .run(function($rootScope, $urlRouter) {
    +       *     $rootScope.$on('$locationChangeSuccess', function(evt) {
    +       *       // Halt state change from even starting
    +       *       evt.preventDefault();
    +       *       // Perform custom logic
    +       *       var meetsRequirement = ...
    +       *       // Continue with the update and state transition if logic allows
    +       *       if (meetsRequirement) $urlRouter.sync();
    +       *     });
    +       * });
    +       * 
    + */ + sync: function() { + update(); + }, + + listen: function() { + return listen(); + }, + + update: function(read) { + if (read) { + location = $location.url(); + return; + } + if ($location.url() === location) return; + + $location.url(location); + $location.replace(); + }, + + push: function(urlMatcher, params, options) { + $location.url(urlMatcher.format(params || {})); + if (options && options.replace) $location.replace(); + }, + + /** + * @ngdoc function + * @name ui.router.router.$urlRouter#href + * @methodOf ui.router.router.$urlRouter + * + * @description + * A URL generation method that returns the compiled URL for a given + * {@link ui.router.util.type:UrlMatcher `UrlMatcher`}, populated with the provided parameters. + * + * @example + *
    +       * $bob = $urlRouter.href(new UrlMatcher("/about/:person"), {
    +       *   person: "bob"
    +       * });
    +       * // $bob == "/about/bob";
    +       * 
    + * + * @param {UrlMatcher} urlMatcher The `UrlMatcher` object which is used as the template of the URL to generate. + * @param {object=} params An object of parameter values to fill the matcher's required parameters. + * @param {object=} options Options object. The options are: + * + * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". + * + * @returns {string} Returns the fully compiled URL, or `null` if `params` fail validation against `urlMatcher` + */ + href: function(urlMatcher, params, options) { + if (!urlMatcher.validates(params)) return null; + + var isHtml5 = $locationProvider.html5Mode(); + var url = urlMatcher.format(params); + options = options || {}; + + if (!isHtml5 && url !== null) { + url = "#" + $locationProvider.hashPrefix() + url; + } + url = appendBasePath(url, isHtml5, options.absolute); + + if (!options.absolute || !url) { + return url; + } + + var slash = (!isHtml5 && url ? '/' : ''), port = $location.port(); + port = (port === 80 || port === 443 ? '' : ':' + port); + + return [$location.protocol(), '://', $location.host(), port, slash, url].join(''); + } + }; + } +} + +angular.module('ui.router.router').provider('$urlRouter', $UrlRouterProvider); + +/** + * @ngdoc object + * @name ui.router.state.$stateProvider + * + * @requires ui.router.router.$urlRouterProvider + * @requires ui.router.util.$urlMatcherFactoryProvider + * + * @description + * The new `$stateProvider` works similar to Angular's v1 router, but it focuses purely + * on state. + * + * A state corresponds to a "place" in the application in terms of the overall UI and + * navigation. A state describes (via the controller / template / view properties) what + * the UI looks like and does at that place. + * + * States often have things in common, and the primary way of factoring out these + * commonalities in this model is via the state hierarchy, i.e. parent/child states aka + * nested states. + * + * The `$stateProvider` provides interfaces to declare these states for your app. + */ +$StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider']; +function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { + + var root, states = {}, $state, queue = {}, abstractKey = 'abstract'; + + // Builds state properties from definition passed to registerState() + var stateBuilder = { + + // Derive parent state from a hierarchical name only if 'parent' is not explicitly defined. + // state.children = []; + // if (parent) parent.children.push(state); + parent: function(state) { + if (isDefined(state.parent) && state.parent) return findState(state.parent); + // regex matches any valid composite state name + // would match "contact.list" but not "contacts" + var compositeName = /^(.+)\.[^.]+$/.exec(state.name); + return compositeName ? findState(compositeName[1]) : root; + }, + + // inherit 'data' from parent and override by own values (if any) + data: function(state) { + if (state.parent && state.parent.data) { + state.data = state.self.data = extend({}, state.parent.data, state.data); + } + return state.data; + }, + + // Build a URLMatcher if necessary, either via a relative or absolute URL + url: function(state) { + var url = state.url, config = { params: state.params || {} }; + + if (isString(url)) { + if (url.charAt(0) == '^') return $urlMatcherFactory.compile(url.substring(1), config); + return (state.parent.navigable || root).url.concat(url, config); + } + + if (!url || $urlMatcherFactory.isMatcher(url)) return url; + throw new Error("Invalid url '" + url + "' in state '" + state + "'"); + }, + + // Keep track of the closest ancestor state that has a URL (i.e. is navigable) + navigable: function(state) { + return state.url ? state : (state.parent ? state.parent.navigable : null); + }, + + // Derive parameters for this state and ensure they're a super-set of parent's parameters + params: function(state) { + if (!state.params) { + return state.url ? state.url.params : state.parent.params; + } + return state.params; + }, + + // If there is no explicit multi-view configuration, make one up so we don't have + // to handle both cases in the view directive later. Note that having an explicit + // 'views' property will mean the default unnamed view properties are ignored. This + // is also a good time to resolve view names to absolute names, so everything is a + // straight lookup at link time. + views: function(state) { + var views = {}; + + forEach(isDefined(state.views) ? state.views : { '': state }, function (view, name) { + if (name.indexOf('@') < 0) name += '@' + state.parent.name; + views[name] = view; + }); + return views; + }, + + ownParams: function(state) { + state.params = state.params || {}; + + if (!state.parent) { + return objectKeys(state.params); + } + var paramNames = {}; forEach(state.params, function (v, k) { paramNames[k] = true; }); + + forEach(state.parent.params, function (v, k) { + if (!paramNames[k]) { + throw new Error("Missing required parameter '" + k + "' in state '" + state.name + "'"); + } + paramNames[k] = false; + }); + var ownParams = []; + + forEach(paramNames, function (own, p) { + if (own) ownParams.push(p); + }); + return ownParams; + }, + + // Keep a full path from the root down to this state as this is needed for state activation. + path: function(state) { + return state.parent ? state.parent.path.concat(state) : []; // exclude root from path + }, + + // Speed up $state.contains() as it's used a lot + includes: function(state) { + var includes = state.parent ? extend({}, state.parent.includes) : {}; + includes[state.name] = true; + return includes; + }, + + $delegates: {} + }; + + function isRelative(stateName) { + return stateName.indexOf(".") === 0 || stateName.indexOf("^") === 0; + } + + function findState(stateOrName, base) { + if (!stateOrName) return undefined; + + var isStr = isString(stateOrName), + name = isStr ? stateOrName : stateOrName.name, + path = isRelative(name); + + if (path) { + if (!base) throw new Error("No reference point given for path '" + name + "'"); + var rel = name.split("."), i = 0, pathLength = rel.length, current = base; + + for (; i < pathLength; i++) { + if (rel[i] === "" && i === 0) { + current = base; + continue; + } + if (rel[i] === "^") { + if (!current.parent) throw new Error("Path '" + name + "' not valid for state '" + base.name + "'"); + current = current.parent; + continue; + } + break; + } + rel = rel.slice(i).join("."); + name = current.name + (current.name && rel ? "." : "") + rel; + } + var state = states[name]; + + if (state && (isStr || (!isStr && (state === stateOrName || state.self === stateOrName)))) { + return state; + } + return undefined; + } + + function queueState(parentName, state) { + if (!queue[parentName]) { + queue[parentName] = []; + } + queue[parentName].push(state); + } + + function registerState(state) { + // Wrap a new object around the state so we can store our private details easily. + state = inherit(state, { + self: state, + resolve: state.resolve || {}, + toString: function() { return this.name; } + }); + + var name = state.name; + if (!isString(name) || name.indexOf('@') >= 0) throw new Error("State must have a valid name"); + if (states.hasOwnProperty(name)) throw new Error("State '" + name + "'' is already defined"); + + // Get parent name + var parentName = (name.indexOf('.') !== -1) ? name.substring(0, name.lastIndexOf('.')) + : (isString(state.parent)) ? state.parent + : ''; + + // If parent is not registered yet, add state to queue and register later + if (parentName && !states[parentName]) { + return queueState(parentName, state.self); + } + + for (var key in stateBuilder) { + if (isFunction(stateBuilder[key])) state[key] = stateBuilder[key](state, stateBuilder.$delegates[key]); + } + states[name] = state; + + // Register the state in the global state list and with $urlRouter if necessary. + if (!state[abstractKey] && state.url) { + $urlRouterProvider.when(state.url, ['$match', '$stateParams', function ($match, $stateParams) { + if ($state.$current.navigable != state || !equalForKeys($match, $stateParams)) { + $state.transitionTo(state, $match, { location: false }); + } + }]); + } + + // Register any queued children + if (queue[name]) { + for (var i = 0; i < queue[name].length; i++) { + registerState(queue[name][i]); + } + } + + return state; + } + + // Checks text to see if it looks like a glob. + function isGlob (text) { + return text.indexOf('*') > -1; + } + + // Returns true if glob matches current $state name. + function doesStateMatchGlob (glob) { + var globSegments = glob.split('.'), + segments = $state.$current.name.split('.'); + + //match greedy starts + if (globSegments[0] === '**') { + segments = segments.slice(segments.indexOf(globSegments[1])); + segments.unshift('**'); + } + //match greedy ends + if (globSegments[globSegments.length - 1] === '**') { + segments.splice(segments.indexOf(globSegments[globSegments.length - 2]) + 1, Number.MAX_VALUE); + segments.push('**'); + } + + if (globSegments.length != segments.length) { + return false; + } + + //match single stars + for (var i = 0, l = globSegments.length; i < l; i++) { + if (globSegments[i] === '*') { + segments[i] = '*'; + } + } + + return segments.join('') === globSegments.join(''); + } + + + // Implicit root state that is always active + root = registerState({ + name: '', + url: '^', + views: null, + 'abstract': true + }); + root.navigable = null; + + + /** + * @ngdoc function + * @name ui.router.state.$stateProvider#decorator + * @methodOf ui.router.state.$stateProvider + * + * @description + * Allows you to extend (carefully) or override (at your own peril) the + * `stateBuilder` object used internally by `$stateProvider`. This can be used + * to add custom functionality to ui-router, for example inferring templateUrl + * based on the state name. + * + * When passing only a name, it returns the current (original or decorated) builder + * function that matches `name`. + * + * The builder functions that can be decorated are listed below. Though not all + * necessarily have a good use case for decoration, that is up to you to decide. + * + * In addition, users can attach custom decorators, which will generate new + * properties within the state's internal definition. There is currently no clear + * use-case for this beyond accessing internal states (i.e. $state.$current), + * however, expect this to become increasingly relevant as we introduce additional + * meta-programming features. + * + * **Warning**: Decorators should not be interdependent because the order of + * execution of the builder functions in non-deterministic. Builder functions + * should only be dependent on the state definition object and super function. + * + * + * Existing builder functions and current return values: + * + * - **parent** `{object}` - returns the parent state object. + * - **data** `{object}` - returns state data, including any inherited data that is not + * overridden by own values (if any). + * - **url** `{object}` - returns a {@link ui.router.util.type:UrlMatcher UrlMatcher} + * or `null`. + * - **navigable** `{object}` - returns closest ancestor state that has a URL (aka is + * navigable). + * - **params** `{object}` - returns an array of state params that are ensured to + * be a super-set of parent's params. + * - **views** `{object}` - returns a views object where each key is an absolute view + * name (i.e. "viewName@stateName") and each value is the config object + * (template, controller) for the view. Even when you don't use the views object + * explicitly on a state config, one is still created for you internally. + * So by decorating this builder function you have access to decorating template + * and controller properties. + * - **ownParams** `{object}` - returns an array of params that belong to the state, + * not including any params defined by ancestor states. + * - **path** `{string}` - returns the full path from the root down to this state. + * Needed for state activation. + * - **includes** `{object}` - returns an object that includes every state that + * would pass a `$state.includes()` test. + * + * @example + *
    +   * // Override the internal 'views' builder with a function that takes the state
    +   * // definition, and a reference to the internal function being overridden:
    +   * $stateProvider.decorator('views', function (state, parent) {
    +   *   var result = {},
    +   *       views = parent(state);
    +   *
    +   *   angular.forEach(views, function (config, name) {
    +   *     var autoName = (state.name + '.' + name).replace('.', '/');
    +   *     config.templateUrl = config.templateUrl || '/partials/' + autoName + '.html';
    +   *     result[name] = config;
    +   *   });
    +   *   return result;
    +   * });
    +   *
    +   * $stateProvider.state('home', {
    +   *   views: {
    +   *     'contact.list': { controller: 'ListController' },
    +   *     'contact.item': { controller: 'ItemController' }
    +   *   }
    +   * });
    +   *
    +   * // ...
    +   *
    +   * $state.go('home');
    +   * // Auto-populates list and item views with /partials/home/contact/list.html,
    +   * // and /partials/home/contact/item.html, respectively.
    +   * 
    + * + * @param {string} name The name of the builder function to decorate. + * @param {object} func A function that is responsible for decorating the original + * builder function. The function receives two parameters: + * + * - `{object}` - state - The state config object. + * - `{object}` - super - The original builder function. + * + * @return {object} $stateProvider - $stateProvider instance + */ + this.decorator = decorator; + function decorator(name, func) { + /*jshint validthis: true */ + if (isString(name) && !isDefined(func)) { + return stateBuilder[name]; + } + if (!isFunction(func) || !isString(name)) { + return this; + } + if (stateBuilder[name] && !stateBuilder.$delegates[name]) { + stateBuilder.$delegates[name] = stateBuilder[name]; + } + stateBuilder[name] = func; + return this; + } + + /** + * @ngdoc function + * @name ui.router.state.$stateProvider#state + * @methodOf ui.router.state.$stateProvider + * + * @description + * Registers a state configuration under a given state name. The stateConfig object + * has the following acceptable properties. + * + * + * + * - **`template`** - {string|function=} - html template as a string or a function that returns + * an html template as a string which should be used by the uiView directives. This property + * takes precedence over templateUrl. + * + * If `template` is a function, it will be called with the following parameters: + * + * - {array.<object>} - state parameters extracted from the current $location.path() by + * applying the current state + * + * + * + * - **`templateUrl`** - {string|function=} - path or function that returns a path to an html + * template that should be used by uiView. + * + * If `templateUrl` is a function, it will be called with the following parameters: + * + * - {array.<object>} - state parameters extracted from the current $location.path() by + * applying the current state + * + * + * + * - **`templateProvider`** - {function=} - Provider function that returns HTML content + * string. + * + * + * + * - **`controller`** - {string|function=} - Controller fn that should be associated with newly + * related scope or the name of a registered controller if passed as a string. + * + * + * + * - **`controllerProvider`** - {function=} - Injectable provider function that returns + * the actual controller or string. + * + * + * + * - **`controllerAs`** – {string=} – A controller alias name. If present the controller will be + * published to scope under the controllerAs name. + * + * + * + * - **`resolve`** - {object.<string, function>=} - An optional map of dependencies which + * should be injected into the controller. If any of these dependencies are promises, + * the router will wait for them all to be resolved or one to be rejected before the + * controller is instantiated. If all the promises are resolved successfully, the values + * of the resolved promises are injected and $stateChangeSuccess event is fired. If any + * of the promises are rejected the $stateChangeError event is fired. The map object is: + * + * - key - {string}: name of dependency to be injected into controller + * - factory - {string|function}: If string then it is alias for service. Otherwise if function, + * it is injected and return value it treated as dependency. If result is a promise, it is + * resolved before its value is injected into controller. + * + * + * + * - **`url`** - {string=} - A url with optional parameters. When a state is navigated or + * transitioned to, the `$stateParams` service will be populated with any + * parameters that were passed. + * + * + * + * - **`params`** - {object=} - An array of parameter names or regular expressions. Only + * use this within a state if you are not using url. Otherwise you can specify your + * parameters within the url. When a state is navigated or transitioned to, the + * $stateParams service will be populated with any parameters that were passed. + * + * + * + * - **`views`** - {object=} - Use the views property to set up multiple views or to target views + * manually/explicitly. + * + * + * + * - **`abstract`** - {boolean=} - An abstract state will never be directly activated, + * but can provide inherited properties to its common children states. + * + * + * + * - **`onEnter`** - {object=} - Callback function for when a state is entered. Good way + * to trigger an action or dispatch an event, such as opening a dialog. + * If minifying your scripts, make sure to use the `['injection1', 'injection2', function(injection1, injection2){}]` syntax. + * + * + * + * - **`onExit`** - {object=} - Callback function for when a state is exited. Good way to + * trigger an action or dispatch an event, such as opening a dialog. + * If minifying your scripts, make sure to use the `['injection1', 'injection2', function(injection1, injection2){}]` syntax. + * + * + * + * - **`reloadOnSearch = true`** - {boolean=} - If `false`, will not retrigger the same state + * just because a search/query parameter has changed (via $location.search() or $location.hash()). + * Useful for when you'd like to modify $location.search() without triggering a reload. + * + * + * + * - **`data`** - {object=} - Arbitrary data object, useful for custom configuration. + * + * @example + *
    +   * // Some state name examples
    +   *
    +   * // stateName can be a single top-level name (must be unique).
    +   * $stateProvider.state("home", {});
    +   *
    +   * // Or it can be a nested state name. This state is a child of the 
    +   * // above "home" state.
    +   * $stateProvider.state("home.newest", {});
    +   *
    +   * // Nest states as deeply as needed.
    +   * $stateProvider.state("home.newest.abc.xyz.inception", {});
    +   *
    +   * // state() returns $stateProvider, so you can chain state declarations.
    +   * $stateProvider
    +   *   .state("home", {})
    +   *   .state("about", {})
    +   *   .state("contacts", {});
    +   * 
    + * + * @param {string} name A unique state name, e.g. "home", "about", "contacts". + * To create a parent/child state use a dot, e.g. "about.sales", "home.newest". + * @param {object} definition State configuration object. + */ + this.state = state; + function state(name, definition) { + /*jshint validthis: true */ + if (isObject(name)) definition = name; + else definition.name = name; + registerState(definition); + return this; + } + + /** + * @ngdoc object + * @name ui.router.state.$state + * + * @requires $rootScope + * @requires $q + * @requires ui.router.state.$view + * @requires $injector + * @requires ui.router.util.$resolve + * @requires ui.router.state.$stateParams + * @requires ui.router.router.$urlRouter + * + * @property {object} params A param object, e.g. {sectionId: section.id)}, that + * you'd like to test against the current active state. + * @property {object} current A reference to the state's config object. However + * you passed it in. Useful for accessing custom data. + * @property {object} transition Currently pending transition. A promise that'll + * resolve or reject. + * + * @description + * `$state` service is responsible for representing states as well as transitioning + * between them. It also provides interfaces to ask for current state or even states + * you're coming from. + */ + this.$get = $get; + $get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$urlRouter']; + function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $urlRouter) { + + var TransitionSuperseded = $q.reject(new Error('transition superseded')); + var TransitionPrevented = $q.reject(new Error('transition prevented')); + var TransitionAborted = $q.reject(new Error('transition aborted')); + var TransitionFailed = $q.reject(new Error('transition failed')); + + // Handles the case where a state which is the target of a transition is not found, and the user + // can optionally retry or defer the transition + function handleRedirect(redirect, state, params, options) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateNotFound + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when a requested state **cannot be found** using the provided state name during transition. + * The event is broadcast allowing any handlers a single chance to deal with the error (usually by + * lazy-loading the unfound state). A special `unfoundState` object is passed to the listener handler, + * you can see its three properties in the example. You can use `event.preventDefault()` to abort the + * transition and the promise returned from `go` will be rejected with a `'transition aborted'` value. + * + * @param {Object} event Event object. + * @param {Object} unfoundState Unfound State information. Contains: `to, toParams, options` properties. + * @param {State} fromState Current state object. + * @param {Object} fromParams Current state params. + * + * @example + * + *
    +       * // somewhere, assume lazy.state has not been defined
    +       * $state.go("lazy.state", {a:1, b:2}, {inherit:false});
    +       *
    +       * // somewhere else
    +       * $scope.$on('$stateNotFound',
    +       * function(event, unfoundState, fromState, fromParams){
    +       *     console.log(unfoundState.to); // "lazy.state"
    +       *     console.log(unfoundState.toParams); // {a:1, b:2}
    +       *     console.log(unfoundState.options); // {inherit:false} + default options
    +       * })
    +       * 
    + */ + var evt = $rootScope.$broadcast('$stateNotFound', redirect, state, params); + + if (evt.defaultPrevented) { + $urlRouter.update(); + return TransitionAborted; + } + + if (!evt.retry) { + return null; + } + + // Allow the handler to return a promise to defer state lookup retry + if (options.$retry) { + $urlRouter.update(); + return TransitionFailed; + } + var retryTransition = $state.transition = $q.when(evt.retry); + + retryTransition.then(function() { + if (retryTransition !== $state.transition) return TransitionSuperseded; + redirect.options.$retry = true; + return $state.transitionTo(redirect.to, redirect.toParams, redirect.options); + }, function() { + return TransitionAborted; + }); + $urlRouter.update(); + + return retryTransition; + } + + root.locals = { resolve: null, globals: { $stateParams: {} } }; + + $state = { + params: {}, + current: root.self, + $current: root, + transition: null + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#reload + * @methodOf ui.router.state.$state + * + * @description + * A method that force reloads the current state. All resolves are re-resolved, events are not re-fired, + * and controllers reinstantiated (bug with controllers reinstantiating right now, fixing soon). + * + * @example + *
    +     * var app angular.module('app', ['ui.router']);
    +     *
    +     * app.controller('ctrl', function ($scope, $state) {
    +     *   $scope.reload = function(){
    +     *     $state.reload();
    +     *   }
    +     * });
    +     * 
    + * + * `reload()` is just an alias for: + *
    +     * $state.transitionTo($state.current, $stateParams, { 
    +     *   reload: true, inherit: false, notify: false 
    +     * });
    +     * 
    + */ + $state.reload = function reload() { + $state.transitionTo($state.current, $stateParams, { reload: true, inherit: false, notify: false }); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#go + * @methodOf ui.router.state.$state + * + * @description + * Convenience method for transitioning to a new state. `$state.go` calls + * `$state.transitionTo` internally but automatically sets options to + * `{ location: true, inherit: true, relative: $state.$current, notify: true }`. + * This allows you to easily use an absolute or relative to path and specify + * only the parameters you'd like to update (while letting unspecified parameters + * inherit from the currently active ancestor states). + * + * @example + *
    +     * var app = angular.module('app', ['ui.router']);
    +     *
    +     * app.controller('ctrl', function ($scope, $state) {
    +     *   $scope.changeState = function () {
    +     *     $state.go('contact.detail');
    +     *   };
    +     * });
    +     * 
    + * + * + * @param {string} to Absolute state name or relative state path. Some examples: + * + * - `$state.go('contact.detail')` - will go to the `contact.detail` state + * - `$state.go('^')` - will go to a parent state + * - `$state.go('^.sibling')` - will go to a sibling state + * - `$state.go('.child.grandchild')` - will go to grandchild state + * + * @param {object=} params A map of the parameters that will be sent to the state, + * will populate $stateParams. Any parameters that are not specified will be inherited from currently + * defined parameters. This allows, for example, going to a sibling state that shares parameters + * specified in a parent state. Parameter inheritance only works between common ancestor states, I.e. + * transitioning to a sibling will get you the parameters for all parents, transitioning to a child + * will get you all current parameters, etc. + * @param {object=} options Options object. The options are: + * + * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` + * will not. If string, must be `"replace"`, which will update url and also replace last history record. + * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), + * defines which state to be relative from. + * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. + * - **`reload`** (v0.2.5) - {boolean=false}, If `true` will force transition even if the state or params + * have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd + * use this when you want to force a reload when *everything* is the same, including search params. + * + * @returns {promise} A promise representing the state of the new transition. + * + * Possible success values: + * + * - $state.current + * + *
    Possible rejection values: + * + * - 'transition superseded' - when a newer transition has been started after this one + * - 'transition prevented' - when `event.preventDefault()` has been called in a `$stateChangeStart` listener + * - 'transition aborted' - when `event.preventDefault()` has been called in a `$stateNotFound` listener or + * when a `$stateNotFound` `event.retry` promise errors. + * - 'transition failed' - when a state has been unsuccessfully found after 2 tries. + * - *resolve error* - when an error has occurred with a `resolve` + * + */ + $state.go = function go(to, params, options) { + return $state.transitionTo(to, params, extend({ inherit: true, relative: $state.$current }, options)); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#transitionTo + * @methodOf ui.router.state.$state + * + * @description + * Low-level method for transitioning to a new state. {@link ui.router.state.$state#methods_go $state.go} + * uses `transitionTo` internally. `$state.go` is recommended in most situations. + * + * @example + *
    +     * var app = angular.module('app', ['ui.router']);
    +     *
    +     * app.controller('ctrl', function ($scope, $state) {
    +     *   $scope.changeState = function () {
    +     *     $state.transitionTo('contact.detail');
    +     *   };
    +     * });
    +     * 
    + * + * @param {string} to State name. + * @param {object=} toParams A map of the parameters that will be sent to the state, + * will populate $stateParams. + * @param {object=} options Options object. The options are: + * + * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` + * will not. If string, must be `"replace"`, which will update url and also replace last history record. + * - **`inherit`** - {boolean=false}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=}, When transitioning with relative path (e.g '^'), + * defines which state to be relative from. + * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. + * - **`reload`** (v0.2.5) - {boolean=false}, If `true` will force transition even if the state or params + * have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd + * use this when you want to force a reload when *everything* is the same, including search params. + * + * @returns {promise} A promise representing the state of the new transition. See + * {@link ui.router.state.$state#methods_go $state.go}. + */ + $state.transitionTo = function transitionTo(to, toParams, options) { + toParams = toParams || {}; + options = extend({ + location: true, inherit: false, relative: null, notify: true, reload: false, $retry: false + }, options || {}); + + var from = $state.$current, fromParams = $state.params, fromPath = from.path; + var evt, toState = findState(to, options.relative); + + if (!isDefined(toState)) { + var redirect = { to: to, toParams: toParams, options: options }; + var redirectResult = handleRedirect(redirect, from.self, fromParams, options); + + if (redirectResult) { + return redirectResult; + } + + // Always retry once if the $stateNotFound was not prevented + // (handles either redirect changed or state lazy-definition) + to = redirect.to; + toParams = redirect.toParams; + options = redirect.options; + toState = findState(to, options.relative); + + if (!isDefined(toState)) { + if (!options.relative) throw new Error("No such state '" + to + "'"); + throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'"); + } + } + if (toState[abstractKey]) throw new Error("Cannot transition to abstract state '" + to + "'"); + if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState); + to = toState; + + var toPath = to.path; + + // Starting from the root of the path, keep all levels that haven't changed + var keep = 0, state = toPath[keep], locals = root.locals, toLocals = []; + + if (!options.reload) { + while (state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams)) { + locals = toLocals[keep] = state.locals; + keep++; + state = toPath[keep]; + } + } + + // If we're going to the same state and all locals are kept, we've got nothing to do. + // But clear 'transition', as we still want to cancel any other pending transitions. + // TODO: We may not want to bump 'transition' if we're called from a location change + // that we've initiated ourselves, because we might accidentally abort a legitimate + // transition initiated from code? + if (shouldTriggerReload(to, from, locals, options)) { + if (to.self.reloadOnSearch !== false) $urlRouter.update(); + $state.transition = null; + return $q.when($state.current); + } + + // Filter parameters before we pass them to event handlers etc. + toParams = filterByKeys(objectKeys(to.params), toParams || {}); + + // Broadcast start event and cancel the transition if requested + if (options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeStart + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when the state transition **begins**. You can use `event.preventDefault()` + * to prevent the transition from happening and then the transition promise will be + * rejected with a `'transition prevented'` value. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + * + * @example + * + *
    +         * $rootScope.$on('$stateChangeStart',
    +         * function(event, toState, toParams, fromState, fromParams){
    +         *     event.preventDefault();
    +         *     // transitionTo() promise will be rejected with
    +         *     // a 'transition prevented' error
    +         * })
    +         * 
    + */ + if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams).defaultPrevented) { + $urlRouter.update(); + return TransitionPrevented; + } + } + + // Resolve locals for the remaining states, but don't update any global state just + // yet -- if anything fails to resolve the current state needs to remain untouched. + // We also set up an inheritance chain for the locals here. This allows the view directive + // to quickly look up the correct definition for each view in the current state. Even + // though we create the locals object itself outside resolveState(), it is initially + // empty and gets filled asynchronously. We need to keep track of the promise for the + // (fully resolved) current locals, and pass this down the chain. + var resolved = $q.when(locals); + + for (var l = keep; l < toPath.length; l++, state = toPath[l]) { + locals = toLocals[l] = inherit(locals); + resolved = resolveState(state, toParams, state === to, resolved, locals); + } + + // Once everything is resolved, we are ready to perform the actual transition + // and return a promise for the new state. We also keep track of what the + // current promise is, so that we can detect overlapping transitions and + // keep only the outcome of the last transition. + var transition = $state.transition = resolved.then(function () { + var l, entering, exiting; + + if ($state.transition !== transition) return TransitionSuperseded; + + // Exit 'from' states not kept + for (l = fromPath.length - 1; l >= keep; l--) { + exiting = fromPath[l]; + if (exiting.self.onExit) { + $injector.invoke(exiting.self.onExit, exiting.self, exiting.locals.globals); + } + exiting.locals = null; + } + + // Enter 'to' states not kept + for (l = keep; l < toPath.length; l++) { + entering = toPath[l]; + entering.locals = toLocals[l]; + if (entering.self.onEnter) { + $injector.invoke(entering.self.onEnter, entering.self, entering.locals.globals); + } + } + + // Run it again, to catch any transitions in callbacks + if ($state.transition !== transition) return TransitionSuperseded; + + // Update globals in $state + $state.$current = to; + $state.current = to.self; + $state.params = toParams; + copy($state.params, $stateParams); + $state.transition = null; + + if (options.location && to.navigable) { + $urlRouter.push(to.navigable.url, to.navigable.locals.globals.$stateParams, { + replace: options.location === 'replace' + }); + } + + if (options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeSuccess + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired once the state transition is **complete**. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + */ + $rootScope.$broadcast('$stateChangeSuccess', to.self, toParams, from.self, fromParams); + } + $urlRouter.update(true); + + return $state.current; + }, function (error) { + if ($state.transition !== transition) return TransitionSuperseded; + + $state.transition = null; + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeError + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when an **error occurs** during transition. It's important to note that if you + * have any errors in your resolve functions (javascript errors, non-existent services, etc) + * they will not throw traditionally. You must listen for this $stateChangeError event to + * catch **ALL** errors. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + * @param {Error} error The resolve error object. + */ + evt = $rootScope.$broadcast('$stateChangeError', to.self, toParams, from.self, fromParams, error); + + if (!evt.defaultPrevented) { + $urlRouter.update(); + } + + return $q.reject(error); + }); + + return transition; + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#is + * @methodOf ui.router.state.$state + * + * @description + * Similar to {@link ui.router.state.$state#methods_includes $state.includes}, + * but only checks for the full state name. If params is supplied then it will be + * tested for strict equality against the current active params object, so all params + * must match with none missing and no extras. + * + * @example + *
    +     * $state.$current.name = 'contacts.details.item';
    +     *
    +     * // absolute name
    +     * $state.is('contact.details.item'); // returns true
    +     * $state.is(contactDetailItemStateObject); // returns true
    +     *
    +     * // relative name (. and ^), typically from a template
    +     * // E.g. from the 'contacts.details' template
    +     * 
    Item
    + *
    + * + * @param {string|object} stateName The state name (absolute or relative) or state object you'd like to check. + * @param {object=} params A param object, e.g. `{sectionId: section.id}`, that you'd like + * to test against the current active state. + * @returns {boolean} Returns true if it is the state. + */ + $state.is = function is(stateOrName, params) { + var state = findState(stateOrName); + + if (!isDefined(state)) { + return undefined; + } + + if ($state.$current !== state) { + return false; + } + + return isDefined(params) && params !== null ? angular.equals($stateParams, params) : true; + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#includes + * @methodOf ui.router.state.$state + * + * @description + * A method to determine if the current active state is equal to or is the child of the + * state stateName. If any params are passed then they will be tested for a match as well. + * Not all the parameters need to be passed, just the ones you'd like to test for equality. + * + * @example + * Partial and relative names + *
    +     * $state.$current.name = 'contacts.details.item';
    +     *
    +     * // Using partial names
    +     * $state.includes("contacts"); // returns true
    +     * $state.includes("contacts.details"); // returns true
    +     * $state.includes("contacts.details.item"); // returns true
    +     * $state.includes("contacts.list"); // returns false
    +     * $state.includes("about"); // returns false
    +     *
    +     * // Using relative names (. and ^), typically from a template
    +     * // E.g. from the 'contacts.details' template
    +     * 
    Item
    + *
    + * + * Basic globbing patterns + *
    +     * $state.$current.name = 'contacts.details.item.url';
    +     *
    +     * $state.includes("*.details.*.*"); // returns true
    +     * $state.includes("*.details.**"); // returns true
    +     * $state.includes("**.item.**"); // returns true
    +     * $state.includes("*.details.item.url"); // returns true
    +     * $state.includes("*.details.*.url"); // returns true
    +     * $state.includes("*.details.*"); // returns false
    +     * $state.includes("item.**"); // returns false
    +     * 
    + * + * @param {string} stateOrName A partial name, relative name, or glob pattern + * to be searched for within the current state name. + * @param {object} params A param object, e.g. `{sectionId: section.id}`, + * that you'd like to test against the current active state. + * @returns {boolean} Returns true if it does include the state + */ + $state.includes = function includes(stateOrName, params) { + if (isString(stateOrName) && isGlob(stateOrName)) { + if (!doesStateMatchGlob(stateOrName)) { + return false; + } + stateOrName = $state.$current.name; + } + var state = findState(stateOrName); + + if (!isDefined(state)) { + return undefined; + } + if (!isDefined($state.$current.includes[state.name])) { + return false; + } + return equalForKeys(params, $stateParams); + }; + + + /** + * @ngdoc function + * @name ui.router.state.$state#href + * @methodOf ui.router.state.$state + * + * @description + * A url generation method that returns the compiled url for the given state populated with the given params. + * + * @example + *
    +     * expect($state.href("about.person", { person: "bob" })).toEqual("/about/bob");
    +     * 
    + * + * @param {string|object} stateOrName The state name or state object you'd like to generate a url from. + * @param {object=} params An object of parameter values to fill the state's required parameters. + * @param {object=} options Options object. The options are: + * + * - **`lossy`** - {boolean=true} - If true, and if there is no url associated with the state provided in the + * first parameter, then the constructed href url will be built from the first navigable ancestor (aka + * ancestor with a valid url). + * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), + * defines which state to be relative from. + * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". + * + * @returns {string} compiled state url + */ + $state.href = function href(stateOrName, params, options) { + options = extend({ + lossy: true, + inherit: true, + absolute: false, + relative: $state.$current + }, options || {}); + + var state = findState(stateOrName, options.relative); + + if (!isDefined(state)) return null; + if (options.inherit) params = inheritParams($stateParams, params || {}, $state.$current, state); + + var nav = (state && options.lossy) ? state.navigable : state; + + if (!nav || !nav.url) { + return null; + } + return $urlRouter.href(nav.url, filterByKeys(objectKeys(state.params), params || {}), { + absolute: options.absolute + }); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#get + * @methodOf ui.router.state.$state + * + * @description + * Returns the state configuration object for any specific state or all states. + * + * @param {string|Sbject=} stateOrName (absolute or relative) If provided, will only get the config for + * the requested state. If not provided, returns an array of ALL state configs. + * @returns {Object|Array} State configuration object or array of all objects. + */ + $state.get = function (stateOrName, context) { + if (arguments.length === 0) return objectKeys(states).map(function(name) { return states[name].self; }); + var state = findState(stateOrName, context); + return (state && state.self) ? state.self : null; + }; + + function resolveState(state, params, paramsAreFiltered, inherited, dst) { + // Make a restricted $stateParams with only the parameters that apply to this state if + // necessary. In addition to being available to the controller and onEnter/onExit callbacks, + // we also need $stateParams to be available for any $injector calls we make during the + // dependency resolution process. + var $stateParams = (paramsAreFiltered) ? params : filterByKeys(objectKeys(state.params), params); + var locals = { $stateParams: $stateParams }; + + // Resolve 'global' dependencies for the state, i.e. those not specific to a view. + // We're also including $stateParams in this; that way the parameters are restricted + // to the set that should be visible to the state, and are independent of when we update + // the global $state and $stateParams values. + dst.resolve = $resolve.resolve(state.resolve, locals, dst.resolve, state); + var promises = [dst.resolve.then(function (globals) { + dst.globals = globals; + })]; + if (inherited) promises.push(inherited); + + // Resolve template and dependencies for all views. + forEach(state.views, function (view, name) { + var injectables = (view.resolve && view.resolve !== state.resolve ? view.resolve : {}); + injectables.$template = [ function () { + return $view.load(name, { view: view, locals: locals, params: $stateParams }) || ''; + }]; + + promises.push($resolve.resolve(injectables, locals, dst.resolve, state).then(function (result) { + // References to the controller (only instantiated at link time) + if (isFunction(view.controllerProvider) || isArray(view.controllerProvider)) { + var injectLocals = angular.extend({}, injectables, locals); + result.$$controller = $injector.invoke(view.controllerProvider, null, injectLocals); + } else { + result.$$controller = view.controller; + } + // Provide access to the state itself for internal use + result.$$state = state; + result.$$controllerAs = view.controllerAs; + dst[name] = result; + })); + }); + + // Wait for all the promises and then return the activation object + return $q.all(promises).then(function (values) { + return dst; + }); + } + + return $state; + } + + function shouldTriggerReload(to, from, locals, options) { + if (to === from && ((locals === from.locals && !options.reload) || (to.self.reloadOnSearch === false))) { + return true; + } + } +} + +angular.module('ui.router.state') + .value('$stateParams', {}) + .provider('$state', $StateProvider); + + +$ViewProvider.$inject = []; +function $ViewProvider() { + + this.$get = $get; + /** + * @ngdoc object + * @name ui.router.state.$view + * + * @requires ui.router.util.$templateFactory + * @requires $rootScope + * + * @description + * + */ + $get.$inject = ['$rootScope', '$templateFactory']; + function $get( $rootScope, $templateFactory) { + return { + // $view.load('full.viewName', { template: ..., controller: ..., resolve: ..., async: false, params: ... }) + /** + * @ngdoc function + * @name ui.router.state.$view#load + * @methodOf ui.router.state.$view + * + * @description + * + * @param {string} name name + * @param {object} options option object. + */ + load: function load(name, options) { + var result, defaults = { + template: null, controller: null, view: null, locals: null, notify: true, async: true, params: {} + }; + options = extend(defaults, options); + + if (options.view) { + result = $templateFactory.fromConfig(options.view, options.params, options.locals); + } + if (result && options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$viewContentLoading + * @eventOf ui.router.state.$view + * @eventType broadcast on root scope + * @description + * + * Fired once the view **begins loading**, *before* the DOM is rendered. + * + * @param {Object} event Event object. + * @param {Object} viewConfig The view config properties (template, controller, etc). + * + * @example + * + *
    +         * $scope.$on('$viewContentLoading',
    +         * function(event, viewConfig){
    +         *     // Access to all the view config properties.
    +         *     // and one special property 'targetView'
    +         *     // viewConfig.targetView
    +         * });
    +         * 
    + */ + $rootScope.$broadcast('$viewContentLoading', options); + } + return result; + } + }; + } +} + +angular.module('ui.router.state').provider('$view', $ViewProvider); + +/** + * @ngdoc object + * @name ui.router.state.$uiViewScrollProvider + * + * @description + * Provider that returns the {@link ui.router.state.$uiViewScroll} service function. + */ +function $ViewScrollProvider() { + + var useAnchorScroll = false; + + /** + * @ngdoc function + * @name ui.router.state.$uiViewScrollProvider#useAnchorScroll + * @methodOf ui.router.state.$uiViewScrollProvider + * + * @description + * Reverts back to using the core [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll) service for + * scrolling based on the url anchor. + */ + this.useAnchorScroll = function () { + useAnchorScroll = true; + }; + + /** + * @ngdoc object + * @name ui.router.state.$uiViewScroll + * + * @requires $anchorScroll + * @requires $timeout + * + * @description + * When called with a jqLite element, it scrolls the element into view (after a + * `$timeout` so the DOM has time to refresh). + * + * If you prefer to rely on `$anchorScroll` to scroll the view to the anchor, + * this can be enabled by calling {@link ui.router.state.$uiViewScrollProvider#methods_useAnchorScroll `$uiViewScrollProvider.useAnchorScroll()`}. + */ + this.$get = ['$anchorScroll', '$timeout', function ($anchorScroll, $timeout) { + if (useAnchorScroll) { + return $anchorScroll; + } + + return function ($element) { + $timeout(function () { + $element[0].scrollIntoView(); + }, 0, false); + }; + }]; +} + +angular.module('ui.router.state').provider('$uiViewScroll', $ViewScrollProvider); + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-view + * + * @requires ui.router.state.$state + * @requires $compile + * @requires $controller + * @requires $injector + * @requires ui.router.state.$uiViewScroll + * @requires $document + * + * @restrict ECA + * + * @description + * The ui-view directive tells $state where to place your templates. + * + * @param {string=} ui-view A view name. The name should be unique amongst the other views in the + * same state. You can have views of the same name that live in different states. + * + * @param {string=} autoscroll It allows you to set the scroll behavior of the browser window + * when a view is populated. By default, $anchorScroll is overridden by ui-router's custom scroll + * service, {@link ui.router.state.$uiViewScroll}. This custom service let's you + * scroll ui-view elements into view when they are populated during a state activation. + * + * *Note: To revert back to old [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll) + * functionality, call `$uiViewScrollProvider.useAnchorScroll()`.* + * + * @param {string=} onload Expression to evaluate whenever the view updates. + * + * @example + * A view can be unnamed or named. + *
    + * 
    + * 
    + * + * + *
    + *
    + * + * You can only have one unnamed view within any template (or root html). If you are only using a + * single view and it is unnamed then you can populate it like so: + *
    + * 
    + * $stateProvider.state("home", { + * template: "

    HELLO!

    " + * }) + *
    + * + * The above is a convenient shortcut equivalent to specifying your view explicitly with the {@link ui.router.state.$stateProvider#views `views`} + * config property, by name, in this case an empty name: + *
    + * $stateProvider.state("home", {
    + *   views: {
    + *     "": {
    + *       template: "

    HELLO!

    " + * } + * } + * }) + *
    + * + * But typically you'll only use the views property if you name your view or have more than one view + * in the same template. There's not really a compelling reason to name a view if its the only one, + * but you could if you wanted, like so: + *
    + * 
    + *
    + *
    + * $stateProvider.state("home", {
    + *   views: {
    + *     "main": {
    + *       template: "

    HELLO!

    " + * } + * } + * }) + *
    + * + * Really though, you'll use views to set up multiple views: + *
    + * 
    + *
    + *
    + *
    + * + *
    + * $stateProvider.state("home", {
    + *   views: {
    + *     "": {
    + *       template: "

    HELLO!

    " + * }, + * "chart": { + * template: "" + * }, + * "data": { + * template: "" + * } + * } + * }) + *
    + * + * Examples for `autoscroll`: + * + *
    + * 
    + * 
    + *
    + * 
    + * 
    + * 
    + * 
    + * 
    + */ +$ViewDirective.$inject = ['$state', '$injector', '$uiViewScroll']; +function $ViewDirective( $state, $injector, $uiViewScroll) { + + function getService() { + return ($injector.has) ? function(service) { + return $injector.has(service) ? $injector.get(service) : null; + } : function(service) { + try { + return $injector.get(service); + } catch (e) { + return null; + } + }; + } + + var service = getService(), + $animator = service('$animator'), + $animate = service('$animate'); + + // Returns a set of DOM manipulation functions based on which Angular version + // it should use + function getRenderer(attrs, scope) { + var statics = function() { + return { + enter: function (element, target, cb) { target.after(element); cb(); }, + leave: function (element, cb) { element.remove(); cb(); } + }; + }; + + if ($animate) { + return { + enter: function(element, target, cb) { $animate.enter(element, null, target, cb); }, + leave: function(element, cb) { $animate.leave(element, cb); } + }; + } + + if ($animator) { + var animate = $animator && $animator(scope, attrs); + + return { + enter: function(element, target, cb) {animate.enter(element, null, target); cb(); }, + leave: function(element, cb) { animate.leave(element); cb(); } + }; + } + + return statics(); + } + + var directive = { + restrict: 'ECA', + terminal: true, + priority: 400, + transclude: 'element', + compile: function (tElement, tAttrs, $transclude) { + return function (scope, $element, attrs) { + var previousEl, currentEl, currentScope, latestLocals, + onloadExp = attrs.onload || '', + autoScrollExp = attrs.autoscroll, + renderer = getRenderer(attrs, scope); + + scope.$on('$stateChangeSuccess', function() { + updateView(false); + }); + scope.$on('$viewContentLoading', function() { + updateView(false); + }); + + updateView(true); + + function cleanupLastView() { + if (previousEl) { + previousEl.remove(); + previousEl = null; + } + + if (currentScope) { + currentScope.$destroy(); + currentScope = null; + } + + if (currentEl) { + renderer.leave(currentEl, function() { + previousEl = null; + }); + + previousEl = currentEl; + currentEl = null; + } + } + + function updateView(firstTime) { + var newScope, + name = getUiViewName(attrs, $element.inheritedData('$uiView')), + previousLocals = name && $state.$current && $state.$current.locals[name]; + + if (!firstTime && previousLocals === latestLocals) return; // nothing to do + newScope = scope.$new(); + latestLocals = $state.$current.locals[name]; + + var clone = $transclude(newScope, function(clone) { + renderer.enter(clone, $element, function onUiViewEnter() { + if (angular.isDefined(autoScrollExp) && !autoScrollExp || scope.$eval(autoScrollExp)) { + $uiViewScroll(clone); + } + }); + cleanupLastView(); + }); + + currentEl = clone; + currentScope = newScope; + /** + * @ngdoc event + * @name ui.router.state.directive:ui-view#$viewContentLoaded + * @eventOf ui.router.state.directive:ui-view + * @eventType emits on ui-view directive scope + * @description * + * Fired once the view is **loaded**, *after* the DOM is rendered. + * + * @param {Object} event Event object. + */ + currentScope.$emit('$viewContentLoaded'); + currentScope.$eval(onloadExp); + } + }; + } + }; + + return directive; +} + +$ViewDirectiveFill.$inject = ['$compile', '$controller', '$state']; +function $ViewDirectiveFill ($compile, $controller, $state) { + return { + restrict: 'ECA', + priority: -400, + compile: function (tElement) { + var initial = tElement.html(); + return function (scope, $element, attrs) { + var current = $state.$current, + name = getUiViewName(attrs, $element.inheritedData('$uiView')), + locals = current && current.locals[name]; + + if (! locals) { + return; + } + + $element.data('$uiView', { name: name, state: locals.$$state }); + $element.html(locals.$template ? locals.$template : initial); + + var link = $compile($element.contents()); + + if (locals.$$controller) { + locals.$scope = scope; + var controller = $controller(locals.$$controller, locals); + if (locals.$$controllerAs) { + scope[locals.$$controllerAs] = controller; + } + $element.data('$ngControllerController', controller); + $element.children().data('$ngControllerController', controller); + } + + link(scope); + }; + } + }; +} + +/** + * Shared ui-view code for both directives: + * Given attributes and inherited $uiView data, return the view's name + */ +function getUiViewName(attrs, inherited) { + var name = attrs.uiView || attrs.name || ''; + return name.indexOf('@') >= 0 ? name : (name + '@' + (inherited ? inherited.state.name : '')); +} + +angular.module('ui.router.state').directive('uiView', $ViewDirective); +angular.module('ui.router.state').directive('uiView', $ViewDirectiveFill); + +function parseStateRef(ref, current) { + var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed; + if (preparsed) ref = current + '(' + preparsed[1] + ')'; + parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/); + if (!parsed || parsed.length !== 4) throw new Error("Invalid state ref '" + ref + "'"); + return { state: parsed[1], paramExpr: parsed[3] || null }; +} + +function stateContext(el) { + var stateData = el.parent().inheritedData('$uiView'); + + if (stateData && stateData.state && stateData.state.name) { + return stateData.state; + } +} + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref + * + * @requires ui.router.state.$state + * @requires $timeout + * + * @restrict A + * + * @description + * A directive that binds a link (`` tag) to a state. If the state has an associated + * URL, the directive will automatically generate & update the `href` attribute via + * the {@link ui.router.state.$state#methods_href $state.href()} method. Clicking + * the link will trigger a state transition with optional parameters. + * + * Also middle-clicking, right-clicking, and ctrl-clicking on the link will be + * handled natively by the browser. + * + * You can also use relative state paths within ui-sref, just like the relative + * paths passed to `$state.go()`. You just need to be aware that the path is relative + * to the state that the link lives in, in other words the state that loaded the + * template containing the link. + * + * You can specify options to pass to {@link ui.router.state.$state#go $state.go()} + * using the `ui-sref-opts` attribute. Options are restricted to `location`, `inherit`, + * and `reload`. + * + * @example + * Here's an example of how you'd use ui-sref and how it would compile. If you have the + * following template: + *
    + * Home | About | Next page
    + * 
    + * 
    + * 
    + * + * Then the compiled html would be (assuming Html5Mode is off and current state is contacts): + *
    + * Home | About | Next page
    + * 
    + * 
      + *
    • + * Joe + *
    • + *
    • + * Alice + *
    • + *
    • + * Bob + *
    • + *
    + * + * Home + *
    + * + * @param {string} ui-sref 'stateName' can be any valid absolute or relative state + * @param {Object} ui-sref-opts options to pass to {@link ui.router.state.$state#go $state.go()} + */ +$StateRefDirective.$inject = ['$state', '$timeout']; +function $StateRefDirective($state, $timeout) { + var allowedOptions = ['location', 'inherit', 'reload']; + + return { + restrict: 'A', + require: ['?^uiSrefActive', '?^uiSrefActiveEq'], + link: function(scope, element, attrs, uiSrefActive) { + var ref = parseStateRef(attrs.uiSref, $state.current.name); + var params = null, url = null, base = stateContext(element) || $state.$current; + var isForm = element[0].nodeName === "FORM"; + var attr = isForm ? "action" : "href", nav = true; + + var options = { relative: base, inherit: true }; + var optionsOverride = scope.$eval(attrs.uiSrefOpts) || {}; + + angular.forEach(allowedOptions, function(option) { + if (option in optionsOverride) { + options[option] = optionsOverride[option]; + } + }); + + var update = function(newVal) { + if (newVal) params = newVal; + if (!nav) return; + + var newHref = $state.href(ref.state, params, options); + + var activeDirective = uiSrefActive[1] || uiSrefActive[0]; + if (activeDirective) { + activeDirective.$$setStateInfo(ref.state, params); + } + if (newHref === null) { + nav = false; + return false; + } + element[0][attr] = newHref; + }; + + if (ref.paramExpr) { + scope.$watch(ref.paramExpr, function(newVal, oldVal) { + if (newVal !== params) update(newVal); + }, true); + params = scope.$eval(ref.paramExpr); + } + update(); + + if (isForm) return; + + element.bind("click", function(e) { + var button = e.which || e.button; + if ( !(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || element.attr('target')) ) { + // HACK: This is to allow ng-clicks to be processed before the transition is initiated: + var transition = $timeout(function() { + $state.go(ref.state, params, options); + }); + e.preventDefault(); + + e.preventDefault = function() { + $timeout.cancel(transition); + }; + } + }); + } + }; +} + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref-active + * + * @requires ui.router.state.$state + * @requires ui.router.state.$stateParams + * @requires $interpolate + * + * @restrict A + * + * @description + * A directive working alongside ui-sref to add classes to an element when the + * related ui-sref directive's state is active, and removing them when it is inactive. + * The primary use-case is to simplify the special appearance of navigation menus + * relying on `ui-sref`, by having the "active" state's menu button appear different, + * distinguishing it from the inactive menu items. + * + * ui-sref-active can live on the same element as ui-sref or on a parent element. The first + * ui-sref-active found at the same level or above the ui-sref will be used. + * + * Will activate when the ui-sref's target state or any child state is active. If you + * need to activate only when the ui-sref target state is active and *not* any of + * it's children, then you will use + * {@link ui.router.state.directive:ui-sref-active-eq ui-sref-active-eq} + * + * @example + * Given the following template: + *
    + * 
    + * 
    + * + * + * When the app state is "app.user" (or any children states), and contains the state parameter "user" with value "bilbobaggins", + * the resulting HTML will appear as (note the 'active' class): + *
    + * 
    + * 
    + * + * The class name is interpolated **once** during the directives link time (any further changes to the + * interpolated value are ignored). + * + * Multiple classes may be specified in a space-separated format: + *
    + * 
      + *
    • + * link + *
    • + *
    + *
    + */ + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref-active-eq + * + * @requires ui.router.state.$state + * @requires ui.router.state.$stateParams + * @requires $interpolate + * + * @restrict A + * + * @description + * The same as {@link ui.router.state.directive:ui-sref-active ui-sref-active} but will will only activate + * when the exact target state used in the `ui-sref` is active; no child states. + * + */ +$StateRefActiveDirective.$inject = ['$state', '$stateParams', '$interpolate']; +function $StateRefActiveDirective($state, $stateParams, $interpolate) { + return { + restrict: "A", + controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) { + var state, params, activeClass; + + // There probably isn't much point in $observing this + // uiSrefActive and uiSrefActiveEq share the same directive object with some + // slight difference in logic routing + activeClass = $interpolate($attrs.uiSrefActiveEq || $attrs.uiSrefActive || '', false)($scope); + + // Allow uiSref to communicate with uiSrefActive[Equals] + this.$$setStateInfo = function (newState, newParams) { + state = $state.get(newState, stateContext($element)); + params = newParams; + update(); + }; + + $scope.$on('$stateChangeSuccess', update); + + // Update route state + function update() { + if (isMatch()) { + $element.addClass(activeClass); + } else { + $element.removeClass(activeClass); + } + } + + function isMatch() { + if (typeof $attrs.uiSrefActiveEq !== 'undefined') { + return $state.$current.self === state && matchesParams(); + } else { + return $state.includes(state.name) && matchesParams(); + } + } + + function matchesParams() { + return !params || equalForKeys(params, $stateParams); + } + }] + }; +} + +angular.module('ui.router.state') + .directive('uiSref', $StateRefDirective) + .directive('uiSrefActive', $StateRefActiveDirective) + .directive('uiSrefActiveEq', $StateRefActiveDirective); + +/** + * @ngdoc filter + * @name ui.router.state.filter:isState + * + * @requires ui.router.state.$state + * + * @description + * Translates to {@link ui.router.state.$state#methods_is $state.is("stateName")}. + */ +$IsStateFilter.$inject = ['$state']; +function $IsStateFilter($state) { + return function(state) { + return $state.is(state); + }; +} + +/** + * @ngdoc filter + * @name ui.router.state.filter:includedByState + * + * @requires ui.router.state.$state + * + * @description + * Translates to {@link ui.router.state.$state#methods_includes $state.includes('fullOrPartialStateName')}. + */ +$IncludedByStateFilter.$inject = ['$state']; +function $IncludedByStateFilter($state) { + return function(state) { + return $state.includes(state); + }; +} + +angular.module('ui.router.state') + .filter('isState', $IsStateFilter) + .filter('includedByState', $IncludedByStateFilter); +})(window, window.angular); diff --git a/app/assets/javascripts/lib/notifier.js.coffee b/app/assets/javascripts/lib/notifier.js.coffee new file mode 100755 index 00000000..1c703cc2 --- /dev/null +++ b/app/assets/javascripts/lib/notifier.js.coffee @@ -0,0 +1,55 @@ +class Notifier + constructor: -> + @removeSwitch() unless window.Notification + @getState() + @checkOrRequirePermission() + $('input[name="notification-checkbox"]').bootstrapSwitch + labelText: gon.i18n.switch.notification + state: @switchOn() + onSwitchChange: @switch + + checkOrRequirePermission: => + if @switchOn() + if Notification.permission == 'default' + @requestPermission(@checkOrRequirePermission) + else if Notification.permission == 'denied' + @setState(false) + @removeSwitch() + + removeSwitch: -> + $('.desktop-real-notification').remove() + + setState: (status) -> + @enableNotification = status + Cookies.set('notification', status, 30) + + getState: -> + @enableNotification = Cookies.get('notification') + + requestPermission: (callback) -> + Notification.requestPermission(callback) + + switch: (event, state) => + if state + @setState(true) + @checkOrRequirePermission() + else + @setState(false) + + switchOn: -> + if @getState() == "true" + true + else + false + + notify: (title, content, logo = '/peatio-notification-logo.png') -> + if @enableNotification == true || @enableNotification == "true" + + if window.Notification + popup = new Notification(title, { 'body': content, 'onclick': onclick, 'icon': logo }) + else + popup = window.webkitNotifications.createNotification(avatar, title, content) + + setTimeout ( => popup.close() ), 8000 + +window.Notifier = Notifier diff --git a/app/assets/javascripts/lib/peatio_model.js.coffee b/app/assets/javascripts/lib/peatio_model.js.coffee new file mode 100755 index 00000000..7a0fc5d8 --- /dev/null +++ b/app/assets/javascripts/lib/peatio_model.js.coffee @@ -0,0 +1,540 @@ +Events = + bind: (ev, callback) -> + evs = ev.split(' ') + @_callbacks = {} unless @hasOwnProperty('_callbacks') and @_callbacks + for name in evs + @_callbacks[name] or= [] + @_callbacks[name].push(callback) + this + + one: (ev, callback) -> + @bind ev, handler = -> + @unbind(ev, handler) + callback.apply(this, arguments) + + trigger: (args...) -> + ev = args.shift() + list = @hasOwnProperty('_callbacks') and @_callbacks?[ev] + return unless list + for callback in list + if callback.apply(this, args) is false + break + true + + listenTo: (obj, ev, callback) -> + obj.bind(ev, callback) + @listeningTo or= [] + @listeningTo.push {obj, ev, callback} + this + + listenToOnce: (obj, ev, callback) -> + listeningToOnce = @listeningToOnce or= [] + obj.bind ev, handler = -> + idx = -1 + for lt, i in listeningToOnce when lt.obj is obj + idx = i if lt.ev is ev and lt.callback is callback + obj.unbind(ev, handler) + listeningToOnce.splice(idx, 1) unless idx is -1 + callback.apply(this, arguments) + listeningToOnce.push {obj, ev, callback, handler} + this + + stopListening: (obj, events, callback) -> + if arguments.length is 0 + for listeningTo in [@listeningTo, @listeningToOnce] + continue unless listeningTo + for lt in listeningTo + lt.obj.unbind(lt.ev, lt.handler or lt.callback) + @listeningTo = undefined + @listeningToOnce = undefined + + else if obj + for listeningTo in [@listeningTo, @listeningToOnce] + continue unless listeningTo + events = if events then events.split(' ') else [undefined] + for ev in events + for idx in [listeningTo.length-1..0] + lt = listeningTo[idx] + continue if callback and (lt.handler or lt.callback) isnt callback + if (not ev) or (ev is lt.ev) + lt.obj.unbind(lt.ev, lt.handler or lt.callback) + listeningTo.splice(idx, 1) unless idx is -1 + else if ev + evts = lt.ev.split(' ') + if ev in evts + evts = (e for e in evts when e isnt ev) + lt.ev = $.trim(evts.join(' ')) + lt.obj.unbind(ev, lt.handler or lt.callback) + + unbind: (ev, callback) -> + if arguments.length is 0 + @_callbacks = {} + return this + return this unless ev + evs = ev.split(' ') + for name in evs + list = @_callbacks?[name] + continue unless list + unless callback + delete @_callbacks[name] + continue + for cb, i in list when (cb is callback) + list = list.slice() + list.splice(i, 1) + @_callbacks[name] = list + break + this + +Events.on = Events.bind +Events.off = Events.unbind + +Log = + trace: true + + logPrefix: '(App)' + + log: (args...) -> + return unless @trace + if @logPrefix then args.unshift(@logPrefix) + console?.log?(args...) + this + +moduleKeywords = ['included', 'extended'] + +class Module + @include: (obj) -> + throw new Error('include(obj) requires obj') unless obj + for key, value of obj when key not in moduleKeywords + @::[key] = value + obj.included?.apply(this) + this + + @extend: (obj) -> + throw new Error('extend(obj) requires obj') unless obj + for key, value of obj when key not in moduleKeywords + @[key] = value + obj.extended?.apply(this) + this + + @proxy: (func) -> + => func.apply(this, arguments) + + proxy: (func) -> + => func.apply(this, arguments) + + constructor: -> + @init?(arguments...) + +class Model extends Module + @extend Events + + @records : [] + @irecords : {} + @attributes : [] + + @configure: (name, attributes...) -> + @className = name + @deleteAll() + @attributes = attributes if attributes.length + @attributes and= makeArray(@attributes) + @attributes or= [] + @unbind() + this + + @toString: -> "#{@className}(#{@attributes.join(", ")})" + + @find: (id, notFound = @notFound) -> + @irecords[id]?.clone() or notFound?(id) + + @findAll: (ids, notFound) -> + (@find(id) for id in ids when @find(id, notFound)) + + @notFound: (id) -> null + + @exists: (id) -> Boolean @irecords[id] + + @addRecord: (record, options = {}) -> + if record.id and @irecords[record.id] + @irecords[record.id].remove(options) + record = @irecords[record.id].load(record) unless options.clear + record.id or= record.cid + @irecords[record.id] ?= record + @irecords[record.cid] ?= record + @records.push(record) + + @refresh: (values, options = {}) -> + @deleteAll() if options.clear + + records = @fromJSON(values) + records = [records] unless isArray(records) + @addRecord(record, options) for record in records + @sort() + + result = @cloneArray(records) + @trigger('refresh', result, options) + result + + @select: (callback) -> + (record.clone() for record in @records when callback(record)) + + @findBy: (name, value) -> + for record in @records + if record[name] is value + return record.clone() + null + + @findAllBy: (name, value) -> + @select (item) -> + item[name] is value + + @each: (callback) -> + callback(record.clone()) for record in @records + + @all: -> + @cloneArray(@records) + + @slice: (begin = 0, end)-> + @cloneArray(@records.slice(begin, end)) + + @first: (end = 1)-> + if end > 1 + @cloneArray(@records.slice(0, end)) + else + @records[0]?.clone() + + @last: (begin)-> + if typeof begin is 'number' + @cloneArray(@records.slice(-begin)) + else + @records[@records.length - 1]?.clone() + + @count: -> + @records.length + + @deleteAll: -> + @records = [] + @irecords = {} + + @destroyAll: (options) -> + record.destroy(options) for record in @records + + @update: (id, atts, options) -> + @find(id).updateAttributes(atts, options) + + @create: (atts, options) -> + record = new @(atts) + record.save(options) + + @destroy: (id, options) -> + @find(id).destroy(options) + + @change: (callbackOrParams) -> + if typeof callbackOrParams is 'function' + @bind('change', callbackOrParams) + else + @trigger('change', arguments...) + + @fetch: (callbackOrParams) -> + if typeof callbackOrParams is 'function' + @bind('fetch', callbackOrParams) + else + @trigger('fetch', arguments...) + + @toJSON: -> + @records + + @fromJSON: (objects) -> + return unless objects + if typeof objects is 'string' + objects = JSON.parse(objects) + if isArray(objects) + for value in objects + if value instanceof this + value + else + new @(value) + else + return objects if objects instanceof this + new @(objects) + + @fromForm: -> + (new this).fromForm(arguments...) + + @sort: -> + if @comparator + @records.sort @comparator + this + + # Private + + @cloneArray: (array) -> + (value.clone() for value in array) + + @idCounter: 0 + + @uid: (prefix = '') -> + uid = prefix + @idCounter++ + uid = @uid(prefix) if @exists(uid) + uid + + # Instance + + constructor: (atts) -> + super + if @constructor.uuid? and typeof @constructor.uuid is 'function' + @cid = @constructor.uuid() + @id = @cid unless @id + else + @cid = atts?.cid or @constructor.uid('c-') + @load atts if atts + + isNew: -> + not @exists() + + isValid: -> + not @validate() + + validate: -> + + load: (atts) -> + if atts.id then @id = atts.id + for key, value of atts + if typeof @[key] is 'function' + continue if typeof value is 'function' + @[key](value) + else + @[key] = value + this + + attributes: -> + result = {} + for key in @constructor.attributes when key of this + if typeof @[key] is 'function' + result[key] = @[key]() + else + result[key] = @[key] + result.id = @id if @id + result + + eql: (rec) -> + rec and rec.constructor is @constructor and + ((rec.cid is @cid) or (rec.id and rec.id is @id)) + + save: (options = {}) -> + unless options.validate is false + error = @validate() + if error + @trigger('error', error) + return false + + @trigger('beforeSave', options) + record = if @isNew() then @create(options) else @update(options) + @stripCloneAttrs() + @trigger('save', options) + record + + stripCloneAttrs: -> + return if @hasOwnProperty 'cid' # Make sure it's not the raw object + for own key, value of this + delete @[key] if key in @constructor.attributes + this + + updateAttribute: (name, value, options) -> + atts = {} + atts[name] = value + @updateAttributes(atts, options) + + updateAttributes: (atts, options) -> + @load(atts) + @save(options) + + changeID: (id) -> + return if id is @id + records = @constructor.irecords + records[id] = records[@id] + delete records[@id] unless @cid is @id + @id = id + @save() + + remove: (options = {}) -> + # Remove record from model + records = @constructor.records.slice(0) + for record, i in records when @eql(record) + records.splice(i, 1) + break + @constructor.records = records + if options.clear + # Remove the ID and CID indexes + delete @constructor.irecords[@id] + delete @constructor.irecords[@cid] + + destroy: (options = {}) -> + options.clear ?= true + @trigger('beforeDestroy', options) + @remove(options) + @destroyed = true + # handle events + @trigger('destroy', options) + @trigger('change', 'destroy', options) + if @listeningTo + @stopListening() + @unbind() + this + + dup: (newRecord = true) -> + atts = @attributes() + if newRecord + delete atts.id + else + atts.cid = @cid + new @constructor(atts) + + clone: -> + createObject(this) + + reload: -> + return this if @isNew() + original = @constructor.find(@id) + @load(original.attributes()) + original + + refresh: (data) -> + # go to the source and load attributes + root = @constructor.irecords[@id] + root.load(data) + @trigger('refresh') + @ + + toJSON: -> + @attributes() + + toString: -> + "<#{@constructor.className} (#{JSON.stringify(this)})>" + + fromForm: (form) -> + result = {} + + for checkbox in $(form).find('[type=checkbox]:not([value])') + result[checkbox.name] = $(checkbox).prop('checked') + + for checkbox in $(form).find('[type=checkbox][name$="[]"]') + name = checkbox.name.replace(/\[\]$/, '') + result[name] or= [] + result[name].push checkbox.value if $(checkbox).prop('checked') + + for key in $(form).serializeArray() + result[key.name] or= key.value + + @load(result) + + exists: -> + @constructor.exists(@id) + + # Private + + update: (options) -> + @trigger('beforeUpdate', options) + + records = @constructor.irecords + records[@id].load @attributes() + + @constructor.sort() + + clone = records[@id].clone() + clone.trigger('update', options) + clone.trigger('change', 'update', options) + clone + + create: (options) -> + @trigger('beforeCreate', options) + @id or= @cid + + record = @dup(false) + @constructor.addRecord(record) + @constructor.sort() + + clone = record.clone() + clone.trigger('create', options) + clone.trigger('change', 'create', options) + clone + + bind: (events, callback) -> + @constructor.bind events, binder = (record) => + if record && @eql(record) + callback.apply(this, arguments) + # create a wrapper function to be called with 'unbind' for each event + for singleEvent in events.split(' ') + do (singleEvent) => + @constructor.bind "unbind", unbinder = (record, event, cb) => + if record && @eql(record) + return if event and event isnt singleEvent + return if cb and cb isnt callback + @constructor.unbind(singleEvent, binder) + @constructor.unbind("unbind", unbinder) + this + + one: (events, callback) -> + @bind events, handler = => + @unbind(events, handler) + callback.apply(this, arguments) + + trigger: (args...) -> + args.splice(1, 0, this) + @constructor.trigger(args...) + + listenTo: -> Events.listenTo.apply @, arguments + listenToOnce: -> Events.listenToOnce.apply @, arguments + stopListening: -> Events.stopListening.apply @, arguments + + unbind: (events, callback) -> + if arguments.length is 0 + @trigger('unbind') + else if events + for event in events.split(' ') + @trigger('unbind', event, callback) + +Model::on = Model::bind +Model::off = Model::unbind + +$ = window?.jQuery or window?.Zepto or (element) -> element + +createObject = Object.create or (o) -> + Func = -> + Func.prototype = o + new Func() + +isArray = (value) -> + Object::toString.call(value) is '[object Array]' + +isBlank = (value) -> + return true unless value + return false for key of value + true + +makeArray = (args) -> + Array::slice.call(args, 0) + +# Globals + +PeatioModel = @PeatioModel= {} +module?.exports = PeatioModel + +PeatioModel.version = '1.3.2.customizatiion' +PeatioModel.isArray = isArray +PeatioModel.isBlank = isBlank +PeatioModel.$ = $ +PeatioModel.Events = Events +PeatioModel.Log = Log +PeatioModel.Module = Module +PeatioModel.Model = Model + +# Global events + +Module.extend.call(PeatioModel, Events) + +Model.setup = (name, attributes = []) -> + class Instance extends this + Instance.configure(name, attributes...) + Instance + +PeatioModel.Class = Module diff --git a/app/assets/javascripts/lib/pusher_connection.js.coffee b/app/assets/javascripts/lib/pusher_connection.js.coffee new file mode 100755 index 00000000..db399ba4 --- /dev/null +++ b/app/assets/javascripts/lib/pusher_connection.js.coffee @@ -0,0 +1,7 @@ +pusher = new Pusher gon.pusher.key, + encrypted: gon.pusher.encrypted + wsHost: gon.pusher.wsHost + wsPort: gon.pusher.wsPort + wssPort: gon.pusher.wssPort + +window.pusher = pusher diff --git a/app/assets/javascripts/lib/pusher_subscriber.js.coffee b/app/assets/javascripts/lib/pusher_subscriber.js.coffee new file mode 100755 index 00000000..ff13313d --- /dev/null +++ b/app/assets/javascripts/lib/pusher_subscriber.js.coffee @@ -0,0 +1,95 @@ +class PusherSubscriber + constructor: -> + pusher_key = $("meta[name=pusher]").attr("content") + @socket = window.pusher + @channels = [] + @subscribeChannels(gon.current_user.sn) + + release: -> + @socket.disconnect() + @channels = [] + + subscribeChannels: (user_sn) => + @subscribeUserChannel(user_sn) + + subscribeUserChannel: (user_sn)-> + channel = @socket.subscribe("private-" + user_sn) + self = @ + channel.bind 'pusher:subscription_succeeded', (status) -> + console.log('Pusher bind member channel successfully') + new MemberHandler(channel) + new AccountHandler(channel) + new DepositHandler(channel) + new WithdrawHandler(channel) + new DepositAddressHandler(channel) + +class EventHandler + constructor: (channel, event) -> + @channel = channel + @channel.bind event, @processWithoutAjax + + process: (msg) => + switch (msg.type) + when "create" then @create(msg.attributes) + when "update" then @update(msg.id, msg.attributes) + when "destroy" then @destroy(msg.id, msg.attributes) + else + throw 'Unknown type:' + type + + processWithoutAjax: => + args = arguments + PeatioModel.Ajax.disable => + @process(args...) + + create: (attributes) => + update: (id, attributes) => + destroy: (id) => + +class MemberHandler extends EventHandler + constructor: (channel) -> + super channel, "members" + +class AccountHandler extends EventHandler + constructor: (channel) -> + super channel, "accounts" + + update: (id, attributes) => + account = Account.findBy("id", id).updateAttributes(attributes) + + +class DepositHandler extends EventHandler + constructor: (channel) -> + super channel, "deposits" + + create: (attributes) => + Deposit.create(attributes) + $.publish 'deposit:create' + + update: (id, attributes) => + Deposit.findBy("id", id).updateAttributes(attributes) + +class WithdrawHandler extends EventHandler + constructor: (channel) -> + super channel, "withdraws" + + create: (attributes) => + Withdraw.create(attributes) + + update: (id, attributes) => + Withdraw.findBy("id", id).updateAttributes(attributes) + + destroy: (id) => + Withdraw.destroy(id) + +class DepositAddressHandler extends EventHandler + constructor: (channel) -> + super channel, "deposit_address" + + create: (attributes) => + account = Account.findBy('id', attributes['account_id']) + account.deposit_address = attributes['deposit_address'] + account.save() + $.publish "deposit_address:create", attributes['deposit_address'] + + +window.PusherSubscriber = PusherSubscriber diff --git a/app/assets/javascripts/lib/sfx.js.coffee b/app/assets/javascripts/lib/sfx.js.coffee new file mode 100755 index 00000000..7fcb73d1 --- /dev/null +++ b/app/assets/javascripts/lib/sfx.js.coffee @@ -0,0 +1,13 @@ +window.sfx_warning = -> + window.sfx('warning') + +window.sfx_success = -> + window.sfx('success') + +window.sfx = (kind) -> + s = $("##{kind}-fx")[0] + return if Cookies.get('sound') == 'false' + return unless s.play + s.pause() + s.currentTime = 0 + s.play() diff --git a/app/assets/javascripts/lib/tiny-pubsub.js b/app/assets/javascripts/lib/tiny-pubsub.js new file mode 100755 index 00000000..5e735c0b --- /dev/null +++ b/app/assets/javascripts/lib/tiny-pubsub.js @@ -0,0 +1,25 @@ +/* + * jQuery Tiny Pub/Sub + * https://github.com/cowboy/jquery-tiny-pubsub + * + * Copyright (c) 2013 "Cowboy" Ben Alman + * Licensed under the MIT license. + */ + +(function($) { + + var o = $({}); + + $.subscribe = function() { + o.on.apply(o, arguments); + }; + + $.unsubscribe = function() { + o.off.apply(o, arguments); + }; + + $.publish = function() { + o.trigger.apply(o, arguments); + }; + +}(jQuery)); diff --git a/app/assets/javascripts/locales/en.js.erb b/app/assets/javascripts/locales/en.js.erb new file mode 100755 index 00000000..375eb813 --- /dev/null +++ b/app/assets/javascripts/locales/en.js.erb @@ -0,0 +1,3 @@ +//= require i18n +<%= JsLocaleHelper.output_locale(:en) %> + diff --git a/app/assets/javascripts/locales/zh-CN.js.erb b/app/assets/javascripts/locales/zh-CN.js.erb new file mode 100755 index 00000000..8095a1e7 --- /dev/null +++ b/app/assets/javascripts/locales/zh-CN.js.erb @@ -0,0 +1,3 @@ +//= require i18n +<%= JsLocaleHelper.output_locale(:'zh-CN') %> + diff --git a/app/assets/javascripts/market.js.coffee b/app/assets/javascripts/market.js.coffee new file mode 100755 index 00000000..fe0ecca0 --- /dev/null +++ b/app/assets/javascripts/market.js.coffee @@ -0,0 +1,65 @@ +#= require es5-shim.min +#= require es5-sham.min +#= require jquery +#= require jquery_ujs +#= require jquery.mousewheel +#= require jquery-timing.min +#= require jquery.nicescroll.min +# +#= require bootstrap +#= require bootstrap-switch.min +# +#= require moment +#= require bignumber +#= require underscore +#= require cookies.min +#= require flight.min +#= require pusher.min + +#= require ./lib/sfx +#= require ./lib/notifier +#= require ./lib/pusher_connection + +#= require highstock +#= require_tree ./highcharts/ + +#= require_tree ./helpers +#= require_tree ./component_mixin +#= require_tree ./component_data +#= require_tree ./component_ui +#= require_tree ./templates + +#= require_self + +$ -> + window.notifier = new Notifier() + + BigNumber.config(ERRORS: false) + + HeaderUI.attachTo('header') + AccountSummaryUI.attachTo('#account_summary') + + FloatUI.attachTo('.float') + KeyBindUI.attachTo(document) + AutoWindowUI.attachTo(window) + + PlaceOrderUI.attachTo('#bid_entry') + PlaceOrderUI.attachTo('#ask_entry') + OrderBookUI.attachTo('#order_book') + DepthUI.attachTo('#depths_wrapper') + + MyOrdersUI.attachTo('#my_orders') + MarketTickerUI.attachTo('#ticker') + MarketSwitchUI.attachTo('#market_list_wrapper') + MarketTradesUI.attachTo('#market_trades_wrapper') + + MarketData.attachTo(document) + GlobalData.attachTo(document, {pusher: window.pusher}) + MemberData.attachTo(document, {pusher: window.pusher}) if gon.accounts + + CandlestickUI.attachTo('#candlestick') + SwitchUI.attachTo('#range_switch, #indicator_switch, #main_indicator_switch, #type_switch') + + $('.panel-body-content').niceScroll + autohidemode: true + cursorborder: "none" diff --git a/app/assets/javascripts/swagger-ui/lib/backbone-min.js b/app/assets/javascripts/swagger-ui/lib/backbone-min.js new file mode 100755 index 00000000..c1c0d4ff --- /dev/null +++ b/app/assets/javascripts/swagger-ui/lib/backbone-min.js @@ -0,0 +1,38 @@ +// Backbone.js 0.9.2 + +// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org +(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks= +{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g= +z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent= +{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null== +b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent: +b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)}; +a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error, +h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t(); +return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending= +{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length|| +!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator); +this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c=b))this.iframe=i('