Lots of stuff
This commit is contained in:
commit
d7316d5799
|
@ -0,0 +1,3 @@
|
|||
/server/config.coffee
|
||||
/node_modules/
|
||||
/knexfile.coffee
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by grunt-nodemon
|
||||
.DS_Store
|
||||
.git/
|
||||
pokebattle-db
|
||||
test/
|
||||
scrapers/*
|
||||
client/*
|
||||
public/*
|
||||
Gruntfile*
|
||||
package.json
|
||||
*.md
|
||||
*.txt
|
||||
Capfile
|
||||
config/*
|
||||
Gemfile
|
||||
Gemfile.lock
|
||||
dump.rdb
|
|
@ -0,0 +1,2 @@
|
|||
load 'deploy'
|
||||
load 'config/deploy' # remove this line to skip loading any of the default tasks
|
|
@ -0,0 +1,3 @@
|
|||
source 'https://rubygems.org'
|
||||
gem "capistrano", '~>2.0'
|
||||
gem "capistrano-node-deploy", :git => 'https://github.com/loopj/capistrano-node-deploy.git', :ref => '1c2279'
|
|
@ -0,0 +1,35 @@
|
|||
GIT
|
||||
remote: https://github.com/loopj/capistrano-node-deploy.git
|
||||
revision: 1c22792d9163591ba4e941887598aed654190a4c
|
||||
ref: 1c2279
|
||||
specs:
|
||||
capistrano-node-deploy (1.2.14)
|
||||
multi_json (~> 1.3.6)
|
||||
railsless-deploy (>= 1.1.0)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
capistrano (2.15.5)
|
||||
highline
|
||||
net-scp (>= 1.0.0)
|
||||
net-sftp (>= 2.0.0)
|
||||
net-ssh (>= 2.0.14)
|
||||
net-ssh-gateway (>= 1.1.0)
|
||||
highline (1.6.20)
|
||||
multi_json (1.3.7)
|
||||
net-scp (1.1.2)
|
||||
net-ssh (>= 2.6.5)
|
||||
net-sftp (2.1.2)
|
||||
net-ssh (>= 2.6.5)
|
||||
net-ssh (2.7.0)
|
||||
net-ssh-gateway (1.2.0)
|
||||
net-ssh (>= 2.6.5)
|
||||
railsless-deploy (1.1.3)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
capistrano (~> 2.0)
|
||||
capistrano-node-deploy!
|
|
@ -0,0 +1,221 @@
|
|||
{exec} = require('child_process')
|
||||
path = require('path')
|
||||
|
||||
assets = require('./assets')
|
||||
|
||||
# asset paths (note: without public/ in front)
|
||||
assetPaths = '''
|
||||
js/data.js
|
||||
js/vendor.js
|
||||
js/templates.js
|
||||
js/replays.js
|
||||
js/app.js
|
||||
css/main.css
|
||||
css/vendor.css
|
||||
'''.trim().split(/\s+/)
|
||||
# Transform them using proper slashes
|
||||
assetPaths = assetPaths.map (assetPath) -> assetPath.split('/').join(path.sep)
|
||||
|
||||
module.exports = (grunt) ->
|
||||
awsConfigPath = 'aws_config.json'
|
||||
if !grunt.file.exists(awsConfigPath)
|
||||
grunt.file.copy("#{awsConfigPath}.example", awsConfigPath)
|
||||
|
||||
grunt.initConfig
|
||||
pkg: grunt.file.readJSON('package.json')
|
||||
concurrent:
|
||||
compile: ["jade", "stylus", "coffee", "concat", "cssmin", "compile:json"]
|
||||
server:
|
||||
tasks: ["nodemon", "watch"]
|
||||
options:
|
||||
logConcurrentOutput: true
|
||||
jade:
|
||||
compile:
|
||||
options:
|
||||
client: true
|
||||
compileDebug: false
|
||||
processName: (fileName) ->
|
||||
templatePath = 'client/views/'
|
||||
index = fileName.lastIndexOf(templatePath) + templatePath.length
|
||||
fileName = fileName.substr(index)
|
||||
fileName.substr(0, fileName.indexOf('.'))
|
||||
files:
|
||||
"public/js/templates.js": "client/views/**/*.jade"
|
||||
stylus:
|
||||
compile:
|
||||
use: [ require('nib') ]
|
||||
files:
|
||||
"public/css/main.css": "client/app/css/main.styl"
|
||||
coffee:
|
||||
compile:
|
||||
files:
|
||||
'public/js/app.js': [
|
||||
"client/app/js/initializers/index.coffee"
|
||||
"client/app/js/initializers/**/*.coffee"
|
||||
"shared/**/*.coffee"
|
||||
"client/app/js/mixins/index.coffee"
|
||||
"client/app/js/mixins/**/*.coffee"
|
||||
"client/app/js/models/battles/pokemon.coffee"
|
||||
"client/app/js/models/battles/team.coffee"
|
||||
"client/app/js/models/battles/**/*.coffee"
|
||||
"client/app/js/models/chats/**/*.coffee"
|
||||
"client/app/js/collections/battles/**/*.coffee"
|
||||
"client/app/js/collections/chats/**/*.coffee"
|
||||
"client/app/js/views/battles/**/*.coffee"
|
||||
"client/app/js/views/teambuilder/**/*.coffee"
|
||||
"client/app/js/views/*.coffee"
|
||||
"client/app/js/client.coffee"
|
||||
"client/app/js/helpers/**/*.coffee"
|
||||
"client/app/js/concerns/**/*.coffee"
|
||||
]
|
||||
# The replay scripts are typically scoped to a battles/ folder
|
||||
'public/js/replays.js': [
|
||||
"client/app/js/initializers/index.coffee"
|
||||
"client/app/js/initializers/**/*.coffee"
|
||||
"shared/**/*.coffee"
|
||||
"client/app/js/mixins/index.coffee"
|
||||
"client/app/js/mixins/battles/**/*.coffee"
|
||||
"client/app/js/models/battles/pokemon.coffee"
|
||||
"client/app/js/models/battles/team.coffee"
|
||||
"client/app/js/models/battles/**/*.coffee"
|
||||
"client/app/js/models/replays/**/*.coffee"
|
||||
"client/app/js/collections/replays/**/*.coffee"
|
||||
"client/app/js/views/battles/**/*.coffee"
|
||||
"client/app/js/views/replays/**/*.coffee"
|
||||
"client/app/js/helpers/**/*.coffee"
|
||||
]
|
||||
uglify:
|
||||
options:
|
||||
compress: true
|
||||
warn: false
|
||||
vendor:
|
||||
files:
|
||||
'public/js/vendor.js': 'public/js/vendor.js'
|
||||
coffee:
|
||||
files:
|
||||
'public/js/app.js': 'public/js/app.js'
|
||||
jade:
|
||||
files:
|
||||
"public/js/templates.js": "public/js/templates.js"
|
||||
json:
|
||||
files:
|
||||
'public/js/data.js': 'public/js/data.js'
|
||||
cssmin:
|
||||
combine:
|
||||
files:
|
||||
'public/css/vendor.css' : [
|
||||
'client/vendor/css/**/*.css'
|
||||
]
|
||||
concat:
|
||||
dist:
|
||||
dest: 'public/js/vendor.js'
|
||||
src: [
|
||||
"client/vendor/js/jquery.js"
|
||||
"client/vendor/js/underscore.js"
|
||||
"client/vendor/js/backbone.js"
|
||||
"client/vendor/js/*.js"
|
||||
]
|
||||
external_daemon:
|
||||
cmd: "redis-server"
|
||||
exec:
|
||||
capistrano:
|
||||
cmd: 'bundle && bundle exec cap deploy'
|
||||
scrape:
|
||||
cmd: ". ./venv/bin/activate && cd ./scrapers/bw && python pokemon.py"
|
||||
watch:
|
||||
templates:
|
||||
files: ['client/views/**/*.jade']
|
||||
tasks: 'jade'
|
||||
css:
|
||||
files: ['client/**/*.styl']
|
||||
tasks: 'stylus'
|
||||
js:
|
||||
files: ['client/app/**/*.coffee', 'shared/**/*.coffee']
|
||||
tasks: 'coffee'
|
||||
vendor:
|
||||
files: ['client/vendor/js/**/*.js']
|
||||
tasks: 'concat'
|
||||
vendor_css:
|
||||
files: ['client/vendor/css/**/*.css']
|
||||
tasks: 'cssmin'
|
||||
json:
|
||||
files: [
|
||||
'**/*.json'
|
||||
'!**/node_modules/**'
|
||||
'server/generations'
|
||||
'server/commands'
|
||||
]
|
||||
tasks: 'compile:json'
|
||||
nodemon:
|
||||
development:
|
||||
options:
|
||||
file: "start.js"
|
||||
ignoredFiles: [
|
||||
'.DS_Store'
|
||||
'.git/'
|
||||
'pokebattle-db'
|
||||
'test/'
|
||||
'scrapers/*'
|
||||
'client/*'
|
||||
'public/*'
|
||||
'Gruntfile*'
|
||||
'package.json'
|
||||
'*.md'
|
||||
'*.txt'
|
||||
'Capfile'
|
||||
'config/*'
|
||||
'Gemfile'
|
||||
'Gemfile.lock'
|
||||
'dump.rdb'
|
||||
]
|
||||
aws: grunt.file.readJSON(awsConfigPath)
|
||||
s3:
|
||||
options:
|
||||
accessKeyId: "<%= aws.accessKeyId %>"
|
||||
secretAccessKey: "<%= aws.secretAccessKey %>"
|
||||
bucket: "s3.pokebattle.com"
|
||||
region: 'us-west-2'
|
||||
build:
|
||||
cwd: "public/"
|
||||
expand: true
|
||||
src: assetPaths
|
||||
dest: assets.S3_ASSET_PREFIX
|
||||
rename: (dest, src) ->
|
||||
assets.get(src)
|
||||
|
||||
grunt.loadNpmTasks('grunt-contrib-jade')
|
||||
grunt.loadNpmTasks('grunt-contrib-stylus')
|
||||
grunt.loadNpmTasks('grunt-contrib-coffee')
|
||||
grunt.loadNpmTasks('grunt-contrib-concat')
|
||||
grunt.loadNpmTasks('grunt-contrib-cssmin')
|
||||
grunt.loadNpmTasks('grunt-contrib-watch')
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify')
|
||||
grunt.loadNpmTasks('grunt-nodemon')
|
||||
grunt.loadNpmTasks('grunt-concurrent')
|
||||
grunt.loadNpmTasks('grunt-external-daemon')
|
||||
grunt.loadNpmTasks('grunt-aws')
|
||||
grunt.loadNpmTasks('grunt-exec')
|
||||
|
||||
grunt.registerTask('compile', ['concurrent:compile', 'uglify'])
|
||||
|
||||
grunt.registerTask('heroku:production', 'compile')
|
||||
grunt.registerTask('heroku:development', 'compile')
|
||||
grunt.registerTask('default', ['concurrent:compile', 'concurrent:server'])
|
||||
|
||||
grunt.registerTask('scrape:pokemon', 'exec:scrape')
|
||||
|
||||
grunt.registerTask 'compile:json', 'Compile all data JSON into one file', ->
|
||||
{GenerationJSON} = require './server/generations'
|
||||
EventPokemon = require './shared/event_pokemon.json'
|
||||
{HelpDescriptions} = require './server/commands'
|
||||
contents = """var Generations = #{JSON.stringify(GenerationJSON)},
|
||||
EventPokemon = #{JSON.stringify(EventPokemon)},
|
||||
HelpDescriptions = #{JSON.stringify(HelpDescriptions)};"""
|
||||
grunt.file.write('./public/js/data.js', contents)
|
||||
|
||||
grunt.registerTask 'deploy:assets', 'Compiles and uploads all assets', ->
|
||||
grunt.task.run(['compile', 's3:build'])
|
||||
|
||||
grunt.registerTask('deploy:server', 'exec:capistrano')
|
||||
|
||||
grunt.registerTask('deploy', ['deploy:assets', 'deploy:server'])
|
|
@ -0,0 +1,7 @@
|
|||
Copyright (c) 2012 David Peter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,18 @@
|
|||
HTMLDOCS = $(DOCS:.md=.html)
|
||||
REPORTER = spec
|
||||
|
||||
test:
|
||||
NODE_ENV=test ./node_modules/.bin/mocha \
|
||||
--reporter $(REPORTER) --require should --compilers coffee:coffee-script/register
|
||||
|
||||
|
||||
test-cov: lib-cov
|
||||
NUGGETBRIDGE_COV=1 $(MAKE) test REPORTER=html-cov > coverage.html
|
||||
|
||||
lib-cov:
|
||||
rm -rf server-cov
|
||||
coffee -c server
|
||||
jscoverage server server-cov
|
||||
rm server/*.js
|
||||
|
||||
.PHONY: test
|
|
@ -0,0 +1,115 @@
|
|||
# pokebattle-sim [![Build Status](https://secure.travis-ci.org/sarenji/pokebattle-sim.png?branch=master)](http://travis-ci.org/sarenji/pokebattle-sim)
|
||||
|
||||
A competitive Pokemon battle simulator playable in the browser.
|
||||
|
||||
## Set up
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
git clone git://github.com/sarenji/pokebattle-sim.git
|
||||
cd pokebattle-sim
|
||||
npm install
|
||||
```
|
||||
|
||||
Next, you need to install two dependencies: redis and PostgreSQL 9.1.
|
||||
|
||||
### Redis
|
||||
|
||||
On Mac OS X with homebrew, you can do:
|
||||
|
||||
```bash
|
||||
brew install redis
|
||||
```
|
||||
|
||||
On Windows, there is a Redis port that works fairly well: https://github.com/rgl/redis/downloads
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
PostgreSQL has installable versions for every major OS. In particular, for Mac OS X, there is Postgres.app.
|
||||
|
||||
When you install PostgreSQL, you should create a database for pokebattle, called `pokebattle_sim`. You can do this two ways:
|
||||
|
||||
```bash
|
||||
# command-line:
|
||||
$ createdb pokebattle_sim
|
||||
|
||||
# or via SQL client:
|
||||
CREATE DATABASE pokebattle_sim;
|
||||
```
|
||||
|
||||
Next, you must migrate the database. Simply run:
|
||||
|
||||
```bash
|
||||
npm install -g knex
|
||||
knex migrate:latest
|
||||
```
|
||||
|
||||
If you get an error complaining that the `postgres` role doesn't exist, run this: `createuser -s -r postgres`.
|
||||
|
||||
## Run server
|
||||
|
||||
We use [Grunt](http://gruntjs.com/) to handle our development. First, you must `npm install -g grunt-cli` to get the grunt runner. Then you can type
|
||||
|
||||
```bash
|
||||
grunt
|
||||
```
|
||||
|
||||
to automatically compile all client-side files and run `nodemon` for you.
|
||||
|
||||
We also [support Vagrant](https://github.com/sarenji/pokebattle-sim/wiki/Running-via-Vagrant) if you are on a Windows machine and so desire.
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
npm test
|
||||
# or
|
||||
npm install -g mocha
|
||||
mocha
|
||||
```
|
||||
|
||||
Or if you're in the Vagrant VM, you can just run
|
||||
|
||||
```bash
|
||||
mocha
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
First, you must get SSH access to the server. Then, to deploy:
|
||||
|
||||
```bash
|
||||
cap staging deploy
|
||||
# test on staging
|
||||
cap production deploy
|
||||
```
|
||||
|
||||
## Guide
|
||||
|
||||
pokebattle-sim is a one-page app. The server serves the client.
|
||||
|
||||
```
|
||||
api/ Hosts the code for the API that we host.
|
||||
client/ Main client code. Contains JS and CSS.
|
||||
config/ For Capistrano and deployment.
|
||||
public/ Public-facing dir. Generated files, fonts, images.
|
||||
server/ Server, battle, move, Pokemon logic, etc.
|
||||
shared/ Files shared between server and client.
|
||||
test/ Automated tests for server and client.
|
||||
views/ All views that are rendered server-side go here.
|
||||
Gruntfile.coffee Contains all tasks for pokebattle-sim, like compiling.
|
||||
start.js The main entry point of pokebattle-sim.
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
All contributions to the simulator logic must come with tests. If a
|
||||
contribution does not come with a test that fails before your contribution and
|
||||
passes after, your contribution will be rejected.
|
||||
|
||||
Other contributions (e.g. to the client) are much less strict!
|
||||
|
||||
## Issues
|
||||
|
||||
Report issues in GitHub's [issue
|
||||
tracker](https://github.com/sarenji/pokebattle-sim/issues).
|
|
@ -0,0 +1,120 @@
|
|||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
|
||||
VAGRANTFILE_API_VERSION = "2"
|
||||
|
||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
# All Vagrant configuration is done here. The most common configuration
|
||||
# options are documented and commented below. For a complete reference,
|
||||
# please see the online documentation at vagrantup.com.
|
||||
|
||||
# Every Vagrant virtual environment requires a box to build off of.
|
||||
config.vm.box = "precise32"
|
||||
|
||||
# The url from where the 'config.vm.box' box will be fetched if it
|
||||
# doesn't already exist on the user's system.
|
||||
config.vm.box_url = "http://files.vagrantup.com/precise32.box"
|
||||
|
||||
config.vm.provision :shell, :path => "bootstrap.sh"
|
||||
|
||||
# Create a forwarded port mapping which allows access to a specific port
|
||||
# within the machine from a port on the host machine. In the example below,
|
||||
# accessing "localhost:8080" will access port 80 on the guest machine.
|
||||
config.vm.network :forwarded_port, guest: 8000, host: 8000
|
||||
|
||||
# Create a private network, which allows host-only access to the machine
|
||||
# using a specific IP.
|
||||
# config.vm.network :private_network, ip: "192.168.33.10"
|
||||
|
||||
# Create a public network, which generally matched to bridged network.
|
||||
# Bridged networks make the machine appear as another physical device on
|
||||
# your network.
|
||||
# config.vm.network :public_network
|
||||
|
||||
# If true, then any SSH connections made will enable agent forwarding.
|
||||
# Default value: false
|
||||
# config.ssh.forward_agent = true
|
||||
|
||||
# Share an additional folder to the guest VM. The first argument is
|
||||
# the path on the host to the actual folder. The second argument is
|
||||
# the path on the guest to mount the folder. And the optional third
|
||||
# argument is a set of non-required options.
|
||||
# config.vm.synced_folder "../data", "/vagrant_data"
|
||||
|
||||
# Provider-specific configuration so you can fine-tune various
|
||||
# backing providers for Vagrant. These expose provider-specific options.
|
||||
# Example for VirtualBox:
|
||||
#
|
||||
# config.vm.provider :virtualbox do |vb|
|
||||
# # Don't boot with headless mode
|
||||
# vb.gui = true
|
||||
#
|
||||
# # Use VBoxManage to customize the VM. For example to change memory:
|
||||
# vb.customize ["modifyvm", :id, "--memory", "1024"]
|
||||
# end
|
||||
#
|
||||
# View the documentation for the provider you're using for more
|
||||
# information on available options.
|
||||
|
||||
# Enable provisioning with Puppet stand alone. Puppet manifests
|
||||
# are contained in a directory path relative to this Vagrantfile.
|
||||
# You will need to create the manifests directory and a manifest in
|
||||
# the file precise32.pp in the manifests_path directory.
|
||||
#
|
||||
# An example Puppet manifest to provision the message of the day:
|
||||
#
|
||||
# # group { "puppet":
|
||||
# # ensure => "present",
|
||||
# # }
|
||||
# #
|
||||
# # File { owner => 0, group => 0, mode => 0644 }
|
||||
# #
|
||||
# # file { '/etc/motd':
|
||||
# # content => "Welcome to your Vagrant-built virtual machine!
|
||||
# # Managed by Puppet.\n"
|
||||
# # }
|
||||
#
|
||||
# config.vm.provision :puppet do |puppet|
|
||||
# puppet.manifests_path = "manifests"
|
||||
# puppet.manifest_file = "init.pp"
|
||||
# end
|
||||
|
||||
# Enable provisioning with chef solo, specifying a cookbooks path, roles
|
||||
# path, and data_bags path (all relative to this Vagrantfile), and adding
|
||||
# some recipes and/or roles.
|
||||
#
|
||||
# config.vm.provision :chef_solo do |chef|
|
||||
# chef.cookbooks_path = "../my-recipes/cookbooks"
|
||||
# chef.roles_path = "../my-recipes/roles"
|
||||
# chef.data_bags_path = "../my-recipes/data_bags"
|
||||
# chef.add_recipe "mysql"
|
||||
# chef.add_role "web"
|
||||
#
|
||||
# # You may also specify custom JSON attributes:
|
||||
# chef.json = { :mysql_password => "foo" }
|
||||
# end
|
||||
|
||||
# Enable provisioning with chef server, specifying the chef server URL,
|
||||
# and the path to the validation key (relative to this Vagrantfile).
|
||||
#
|
||||
# The Opscode Platform uses HTTPS. Substitute your organization for
|
||||
# ORGNAME in the URL and validation key.
|
||||
#
|
||||
# If you have your own Chef Server, use the appropriate URL, which may be
|
||||
# HTTP instead of HTTPS depending on your configuration. Also change the
|
||||
# validation key to validation.pem.
|
||||
#
|
||||
# config.vm.provision :chef_client do |chef|
|
||||
# chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME"
|
||||
# chef.validation_key_path = "ORGNAME-validator.pem"
|
||||
# end
|
||||
#
|
||||
# If you're using the Opscode platform, your validator client is
|
||||
# ORGNAME-validator, replacing ORGNAME with your organization name.
|
||||
#
|
||||
# If you have your own Chef Server, the default validation client name is
|
||||
# chef-validator, unless you changed the configuration.
|
||||
#
|
||||
# chef.validation_client_name = "ORGNAME-validator"
|
||||
end
|
|
@ -0,0 +1,179 @@
|
|||
restify = require('restify')
|
||||
generations = require('../server/generations')
|
||||
learnsets = require('../shared/learnsets')
|
||||
{makeBiasedRng} = require("../shared/bias_rng")
|
||||
GenerationJSON = generations.GenerationJSON
|
||||
|
||||
getName = (name) ->
|
||||
require('./name_map.json')[name]
|
||||
|
||||
slugify = (str) ->
|
||||
str.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/\-{2,}/g, '-')
|
||||
|
||||
slugifyArray = (array) ->
|
||||
hash = {}
|
||||
for element in array
|
||||
hash[slugify(element)] = element
|
||||
hash
|
||||
|
||||
attachAPIEndpoints = (server) ->
|
||||
for gen in generations.ALL_GENERATIONS
|
||||
do (gen) ->
|
||||
json = GenerationJSON[gen.toUpperCase()]
|
||||
GenMoves = slugifyArray(json.MoveList)
|
||||
GenAbilities = slugifyArray(json.AbilityList)
|
||||
GenTypes = slugifyArray(json.TypeList)
|
||||
try
|
||||
# Preload Battle
|
||||
{Battle} = require("../server/#{gen}/battle")
|
||||
catch
|
||||
# TODO: There is no Battle object for this gen
|
||||
|
||||
intGeneration = generations.GENERATION_TO_INT[gen]
|
||||
server.get "#{gen}/moves", (req, res, next) ->
|
||||
res.send(json.MoveData)
|
||||
return next()
|
||||
|
||||
server.get "#{gen}/pokemon", (req, res, next) ->
|
||||
res.send(json.FormeData)
|
||||
return next()
|
||||
|
||||
server.get "#{gen}/pokemon/:species", (req, res, next) ->
|
||||
species = getName(req.params.species)
|
||||
return next(new restify.ResourceNotFoundError("Could not find Pokemon: #{req.params.species}")) if !species
|
||||
pokemon = json.FormeData[species]
|
||||
res.send(pokemon)
|
||||
return next()
|
||||
|
||||
server.get "#{gen}/items", (req, res, next) ->
|
||||
res.send(items: json.ItemList)
|
||||
return next()
|
||||
|
||||
server.get "#{gen}/moves", (req, res, next) ->
|
||||
res.send(moves: json.MoveList)
|
||||
return next()
|
||||
|
||||
server.get "#{gen}/moves/:name", (req, res, next) ->
|
||||
move = GenMoves[req.params.name]
|
||||
return next(new restify.ResourceNotFoundError("Could not find Move: #{req.params.name}")) if !move
|
||||
res.send(pokemon: json.MoveMap[move])
|
||||
return next()
|
||||
|
||||
server.get "#{gen}/abilities", (req, res, next) ->
|
||||
res.send(abilities: json.AbilityList)
|
||||
return next()
|
||||
|
||||
server.get "#{gen}/abilities/:name", (req, res, next) ->
|
||||
ability = GenAbilities[req.params.name]
|
||||
return next(new restify.ResourceNotFoundError("Could not find Ability: #{req.params.name}")) if !ability
|
||||
res.send(pokemon: json.AbilityMap[ability])
|
||||
return next()
|
||||
|
||||
server.get "#{gen}/types", (req, res, next) ->
|
||||
res.send(types: json.TypeList)
|
||||
return next()
|
||||
|
||||
server.get "#{gen}/types/:name", (req, res, next) ->
|
||||
type = GenTypes[req.params.name]
|
||||
return next(new restify.ResourceNotFoundError("Could not find Type: #{req.params.name}")) if !type
|
||||
res.send(pokemon: json.TypeMap[type])
|
||||
return next()
|
||||
|
||||
server.get "#{gen}/pokemon/:species/moves", (req, res, next) ->
|
||||
species = getName(req.params.species)
|
||||
pokemon = {species: species}
|
||||
moves = learnsets.learnableMoves(GenerationJSON, pokemon, intGeneration)
|
||||
return next(new restify.ResourceNotFoundError("Could not find moves for Pokemon: #{req.params.species}")) if !moves || moves.length == 0
|
||||
res.send(moves: moves)
|
||||
return next()
|
||||
|
||||
server.get "#{gen}/pokemon/:species/:forme/moves", (req, res, next) ->
|
||||
species = getName(req.params.species)
|
||||
pokemon = {species: species, forme: req.params.forme}
|
||||
moves = learnsets.learnableMoves(GenerationJSON, pokemon, intGeneration)
|
||||
return next(new restify.ResourceNotFoundError("Could not find moves for Pokemon: #{req.params.species}")) if !moves || moves.length == 0
|
||||
res.send(moves: moves)
|
||||
return next()
|
||||
|
||||
checkMoveset = (req, res, next) ->
|
||||
species = getName(req.params.species)
|
||||
return next(new restify.ResourceNotFoundError("Could not find Pokemon: #{req.params.species}")) if !species
|
||||
pokemon = {species: species}
|
||||
pokemon.forme = req.params.forme if req.params.forme
|
||||
moveset = req.query.moves?.split(/,/) || []
|
||||
valid = learnsets.checkMoveset(GenerationJSON, pokemon, intGeneration, moveset)
|
||||
errors = []
|
||||
errors.push("Invalid moveset") if !valid
|
||||
res.send(errors: errors)
|
||||
return next()
|
||||
|
||||
server.get "#{gen}/pokemon/:species/check", checkMoveset
|
||||
server.get "#{gen}/pokemon/:species/:forme/check", checkMoveset
|
||||
|
||||
server.put "#{gen}/damagecalc", (req, res, next) ->
|
||||
# todo: catch any invalid data.
|
||||
moveName = req.params.move
|
||||
attacker = req.params.attacker
|
||||
defender = req.params.defender
|
||||
|
||||
players = [
|
||||
{id: "0", name: "0", team: [attacker]}
|
||||
{id: "1", name: "1", team: [defender]}
|
||||
]
|
||||
battle = new Battle('id', players, numActive: 1)
|
||||
|
||||
move = battle.getMove(moveName)
|
||||
if not move
|
||||
return next(new restify.BadRequest("Invalid move #{moveName}"))
|
||||
|
||||
battle.begin()
|
||||
attackerPokemon = battle.getTeam("0").at(0)
|
||||
defenderPokemon = battle.getTeam("1").at(0)
|
||||
|
||||
# bias the RNG to remove randmomness like critical hits
|
||||
makeBiasedRng(battle)
|
||||
battle.rng.bias("next", "ch", 1)
|
||||
battle.rng.bias("randInt", "miss", 0)
|
||||
battle.rng.bias("next", "secondary effect", 0)
|
||||
battle.rng.bias("randInt", "flinch", 100)
|
||||
|
||||
# calculate min damage
|
||||
battle.rng.bias("randInt", "damage roll", 15)
|
||||
minDamage = move.calculateDamage(battle, attackerPokemon, defenderPokemon)
|
||||
|
||||
# calculate max damage
|
||||
battle.rng.bias("randInt", "damage roll", 0)
|
||||
maxDamage = move.calculateDamage(battle, attackerPokemon, defenderPokemon)
|
||||
|
||||
# TODO: Add remaining HP or anything else that's requested
|
||||
res.send(
|
||||
moveType: move.getType(battle, attackerPokemon, defenderPokemon)
|
||||
basePower: move.basePower(battle, attackerPokemon, defenderPokemon)
|
||||
minDamage: minDamage
|
||||
maxDamage: maxDamage
|
||||
defenderMaxHP: defenderPokemon.stat('hp')
|
||||
)
|
||||
|
||||
return next()
|
||||
|
||||
@createServer = (port, done) ->
|
||||
server = restify.createServer
|
||||
name: 'pokebattle-api'
|
||||
version: '0.0.0'
|
||||
server.pre(restify.pre.sanitizePath())
|
||||
server.use(restify.acceptParser(server.acceptable))
|
||||
server.use(restify.queryParser())
|
||||
server.use(restify.bodyParser())
|
||||
server.use(restify.gzipResponse())
|
||||
|
||||
server.use (req, res, next) ->
|
||||
res.charSet('utf8')
|
||||
return next()
|
||||
|
||||
attachAPIEndpoints(server)
|
||||
|
||||
server.listen port, ->
|
||||
console.log('%s listening at %s', server.name, server.url)
|
||||
done?()
|
||||
|
||||
server
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,48 @@
|
|||
crypto = require 'crypto'
|
||||
path = require 'path'
|
||||
fs = require 'fs'
|
||||
|
||||
config = require('./server/config')
|
||||
|
||||
cachedVersion = null
|
||||
cachedAssetHash = null
|
||||
cachedFingerprints = {}
|
||||
|
||||
S3_ASSET_PREFIX = 'sim/'
|
||||
|
||||
get = (src, options = {}) ->
|
||||
return src if config.IS_LOCAL
|
||||
hash = options.fingerprint || getFingerprint(src)
|
||||
extName = path.extname(src)
|
||||
"#{S3_ASSET_PREFIX}#{src[0...-extName.length]}-#{hash}#{extName}"
|
||||
|
||||
getFingerprint = (src) ->
|
||||
return cachedFingerprints[src] if cachedFingerprints[src]
|
||||
contents = fs.readFileSync("public/#{src}")
|
||||
fingerprint = crypto.createHash('md5').update(contents).digest('hex')
|
||||
cachedFingerprints[src] = fingerprint
|
||||
fingerprint
|
||||
|
||||
getAbsolute = (src, options = {}) ->
|
||||
prefix = (if config.IS_LOCAL then "" else "//media.pokebattle.com")
|
||||
"#{prefix}/#{get(src, options)}"
|
||||
|
||||
# Returns a MD5 hash representing the version of the assets.
|
||||
getVersion = ->
|
||||
return cachedVersion if cachedVersion
|
||||
hash = crypto.createHash('md5')
|
||||
for jsPath in fs.readdirSync('public/js')
|
||||
hash = hash.update(fs.readFileSync("public/js/#{jsPath}"))
|
||||
cachedVersion = hash.digest('hex')
|
||||
cachedVersion
|
||||
|
||||
# Returns a hash of asset hashes, keyed by filename
|
||||
asHash = ->
|
||||
return cachedAssetHash if cachedAssetHash
|
||||
cachedAssetHash = {}
|
||||
for jsPath in fs.readdirSync('public/js')
|
||||
jsPath = "js/#{jsPath}"
|
||||
cachedAssetHash[jsPath] = getFingerprint(jsPath)
|
||||
cachedAssetHash
|
||||
|
||||
module.exports = {S3_ASSET_PREFIX, get, asHash, getAbsolute, getVersion}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"accessKeyId": "akid",
|
||||
"secretAccessKey": "secret",
|
||||
"region": "us-west-2",
|
||||
"sslEnabled": true
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"accessKeyId": "akid",
|
||||
"secretAccessKey": "secret",
|
||||
"region": "us-west-2",
|
||||
"sslEnabled": true
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Set the USER and HOME variables to be the vagrant user. By default, the script is run as root
|
||||
export USER='vagrant'
|
||||
export HOME='/home/vagrant'
|
||||
|
||||
# Install some necessary packages
|
||||
sudo apt-get install -y redis-server curl git make g++
|
||||
|
||||
# Install NVM. Use source ~/.profile to "restart" the shell
|
||||
curl https://raw.github.com/creationix/nvm/master/install.sh | sh
|
||||
source $HOME/.profile
|
||||
|
||||
nvm install 0.10
|
||||
nvm alias default 0.10
|
||||
|
||||
npm install -g coffee-script grunt-cli mocha
|
||||
|
||||
# Rebuild
|
||||
cd /vagrant
|
||||
npm install --no-bin-links
|
|
@ -0,0 +1,538 @@
|
|||
outline-text(clr = #000)
|
||||
text-shadow 0 1px 0 clr, 0 -1px 0 clr,
|
||||
-1px 0 0 clr, 1px 0 0 clr
|
||||
|
||||
divide(a, b)
|
||||
a / b
|
||||
|
||||
create-projectile(clr)
|
||||
trans-clr = rgba(black, 0)
|
||||
background radial-gradient(ellipse at center, lighten(clr, 50%) 0%, clr 50%, trans-clr 51%, trans-clr 100%)
|
||||
|
||||
$padding = 10px
|
||||
$battle-width = 600px
|
||||
$battle-height = 300px
|
||||
$pane-width = $battle-width + $padding + $padding
|
||||
$screen-width = 400px
|
||||
$side-width = ($battle-width - $screen-width) / 2
|
||||
|
||||
.battle_window
|
||||
.chat
|
||||
left $pane-width
|
||||
|
||||
.battle
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
bottom 0
|
||||
width $pane-width
|
||||
box-sizing border-box
|
||||
padding $padding
|
||||
h2
|
||||
font-size 0.6875em
|
||||
text-transform uppercase
|
||||
letter-spacing 1px
|
||||
border-bottom 1px solid rgba(0, 0, 0, .10)
|
||||
line-height normal
|
||||
.battle_container
|
||||
position relative
|
||||
width $battle-width
|
||||
height $battle-height
|
||||
narrow-font()
|
||||
&.battle_bg_0
|
||||
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg.png")
|
||||
&.battle_bg_1
|
||||
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg_1.png")
|
||||
&.battle_bg_2
|
||||
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg_2.png")
|
||||
&.battle_bg_3
|
||||
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg_3.png")
|
||||
&.battle_bg_4
|
||||
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg_4.png")
|
||||
&.battle_bg_5
|
||||
background-image url("//91.121.152.74:8000/Sprites/battle/backgrounds/battle_bg_5.png")
|
||||
.battle_user_info
|
||||
.pokemon_icons
|
||||
margin 0 auto
|
||||
width 80px
|
||||
.icon_wrapper
|
||||
width 40px
|
||||
float left
|
||||
&:nth-child(even)
|
||||
position relative
|
||||
top 6px
|
||||
.fill-left, .fill-right
|
||||
width $side-width
|
||||
.left
|
||||
bottom 20px
|
||||
left 0
|
||||
.right
|
||||
top 20px
|
||||
right 0
|
||||
.left, .right
|
||||
position absolute
|
||||
width $side-width
|
||||
color #fff
|
||||
text-align center
|
||||
outline-text()
|
||||
font-size 13px
|
||||
.icon_wrapper
|
||||
position relative
|
||||
&:hover
|
||||
border-radius 5px
|
||||
background-color rgba(187, 184, 248, .5)
|
||||
.pokemon_hp_background
|
||||
position absolute
|
||||
right 6px
|
||||
bottom 2px
|
||||
width 4px
|
||||
height 3px
|
||||
border 1px solid rgba(32, 32, 32, .4)
|
||||
background rgba(32, 32, 32, .4)
|
||||
pointer-events none
|
||||
.pokemon_hp
|
||||
absolute bottom left
|
||||
right 0
|
||||
&.green
|
||||
background #0f0
|
||||
&.yellow
|
||||
background #ff0
|
||||
&.red
|
||||
background #f00
|
||||
|
||||
.battle-timer
|
||||
font-size 2em
|
||||
&.battle-timer-low
|
||||
color #f33
|
||||
&.battle-timer-small
|
||||
font-size 1em
|
||||
&:before
|
||||
content: '('
|
||||
&:after
|
||||
content: ')'
|
||||
|
||||
.battle_overlays
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
right 0
|
||||
bottom 0
|
||||
pointer-events none
|
||||
overflow hidden
|
||||
.battle_overlay
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
right 0
|
||||
bottom 0
|
||||
&.weather
|
||||
&.rain
|
||||
background rgba(0, 48, 196, .3)
|
||||
&.hail
|
||||
background rgba(128, 255, 255, .3)
|
||||
&.sand
|
||||
background linear-gradient(top, rgba(128, 64, 0, .3), rgba(64, 32, 48, .5))
|
||||
&.sun
|
||||
background rgba(255, 196, 64, .3)
|
||||
|
||||
.battle_pane
|
||||
position absolute
|
||||
width $screen-width
|
||||
height $battle-height
|
||||
top 0
|
||||
left $side-width
|
||||
.pokemon
|
||||
&.bottom
|
||||
.pokemon-info
|
||||
bottom 35%
|
||||
left 200px
|
||||
.sprite
|
||||
&.fade
|
||||
position absolute
|
||||
left -20px
|
||||
opacity .3
|
||||
&.top
|
||||
.pokemon-info
|
||||
top 20%
|
||||
right 238px
|
||||
.sprite
|
||||
&.fade
|
||||
position absolute
|
||||
left 20px
|
||||
opacity .3
|
||||
|
||||
.pokemon-info
|
||||
position absolute
|
||||
width 144px
|
||||
height 6px
|
||||
border 1px solid #fff
|
||||
border-radius 2px
|
||||
box-shadow 0 0 0 1px #000
|
||||
background rgba(0, 8, 32, .5)
|
||||
.hp, .hp-gradient
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
bottom 0
|
||||
width 100%
|
||||
transition width .4s cubic-bezier(0, 1, .5, 1)
|
||||
.hp-gradient
|
||||
background linear-gradient(top, transparent 40%, rgba(0,32,64,0.3) 60%)
|
||||
.hp
|
||||
background #0f0
|
||||
.hp-text
|
||||
position absolute
|
||||
top -1px
|
||||
bottom @top
|
||||
left 100%
|
||||
background #333
|
||||
color #fff
|
||||
font-size 8px
|
||||
line-height @font-size
|
||||
padding 0 4px
|
||||
border-radius 0 2px 2px 0
|
||||
box-shadow 0 0 0 1px #222
|
||||
font-family Verdana, sans-serif
|
||||
.pokemon-meta
|
||||
position absolute
|
||||
left 0
|
||||
bottom 100%
|
||||
color #fff
|
||||
pointer-events visible
|
||||
outline-text()
|
||||
.pokemon-name
|
||||
font-weight 700
|
||||
color #fff
|
||||
.pokemon-level, .gender
|
||||
font-size 75%
|
||||
.pokemon-level-text
|
||||
color #EFA532
|
||||
.pokemon-effects
|
||||
position absolute
|
||||
top 9px
|
||||
color #fff
|
||||
normal-font()
|
||||
.pokemon-effect
|
||||
font-size .625em
|
||||
float left
|
||||
border-radius 3px
|
||||
background #333
|
||||
color #fff
|
||||
padding 0 4px
|
||||
border 1px solid rgba(0, 0, 0, .5)
|
||||
&.sleep
|
||||
background #535
|
||||
&.burn
|
||||
background #c33
|
||||
&.freeze
|
||||
background #38b
|
||||
&.paralyze
|
||||
background #b93
|
||||
&.poison, &.toxic
|
||||
background #b3b
|
||||
&.boost
|
||||
background #5a3
|
||||
&.negative
|
||||
background #a53
|
||||
.sprite
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
pointer-events visible
|
||||
transition left 1s ease
|
||||
img
|
||||
position absolute
|
||||
max-width none
|
||||
.pokemon
|
||||
position absolute
|
||||
width 100%
|
||||
height 100%
|
||||
top 0
|
||||
left 0
|
||||
pointer-events none
|
||||
overflow hidden
|
||||
|
||||
.pokeball
|
||||
position absolute
|
||||
background url("//media.pokebattle.com/img/battle/pokeballs.gif") -133px -10px
|
||||
transition opacity .25s ease-in
|
||||
|
||||
.percentage
|
||||
font-family "Helvetica Neue", sans-serif
|
||||
font-weight 700
|
||||
padding 4px
|
||||
border-radius 3px
|
||||
background #4e4
|
||||
transition left 5s linear
|
||||
color #fff
|
||||
border 1px solid rgba(0, 0, 0, .3)
|
||||
box-shadow 2px 2px 2px rgba(0, 0, 0, .3)
|
||||
&.red
|
||||
background #e44
|
||||
|
||||
.team-stealth-rock
|
||||
position absolute
|
||||
background url("//media.pokebattle.com/img/battle/stealth_rock.png")
|
||||
width 28px
|
||||
height 35px
|
||||
pointer-events none
|
||||
|
||||
.team-toxic-spikes
|
||||
position absolute
|
||||
background url("//media.pokebattle.com/img/battle/toxic_spikes.png")
|
||||
width 21px
|
||||
height 21px
|
||||
pointer-events none
|
||||
|
||||
.team-spikes
|
||||
position absolute
|
||||
background url("//media.pokebattle.com/img/battle/spikes.png")
|
||||
width 21px
|
||||
height 21px
|
||||
pointer-events none
|
||||
|
||||
.team-sticky-web
|
||||
position absolute
|
||||
background url("//media.pokebattle.com/img/battle/sticky_web.png")
|
||||
width 150px
|
||||
height 151px
|
||||
pointer-events none
|
||||
|
||||
.team-screen
|
||||
position absolute
|
||||
opacity .5
|
||||
width 1px
|
||||
height @width
|
||||
pointer-events none
|
||||
&.rounded
|
||||
border-radius 100%
|
||||
for $pair in ('blue' $water) ('yellow' $electric) ('pink' $fairy)
|
||||
&.{$pair[0]}
|
||||
border 1px solid darken($pair[1], 20%)
|
||||
background-color $pair[1]
|
||||
|
||||
.projectile
|
||||
position absolute
|
||||
width 64px
|
||||
height @width
|
||||
pointer-events none
|
||||
for $array in $types
|
||||
&.{$array[0]}
|
||||
create-projectile($array[1])
|
||||
|
||||
.battle_summary
|
||||
position absolute
|
||||
bottom 0
|
||||
left $side-width
|
||||
right @left
|
||||
background rgba(black, .5)
|
||||
color white
|
||||
display none
|
||||
text-shadow none
|
||||
padding 4px 10px
|
||||
font-size .875em
|
||||
p
|
||||
margin 0
|
||||
font-size .8125em
|
||||
&.big
|
||||
font-size 1em
|
||||
margin-top 4px
|
||||
&:first-child
|
||||
margin-top 0
|
||||
|
||||
.ability_activation
|
||||
position absolute
|
||||
width 150px
|
||||
background rgba(black, .5)
|
||||
text-align center
|
||||
opacity 0
|
||||
color white
|
||||
outline-text()
|
||||
pointer-events none
|
||||
$radius = 4px
|
||||
strong
|
||||
font-size 1.2em
|
||||
display block
|
||||
&.front
|
||||
border-radius $radius 0 0 $radius
|
||||
top 64px
|
||||
right 0
|
||||
&.back
|
||||
border-radius 0 $radius $radius 0
|
||||
bottom 64px
|
||||
left 0
|
||||
|
||||
.moves
|
||||
.button
|
||||
display block
|
||||
float none
|
||||
margin 0 0 5px
|
||||
&.hidden
|
||||
display none
|
||||
|
||||
.switches
|
||||
.button
|
||||
display block
|
||||
float none
|
||||
&.disabled
|
||||
.pokemon_icon
|
||||
opacity .5
|
||||
.pokemon_icon
|
||||
display block
|
||||
|
||||
.mega-evolve
|
||||
text-align center
|
||||
font-size .8em
|
||||
|
||||
.gender
|
||||
&.gender_female
|
||||
color #EA597A
|
||||
&.gender_male
|
||||
color #62CBB0
|
||||
|
||||
.team_icons
|
||||
width 6 * 40px
|
||||
|
||||
.pokemon_icon
|
||||
position relative
|
||||
width 32px
|
||||
height 32px
|
||||
display inline-block
|
||||
margin 0 auto
|
||||
background url("//media.pokebattle.com/img/pokemon_icons_40px.png") top left
|
||||
vertical-align middle
|
||||
&.fainted
|
||||
opacity 0.5
|
||||
|
||||
.battle_teams
|
||||
position absolute
|
||||
left $side-width
|
||||
right $side-width
|
||||
text-align center
|
||||
margin 0 auto
|
||||
width 300px
|
||||
normal-font()
|
||||
li
|
||||
position relative
|
||||
.gender
|
||||
position absolute
|
||||
top 0
|
||||
right 2px
|
||||
font-size .75em
|
||||
text-shadow 0 1px 0 #333
|
||||
.level
|
||||
position absolute
|
||||
bottom 0
|
||||
right 2px
|
||||
font-size .75em
|
||||
line-height @font-size
|
||||
outline-text(#fff)
|
||||
narrow-font()
|
||||
|
||||
.battle_team_preview
|
||||
background #eee
|
||||
padding 10px
|
||||
border-radius 4px
|
||||
margin 10px 0
|
||||
border 1px solid #aaa
|
||||
|
||||
.px_40, .sortable-placeholder
|
||||
width 40px
|
||||
margin 0 2px
|
||||
padding-left 0
|
||||
padding-right 0
|
||||
float left
|
||||
|
||||
.submit_arrangement
|
||||
margin 10px 0 0
|
||||
|
||||
.team_pokemon
|
||||
position relative
|
||||
width 40px
|
||||
height 32px
|
||||
margin 0 auto
|
||||
|
||||
.arrange_team
|
||||
user-select none
|
||||
li
|
||||
cursor pointer
|
||||
border-radius 4px
|
||||
&.img-polaroid.active
|
||||
background-color #ccc
|
||||
|
||||
.lead_text
|
||||
font-size .8125em
|
||||
min-height 0
|
||||
|
||||
.pokemon_icon
|
||||
top 0
|
||||
left 0
|
||||
|
||||
.sortable-placeholder
|
||||
border 1px dashed #ccc
|
||||
background none
|
||||
height 40px
|
||||
|
||||
.unstyled
|
||||
margin 0
|
||||
|
||||
.well-battle-actions
|
||||
font-size 1.5em
|
||||
height 3em
|
||||
line-height @height
|
||||
text-align center
|
||||
|
||||
.save-log
|
||||
display block
|
||||
text-align center
|
||||
|
||||
.drop
|
||||
background linear-gradient(top, rgba($water, .7), rgba(255, 255, 255, 0.3))
|
||||
width 1px
|
||||
height 89px
|
||||
position absolute
|
||||
top 0
|
||||
transform rotate(-20deg)
|
||||
animation fall .63s linear infinite
|
||||
|
||||
@keyframes fall
|
||||
to
|
||||
margin-top 2 * $battle-height
|
||||
margin-left 2 * $battle-height * .36
|
||||
|
||||
.ray
|
||||
position absolute
|
||||
background rgba(230, 192, 70, .5)
|
||||
width ($battle-width / 10)
|
||||
height 100%
|
||||
transition all 1000ms ease
|
||||
transform rotate(360deg) skewX(30deg)
|
||||
animation fade-in-out 3s ease infinite
|
||||
|
||||
@keyframes fade-in-out
|
||||
0%, 100%
|
||||
opacity 0
|
||||
50%
|
||||
opacity .3
|
||||
|
||||
.sand_overlay
|
||||
absolute top left
|
||||
background url("//media.pokebattle.com/img/battle/weather_sand.png")
|
||||
width 600px
|
||||
height 600px
|
||||
opacity .5
|
||||
animation sandstorm .75s linear infinite
|
||||
|
||||
@keyframes sandstorm
|
||||
0%
|
||||
transform translate(0, 0)
|
||||
100%
|
||||
transform translate(200%, 100%)
|
||||
|
||||
.hailstone
|
||||
position absolute
|
||||
border-radius 4px
|
||||
width 10px
|
||||
height @width
|
||||
background rgba(230, 200, 255, .5)
|
||||
animation fall .4s linear infinite
|
|
@ -0,0 +1,59 @@
|
|||
.battle-list
|
||||
position absolute
|
||||
top 0
|
||||
bottom 0
|
||||
left 0
|
||||
right 0
|
||||
overflow auto
|
||||
|
||||
h2
|
||||
display block
|
||||
margin-left 10px
|
||||
|
||||
.battle-list-collection
|
||||
padding-right 10px
|
||||
|
||||
.empty
|
||||
margin-left 10px
|
||||
|
||||
.battle-list-item-wrapper
|
||||
float left
|
||||
width 100%
|
||||
padding-left 10px
|
||||
padding-bottom 10px
|
||||
box-sizing border-box
|
||||
|
||||
.battle-list-item
|
||||
cursor pointer
|
||||
border 1px solid #DDD
|
||||
background #F5F5F5
|
||||
box-shadow 1px 2px 0 rgba(0, 0, 0, 0.2)
|
||||
padding 10px
|
||||
|
||||
&:hover
|
||||
background: #FFF
|
||||
|
||||
.vs
|
||||
font-weight bold
|
||||
margin 0 5px
|
||||
|
||||
// Normally, our media queries use max-width to scale down, but for this page we want to scale up
|
||||
@media screen and (min-width: 1000px)
|
||||
.battle-list-item-wrapper
|
||||
width 50%
|
||||
|
||||
@media screen and (min-width: 1400px)
|
||||
.battle-list-item-wrapper
|
||||
width 33%
|
||||
|
||||
@media screen and (min-width: 1800px)
|
||||
.battle-list-item-wrapper
|
||||
width 25%
|
||||
|
||||
@media screen and (min-width: 2200px)
|
||||
.battle-list-item-wrapper
|
||||
width 20%
|
||||
|
||||
@media screen and (min-width: 2600px)
|
||||
.battle-list-item-wrapper
|
||||
width 16.6%
|
|
@ -0,0 +1,100 @@
|
|||
button-bg(clr)
|
||||
clr = lighten(clr, 50%)
|
||||
light = lighten(clr, 30%)
|
||||
dark-clr = darken(clr, 20%)
|
||||
darker-clr = darken(clr, 50%)
|
||||
darkest-clr = darken(darker-clr, 20%)
|
||||
btn-clr = darken(darkest-clr, 30%)
|
||||
background linear-gradient(top, clr, dark-clr)
|
||||
border 1px solid darker-clr
|
||||
border-top-color rgba(0, 0, 0, .1)
|
||||
border-left-color rgba(0, 0, 0, .2)
|
||||
border-right-color @border-left-color
|
||||
border-bottom-color rgba(0, 0, 0, .3)
|
||||
color btn-clr
|
||||
&:hover
|
||||
background linear-gradient(top, light, clr)
|
||||
color lighten(btn-clr, 20%)
|
||||
text-shadow 0 -1px 0 rgba(255, 255, 255, .10)
|
||||
&:active, &.pressed
|
||||
color darken(btn-clr, 50%)
|
||||
text-shadow none
|
||||
border-color darkest-clr
|
||||
&.pressed
|
||||
background darker-clr
|
||||
&:active
|
||||
background darkest-clr
|
||||
|
||||
.button
|
||||
clearfix()
|
||||
button-bg(#eee)
|
||||
display inline-block
|
||||
height 32px
|
||||
line-height @height
|
||||
border-radius 4px
|
||||
margin-bottom 6px
|
||||
padding 0 10px
|
||||
cursor pointer
|
||||
color #333
|
||||
text-shadow 0 -1px 0 rgba(255, 255, 255, .25)
|
||||
box-shadow inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.25)
|
||||
position relative
|
||||
&.block
|
||||
display block
|
||||
&:hover
|
||||
box-shadow inset 0 1px 0 rgba(255,255,255,.125), 0 1px 2px rgba(0,0,0,.125)
|
||||
.main_text
|
||||
narrow-font()
|
||||
font-weight 700
|
||||
float left
|
||||
.meta_info
|
||||
font-size 12px
|
||||
float right
|
||||
font-family "Helvetica Neue", sans-serif
|
||||
&.big
|
||||
font-size 1.5em
|
||||
height 3em
|
||||
line-height @height
|
||||
text-align center
|
||||
.main_text
|
||||
float none
|
||||
.meta_info
|
||||
position absolute
|
||||
top 0
|
||||
right 1em
|
||||
&.button_blue
|
||||
background linear-gradient(top, #0066cc, #0044cc)
|
||||
text-shadow 0 -1px rgba(0, 0, 0, .3)
|
||||
color #fff
|
||||
&:hover
|
||||
background linear-gradient(top, #0088cc, #0066cc)
|
||||
&.button_red
|
||||
background linear-gradient(top, #cc6600, #cc4400)
|
||||
text-shadow 0 -1px rgba(0, 0, 0, .3)
|
||||
color #fff
|
||||
&:hover
|
||||
background linear-gradient(top, #cc8800, #cc6600)
|
||||
for $array in $types
|
||||
&.{$array[0]}
|
||||
button-bg($array[1])
|
||||
&.disabled, &.disabled:hover
|
||||
background #333
|
||||
border-color #000
|
||||
cursor default
|
||||
color #666
|
||||
text-shadow none
|
||||
box-shadow none
|
||||
|
||||
.hover_info_moves
|
||||
.hover_info
|
||||
width 196px
|
||||
display block
|
||||
margin 0
|
||||
&:first-child
|
||||
border-radius 4px 4px 0 0
|
||||
&:last-child
|
||||
border-radius 0 0 4px 4px
|
||||
border-top none
|
||||
&:not(:first-child):not(:last-child)
|
||||
border-radius 0
|
||||
border-top none
|
|
@ -0,0 +1,100 @@
|
|||
.chat_window .chat
|
||||
left 350px
|
||||
|
||||
.chat
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
bottom 0
|
||||
&.without_spectators
|
||||
.message_pane
|
||||
right 0
|
||||
.user_list
|
||||
display none
|
||||
&.without_chat_input
|
||||
.messages
|
||||
bottom 0
|
||||
.chat_input_pane
|
||||
display none
|
||||
|
||||
.user_list
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
bottom 0
|
||||
width 25%
|
||||
overflow-y auto
|
||||
p
|
||||
padding 2px 6px
|
||||
margin 0
|
||||
ul
|
||||
margin 0
|
||||
padding 0
|
||||
list-style none
|
||||
li
|
||||
padding 2px 6px
|
||||
&:hover
|
||||
background #fb7
|
||||
|
||||
.message_pane
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
right 25%
|
||||
bottom 0
|
||||
box-shadow 0 0 5px rgba(0, 8, 16, .2)
|
||||
|
||||
.messages
|
||||
overflow-y auto
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
right 0
|
||||
bottom 30px
|
||||
border-left 1px solid rgba(0, 0, 0, .2)
|
||||
border-right @border-left
|
||||
background white
|
||||
word-wrap break-word
|
||||
h2, h3, p
|
||||
padding 0 16px 2px
|
||||
p
|
||||
font-size .875em
|
||||
text-rendering optimizeLegibility
|
||||
&.move_message
|
||||
font-size 1em
|
||||
margin-top 10px
|
||||
|
||||
a.pokemon-link
|
||||
color inherit
|
||||
|
||||
.alert
|
||||
margin 0.75em 16px
|
||||
|
||||
.chat_input_pane
|
||||
position absolute
|
||||
width 100%
|
||||
bottom 0
|
||||
height 30px
|
||||
|
||||
.chat_input_wrapper
|
||||
overflow: hidden // Make it take the remainder of the space not used by the button
|
||||
|
||||
.chat_input
|
||||
box-sizing border-box
|
||||
border 2px solid #384035
|
||||
padding 2px 6px
|
||||
margin 0
|
||||
width 100%
|
||||
height 30px
|
||||
border-radius 2px
|
||||
&:hover
|
||||
border-color #5D736B
|
||||
&:focus
|
||||
border-color #F27405
|
||||
outline none
|
||||
|
||||
.chat_input_send
|
||||
box-sizing border-box
|
||||
float right
|
||||
margin 0
|
||||
height 30px
|
|
@ -0,0 +1,54 @@
|
|||
$body-color = rgba(223, 231, 232, .5)
|
||||
$background-color = #f3f3f3
|
||||
$positive-color = #38AB46
|
||||
$negative-color = #DC4E48
|
||||
|
||||
$off-white = #f5f5f5
|
||||
$grey = #999
|
||||
$yellow = #b92
|
||||
|
||||
$normal = #A9A57A
|
||||
$fire = #E28544
|
||||
$water = #6A97ED
|
||||
$electric = #F0C944
|
||||
$grass = #8FC052
|
||||
$ice = #A2CFD0
|
||||
$fighting = #B04137
|
||||
$poison = #9253A0
|
||||
$ground = #DCBE6F
|
||||
$flying = #A09AEE
|
||||
$psychic = #E0698B
|
||||
$bug = #AEB22D
|
||||
$rock = #B69D42
|
||||
$ghost = #6A5F97
|
||||
$dragon = #5C59F1
|
||||
$dark = #685447
|
||||
$steel = #B7BACF
|
||||
$fairy = #F7B5F7
|
||||
|
||||
$types = ('normal' $normal) ('fire' $fire) ('water' $water) ('electric' $electric) ('grass' $grass) ('ice' $ice) ('fighting' $fighting) ('poison' $poison) ('ground' $ground) ('flying' $flying) ('psychic' $psychic) ('bug' $bug) ('rock' $rock) ('ghost' $ghost) ('dragon' $dragon) ('dark' $dark) ('steel' $steel) ('fairy' $fairy)
|
||||
|
||||
.bg-blue
|
||||
background-color lighten($ice, 50%)
|
||||
|
||||
.bg-faded-white
|
||||
background rgba(white, .5)
|
||||
|
||||
.bg-off-white
|
||||
background $off-white
|
||||
|
||||
.hover-bg-white
|
||||
&:hover
|
||||
background white
|
||||
|
||||
.bg-faded-blue
|
||||
background rgba(0, 20, 64, .5)
|
||||
|
||||
.red
|
||||
color $negative-color
|
||||
|
||||
.yellow
|
||||
color $yellow
|
||||
|
||||
.grey
|
||||
color $grey
|
|
@ -0,0 +1,131 @@
|
|||
narrow-font()
|
||||
font-family "PT Sans Narrow", "Helvetica Neue", sans-serif
|
||||
|
||||
normal-font()
|
||||
font-family 'proxima-nova', 'Helvetica Neue', Calibri, 'Droid Sans', Helvetica, Arial, sans-serif
|
||||
|
||||
$body_wrapper = 30px
|
||||
$nav_size = 150px
|
||||
$header_size = 52px
|
||||
|
||||
html, body
|
||||
color #333332
|
||||
padding 0
|
||||
margin 0
|
||||
font-size 100%
|
||||
normal-font()
|
||||
background $background-color url("//media.pokebattle.com/img/bg.png")
|
||||
line-height normal
|
||||
|
||||
ul, ol
|
||||
list-style none
|
||||
|
||||
ul, ol, p
|
||||
margin 0
|
||||
padding 0
|
||||
|
||||
#content
|
||||
position absolute
|
||||
top 0
|
||||
left $nav_size
|
||||
right 0
|
||||
bottom 0
|
||||
box-shadow 0 0 10px #000
|
||||
|
||||
.no-sidebar
|
||||
#content
|
||||
left 0
|
||||
#navigation
|
||||
display none
|
||||
|
||||
#main-section
|
||||
position absolute
|
||||
top $header_size
|
||||
left 0
|
||||
right 0
|
||||
bottom 0
|
||||
|
||||
.window
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
right 0
|
||||
bottom 0
|
||||
|
||||
a, .fake_link
|
||||
color #F27405
|
||||
text-decoration none
|
||||
cursor pointer
|
||||
&:hover
|
||||
color #BD4F00
|
||||
|
||||
.nav
|
||||
clearfix()
|
||||
list-style none
|
||||
padding 0
|
||||
margin 0
|
||||
li
|
||||
float left
|
||||
border-radius 4px 4px 0 0
|
||||
margin-left 2px
|
||||
margin-right @margin-left
|
||||
&:hover
|
||||
background-color #d9d7d7
|
||||
&.active
|
||||
border 1px solid #d9d7d7
|
||||
border-bottom none
|
||||
background white
|
||||
a, .fake_link, &.fake_link
|
||||
color #333
|
||||
text-decoration none
|
||||
cursor default
|
||||
a, .fake_link
|
||||
padding 5px 15px
|
||||
|
||||
.popup-absolute
|
||||
position absolute
|
||||
top 20px
|
||||
left 10px
|
||||
right @left
|
||||
text-align center
|
||||
z-index 100
|
||||
|
||||
.popover
|
||||
normal-font()
|
||||
.popover-title
|
||||
font-size 16px
|
||||
.popover-content
|
||||
font-size 14px
|
||||
|
||||
.loading-container
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
right 0
|
||||
bottom 0
|
||||
background rgba(0, 0, 0, .7)
|
||||
color white
|
||||
font-size 4em
|
||||
text-align center
|
||||
z-index 99
|
||||
display table
|
||||
width 100%
|
||||
height 100%
|
||||
|
||||
.loading-message
|
||||
display table-cell
|
||||
vertical-align middle
|
||||
|
||||
.team-pbv
|
||||
font-size 0.75em
|
||||
|
||||
// Typography
|
||||
|
||||
.monospace
|
||||
font-family "Monaco", monospace
|
||||
|
||||
abbr
|
||||
border-bottom 1px dotted #333
|
||||
|
||||
.italic
|
||||
font-style italic
|
|
@ -0,0 +1,103 @@
|
|||
$header_size = 52px
|
||||
|
||||
#header
|
||||
height $header_size
|
||||
line-height @height
|
||||
width 100%
|
||||
background #38342B
|
||||
z-index 10
|
||||
position absolute
|
||||
ul
|
||||
margin 0
|
||||
|
||||
.icon
|
||||
position relative
|
||||
top 3px
|
||||
font-size 20px
|
||||
|
||||
#main-nav
|
||||
padding 0 18px
|
||||
|
||||
#main-nav, #header li
|
||||
line-height 42px
|
||||
|
||||
#logo
|
||||
float left
|
||||
width 120px
|
||||
margin-right 15px
|
||||
|
||||
#logo img
|
||||
position relative
|
||||
z-index: 100
|
||||
top 3px
|
||||
|
||||
#sections
|
||||
margin-left 30px
|
||||
margin-right 120px
|
||||
visibility visible
|
||||
|
||||
#leftmenu
|
||||
display none
|
||||
|
||||
#leftmenu a
|
||||
color #fff
|
||||
padding 5px
|
||||
margin-left 15px
|
||||
&:hover
|
||||
border-radius 5px
|
||||
background #D94E47
|
||||
text-decoration none
|
||||
|
||||
#rightmenu {
|
||||
margin-left: -40px;
|
||||
}
|
||||
nav ul {
|
||||
list-style: none;
|
||||
font-size: .95em;
|
||||
}
|
||||
nav li {
|
||||
display: block;
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
nav a {
|
||||
display: block;
|
||||
padding-left: .75em;
|
||||
padding-right: .75em;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav a:hover, nav li:hover {
|
||||
background: #D94E47;
|
||||
color: #6C2724;
|
||||
text-decoration none
|
||||
}
|
||||
|
||||
#sub-nav {
|
||||
height: 10px;
|
||||
background: #D94E47;
|
||||
z-index: 6;
|
||||
position absolute
|
||||
bottom 0
|
||||
left 0
|
||||
right @left
|
||||
}
|
||||
|
||||
@keyframes anim-rotate {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner-anim {
|
||||
display: inline-block;
|
||||
animation: anim-rotate 1s infinite linear;
|
||||
}
|
||||
|
||||
.spinner-anim.hidden {
|
||||
display: none;
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
for $i in (1 2 3)
|
||||
.z{$i}
|
||||
z-index $i
|
||||
|
||||
for $i in (1 2 3 4)
|
||||
.m{$i}
|
||||
margin $i * 10px
|
||||
.p{$i}
|
||||
padding $i * 10px
|
||||
|
||||
for $i in (1 2 3 4)
|
||||
// margin
|
||||
.ml{$i}
|
||||
margin-left $i * 10px
|
||||
.mr{$i}
|
||||
margin-right $i * 10px
|
||||
.mt{$i}
|
||||
margin-top $i * 10px
|
||||
.mb{$i}
|
||||
margin-bottom $i * 10px
|
||||
// padding
|
||||
.pl{$i}
|
||||
padding-left $i * 10px
|
||||
.pr{$i}
|
||||
padding-right $i * 10px
|
||||
.pt{$i}
|
||||
padding-top $i * 10px
|
||||
.pb{$i}
|
||||
padding-bottom $i * 10px
|
||||
|
||||
.mt-header
|
||||
margin-top $header_size
|
||||
|
||||
.left
|
||||
float left
|
||||
|
||||
.right
|
||||
float right
|
||||
|
||||
.center
|
||||
text-align center
|
||||
|
||||
.align-left
|
||||
text-align left
|
||||
|
||||
.align-right
|
||||
text-align right
|
||||
|
||||
.flex-center
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
|
||||
.flex-column
|
||||
flex-direction column
|
||||
|
||||
.hidden
|
||||
display none
|
||||
|
||||
.absolute
|
||||
position absolute
|
||||
|
||||
.fill
|
||||
absolute top left
|
||||
bottom 0
|
||||
right 0
|
||||
|
||||
.fill-left
|
||||
absolute top left
|
||||
bottom 0
|
||||
|
||||
.fill-right
|
||||
absolute top right
|
||||
bottom 0
|
||||
|
||||
.relative
|
||||
position relative
|
||||
|
||||
.rounded
|
||||
border-radius 3px
|
||||
|
||||
.border
|
||||
border 1px solid #ddd
|
||||
|
||||
.block
|
||||
display block
|
||||
|
||||
.inline-block
|
||||
display inline-block
|
||||
|
||||
.inline
|
||||
display inline
|
||||
|
||||
.box-shadow
|
||||
box-shadow 1px 2px 0 rgba(0, 0, 0, .2)
|
||||
|
||||
.tiny-type
|
||||
font-size .75em
|
||||
|
||||
.clickable-box
|
||||
@extend .border
|
||||
@extend .bg-off-white
|
||||
@extend .hover-bg-white
|
||||
@extend .rounded
|
||||
@extend .box-shadow
|
|
@ -0,0 +1,15 @@
|
|||
@import 'nib'
|
||||
@import 'colors'
|
||||
@import 'core'
|
||||
@import 'layout'
|
||||
@import 'header'
|
||||
@import 'buttons'
|
||||
@import 'navigation'
|
||||
@import 'chat'
|
||||
@import 'main_buttons'
|
||||
@import 'battle'
|
||||
@import 'teambuilder'
|
||||
@import 'battle_list'
|
||||
@import 'messages'
|
||||
@import 'miscellaneous'
|
||||
@import 'responsive'
|
|
@ -0,0 +1,34 @@
|
|||
.main_buttons
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
bottom 0
|
||||
box-sizing border-box
|
||||
padding 30px 30px 0 30px
|
||||
width 350px
|
||||
overflow auto
|
||||
|
||||
.button
|
||||
display block
|
||||
float none
|
||||
text-align center
|
||||
margin 0 auto
|
||||
|
||||
p
|
||||
margin 1em 0 0 0
|
||||
|
||||
.section
|
||||
width 100%
|
||||
box-sizing border-box
|
||||
background rgba(64, 64, 64, .15)
|
||||
margin-bottom 15px
|
||||
padding 8px
|
||||
border-radius 8px
|
||||
|
||||
.select-team
|
||||
.team-pbv
|
||||
margin-top -2px
|
||||
|
||||
.no-alt-label
|
||||
margin-left 5px
|
||||
color #aaa
|
|
@ -0,0 +1,110 @@
|
|||
#messages
|
||||
position absolute
|
||||
bottom 0
|
||||
right 0
|
||||
width 100%
|
||||
font-size .875em
|
||||
|
||||
.popup
|
||||
position fixed
|
||||
bottom 0
|
||||
float right
|
||||
width 270px
|
||||
margin-left 10px
|
||||
box-shadow 0 2px 6px rgba(0, 0, 0, .2)
|
||||
|
||||
&.new_message
|
||||
.title
|
||||
background #d94e47
|
||||
|
||||
.popup_messages
|
||||
background white
|
||||
height 270px
|
||||
border-bottom 1px solid #ccc
|
||||
border-left 1px solid #ccc
|
||||
border-right @border-left
|
||||
overflow-y scroll
|
||||
line-height 1.3em
|
||||
|
||||
&.small
|
||||
height 150px
|
||||
|
||||
p
|
||||
margin 0 6px
|
||||
overflow hidden
|
||||
word-wrap break-word
|
||||
|
||||
.popup_menu
|
||||
background linear-gradient(top, #f0f0f0, #eee)
|
||||
border-left 1px solid #ccc
|
||||
border-right @border-left
|
||||
border-bottom @border-left
|
||||
|
||||
.popup_menu_button
|
||||
border-right 1px solid #ddd
|
||||
float left
|
||||
padding 8px
|
||||
cursor pointer
|
||||
&:hover
|
||||
background #ddd
|
||||
|
||||
.chat_input_pane
|
||||
position relative
|
||||
background #eee
|
||||
height 36px
|
||||
|
||||
.chat_input_wrapper
|
||||
padding 3px
|
||||
border-left 1px solid #ccc
|
||||
border-right @border-left
|
||||
|
||||
.title
|
||||
background #333
|
||||
border-radius 3px 3px 0 0
|
||||
padding 8px 0 8px 12px
|
||||
color white
|
||||
|
||||
&.new
|
||||
background #d94e47
|
||||
|
||||
.title_buttons
|
||||
position absolute
|
||||
top 0
|
||||
right 2px
|
||||
|
||||
.title_button
|
||||
display inline-block
|
||||
cursor pointer
|
||||
padding 5px
|
||||
margin 4px
|
||||
&:hover
|
||||
background #706666
|
||||
|
||||
.icon-minus
|
||||
position relative
|
||||
top 4px
|
||||
|
||||
.challenge
|
||||
background #f0f0f0
|
||||
border-right 1px solid #ccc
|
||||
border-left @border-right
|
||||
border-bottom @border-right
|
||||
transition bottom .25s ease-out
|
||||
|
||||
.challenge_data
|
||||
padding 8px
|
||||
|
||||
.challenge_clauses
|
||||
height 50px
|
||||
overflow-y auto
|
||||
|
||||
p
|
||||
margin 1em 0 0
|
||||
&:first-child
|
||||
margin-top 0
|
||||
|
||||
.challenge_buttons
|
||||
padding 8px
|
||||
border-top 1px solid #ccc
|
||||
.button
|
||||
margin-right 8px
|
|
@ -0,0 +1,93 @@
|
|||
.clearfix:before, .clearfix:after
|
||||
content ""
|
||||
display table
|
||||
line-height 0
|
||||
|
||||
.clearfix:after
|
||||
clear both
|
||||
|
||||
::selection
|
||||
background #F27405
|
||||
|
||||
.select
|
||||
cursor pointer
|
||||
border 1px solid #aaa
|
||||
border-radius 3px
|
||||
background #f0f0f0
|
||||
padding 10px
|
||||
&:hover
|
||||
background #fff
|
||||
border-color #bbb
|
||||
|
||||
&.disabled
|
||||
background #ccc
|
||||
cursor auto
|
||||
|
||||
.alt-input
|
||||
$button-size = 65px
|
||||
$num-buttons = 2
|
||||
$alt-input-height = 38px
|
||||
width 100%
|
||||
padding-right ($button-size * $num-buttons)
|
||||
box-sizing border-box
|
||||
|
||||
.input-wrapper
|
||||
width 100%
|
||||
float left
|
||||
input
|
||||
width 100%
|
||||
box-sizing border-box
|
||||
height $alt-input-height
|
||||
border-radius 4px 0 0 4px
|
||||
margin-bottom 0
|
||||
|
||||
.buttons-wrapper
|
||||
margin-right -($button-size * $num-buttons)
|
||||
float left
|
||||
.button
|
||||
float left
|
||||
width $button-size
|
||||
box-sizing border-box
|
||||
height $alt-input-height
|
||||
padding 0
|
||||
border-radius 0
|
||||
margin-left -1px
|
||||
margin-bottom 0
|
||||
.button:last-child
|
||||
border-radius 0 4px 4px 0
|
||||
|
||||
.challenge_clauses
|
||||
list-style none
|
||||
margin 0
|
||||
input
|
||||
vertical-align top
|
||||
margin-top 3px
|
||||
label
|
||||
display block
|
||||
margin-bottom 0
|
||||
&.disabled
|
||||
cursor not-allowed
|
||||
color #bbb
|
||||
|
||||
.achievement
|
||||
img
|
||||
float left
|
||||
width 75px
|
||||
h2
|
||||
margin-top 0
|
||||
.achievement-info
|
||||
padding-left 90px
|
||||
|
||||
#achievements-modal
|
||||
border none
|
||||
animation: achievement-glow 2s infinite
|
||||
|
||||
$animation-glow-color = #4EABDA
|
||||
|
||||
@keyframes achievement-glow
|
||||
0%
|
||||
box-shadow 1px 1px 30px 2px $animation-glow-color
|
||||
50%
|
||||
box-shadow 1px 1px 45px 2px lighten($animation-glow-color, 20%)
|
||||
100%
|
||||
box-shadow 1px 1px 30px 2px $animation-glow-color
|
|
@ -0,0 +1,113 @@
|
|||
h1
|
||||
narrow-font()
|
||||
font-size 2em
|
||||
font-weight 700
|
||||
line-height 50px
|
||||
margin 0
|
||||
|
||||
#header h1
|
||||
text-transform lowercase
|
||||
margin 0 $body_wrapper
|
||||
|
||||
#navigation
|
||||
$clr = #44484e
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
bottom 0
|
||||
width $nav_size
|
||||
background $clr
|
||||
font-size .875em
|
||||
overflow hidden
|
||||
.logo
|
||||
display block
|
||||
margin 0
|
||||
font-size 32px
|
||||
color rgba(255, 255, 255, .8)
|
||||
text-shadow 0 1px 0 #333
|
||||
text-align center
|
||||
height $header_size
|
||||
line-height $header_size
|
||||
.logo:hover
|
||||
cursor pointer
|
||||
.nav
|
||||
margin-bottom 1em
|
||||
a, .fake_link
|
||||
display block
|
||||
padding 4px 16px
|
||||
line-height 1.5em
|
||||
color #ccd
|
||||
border none
|
||||
background none
|
||||
float none
|
||||
margin 0
|
||||
border-radius 0
|
||||
&:hover
|
||||
background lighten($clr, 10%)
|
||||
text-decoration none
|
||||
color #fff
|
||||
h2
|
||||
text-transform uppercase
|
||||
line-height normal
|
||||
margin 0
|
||||
font-size .929em
|
||||
padding 4px 16px
|
||||
letter-spacing 1px
|
||||
color lighten($clr, 60%)
|
||||
background darken($clr, 20%)
|
||||
text-shadow 0 1px 0 rgba(0, 0, 0, .3)
|
||||
color #777
|
||||
border-bottom 1px solid #555
|
||||
border-top 1px solid #111
|
||||
|
||||
.nav_item
|
||||
position relative
|
||||
&.active, &.active:hover
|
||||
background #d94e47
|
||||
color #fff
|
||||
cursor default
|
||||
.nav_meta
|
||||
position absolute
|
||||
right 0
|
||||
font-size .714em
|
||||
.notifications, .close
|
||||
height 1.3em * (1em / .714em) // 1.3 height adjusted by the nav_meta font size
|
||||
width @height
|
||||
line-height @height
|
||||
text-align center
|
||||
margin-top 0.1em // the container is 1.5em and the contents are 1.3em
|
||||
margin-right 5px
|
||||
float left
|
||||
opacity 1
|
||||
font-size 1em
|
||||
text-shadow none
|
||||
.notifications
|
||||
border-radius 4px
|
||||
background #a30
|
||||
color #fff
|
||||
&:hover .notifications
|
||||
background #b41
|
||||
.close
|
||||
background #333
|
||||
border-radius 1em
|
||||
color #fff
|
||||
cursor pointer
|
||||
&:hover
|
||||
background #555
|
||||
|
||||
.user-info
|
||||
float right
|
||||
margin 0 16px
|
||||
|
||||
.team-dropdown
|
||||
max-height 300px
|
||||
overflow-y auto
|
||||
|
||||
.team-pbv
|
||||
margin-top -5px
|
||||
|
||||
.dropdown
|
||||
li
|
||||
border-bottom 1px solid #aaa
|
||||
li:last-of-type
|
||||
border none !important
|
|
@ -0,0 +1,122 @@
|
|||
@media screen and (max-width: 319px)
|
||||
#logo
|
||||
visibility hidden
|
||||
|
||||
@media screen and (max-width: 480px)
|
||||
.modal
|
||||
top 10px
|
||||
left 10px
|
||||
right 10px
|
||||
.modal-header .close
|
||||
padding 10px
|
||||
margin -10px
|
||||
|
||||
@media screen and (max-width: 640px)
|
||||
.popover
|
||||
display none !important
|
||||
|
||||
#main-nav
|
||||
padding 0
|
||||
|
||||
#logo
|
||||
float none
|
||||
margin 0 auto!important
|
||||
|
||||
#sections
|
||||
visibility hidden
|
||||
|
||||
#leftmenu
|
||||
display block
|
||||
width 0
|
||||
|
||||
#navigation
|
||||
display none
|
||||
top $header_size
|
||||
z-index 100
|
||||
|
||||
#navigation .logo
|
||||
display none
|
||||
|
||||
#navigation.active
|
||||
display block
|
||||
animation slidenav 0.3s
|
||||
|
||||
@keyframes slidenav
|
||||
0%
|
||||
left -100%
|
||||
100%
|
||||
left 0
|
||||
|
||||
#content
|
||||
left 0
|
||||
|
||||
@media screen and (max-width: 767px)
|
||||
.chat_window
|
||||
.main_buttons
|
||||
width 100%
|
||||
padding 10px
|
||||
.chat
|
||||
display none
|
||||
|
||||
// Modals
|
||||
.modal
|
||||
position fixed
|
||||
top 20px
|
||||
left 20px
|
||||
right 20px
|
||||
width auto
|
||||
margin 0
|
||||
&.fade
|
||||
top -100px
|
||||
&.fade.in
|
||||
top 20px
|
||||
|
||||
// Shrink battle window
|
||||
.battle_window
|
||||
.battle
|
||||
width 420px
|
||||
margin 0 auto
|
||||
right 0
|
||||
padding 10px 0 0
|
||||
.battle_summary
|
||||
bottom 50px
|
||||
left 0
|
||||
right 0
|
||||
.battle_container
|
||||
width 400px
|
||||
height 400px
|
||||
margin 0 auto
|
||||
.battle_pane
|
||||
left 0
|
||||
top 50px
|
||||
.battle_teams
|
||||
left 50px
|
||||
right @left
|
||||
top 50px
|
||||
.battle_user_info
|
||||
.left
|
||||
bottom 0
|
||||
.right
|
||||
top 0
|
||||
.left, .right
|
||||
width 100%
|
||||
left 0
|
||||
.battle-timer
|
||||
display inline-block
|
||||
margin-right 10px
|
||||
.pokemon_icons
|
||||
display inline-block
|
||||
vertical-align bottom
|
||||
width auto
|
||||
position relative
|
||||
top -4px
|
||||
.icon_wrapper:nth-child(even)
|
||||
top 0
|
||||
.moves.span8, .switches.span4
|
||||
width 100%
|
||||
margin-left 0
|
||||
|
||||
@media screen and (max-width: 980px)
|
||||
.battle_window
|
||||
.chat
|
||||
display none
|
|
@ -0,0 +1,457 @@
|
|||
$header = 50px
|
||||
$pokemon-list-height = 50px
|
||||
|
||||
// TODO: Make this a global style
|
||||
.teambuilder input
|
||||
.teambuilder select
|
||||
box-sizing border-box
|
||||
height 28px
|
||||
|
||||
.teambuilder .selectize-input input
|
||||
height 18px
|
||||
|
||||
.teambuilder
|
||||
position absolute
|
||||
top 0
|
||||
bottom 0
|
||||
left 0
|
||||
right 0
|
||||
|
||||
select, input
|
||||
margin-bottom 0!important
|
||||
width 100%
|
||||
|
||||
.teambuilder .meta-info
|
||||
max-width 1368px
|
||||
|
||||
.teambuilder .meta-info .left-side, .teambuilder .meta-info .right-side
|
||||
float left
|
||||
box-sizing border-box
|
||||
|
||||
.teambuilder .navigation
|
||||
position absolute
|
||||
top $header
|
||||
bottom 0
|
||||
background-color $body-color
|
||||
width: 150px
|
||||
|
||||
ul
|
||||
list-style-type none
|
||||
padding 0
|
||||
margin 0
|
||||
|
||||
li, .nav-button
|
||||
display block
|
||||
box-sizing border-box
|
||||
padding-left 1em
|
||||
height $pokemon-list-height
|
||||
line-height $pokemon-list-height
|
||||
font-size 14px
|
||||
|
||||
li:hover, .nav-button:hover
|
||||
cursor pointer
|
||||
background-color rgba(0, 0, 0, 0.1)
|
||||
|
||||
li.active
|
||||
background-color rgba(200, 210, 255, 0.5)
|
||||
|
||||
.pokemon_icon
|
||||
float none
|
||||
display inline-block
|
||||
margin 0
|
||||
vertical-align middle
|
||||
|
||||
.pokemon-pbv
|
||||
margin-top 7px
|
||||
font-size 0.75em
|
||||
|
||||
.pokemon-middle
|
||||
line-height 50%
|
||||
display inline-block
|
||||
vertical-align middle
|
||||
|
||||
.teambuilder .meta-info .left-side
|
||||
padding-left 180px
|
||||
width 45%
|
||||
|
||||
.teambuilder .meta-info .right-side
|
||||
width 55%
|
||||
padding-left 20px
|
||||
|
||||
.teambuilder .species
|
||||
float left
|
||||
width 180px
|
||||
margin-left -185px
|
||||
|
||||
.species_list
|
||||
width 100%
|
||||
|
||||
.pbv
|
||||
float right
|
||||
color #888
|
||||
|
||||
.teambuilder .shiny-switch
|
||||
display inline-block
|
||||
width 14px
|
||||
height 14px
|
||||
margin-right 10px
|
||||
margin-left -(@margin-right + @width)
|
||||
vertical-align middle
|
||||
cursor pointer
|
||||
background-image URL('http://91.121.152.74:8000/Sprites/images/noshiny.png')
|
||||
background-size cover
|
||||
|
||||
&.selected
|
||||
background-image URL('http://91.121.152.74:8000/Sprites/images/shiny.png')
|
||||
|
||||
|
||||
.teambuilder .happiness-switch
|
||||
display inline-block
|
||||
position relative
|
||||
width 14px
|
||||
height 14px
|
||||
margin-left 10px
|
||||
margin-right -(@margin-left + @width)
|
||||
cursor pointer
|
||||
vertical-align middle
|
||||
|
||||
$happy = #FF3C3C
|
||||
$unhappy = #333
|
||||
|
||||
&:before, &:after
|
||||
position absolute
|
||||
content ""
|
||||
left 8px
|
||||
top 0
|
||||
width 8px
|
||||
height 13px
|
||||
background $happy
|
||||
border-radius 7px 7px 0 0
|
||||
transform rotate(-45deg)
|
||||
transform-origin 0 100%
|
||||
&:after
|
||||
left 0
|
||||
transform rotate(45deg)
|
||||
transform-origin 100% 100%
|
||||
|
||||
&:hover:before, &:hover:after
|
||||
background darken($happy, 30%)
|
||||
|
||||
&.selected:before, &.selected:after
|
||||
background $unhappy
|
||||
|
||||
&.selected:hover:before, &.selected:hover:after
|
||||
background lighten($unhappy, 30%)
|
||||
|
||||
.teambuilder .team_meta
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
right 0
|
||||
height $header
|
||||
line-height @height
|
||||
.team_name
|
||||
font-weight 700
|
||||
font-size 1.5em
|
||||
height $header
|
||||
line-height @height
|
||||
float left
|
||||
margin 0
|
||||
width auto
|
||||
padding 0 (1em/@font-size)
|
||||
box-sizing border-box
|
||||
|
||||
.team_name:hover:not(:focus)
|
||||
text-decoration underline
|
||||
cursor pointer
|
||||
|
||||
.team_meta_buttons
|
||||
float right
|
||||
margin-top 7px
|
||||
.button
|
||||
float left
|
||||
margin-right 10px
|
||||
|
||||
.teambuilder .meta-info .non-stats
|
||||
display table
|
||||
width 100%
|
||||
|
||||
.teambuilder_row, .pbv-row
|
||||
display table-row
|
||||
line-height 1.5em
|
||||
|
||||
.teambuilder_col
|
||||
display table-cell
|
||||
box-sizing border-box
|
||||
height 2em
|
||||
margin 0
|
||||
&:first-child
|
||||
text-align right
|
||||
padding-right 10px
|
||||
|
||||
.pbv-row .left, .pbv-row .right
|
||||
box-sizing border-box
|
||||
height 2em
|
||||
|
||||
.non-stat-label
|
||||
width 1px // minimum possible size
|
||||
|
||||
.teambuilder .stats
|
||||
width 100%
|
||||
margin-bottom 1.5em
|
||||
|
||||
td, th
|
||||
text-align left
|
||||
padding-right 10px
|
||||
white-space nowrap
|
||||
line-height 1.5em
|
||||
height 1.5em
|
||||
|
||||
.stat-label, .stat-total, .base-stat
|
||||
width 1px // minimum possible size
|
||||
|
||||
td:first-child
|
||||
text-align right
|
||||
|
||||
input
|
||||
width 100%
|
||||
text-align right
|
||||
|
||||
th.ev-cell, th.iv-cell
|
||||
text-align right
|
||||
padding-right 17px // match with value in textfield (10px from the main cell + the input padding)
|
||||
|
||||
td.ev-cell, td.iv-cell
|
||||
width 50px
|
||||
|
||||
.plus-nature
|
||||
color $positive-color
|
||||
font-weight bold
|
||||
|
||||
.minus-nature
|
||||
color $negative-color
|
||||
|
||||
.remaining-evs
|
||||
float left
|
||||
|
||||
.hidden-power
|
||||
float right
|
||||
|
||||
.ev-lock
|
||||
float left
|
||||
margin-right 5px
|
||||
cursor pointer
|
||||
.icon-lock
|
||||
color #444
|
||||
.icon-unlocked
|
||||
color #888
|
||||
|
||||
.remaining-evs-amount.over-limit
|
||||
color $negative-color
|
||||
font-weight bold
|
||||
|
||||
.teambuilder select.select-hidden-power
|
||||
width auto
|
||||
|
||||
.teambuilder .pokemon_edit
|
||||
position absolute
|
||||
overflow auto
|
||||
left 150px
|
||||
top $header
|
||||
bottom 0
|
||||
right 0
|
||||
background white
|
||||
padding 15px
|
||||
overflow-y scroll
|
||||
|
||||
.teambuilder .forme-sprite-box
|
||||
height 120px
|
||||
line-height @height
|
||||
text-align center
|
||||
.forme-sprite
|
||||
display inline-block
|
||||
|
||||
.teambuilder .species-types
|
||||
text-align center
|
||||
margin-bottom 5px
|
||||
margin-top 50px
|
||||
img
|
||||
margin 0 2px
|
||||
|
||||
.teambuilder .selected_moves
|
||||
.header
|
||||
margin-bottom 8px
|
||||
.moves-label
|
||||
font-weight bold
|
||||
font-size 2em
|
||||
line-height 1em
|
||||
a
|
||||
line-height 2em
|
||||
|
||||
.move-button, input
|
||||
display block
|
||||
margin 0
|
||||
box-sizing border-box
|
||||
height 32px
|
||||
|
||||
.close
|
||||
opacity .4
|
||||
line-height 30px
|
||||
width 20px
|
||||
text-align center
|
||||
float right
|
||||
.close:hover, .close:focus
|
||||
opacity .8
|
||||
font-weight bold
|
||||
|
||||
.table-moves
|
||||
border-collapse separate
|
||||
border solid 1px #DDD
|
||||
thead th
|
||||
background whitesmoke
|
||||
border-bottom solid 1px #DDD
|
||||
tbody tr
|
||||
cursor pointer
|
||||
td
|
||||
border 1px solid transparent
|
||||
&.active td
|
||||
background #f0f0f9
|
||||
&:first-child
|
||||
text-decoration underline
|
||||
&.selected td
|
||||
background #f0f0f9
|
||||
&:first-child
|
||||
font-weight bold
|
||||
img
|
||||
margin-bottom 3px
|
||||
.name
|
||||
white-space nowrap
|
||||
|
||||
.teambuilder .table-moves
|
||||
margin-top 8px
|
||||
|
||||
.teambuilder .description
|
||||
width 60%
|
||||
|
||||
.teambuilder .display_teams
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
bottom 0
|
||||
right 0
|
||||
overflow auto
|
||||
|
||||
h2
|
||||
margin-left 10px
|
||||
margin-bottom 0
|
||||
|
||||
.select-team
|
||||
h2
|
||||
text-overflow ellipsis
|
||||
white-space nowrap
|
||||
overflow hidden
|
||||
width 192px
|
||||
margin 0
|
||||
font-size 1.5em
|
||||
|
||||
.add-new-team
|
||||
margin 10px
|
||||
|
||||
.team-meta
|
||||
font-size .75em
|
||||
text-align right
|
||||
margin-top 10px
|
||||
|
||||
.team-pbv
|
||||
margin-top -5px
|
||||
|
||||
textarea.textarea_modal
|
||||
box-sizing border-box
|
||||
width 100%
|
||||
height 300px
|
||||
|
||||
@media screen and (max-width: 1268px)
|
||||
// stack non-stats and stats
|
||||
.teambuilder .meta-info .left-side, .teambuilder .meta-info .right-side
|
||||
display block
|
||||
width 100%
|
||||
|
||||
// stack non-stats and stats
|
||||
.teambuilder .non-stats
|
||||
margin 0!important
|
||||
|
||||
// stack non-stats and stats
|
||||
.teambuilder .meta-info .right-side
|
||||
padding-left 0
|
||||
|
||||
// add some separation between stats and non-stats
|
||||
.teambuilder .stats
|
||||
margin-top 1.5em
|
||||
|
||||
@media screen and (max-width: 1000px)
|
||||
// Hide move descriptions
|
||||
.table-moves .description
|
||||
display none
|
||||
|
||||
// Make selected moves take up the entire row
|
||||
.teambuilder .selected_moves .span3
|
||||
width 100%
|
||||
margin-left 0
|
||||
|
||||
@media screen and (max-width: 800px)
|
||||
// Move side navigation to above
|
||||
.teambuilder .navigation li, teambuilder .navigation .nav-button
|
||||
float left
|
||||
width (100/6)%
|
||||
font-size 0 // Hide the text
|
||||
text-align center
|
||||
|
||||
// Move side navigation to above
|
||||
.teambuilder .pokemon_edit
|
||||
left 0
|
||||
top $header + $pokemon-list-height
|
||||
|
||||
// Move side navigation to above
|
||||
.teambuilder .navigation
|
||||
width 100%
|
||||
|
||||
@media screen and (max-width: 600px)
|
||||
// Scroll at the teambuilder level instead of the pokemon_edit level
|
||||
// TODO: This should kick in on tablet landscape modes,
|
||||
// with large widths but small heights (ex: nexus 7)
|
||||
.teambuilder
|
||||
overflow auto
|
||||
|
||||
.team_meta
|
||||
position static
|
||||
height auto
|
||||
|
||||
.navigation
|
||||
position static
|
||||
|
||||
.pokemon_edit
|
||||
position static
|
||||
overflow visible
|
||||
|
||||
textarea.textarea_modal
|
||||
height auto
|
||||
|
||||
@media screen and (max-width: 480px)
|
||||
// split species and non-stats
|
||||
.teambuilder .meta-info .left-side
|
||||
padding-left 0
|
||||
|
||||
// split species and non-stats
|
||||
.teambuilder .species, .teambuilder .non-stats
|
||||
margin-left 0
|
||||
width 100%
|
||||
margin-bottom 1.5em
|
||||
|
||||
// hide ev-bars
|
||||
.teambuilder .ev-range-cell
|
||||
display none
|
||||
|
||||
// Hide pp and acc cells
|
||||
.table-moves
|
||||
.pp, .acc
|
||||
display none
|
|
@ -0,0 +1,46 @@
|
|||
return if PokeBattle.autoConnect == false
|
||||
|
||||
PokeBattle.primus = Primus.connect()
|
||||
|
||||
PokeBattle.primus.on 'listChatroom', (id, users) ->
|
||||
if room = PokeBattle.rooms.get(id: id)
|
||||
room.get('users').reset(users)
|
||||
else
|
||||
room = PokeBattle.rooms.add(id: id, users: users)
|
||||
new ChatView(model: room, el: $('#chat-section .chat')).render()
|
||||
|
||||
PokeBattle.primus.on 'userMessage', (id, username, data) ->
|
||||
room = PokeBattle.rooms.get(id)
|
||||
room.userMessage(username, data)
|
||||
|
||||
PokeBattle.primus.on 'rawMessage', (id, message) ->
|
||||
room = PokeBattle.rooms.get(id)
|
||||
room.rawMessage(message)
|
||||
|
||||
PokeBattle.primus.on 'announce', (id, klass, message) ->
|
||||
room = PokeBattle.rooms.get(id)
|
||||
room.announce(klass, message)
|
||||
|
||||
PokeBattle.primus.on 'joinChatroom', (id, user) ->
|
||||
room = PokeBattle.rooms.get(id)
|
||||
room.get('users').add(user)
|
||||
|
||||
PokeBattle.primus.on 'leaveChatroom', (id, user) ->
|
||||
room = PokeBattle.rooms.get(id)
|
||||
room.get('users').remove(user)
|
||||
|
||||
PokeBattle.primus.on 'topic', (topic) ->
|
||||
# TODO: Hardcoded
|
||||
room = PokeBattle.rooms.get("Lobby")
|
||||
room.setTopic(topic)
|
||||
|
||||
PokeBattle.userList = new UserList()
|
||||
PokeBattle.battles = new BattleCollection([])
|
||||
PokeBattle.messages = new PrivateMessages([])
|
||||
PokeBattle.rooms = new Rooms([])
|
||||
|
||||
$ ->
|
||||
PokeBattle.navigation = new SidebarView(el: $('#navigation'))
|
||||
PokeBattle.teambuilder = new TeambuilderView(el: $("#teambuilder-section"))
|
||||
PokeBattle.battleList = new BattleListView(el: $("#battle-list-section"))
|
||||
new PrivateMessagesView(el: $("#messages"), collection: PokeBattle.messages)
|
|
@ -0,0 +1,8 @@
|
|||
class @BattleCollection extends Backbone.Collection
|
||||
model: Battle
|
||||
|
||||
isPlaying: ->
|
||||
@find((battle) -> battle.isPlaying())?
|
||||
|
||||
playingBattles: ->
|
||||
@filter((battle) -> battle.isPlaying())
|
|
@ -0,0 +1,2 @@
|
|||
class @PrivateMessages extends Backbone.Collection
|
||||
model: PrivateMessage
|
|
@ -0,0 +1,9 @@
|
|||
class @Rooms extends Backbone.Collection
|
||||
model: Room
|
||||
|
||||
# Delegate room events to every single room in this collection.
|
||||
for eventName in Room::EVENTS
|
||||
do (eventName) =>
|
||||
this::[eventName] = (args...) ->
|
||||
@each (room) ->
|
||||
room[eventName].apply(room, args)
|
|
@ -0,0 +1,15 @@
|
|||
class @UserList extends Backbone.Collection
|
||||
model: User
|
||||
|
||||
comparator: (a, b) =>
|
||||
aAuthority = a.get('authority')
|
||||
bAuthority = b.get('authority')
|
||||
aName = "#{a.id}".toLowerCase()
|
||||
bName = "#{b.id}".toLowerCase()
|
||||
if aAuthority < bAuthority then 1
|
||||
else if aAuthority > bAuthority then -1
|
||||
else if aName < bName then -1
|
||||
else if aName > bName then 1
|
||||
else 0
|
||||
|
||||
initialize: =>
|
|
@ -0,0 +1,2 @@
|
|||
class @Replays extends Backbone.Collection
|
||||
model: Replay
|
|
@ -0,0 +1,5 @@
|
|||
$currentModal = null
|
||||
|
||||
PokeBattle.primus.on 'achievementsEarned', (achievements) ->
|
||||
# TODO: Add achievements to the current modal if one is already open
|
||||
$currentModal = PokeBattle.modal('modals/achievements', window: window, achievements: achievements)
|
|
@ -0,0 +1,10 @@
|
|||
PokeBattle.primus.on 'altList', (list) ->
|
||||
PokeBattle.alts.list = list
|
||||
|
||||
PokeBattle.primus.on 'altCreated', (altName) ->
|
||||
PokeBattle.alts.list.push(altName)
|
||||
|
||||
PokeBattle.alts =
|
||||
list: []
|
||||
createAlt: (altName) ->
|
||||
PokeBattle.primus.send('createAlt', altName)
|
|
@ -0,0 +1,52 @@
|
|||
# Send primus event when leaving battles.
|
||||
PokeBattle.battles.on 'remove', (battle) ->
|
||||
PokeBattle.primus.send('leaveChatroom', battle.id)
|
||||
|
||||
# Event listeners
|
||||
PokeBattle.primus.on 'updateBattle', (id, queue) ->
|
||||
battle = PokeBattle.battles.get(id)
|
||||
if !battle
|
||||
console.log "Received events for #{id}, but no longer in battle!"
|
||||
return
|
||||
battle.update(queue)
|
||||
|
||||
# Create a BattleView when spectating a battle
|
||||
PokeBattle.primus.on 'spectateBattle', (id, format, numActive, index, playerIds, log) ->
|
||||
if PokeBattle.battles.get(id)
|
||||
console.log "Already spectating battle #{id}!"
|
||||
return
|
||||
battle = new Battle({id, format, numActive, index, playerIds})
|
||||
|
||||
# Create BattleView
|
||||
$battle = $(JST['battle_window']({battle, window}))
|
||||
$('#main-section').append($battle)
|
||||
battle.view = new BattleView(el: $battle, model: battle)
|
||||
battle.view.skip = 0
|
||||
battle.view.$('.battle_pane').hide()
|
||||
|
||||
# Add to collection
|
||||
PokeBattle.battles.add(battle)
|
||||
|
||||
# Update log
|
||||
battle.update(log)
|
||||
|
||||
PokeBattle.primus.on 'updateTimers', (id, timers) ->
|
||||
battle = PokeBattle.battles.get(id)
|
||||
if !battle
|
||||
console.log "Received events for #{id}, but no longer in battle!"
|
||||
return
|
||||
battle.view.updateTimers(timers)
|
||||
|
||||
PokeBattle.primus.on 'resumeTimer', (id, player) ->
|
||||
battle = PokeBattle.battles.get(id)
|
||||
if !battle
|
||||
console.log "Received events for #{id}, but no longer in battle!"
|
||||
return
|
||||
battle.view.resumeTimer(player)
|
||||
|
||||
PokeBattle.primus.on 'pauseTimer', (id, player, timeSinceLastAction) ->
|
||||
battle = PokeBattle.battles.get(id)
|
||||
if !battle
|
||||
console.log "Received events for #{id}, but no longer in battle!"
|
||||
return
|
||||
battle.view.pauseTimer(player, timeSinceLastAction)
|
|
@ -0,0 +1,2 @@
|
|||
PokeBattle.primus.on 'battleList', (battles) ->
|
||||
PokeBattle.battleList.refreshListComplete(battles)
|
|
@ -0,0 +1,290 @@
|
|||
PokeBattle.commands ?= {}
|
||||
|
||||
Commands = {}
|
||||
|
||||
desc = (description) ->
|
||||
desc.lastDescription = description
|
||||
|
||||
makeCommand = (commandNames..., func) ->
|
||||
for commandName in commandNames
|
||||
Commands[commandName] = func
|
||||
|
||||
# Generate description
|
||||
description = ""
|
||||
if commandNames.length > 1
|
||||
aliases = commandNames[1...].map((n) -> "/#{n}").join(', ')
|
||||
description += " <i>Also #{aliases}. </i>"
|
||||
description += desc.lastDescription
|
||||
# TODO: Hardcoded user level
|
||||
HelpDescriptions['1'][commandNames[0]] = description
|
||||
delete desc.lastDescription
|
||||
|
||||
parseCommand = (line) ->
|
||||
[ commandName, args... ] = line.split(/\s+/)
|
||||
if commandName[0] == '/'
|
||||
# It's a command. Remove leading slash
|
||||
commandName = commandName[1...]
|
||||
args = args.join(' ').split(/,/g)
|
||||
return [commandName, args]
|
||||
return null
|
||||
|
||||
PokeBattle.commands.execute = (room, line) ->
|
||||
result = parseCommand(line)
|
||||
return false if !result
|
||||
[commandName, args] = result
|
||||
command = Commands[commandName]
|
||||
if !command
|
||||
# Fall-through to server.
|
||||
return false
|
||||
command(room, args...)
|
||||
return true
|
||||
|
||||
desc 'Displays a list of all commands.'
|
||||
makeCommand "commands", "help", "h", (room) ->
|
||||
user = room.get('users').get(PokeBattle.username)
|
||||
|
||||
for level, descriptions of HelpDescriptions
|
||||
level = Number(level)
|
||||
continue if user.get('authority') < level
|
||||
|
||||
message = []
|
||||
# TODO: Hardcoded levels
|
||||
authLevels = {1: "USER", 2: "DRIVER", 3: "MOD", 4: "ADMIN", 5: "OWNER"}
|
||||
humanLevel = authLevels[level]
|
||||
message.push("<b>#{humanLevel} COMMANDS:</b>")
|
||||
for name, description of descriptions
|
||||
message.push("<b>/#{name}:</b> #{description}")
|
||||
message = message.join("<br>")
|
||||
room.announce('success', message)
|
||||
true
|
||||
|
||||
desc 'Opens the challenge for a specific user. Usage: /challenge username'
|
||||
makeCommand "challenge", "chall", "c", (room, username) ->
|
||||
if !username
|
||||
PokeBattle.events.trigger("errorMessage", "Usage: /challenge username")
|
||||
return
|
||||
message = PokeBattle.messages.add(id: username)
|
||||
message.openChallenge(username)
|
||||
|
||||
desc 'Private messages a certain user. Usage: /message username, message'
|
||||
makeCommand "message", "msg", "pm", "whisper", "w", (room, username, messages...) ->
|
||||
username = username?.trim()
|
||||
if !username
|
||||
PokeBattle.events.trigger("errorMessage", "Usage: /message username, msg")
|
||||
return
|
||||
message = PokeBattle.messages.add(id: username)
|
||||
|
||||
if messages.length > 0
|
||||
text = messages.join(',')
|
||||
PokeBattle.primus.send('privateMessage', message.id, text)
|
||||
else
|
||||
# The PM is opened without a message.
|
||||
message.trigger('open', message)
|
||||
|
||||
desc 'Clears the chat.'
|
||||
makeCommand "clear", (room) ->
|
||||
room.clear()
|
||||
|
||||
desc 'Displays a Pokemon\'s PokeBattle value, or displays all Pokemon at or under a particular PBV. Usage: /pbv pkmn1, pkmn2, OR /pbv number'
|
||||
makeCommand "pbv", (room, pokemon...) ->
|
||||
pbv = Number(pokemon[0])
|
||||
if !isNaN(pbv)
|
||||
messages = findPokemonAtPBV(pbv)
|
||||
else
|
||||
messages = findTotalPBV(pokemon)
|
||||
|
||||
if messages.length == 0
|
||||
room.announce('error', "<b>PBV error:</b> Enter valid Pokemon or PBV.")
|
||||
else
|
||||
room.announce('success', "<b>PBV:</b> #{messages.join('; ')}")
|
||||
|
||||
findPokemonAtPBV = (pbv) ->
|
||||
messages = []
|
||||
counter = 0
|
||||
for speciesName, formes of window.Generations.XY.FormeData
|
||||
for formeName, formeData of formes
|
||||
if formeData.pokeBattleValue <= pbv
|
||||
counter += 1
|
||||
dexEntry = "pokemon/#{slugify(speciesName)}/#{slugify(formeName)}"
|
||||
icon = pokemonIcon(speciesName, formeName)
|
||||
formattedName = formatName(speciesName, formeName)
|
||||
messages.push("#{linkToDex(dexEntry, icon + formattedName)}:
|
||||
#{formeData.pokeBattleValue}")
|
||||
if messages.length > 10
|
||||
messages = _.sample(messages, 10)
|
||||
messages.push(linkToDex("pokemon/?pbv=<#{pbv + 1}",
|
||||
"See more Pokemon »"))
|
||||
if messages.length > 0
|
||||
plural = if messages.length == 1 then "is" else "are"
|
||||
messages.unshift("There #{plural} #{counter} Pokemon with a PBV of
|
||||
#{pbv} or less")
|
||||
messages
|
||||
|
||||
findTotalPBV = (pokemon) ->
|
||||
pokemon = _(pokemon).map(findPokemon)
|
||||
messages = []
|
||||
total = 0
|
||||
for array in pokemon
|
||||
continue unless array
|
||||
[speciesName, formeName] = array
|
||||
pbv = PokeBattle.PBV.determinePBV(window.Generations.XY,
|
||||
species: speciesName, forme: formeName)
|
||||
total += pbv
|
||||
dexEntry = "pokemon/#{slugify(speciesName)}/#{slugify(formeName)}"
|
||||
icon = pokemonIcon(speciesName, formeName)
|
||||
formattedName = formatName(speciesName, formeName)
|
||||
messages.push("#{linkToDex(dexEntry, icon + formattedName)}: #{pbv}")
|
||||
messages.push("Total: #{total}") if messages.length > 1
|
||||
messages
|
||||
|
||||
desc 'Looks up information about a Pokemon, move, item, or ability.'
|
||||
makeCommand "data", "dex", (room, query) ->
|
||||
if (pokemon = findPokemon(query))
|
||||
message = dataPokemon(pokemon)
|
||||
else if (item = findItem(query))
|
||||
message = dataItem(item)
|
||||
else if (move = findMove(query))
|
||||
message = dataMove(move)
|
||||
else if (ability = findAbility(query))
|
||||
message = dataAbility(ability)
|
||||
else
|
||||
room.announce("error", "<b>Data error:</b> Enter a valid Pokemon, item,
|
||||
move, or ability.</div>")
|
||||
return
|
||||
room.announce('success', message)
|
||||
|
||||
dataPokemon = (pokemon) ->
|
||||
[speciesName, formeName] = pokemon
|
||||
[speciesSlug, formeSlug] = [slugify(speciesName), slugify(formeName)]
|
||||
forme = window.Generations.XY.FormeData[speciesName][formeName]
|
||||
{types, abilities, hiddenAbility, stats, pokeBattleValue} = forme
|
||||
|
||||
# Format abilities
|
||||
abilities = _.clone(abilities)
|
||||
abilities.push(hiddenAbility) if hiddenAbility?
|
||||
abilities = _(abilities).map((a) -> linkToDex("abilities/#{slugify(a)}", a))
|
||||
abilities = abilities.join('/')
|
||||
abilities += " (H)" if hiddenAbility?
|
||||
|
||||
# Format types, stats, and icon
|
||||
types = _(types).map (t) ->
|
||||
linkToDex("types/#{slugify(t)}",
|
||||
"<img src='#{window.TypeSprite(t)}' alt='#{t}'/>")
|
||||
statNames = [ 'HP', 'Attack', 'Defense', 'Sp.Attack', 'Sp.Defense', 'Speed']
|
||||
stats = [ stats.hp, stats.attack, stats.defense,
|
||||
stats.specialAttack, stats.specialDefense, stats.speed ]
|
||||
statsText = _.map(_.zip(statNames, stats), (a) -> a.join(': ')).join(' / ')
|
||||
|
||||
# Build data
|
||||
message = """#{pokemonIcon(speciesName, formeName, "left")}
|
||||
<p class="ml3">
|
||||
<b>#{formatName(speciesName, formeName)}:</b> #{types.join('')} |
|
||||
#{abilities}<br />#{statsText} |
|
||||
#{_(stats).reduce((a, b) -> a + b)} <abbr title="Base Stat Total">BST</abbr>
|
||||
| <abbr title="PokeBattle Value">PBV</abbr>: #{pokeBattleValue}
|
||||
#{linkToDex("pokemon/#{speciesSlug}/#{formeSlug}", "See dex entry »")}
|
||||
</p>
|
||||
"""
|
||||
message
|
||||
|
||||
dataItem = (itemName) ->
|
||||
item = window.Generations.XY.ItemData[itemName]
|
||||
message = "<b>#{itemName}:</b> #{item.description}"
|
||||
message += " Natural Gift is #{item.naturalGift.type} type
|
||||
and has #{item.naturalGift.power} base power." if item.naturalGift
|
||||
message += " Fling has #{item.flingPower} base power." if item.flingPower
|
||||
message += " Currently unreleased in Gen 6." if item.unreleased
|
||||
message
|
||||
|
||||
dataMove = (moveName) ->
|
||||
move = window.Generations.XY.MoveData[moveName]
|
||||
type = linkToDex("types/#{slugify(move.type)}",
|
||||
"<img src='#{window.TypeSprite(move.type)}' alt='#{move.type}'/>")
|
||||
category = """<img src="#{CategorySprite(move.damage)}"
|
||||
alt="#{move.damage}"/>"""
|
||||
target = """<img src="#{TargetSprite(move)}"
|
||||
alt="#{move.target}"/>"""
|
||||
power = move.power || "—"
|
||||
acc = move.accuracy || "—"
|
||||
maxpp = Math.floor(move.pp * 8/5)
|
||||
if move.priority > 0
|
||||
priority = "+#{move.priority}"
|
||||
else if move.priority < 0
|
||||
priority = move.priority
|
||||
message = """<b>#{moveName}:</b> #{type} #{category} #{target} """
|
||||
message += "<b>Power:</b> #{power} <b>Acc:</b> #{acc} <b>PP:</b> #{move.pp} (max #{maxpp})"
|
||||
message += "<br />"
|
||||
message += "Priority #{priority}. " if priority
|
||||
message += move.description
|
||||
message += " "
|
||||
message += linkToDex("moves/#{slugify(moveName)}",
|
||||
"See who learns this move »")
|
||||
message
|
||||
|
||||
dataAbility = (abilityName) ->
|
||||
ability = window.Generations.XY.AbilityData[abilityName]
|
||||
message = """<b>#{abilityName}:</b> #{ability.description}
|
||||
#{linkToDex("abilities/#{slugify(abilityName)}",
|
||||
"See who obtains this ability »")}"""
|
||||
message
|
||||
|
||||
# Finds the most lenient match possible.
|
||||
findPokemon = (pokemonName) ->
|
||||
pokemonName = normalize(pokemonName)
|
||||
for speciesName, speciesData of window.Generations.XY.FormeData
|
||||
for formeName of speciesData
|
||||
name = speciesName
|
||||
name += formeName unless formeName == 'default'
|
||||
name = normalize(name)
|
||||
name += name
|
||||
return [speciesName, formeName] if name.indexOf(pokemonName) != -1
|
||||
# Return blank match
|
||||
null
|
||||
|
||||
# Finds the most lenient match possible.
|
||||
findItem = (itemName) ->
|
||||
normalized = normalize(itemName)
|
||||
for name of window.Generations.XY.ItemData
|
||||
return name if normalized == normalize(name)
|
||||
# Return blank match
|
||||
null
|
||||
|
||||
# Finds the most lenient match possible.
|
||||
findMove = (moveName) ->
|
||||
normalized = normalize(moveName)
|
||||
for name of window.Generations.XY.MoveData
|
||||
return name if normalized == normalize(name)
|
||||
# Return blank match
|
||||
null
|
||||
|
||||
# Finds the most lenient match possible.
|
||||
findAbility = (abilityName) ->
|
||||
normalized = normalize(abilityName)
|
||||
for name of window.Generations.XY.AbilityData
|
||||
return name if normalized == normalize(name)
|
||||
# Return blank match
|
||||
null
|
||||
|
||||
slugify = (str) ->
|
||||
str.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/\-{2,}/g, '-')
|
||||
|
||||
normalize = (str) ->
|
||||
str.trim().toLowerCase().replace(/[^a-zA-Z0-9]+/g, '')
|
||||
|
||||
formatName = (speciesName, formeName) ->
|
||||
if formeName == 'default'
|
||||
pokemonName = speciesName
|
||||
else
|
||||
pokemonName = speciesName
|
||||
pokemonName += ' '
|
||||
pokemonName += formeName.split('-')
|
||||
.map((n) -> n[0].toUpperCase() + n[1...])
|
||||
.join('-')
|
||||
return pokemonName
|
||||
|
||||
linkToDex = (slug, text) ->
|
||||
"<a href='//pokebattle.com/dex/#{slug}' target='_blank'>#{text}</a>"
|
||||
|
||||
pokemonIcon = (speciesName, formeName, classes="") ->
|
||||
style = window.PokemonIconBackground(speciesName, formeName)
|
||||
"""<span class="pokemon_icon #{classes}" style="#{style}"></span>"""
|
|
@ -0,0 +1,39 @@
|
|||
$body = $("body")
|
||||
$popup = $('#popup')
|
||||
|
||||
PokeBattle.primus.on 'open', ->
|
||||
$popup.hide()
|
||||
PokeBattle.rooms.rawMessage("Connected to the server!", class: "yellow italic")
|
||||
|
||||
PokeBattle.primus.on 'reconnecting', ->
|
||||
PokeBattle.rooms.rawMessage("Lost connection to the server...", class: "red italic")
|
||||
|
||||
PokeBattle.primus.on 'end', ->
|
||||
PokeBattle.rooms.rawMessage("Connection terminated!", class: "red italic")
|
||||
|
||||
PokeBattle.primus.on 'reconnecting', (opts) ->
|
||||
seconds = Math.floor(opts.timeout / 1000)
|
||||
if $popup.length == 0
|
||||
$popup = $('<div id="popup" class="reconnect popup-absolute"/>').hide()
|
||||
$div = $('<span/>')
|
||||
.addClass('alert')
|
||||
.html("<strong>You lost your connection to PokeBattle!</strong>
|
||||
<span class='reconnect-text'></span>")
|
||||
.appendTo($popup)
|
||||
$popup.appendTo($body)
|
||||
$popup.find('.reconnect-text').html("""Reconnecting in
|
||||
<span class='reconnect-timer'>#{seconds}</span> seconds...""")
|
||||
$popup.fadeIn(250)
|
||||
$body.data('seconds', seconds)
|
||||
reconnectTimer($popup)
|
||||
|
||||
reconnectTimer = ($popup) ->
|
||||
seconds = $body.data('seconds') ? 1
|
||||
if seconds == 0
|
||||
$popup.find('.reconnect-text').text("Reconnecting...")
|
||||
else
|
||||
time = "#{Math.floor(seconds / 60)}:"
|
||||
time += "00#{seconds % 60}".substr(-2)
|
||||
$popup.find('.reconnect-timer').text(time)
|
||||
$body.data('seconds', seconds - 1)
|
||||
setTimeout(reconnectTimer.bind(this, $popup), 1000)
|
|
@ -0,0 +1,57 @@
|
|||
PokeBattle.primus.on 'errorMessage', (args...) ->
|
||||
PokeBattle.events.trigger('errorMessage', args...)
|
||||
|
||||
PokeBattle.events.on "errorMessage", (type, args...) ->
|
||||
e = PokeBattle.errors
|
||||
switch type
|
||||
when e.INVALID_SESSION
|
||||
$('#errors-modal').remove() if $('#errors-modal').length > 0
|
||||
options =
|
||||
title: "Your login timed out!"
|
||||
body: """To access the simulator, you need to
|
||||
<a href="http://91.121.152.74/">login again</a>."""
|
||||
$modal = PokeBattle.modal('modals/errors', options)
|
||||
$modal.find('.modal-footer button').first().focus()
|
||||
PokeBattle.primus.end()
|
||||
|
||||
when e.BANNED
|
||||
$('#errors-modal').remove() if $('#errors-modal').length > 0
|
||||
[reason, length] = args
|
||||
if length < 0
|
||||
length = "is permanent"
|
||||
else
|
||||
length = "lasts for #{Math.round(length / 60)} minute(s)"
|
||||
body = "This ban #{length}."
|
||||
if reason
|
||||
body += "You were banned for the following reason: #{reason}"
|
||||
options =
|
||||
title: "You have been banned!"
|
||||
body: body
|
||||
$modal = PokeBattle.modal('modals/errors', options)
|
||||
$modal.find('.modal-footer button').first().focus()
|
||||
PokeBattle.primus.end()
|
||||
|
||||
when e.FIND_BATTLE
|
||||
PokeBattle.events.trigger("findBattleCanceled")
|
||||
|
||||
# Show errors
|
||||
[errors] = args
|
||||
alert(errors)
|
||||
when e.BATTLE_DNE
|
||||
[battleId] = args
|
||||
message = 'This battle no longer exists.'
|
||||
PokeBattle.rooms.get(battleId)?.announce('error', message)
|
||||
when e.COMMAND_ERROR
|
||||
[ roomId, message ] = args
|
||||
PokeBattle.rooms.get(roomId).announce('error', message)
|
||||
when e.PRIVATE_MESSAGE
|
||||
[ toUser, messageText ] = args
|
||||
message = PokeBattle.messages.get(toUser)
|
||||
message.add(toUser, messageText, type: "error")
|
||||
when e.INVALID_ALT_NAME
|
||||
[ messageText ] = args
|
||||
alert(messageText)
|
||||
PokeBattle.events.trigger("invalidAltName")
|
||||
else
|
||||
console.log("Received error: #{type}")
|
||||
console.log(" with content: #{args}")
|
|
@ -0,0 +1,7 @@
|
|||
# Prevents a clean escape while you're in a battle.
|
||||
$(window).on 'beforeunload', ->
|
||||
if PokeBattle.battles.isPlaying()
|
||||
"You are currently in a battle."
|
||||
else
|
||||
# Do not prompt
|
||||
return
|
|
@ -0,0 +1,4 @@
|
|||
PokeBattle.primus.on 'data', (args...) ->
|
||||
try
|
||||
console.log(args...) if window.localStorage.debug == 'true'
|
||||
catch
|
|
@ -0,0 +1,50 @@
|
|||
$ ->
|
||||
$mainButtons = $('.main_buttons')
|
||||
|
||||
$mainButtons.on 'click', '.teambuilder_button', (e) ->
|
||||
PokeBattle.navigation.showTeambuilder()
|
||||
|
||||
createChallengePane
|
||||
eventName: "findBattle"
|
||||
populate: $mainButtons.find('.find_battle_select_team')
|
||||
button: $mainButtons.find('.find_battle')
|
||||
defaultClauses: [
|
||||
Conditions.SLEEP_CLAUSE
|
||||
Conditions.EVASION_CLAUSE
|
||||
Conditions.SPECIES_CLAUSE
|
||||
Conditions.OHKO_CLAUSE
|
||||
Conditions.PRANKSTER_SWAGGER_CLAUSE
|
||||
Conditions.UNRELEASED_BAN
|
||||
Conditions.RATED_BATTLE
|
||||
Conditions.TIMED_BATTLE
|
||||
]
|
||||
blockedClauses: true
|
||||
|
||||
$mainButtons.find('.find_battle').on 'challenge', ->
|
||||
$this = $(this)
|
||||
$this.find('.find-icon')
|
||||
.addClass('icon-spinner spinner-anim')
|
||||
.removeClass("icon-earth")
|
||||
|
||||
$mainButtons.find('.display_credits').click ->
|
||||
$modal = PokeBattle.modal('modals/credits')
|
||||
$modal.find('.modal-footer button').first().focus()
|
||||
|
||||
# Depresss Find Battle once one is found
|
||||
depressFindBattle = ->
|
||||
$mainButtons = $('.main_buttons')
|
||||
$button = $mainButtons.find('.find_battle')
|
||||
$button.removeClass("disabled")
|
||||
$button.find('.find-icon')
|
||||
.removeClass("icon-spinner spinner-anim")
|
||||
.addClass("icon-earth")
|
||||
$mainButtons.find('.find_battle_select_team .select').removeClass('disabled')
|
||||
|
||||
$(window).load ->
|
||||
$mainButtons = $('.main_buttons')
|
||||
PokeBattle.battles.on 'add', (battle) ->
|
||||
if !battle.get('spectating')
|
||||
depressFindBattle()
|
||||
|
||||
PokeBattle.primus.on 'findBattleCanceled', depressFindBattle
|
||||
PokeBattle.events.on 'findBattleCanceled', depressFindBattle
|
|
@ -0,0 +1,31 @@
|
|||
# Clicking a person's name will open a private message session with that person.
|
||||
$(document).on 'click', '.open_pm', ->
|
||||
$this = $(this)
|
||||
message = PokeBattle.messages.add(id: $this.data('user-id'))
|
||||
message.trigger('open', message)
|
||||
message.trigger('focus', message)
|
||||
|
||||
# Receive private message events
|
||||
PokeBattle.primus.on 'privateMessage', (messageId, fromUserId, messageText) ->
|
||||
message = PokeBattle.messages.add(id: messageId)
|
||||
message.add(fromUserId, messageText)
|
||||
|
||||
# Challenges
|
||||
PokeBattle.primus.on 'challenge', (fromUserId, generation, conditions) ->
|
||||
message = PokeBattle.messages.add(id: fromUserId)
|
||||
message.add(fromUserId, "You have been challenged!", type: "alert")
|
||||
message.openChallenge(fromUserId, generation, conditions)
|
||||
|
||||
PokeBattle.primus.on 'cancelChallenge', (fromUserId) ->
|
||||
message = PokeBattle.messages.add(id: fromUserId)
|
||||
message.add(fromUserId, "The challenge was canceled!", type: "alert")
|
||||
message.closeChallenge(fromUserId)
|
||||
|
||||
PokeBattle.primus.on 'rejectChallenge', (fromUserId) ->
|
||||
message = PokeBattle.messages.add(id: fromUserId)
|
||||
message.add(fromUserId, "The challenge was rejected!", type: "alert")
|
||||
message.closeChallenge(fromUserId)
|
||||
|
||||
PokeBattle.primus.on 'challengeSuccess', (fromUserId) ->
|
||||
message = PokeBattle.messages.add(id: fromUserId)
|
||||
message.closeChallenge(fromUserId)
|
|
@ -0,0 +1,21 @@
|
|||
class PokeBattleRouter extends Backbone.Router
|
||||
routes:
|
||||
"" : "main"
|
||||
"battles/:id" : "spectateBattle"
|
||||
|
||||
main: =>
|
||||
$navigation = $('#navigation')
|
||||
$navigation.find('.nav_item').first().click()
|
||||
|
||||
spectateBattle: (id) =>
|
||||
if PokeBattle.battles.get(id)
|
||||
PokeBattle.navigation.changeWindowToBattle(id)
|
||||
else
|
||||
PokeBattle.primus.send('spectateBattle', id)
|
||||
|
||||
PokeBattle.router = new PokeBattleRouter()
|
||||
|
||||
PokeBattle.primus.once "loginSuccess", ->
|
||||
return if Backbone.History.started
|
||||
PokeBattle.events.trigger("ready")
|
||||
routed = Backbone.history.start(pushState: true)
|
|
@ -0,0 +1,4 @@
|
|||
$(document).on 'click', '.spectate', ->
|
||||
battleId = $(this).data('battle-id')
|
||||
PokeBattle.router.navigate("battles/#{battleId}", trigger: true)
|
||||
return false
|
|
@ -0,0 +1,6 @@
|
|||
PokeBattle.primus.on 'version', (version) ->
|
||||
if version != PokeBattle.CLIENT_VERSION
|
||||
$modal = PokeBattle.modal 'modals/new_client', ($modal) ->
|
||||
$modal.on 'click', '.button_refresh', ->
|
||||
window.location.reload(true)
|
||||
$modal.find('button').first().focus()
|
|
@ -0,0 +1,57 @@
|
|||
DEFAULT_INTERVAL = 1000
|
||||
original = document.title
|
||||
timeoutId = undefined
|
||||
|
||||
$.flashTitle = (newMsg, interval) ->
|
||||
if newMsg == false
|
||||
# stop flashing and reset title
|
||||
clearTimeout(timeoutId)
|
||||
document.title = original
|
||||
else
|
||||
# loop flashing
|
||||
interval = interval || DEFAULT_INTERVAL
|
||||
timeoutId = setTimeout( ->
|
||||
clearTimeout(timeoutId)
|
||||
document.title = if (document.title == original) then newMsg else original
|
||||
timeoutId = setTimeout(arguments.callee, interval)
|
||||
, interval)
|
||||
|
||||
|
||||
PokeBattle.NotificationTypes =
|
||||
PRIVATE_MESSAGE:
|
||||
showDesktop: false
|
||||
BATTLE_STARTED:
|
||||
showDesktop: true
|
||||
prefix: "bs"
|
||||
title: "Battle Started"
|
||||
body: "Your battle has started!"
|
||||
ACTION_REQUESTED:
|
||||
showDesktop: true
|
||||
prefix: "ar"
|
||||
title: "Battle Action Requested"
|
||||
body: "A battle is ready for your input!"
|
||||
|
||||
notifications = []
|
||||
|
||||
# Currently called in concerns/find_battle.coffee
|
||||
PokeBattle.requestNotifyPermission = =>
|
||||
if notify.permissionLevel() == notify.PERMISSION_DEFAULT
|
||||
notify.requestPermission()
|
||||
|
||||
# TODO: Count the notifications by unique type/identifier combos
|
||||
PokeBattle.notifyUser = (type, identifier) =>
|
||||
return if document.hasFocus()
|
||||
$.flashTitle "You have new notification(s)"
|
||||
|
||||
if type.showDesktop
|
||||
notification = notify.createNotification type.title,
|
||||
icon: "//media.pokebattle.com/logo/pb_red.png"
|
||||
body: type.body
|
||||
tag: "PokeBattle_#{type.prefix}_#{identifier}"
|
||||
|
||||
notifications.push notification
|
||||
|
||||
$(window).focus ->
|
||||
$.flashTitle(false)
|
||||
notification.close() for notification in notifications
|
||||
notifications = []
|
|
@ -0,0 +1,70 @@
|
|||
class TeamStore extends Backbone.Collection
|
||||
model: Team
|
||||
|
||||
initialize: ->
|
||||
@on('add remove reset remoteSync', @saveLocally)
|
||||
@loadLocally()
|
||||
|
||||
# Only locally save teams without an id or is trying to save.
|
||||
unsavedTeams: =>
|
||||
@filter((team) -> !team.id || team.get('saving'))
|
||||
|
||||
saveLocally: =>
|
||||
teams = @unsavedTeams()
|
||||
json = _.map(teams, (team) -> team.toJSON())
|
||||
try
|
||||
window.localStorage.setItem('local_teams', JSON.stringify(json))
|
||||
catch
|
||||
console.error("Failed to save teams locally.")
|
||||
|
||||
loadLocally: =>
|
||||
try
|
||||
json = window.localStorage.getItem('local_teams')
|
||||
return unless json
|
||||
teams = JSON.parse(json)
|
||||
@add(teams) if teams.length > 0
|
||||
catch
|
||||
console.error("Failed to load teams locally.")
|
||||
|
||||
saveRemotely: =>
|
||||
teams = @unsavedTeams()
|
||||
team.save() for team in teams
|
||||
|
||||
PokeBattle.TeamStore = new TeamStore()
|
||||
|
||||
PokeBattle.primus.on 'receiveTeams', (remoteTeams) ->
|
||||
remoteTeams = remoteTeams.map (team) ->
|
||||
team.teambuilder = true
|
||||
new Team(team)
|
||||
|
||||
# First, find teams that are already saved locally -- these exclude deleted
|
||||
# teams on either side. The remote copy of the team is checked against the
|
||||
# local copy of the team. If they differ, display a modal asking whether to
|
||||
# override or keep the local changes.
|
||||
ids = PokeBattle.TeamStore.pluck('id')
|
||||
for remoteTeam in remoteTeams when remoteTeam.id in ids
|
||||
remoteJSON = remoteTeam.toJSON()
|
||||
localTeam = PokeBattle.TeamStore.get(remoteTeam.id)
|
||||
unsavedTeam = localTeam.clone()
|
||||
unsavedTeam.set(localTeam.previousAttributes(), silent: true)
|
||||
localJSON = unsavedTeam.toJSON()
|
||||
if !_.isEqual(remoteJSON, localJSON)
|
||||
# Whoa! Versions are different! Let's ask the user what to do.
|
||||
teamText = PokeBattle.exportTeam(remoteJSON.pokemon)
|
||||
domId = "teams-differ-#{remoteTeam.id}"
|
||||
$modal = PokeBattle.modal('modals/teams_differ', domId, {teamText})
|
||||
$modal.find('textarea').first().focus()
|
||||
do (localTeam, remoteJSON) ->
|
||||
# We want to override the current version with the one on the server.
|
||||
# This is extremely hacky due to hidden state and clones everywhere on
|
||||
# the teambuilder.
|
||||
$modal.find('.button_override').one 'click', ->
|
||||
localTeam.set(remoteJSON, silent: true)
|
||||
localTeam.trigger('render', localTeam)
|
||||
$modal.modal('hide')
|
||||
|
||||
# Now, add teams we haven't seen yet to the store.
|
||||
PokeBattle.TeamStore.add(remoteTeams)
|
||||
|
||||
PokeBattle.primus.on 'loginSuccess', ->
|
||||
PokeBattle.primus.send('requestTeams')
|
|
@ -0,0 +1,9 @@
|
|||
# Used to handle the show navigation button in the header, which is visible when the browser
|
||||
# window becomes small enough.
|
||||
$ ->
|
||||
$navigation = $("#navigation")
|
||||
$(".show-navigation").click =>
|
||||
active = $navigation.hasClass('active')
|
||||
$navigation.toggleClass('active', !active)
|
||||
|
||||
$navigation.on 'click', '.nav_item', -> $navigation.removeClass("active")
|
|
@ -0,0 +1,199 @@
|
|||
# eventName should be one of "challenge" or "find battle"
|
||||
# opts may include whether to enable clauses, for example
|
||||
@createChallengePane = (opts) ->
|
||||
$wrapper = opts.populate
|
||||
$button = opts.button
|
||||
$accept = opts.acceptButton || $()
|
||||
$reject = opts.rejectButton || $()
|
||||
$buttons = $button.add($accept).add($reject)
|
||||
eventName = opts.eventName
|
||||
capitalizedEventName = "#{eventName[0].toUpperCase()}#{eventName.substr(1)}"
|
||||
acceptEventName = "accept#{capitalizedEventName}"
|
||||
rejectEventName = "reject#{capitalizedEventName}"
|
||||
cancelEventName = "cancel#{capitalizedEventName}"
|
||||
generation = opts.generation
|
||||
personId = opts.personId
|
||||
defaultClauses = opts.defaultClauses || []
|
||||
blockedClauses = opts.blockedClauses ? false
|
||||
|
||||
selectedTeamId = null
|
||||
selectedAlt = null
|
||||
|
||||
getSelectedTeam = ->
|
||||
PokeBattle.TeamStore.get(selectedTeamId) || PokeBattle.TeamStore.at(0)
|
||||
|
||||
renderCurrentTeam = ($context) ->
|
||||
$selectTeam = $context.find('.select-team')
|
||||
if PokeBattle.TeamStore.length > 0
|
||||
currentTeam = getSelectedTeam()
|
||||
html = JST['team_dropdown'](window: window, team: currentTeam)
|
||||
$selectTeam.html(html)
|
||||
else
|
||||
$selectTeam.html("You have no teams!")
|
||||
|
||||
cancelChallenge = ->
|
||||
enableButtons()
|
||||
if personId
|
||||
PokeBattle.primus.send(cancelEventName, personId)
|
||||
else
|
||||
format = $selectFormat.data('format')
|
||||
PokeBattle.primus.send(cancelEventName, format)
|
||||
$button.trigger('cancelChallenge')
|
||||
|
||||
disableButtons = ->
|
||||
$wrapper.find('.select').addClass('disabled')
|
||||
$buttons.addClass('disabled')
|
||||
|
||||
# Enable buttons
|
||||
enableButtons = ->
|
||||
$buttons.removeClass('disabled')
|
||||
|
||||
toggleAltInput = (visible) ->
|
||||
$wrapper.find('.alt-input').toggleClass("hidden", !visible)
|
||||
$wrapper.find('.alt-dropdown-section').toggleClass("hidden", visible)
|
||||
$wrapper.find('.alt-input input').focus() if visible
|
||||
|
||||
isAttachedToDom = ->
|
||||
$.contains(document, $wrapper.get(0))
|
||||
|
||||
altCreatedEvent = ->
|
||||
return PokeBattle.primus.off('altCreated', altCreatedEvent) unless isAttachedToDom()
|
||||
$wrapper.find('.alt-input input').val("")
|
||||
toggleAltInput(false)
|
||||
|
||||
PokeBattle.primus.on 'altCreated', altCreatedEvent
|
||||
|
||||
enableButtons()
|
||||
|
||||
$wrapper.html(JST['new_battle']({window, defaultClauses}))
|
||||
$selectFormat = $wrapper.find(".select-format")
|
||||
|
||||
# Implement finding battle/challenging
|
||||
$button.on 'click.challenge', ->
|
||||
# Start requesting for notify permission here
|
||||
PokeBattle.requestNotifyPermission()
|
||||
|
||||
format = $selectFormat.data('format')
|
||||
|
||||
# Toggle state when you press the button.
|
||||
if !$button.hasClass('disabled')
|
||||
team = getSelectedTeam()
|
||||
unless team
|
||||
alert("You need to create a team using the Teambuilder before you can battle.")
|
||||
PokeBattle.navigation.showTeambuilder()
|
||||
return
|
||||
|
||||
disableButtons()
|
||||
teamJSON = team.toNonNullJSON().pokemon
|
||||
# Send the event
|
||||
if personId
|
||||
$clauses = $wrapper.find('input:checked[type="checkbox"]')
|
||||
clauses = []
|
||||
$clauses.each(-> clauses.push(parseInt($(this).val(), 10)))
|
||||
PokeBattle.primus.send(eventName, personId, format, teamJSON, clauses, selectedAlt)
|
||||
else
|
||||
PokeBattle.primus.send(eventName, format, teamJSON, selectedAlt)
|
||||
$button.addClass('disabled').trigger('challenge')
|
||||
else
|
||||
cancelChallenge()
|
||||
|
||||
# Implement accept/reject buttons.
|
||||
$accept.on 'click.challenge', ->
|
||||
return if $(this).hasClass('disabled')
|
||||
team = getSelectedTeam()
|
||||
unless team
|
||||
alert("You need to create a team using the Teambuilder before you can battle.")
|
||||
PokeBattle.navigation.showTeambuilder()
|
||||
return
|
||||
disableButtons()
|
||||
teamJSON = team.toNonNullJSON().pokemon
|
||||
PokeBattle.primus.send(acceptEventName, personId, teamJSON, selectedAlt)
|
||||
|
||||
$reject.on 'click.challenge', ->
|
||||
return if $(this).hasClass('disabled')
|
||||
disableButtons()
|
||||
PokeBattle.primus.send(rejectEventName, personId)
|
||||
|
||||
# Clicking the alts dropdown brings down an alt selection dropdown menu
|
||||
$wrapper.find('.select-alt').click (e) ->
|
||||
html = JST['alt_dropdown'](alts: PokeBattle.alts.list, username: PokeBattle.username)
|
||||
$wrapper.find('.alt-dropdown').html(html)
|
||||
|
||||
# Selecting an alt from the dropdown
|
||||
$wrapper.find('.alt-dropdown').on 'click', '.select-alt-dropdown-item', (e) ->
|
||||
selectedAlt = $(this).data('alt-name')
|
||||
$wrapper.find('.select-alt').html($(this).html())
|
||||
|
||||
# When add alt is clicked, show the alt input form
|
||||
$wrapper.find('.alt-dropdown').on 'click', '.add-alt-dropdown-item', (e) ->
|
||||
toggleAltInput(true)
|
||||
|
||||
# Clicking the Add Alt Button
|
||||
$wrapper.find('.alt-input .add-button').click (e) ->
|
||||
altName = $wrapper.find('.alt-input input').val().trim()
|
||||
PokeBattle.alts.createAlt(altName)
|
||||
|
||||
# Clicking the Cancel Add Alt Button
|
||||
$wrapper.find('.alt-input .cancel-button').click (e) ->
|
||||
toggleAltInput(false)
|
||||
|
||||
# Clicking the team dropdown brings down a team selection menu.
|
||||
# Also updates the allTeams collection
|
||||
$wrapper.find('.select-team').click (e) ->
|
||||
allTeams = PokeBattle.TeamStore.models || []
|
||||
html = JST['team_dropdown'](window: window, teams: allTeams)
|
||||
$wrapper.find('.team-dropdown').html(html)
|
||||
|
||||
# Selecting a team from the menu
|
||||
$wrapper.find('.team-dropdown').on 'click', '.select-team-dropdown-item', (e) ->
|
||||
slot = $(e.currentTarget).data('slot')
|
||||
selectedTeamId = PokeBattle.TeamStore.at(slot).id
|
||||
renderCurrentTeam($wrapper)
|
||||
|
||||
# Selecting build team from the menu
|
||||
$wrapper.find('.team-dropdown').on 'click', '.build-team-option', (e) ->
|
||||
PokeBattle.navigation.showTeambuilder()
|
||||
|
||||
# Selecting the format changes the dropdown.
|
||||
$wrapper.find('.format-dropdown').on 'click', '.select-format-dropdown-item', (e) ->
|
||||
$target = $(e.currentTarget)
|
||||
format = $target.data('format')
|
||||
$selectFormat.text($target.text())
|
||||
$selectFormat.data('format', format)
|
||||
|
||||
# Select non-alt option
|
||||
$wrapper.find('.select-alt').html(JST['alt_dropdown'](alt: null, username: PokeBattle.username))
|
||||
|
||||
# Auto-select format.
|
||||
if generation
|
||||
# If a generation is passed, auto-select it.
|
||||
$format = $wrapper.find(".format-dropdown a[data-format='#{generation}']")
|
||||
$format.first().click()
|
||||
$wrapper.find('.select-format').addClass('disabled')
|
||||
else
|
||||
# Auto-select first available format.
|
||||
$wrapper.find('.format-dropdown a').first().click()
|
||||
|
||||
if blockedClauses
|
||||
$checkboxes = $wrapper.find('input[type="checkbox"]')
|
||||
if blockedClauses != true
|
||||
$checkboxes = $checkboxes.filter ->
|
||||
clause = Number($(this).data('clause'))
|
||||
clause in blockedClauses
|
||||
$checkboxes.prop('disabled', true)
|
||||
$checkboxes.closest('label').addClass('disabled')
|
||||
|
||||
renderCurrentTeam($wrapper)
|
||||
|
||||
# Called when a team has been updated
|
||||
teamUpdated = ->
|
||||
# If this challenge panel no longer exists, remove the callback
|
||||
if not isAttachedToDom()
|
||||
PokeBattle.TeamStore.off 'add remove reset saved', teamUpdated
|
||||
return
|
||||
|
||||
# Rerender the current team
|
||||
renderCurrentTeam($wrapper)
|
||||
|
||||
# Start listening for team updated events
|
||||
PokeBattle.TeamStore.on 'add remove reset saved', teamUpdated
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,15 @@
|
|||
# domId is optional:
|
||||
#
|
||||
# PokeBattle.modal(modalPath, [domId], options, initialize)
|
||||
PokeBattle.modal = (modalPath, domId, options, initialize) ->
|
||||
[domId, options, initialize] = [null, domId, options] if !_.isString(domId)
|
||||
[options, initialize] = [{}, options] if _.isFunction(options)
|
||||
$modal = $(JST[modalPath](options))
|
||||
id = '#' + (domId || $modal.prop('id'))
|
||||
if $(id).length == 0
|
||||
$modal.appendTo($('body'))
|
||||
initialize?($modal)
|
||||
else
|
||||
$modal = $(id).last()
|
||||
$modal.modal('show')
|
||||
return $modal
|
|
@ -0,0 +1,262 @@
|
|||
HiddenPower = (if module? then require('../../../../shared/hidden_power') else window.HiddenPower ?= {})
|
||||
|
||||
@PokeBattle ?= {}
|
||||
@PokeBattle.parseTeam = (teamString) ->
|
||||
text = teamString.split('\n')
|
||||
team = []
|
||||
pokemonRegex = /^(.*?)\s*(\(M\)|\(F\)|)?(?:\s*@\s*(.*))?$/
|
||||
pokemon = null
|
||||
for line in text
|
||||
line = line.trim()
|
||||
if line.length == 0
|
||||
pokemon = null
|
||||
else if !pokemon
|
||||
[ all, pokemonLine, gender, item ] = line.match(pokemonRegex)
|
||||
pokemon = {}
|
||||
team.push(pokemon)
|
||||
|
||||
if pokemonLine.match(/(.*?)\s*\((.*)\)/)
|
||||
pokemon.name = RegExp.$1
|
||||
pokemonLine = RegExp.$2
|
||||
convertNameToSpeciesAndForme(pokemon, pokemonLine.trim())
|
||||
pokemon.gender = gender[1] if gender # (M) and (F)
|
||||
pokemon.item = item if item
|
||||
for olditem, newitem of Aliases.items
|
||||
if olditem is pokemon.item
|
||||
pokemon.item = newitem
|
||||
else if line.match(/^(?:Trait|Ability):\s+(.*)$/i)
|
||||
pokemon.ability = RegExp.$1
|
||||
for oldability, newability of Aliases.abilities
|
||||
if pokemon.ability is oldability
|
||||
pokemon.ability = newability
|
||||
else if line.match(/^Level:\s+(.*)$/i)
|
||||
pokemon.level = Number(RegExp.$1) || 100
|
||||
else if line.match(/^Happiness:\s+(.*)$/i)
|
||||
pokemon.happiness = Number(RegExp.$1) || 0
|
||||
else if line.match(/^Shiny: Yes$/i)
|
||||
pokemon.shiny = true
|
||||
else if line.match(/^EVs: (.*)$/i)
|
||||
evs = RegExp.$1.split(/\//g)
|
||||
pokemon.evs = {}
|
||||
for ev in evs
|
||||
ev = ev.trim()
|
||||
[ numberString, rawStat ] = ev.split(/\s+/)
|
||||
pokemon.evs[statsHash[rawStat]] = Number(numberString) || 0
|
||||
else if line.match(/^IVs: (.*)$/i)
|
||||
ivs = RegExp.$1.split(/\//g)
|
||||
pokemon.ivs = {}
|
||||
for iv in ivs
|
||||
iv = iv.trim()
|
||||
[ numberString, rawStat ] = iv.split(/\s+/)
|
||||
pokemon.ivs[statsHash[rawStat]] = Number(numberString) || 0
|
||||
else if line.match(/^([A-Za-z]+) nature/i)
|
||||
pokemon.nature = RegExp.$1
|
||||
else if line.match(/^[\-\~]\s*(.*)/)
|
||||
moveName = RegExp.$1
|
||||
for oldmove, newmove of Aliases.moves
|
||||
if moveName is oldmove
|
||||
moveName = newmove
|
||||
if /Hidden Power /.test(moveName)
|
||||
if !pokemon.ivs
|
||||
moveName.match(/Hidden Power (.*)/i)
|
||||
hiddenPowerType = RegExp.$1.trim().toLowerCase().replace(/\W+/g, '')
|
||||
pokemon.ivs = HiddenPower.BW.ivs[hiddenPowerType] || {}
|
||||
moveName = 'Hidden Power'
|
||||
pokemon.moves ?= []
|
||||
pokemon.moves.push(moveName)
|
||||
return team
|
||||
|
||||
@PokeBattle.exportTeam = (json) ->
|
||||
s = []
|
||||
for pokemon in json
|
||||
s.push("")
|
||||
|
||||
species = pokemon.species
|
||||
if pokemon.forme && pokemon.forme != "default"
|
||||
species += "-#{pokemon.forme[0].toUpperCase()}"
|
||||
|
||||
mainLine = []
|
||||
if pokemon.name
|
||||
mainLine.push(pokemon.name)
|
||||
mainLine.push("(#{species})")
|
||||
else
|
||||
mainLine.push(species)
|
||||
|
||||
mainLine.push("(#{pokemon.gender})") if pokemon.gender
|
||||
mainLine.push("@ #{pokemon.item}") if pokemon.item
|
||||
s.push(mainLine.join(' '))
|
||||
|
||||
# Ability
|
||||
s.push("Ability: #{pokemon.ability}") if pokemon.ability
|
||||
|
||||
# EVs
|
||||
if pokemon.evs
|
||||
evArray = for stat, amount of pokemon.evs when amount > 0
|
||||
"#{amount} #{reverseStatsHash[stat]}"
|
||||
s.push("EVs: #{evArray.join(" / ")}") if evArray.length > 0
|
||||
|
||||
# IVs
|
||||
if pokemon.ivs
|
||||
ivArray = for stat, amount of pokemon.ivs when amount < 31
|
||||
"#{amount} #{reverseStatsHash[stat]}"
|
||||
s.push("IVs: #{ivArray.join(" / ")}") if ivArray.length > 0
|
||||
|
||||
# Nature
|
||||
s.push("#{pokemon.nature} nature") if pokemon.nature
|
||||
|
||||
# Level
|
||||
s.push("Level: #{pokemon.level}") if pokemon.level && pokemon.level != 100
|
||||
|
||||
# Shiny
|
||||
s.push("Shiny: Yes") if pokemon.shiny
|
||||
|
||||
# Happiness
|
||||
if pokemon.happiness && pokemon.happiness != 100
|
||||
s.push("Happiness: #{pokemon.happiness}")
|
||||
|
||||
# Moves
|
||||
if pokemon.moves
|
||||
s.push("- #{moveName}") for moveName in pokemon.moves
|
||||
s.push("\n") # Trailing newlines, just in case.
|
||||
s.join("\n")
|
||||
|
||||
Aliases =
|
||||
moves:
|
||||
"Ancient Power" : "AncientPower"
|
||||
"Bubble Beam" : "BubbleBeam"
|
||||
"Double Slap" : "DoubleSlap"
|
||||
"Dragon Breath" : "DragonBreath"
|
||||
"Dynamic Punch" : "DynamicPunch"
|
||||
"Extreme Speed" : "ExtremeSpeed"
|
||||
"Feint Attack" : "Faint Attack"
|
||||
"Feather Dance" : "FeatherDance"
|
||||
"Grass Whistle" : "GrassWhistle"
|
||||
"High Jump Kick" : "Hi Jump Kick"
|
||||
"Poison Powder" : "PoisonPowder"
|
||||
"Sand Attack" : "Sand-Attack"
|
||||
"Self-Destruct" : "Selfdestruct"
|
||||
"Smelling Salts" : "SmellingSalt"
|
||||
"Smokescreen" : "SmokeScreen"
|
||||
"Soft-Boiled" : "Softboiled"
|
||||
"Solar Beam" : "SolarBeam"
|
||||
"Sonic Boom" : "SonicBoom"
|
||||
"Thunder Punch" : "ThunderPunch"
|
||||
"Thunder Shock" : "ThunderShock"
|
||||
"Vice Grip" : "ViceGrip"
|
||||
abilities:
|
||||
"Compound Eyes" : "Compoundeyes"
|
||||
"Lightning Rod" : "Lightningrod"
|
||||
items:
|
||||
"Balm Mushroom" : "BalmMushroom"
|
||||
"Black Glasses" : "BlackGlasses"
|
||||
"Bright Powder" : "BrightPowder"
|
||||
"Deep Sea Scale" : "DeepSeaScale"
|
||||
"Deep Sea Tooth" : "DeepSeaTooth"
|
||||
"Energy Powder" : "EnergyPowder"
|
||||
"Never-Melt Ice" : "NeverMeltIce"
|
||||
"Paralyze Heal" : "Parlyz Heal"
|
||||
"Rage Candy Bar" : "RageCandyBar"
|
||||
"Silver Powder" : "SilverPowder"
|
||||
"Thunder Stone" : "Thunderstone"
|
||||
"Tiny Mushroom" : "TinyMushroom"
|
||||
"Twisted Spoon" : "TwistedSpoon"
|
||||
"X Defense" : "X Defend"
|
||||
"X Sp. Atk" : "X Special"
|
||||
|
||||
statsHash =
|
||||
'hp' : 'hp'
|
||||
'Hp' : 'hp'
|
||||
'HP' : 'hp'
|
||||
'Atk' : 'attack'
|
||||
'Def' : 'defense'
|
||||
'SAtk' : 'specialAttack'
|
||||
'SpA' : 'specialAttack'
|
||||
'SDef' : 'specialDefense'
|
||||
'SpD' : 'specialDefense'
|
||||
'Spe' : 'speed'
|
||||
'Spd' : 'speed'
|
||||
|
||||
reverseStatsHash =
|
||||
'hp' : 'HP'
|
||||
'attack' : 'Atk'
|
||||
'defense' : 'Def'
|
||||
'specialAttack' : 'SAtk'
|
||||
'specialDefense' : 'SDef'
|
||||
'speed' : 'Spe'
|
||||
|
||||
convertNameToSpeciesAndForme = (pokemon, species) ->
|
||||
if species.match(/(Thundurus|Landorus|Tornadus)\-T(herian)?/i)
|
||||
pokemon.species = RegExp.$1
|
||||
pokemon.forme = 'therian'
|
||||
else if species.match(/Shaymin-S(ky)?/i)
|
||||
pokemon.species = "Shaymin"
|
||||
pokemon.forme = 'sky'
|
||||
else if species.match(/Giratina-O(rigin)?/i)
|
||||
pokemon.species = "Giratina"
|
||||
pokemon.forme = 'origin'
|
||||
else if species.match(/Arceus(\-.*)?/)
|
||||
pokemon.species = "Arceus"
|
||||
else if species.match(/Kyurem-B(lack)?/)
|
||||
pokemon.species = "Kyurem"
|
||||
pokemon.forme = "black"
|
||||
else if species.match(/Kyurem-W(hite)?/)
|
||||
pokemon.species = "Kyurem"
|
||||
pokemon.forme = "white"
|
||||
else if species.match(/Rotom-W|Rotom-Wash/)
|
||||
pokemon.species = "Rotom"
|
||||
pokemon.forme = "wash"
|
||||
else if species.match(/Rotom-S|Rotom-Fan/)
|
||||
pokemon.species = "Rotom"
|
||||
pokemon.forme = "fan"
|
||||
else if species.match(/Rotom-H|Rotom-Heat/)
|
||||
pokemon.species = "Rotom"
|
||||
pokemon.forme = "heat"
|
||||
else if species.match(/Rotom-F|Rotom-Frost/)
|
||||
pokemon.species = "Rotom"
|
||||
pokemon.forme = "frost"
|
||||
else if species.match(/Rotom-C|Rotom-Mow/)
|
||||
pokemon.species = "Rotom"
|
||||
pokemon.forme = "mow"
|
||||
else if species.match(/Deoxys-A|Deoxys-Attack/)
|
||||
pokemon.species = "Deoxys"
|
||||
pokemon.forme = "attack"
|
||||
else if species.match(/Deoxys-D|Deoxys-Defense/)
|
||||
pokemon.species = "Deoxys"
|
||||
pokemon.forme = "defense"
|
||||
else if species.match(/Deoxys-S|Deoxys-Speed/)
|
||||
pokemon.species = "Deoxys"
|
||||
pokemon.forme = "speed"
|
||||
else if species.match(/Basculin-Blue-Striped|Basculin-A/)
|
||||
pokemon.species = "Basculin"
|
||||
pokemon.forme = "blue-striped"
|
||||
else if species.match(/Keldeo-Resolute|Keldeo-R/)
|
||||
pokemon.species = "Keldeo"
|
||||
pokemon.forme = "resolute"
|
||||
else if species.match(/Shellos-East/)
|
||||
pokemon.species = "Shellos"
|
||||
# TODO: Read east forme
|
||||
pokemon.forme = "default"
|
||||
else if species.match(/Gastrodon-East/)
|
||||
pokemon.species = "Gastrodon"
|
||||
# TODO: Read east forme
|
||||
pokemon.forme = "default"
|
||||
else if species.match(/Wormadam-Sandy|Wormadam-G/)
|
||||
pokemon.species = "Wormadam"
|
||||
pokemon.forme = "sandy"
|
||||
else if species.match(/Wormadam-Trash|Wormadam-S/)
|
||||
pokemon.species = "Wormadam"
|
||||
pokemon.forme = "trash"
|
||||
else if species.match(/Deerling-.*/)
|
||||
pokemon.species = "Deerling"
|
||||
# TODO: Read other formes
|
||||
pokemon.forme = null
|
||||
else if species.match(/Sawsbuck-.*/)
|
||||
pokemon.species = "Sawsbuck"
|
||||
# TODO: Read other formes
|
||||
pokemon.forme = null
|
||||
else if species.match(/Unown-.*/)
|
||||
pokemon.species = "Unown"
|
||||
# TODO: Read other formes
|
||||
pokemon.forme = null
|
||||
else
|
||||
pokemon.species = species
|
|
@ -0,0 +1,17 @@
|
|||
# TODO: Move more timer functionality into here
|
||||
|
||||
# TODO: Encapsulate this better. Maybe put humanizeTime in a time object with more functions?
|
||||
PokeBattle.humanizeTime = (unixTime) =>
|
||||
unixTime = 0 if !unixTime? || unixTime < 0
|
||||
seconds = Math.floor(unixTime / 1000) % 60
|
||||
minutes = Math.floor(unixTime / 1000 / 60)
|
||||
seconds = String(seconds)
|
||||
return minutes + ":" + "00".substr(seconds.length) + seconds
|
||||
|
||||
$ ->
|
||||
window.setInterval( ->
|
||||
$(".elapsed-time").each ->
|
||||
$el = $(this)
|
||||
elapsedTime = Date.now() - $el.data("time-start")
|
||||
$el.text(PokeBattle.humanizeTime(elapsedTime))
|
||||
, 500)
|
|
@ -0,0 +1,4 @@
|
|||
@PokeBattle ?= {}
|
||||
PokeBattle.battles = null
|
||||
PokeBattle.events = {}
|
||||
_.extend(PokeBattle.events, Backbone.Events)
|
|
@ -0,0 +1,3 @@
|
|||
PokeBattle.events.once "ready", ->
|
||||
$loading = $(".loading-container")
|
||||
$loading.fadeOut(-> $loading.remove())
|
|
@ -0,0 +1,3 @@
|
|||
# This is turned off by default, due to performance reasons.
|
||||
# But we'd like to listen to this event for the teambuilder.
|
||||
Backbone.Associations.EVENTS_NC = true
|
|
@ -0,0 +1,210 @@
|
|||
@PokeBattle.mixins.BattleProtocolParser =
|
||||
update: (actions) ->
|
||||
return if actions.length == 0
|
||||
@notify()
|
||||
hadStuff = (@updateQueue.length > 0)
|
||||
@updateQueue.push(actions...)
|
||||
@_update() unless hadStuff
|
||||
|
||||
_update: (wasAtBottom) ->
|
||||
view = @view
|
||||
queue = @updateQueue
|
||||
return if !queue # closed battle in the middle of getting updates
|
||||
if queue.length == 0
|
||||
view.renderUserInfo()
|
||||
view.resetPopovers()
|
||||
if wasAtBottom || view.skip? then view.chatView.scrollToBottom()
|
||||
if view.skip?
|
||||
delete view.skip
|
||||
view.$('.battle_pane').show()
|
||||
return
|
||||
wasAtBottom ||= view.chatView.isAtBottom()
|
||||
action = queue.shift()
|
||||
[ type, rest... ] = action
|
||||
protocol = (key for key, value of Protocol when value == type)[0]
|
||||
try
|
||||
if window.localStorage.debug == 'true'
|
||||
console.log "Received protocol: #{protocol} with args: #{rest}"
|
||||
catch
|
||||
|
||||
done = () =>
|
||||
return if done.called
|
||||
done.called = true
|
||||
if view.skip?
|
||||
@_update.call(this, wasAtBottom)
|
||||
else
|
||||
# setTimeout 0 lets the browser breathe.
|
||||
setTimeout(@_update.bind(this, wasAtBottom), 0)
|
||||
|
||||
doneTimeout = ->
|
||||
setTimeout(done, 0)
|
||||
|
||||
doneSpeedTimeout = () =>
|
||||
if view.skip? || view.speed <= 1
|
||||
done()
|
||||
else
|
||||
setTimeout(done, (view.speed - 1) * 1000)
|
||||
|
||||
try
|
||||
switch type
|
||||
when Protocol.CHANGE_HP
|
||||
[player, slot, newPercent] = rest
|
||||
pokemon = @getPokemon(player, slot)
|
||||
pokemon.set('percent', newPercent)
|
||||
if view.skip? then done() else setTimeout(done, 500)
|
||||
when Protocol.CHANGE_EXACT_HP
|
||||
[player, slot, newHP] = rest
|
||||
pokemon = @getPokemon(player, slot)
|
||||
pokemon.set('hp', newHP)
|
||||
done()
|
||||
when Protocol.SWITCH_OUT
|
||||
[player, slot] = rest
|
||||
view.switchOut(player, slot, done)
|
||||
when Protocol.SWITCH_IN
|
||||
# TODO: Get Pokemon data, infer which Pokemon it is.
|
||||
# Currently, it cheats with `fromSlot`.
|
||||
[player, toSlot, fromSlot] = rest
|
||||
team = @getTeam(player).get('pokemon').models
|
||||
[team[toSlot], team[fromSlot]] = [team[fromSlot], team[toSlot]]
|
||||
# TODO: Again, automatic.
|
||||
view.switchIn(player, toSlot, fromSlot, doneSpeedTimeout)
|
||||
when Protocol.CHANGE_PP
|
||||
[player, slot, moveIndex, newPP] = rest
|
||||
pokemon = @getPokemon(player, slot)
|
||||
pokemon.setPP(moveIndex, newPP)
|
||||
done()
|
||||
when Protocol.REQUEST_ACTIONS
|
||||
[validActions] = rest
|
||||
view.enableButtons(validActions)
|
||||
PokeBattle.notifyUser(PokeBattle.NotificationTypes.ACTION_REQUESTED, @id + "_" + @get('turn'))
|
||||
done()
|
||||
when Protocol.START_TURN
|
||||
[turn] = rest
|
||||
view.beginTurn(turn, doneTimeout)
|
||||
when Protocol.CONTINUE_TURN
|
||||
view.continueTurn(doneTimeout)
|
||||
when Protocol.RAW_MESSAGE
|
||||
[message] = rest
|
||||
view.addLog("#{message}<br>")
|
||||
done()
|
||||
when Protocol.FAINT
|
||||
[player, slot] = rest
|
||||
view.faint(player, slot, done)
|
||||
when Protocol.MAKE_MOVE
|
||||
# TODO: Send move id instead
|
||||
[player, slot, moveName] = rest
|
||||
view.logMove(player, slot, moveName, done)
|
||||
when Protocol.END_BATTLE
|
||||
[winner] = rest
|
||||
view.announceWinner(winner, done)
|
||||
when Protocol.FORFEIT_BATTLE
|
||||
[forfeiter] = rest
|
||||
view.announceForfeit(forfeiter, done)
|
||||
when Protocol.TIMER_WIN
|
||||
[winner] = rest
|
||||
view.announceTimer(winner, done)
|
||||
when Protocol.BATTLE_EXPIRED
|
||||
view.announceExpiration(done)
|
||||
when Protocol.MOVE_SUCCESS
|
||||
[player, slot, targetSlots, moveName] = rest
|
||||
view.moveSuccess(player, slot, targetSlots, moveName, done)
|
||||
when Protocol.CANNED_TEXT
|
||||
cannedInteger = rest.splice(0, 1)
|
||||
view.parseCannedText(cannedInteger, rest, done)
|
||||
when Protocol.EFFECT_END
|
||||
[player, slot, effect] = rest
|
||||
view.endEffect(player, slot, effect, done)
|
||||
when Protocol.POKEMON_ATTACH
|
||||
[player, slot, attachment] = rest
|
||||
view.attachPokemon(player, slot, attachment, done)
|
||||
when Protocol.TEAM_ATTACH
|
||||
[player, attachment] = rest
|
||||
view.attachTeam(player, attachment, done)
|
||||
when Protocol.BATTLE_ATTACH
|
||||
[attachment] = rest
|
||||
view.attachBattle(attachment, done)
|
||||
when Protocol.POKEMON_UNATTACH
|
||||
[player, slot, attachment] = rest
|
||||
view.unattachPokemon(player, slot, attachment, done)
|
||||
when Protocol.TEAM_UNATTACH
|
||||
[player, attachment] = rest
|
||||
view.unattachTeam(player, attachment, done)
|
||||
when Protocol.BATTLE_UNATTACH
|
||||
[attachment] = rest
|
||||
view.unattachBattle(attachment, done)
|
||||
when Protocol.INITIALIZE
|
||||
# TODO: Handle non-team-preview
|
||||
[teams] = rest
|
||||
@receiveTeams(teams)
|
||||
view.preloadImages()
|
||||
if !@get('spectating')
|
||||
PokeBattle.notifyUser(PokeBattle.NotificationTypes.BATTLE_STARTED, @id)
|
||||
done()
|
||||
when Protocol.START_BATTLE
|
||||
view.removeTeamPreview()
|
||||
view.renderBattle()
|
||||
done()
|
||||
when Protocol.REARRANGE_TEAMS
|
||||
arrangements = rest
|
||||
@get('teams').forEach (team, i) ->
|
||||
team.rearrange(arrangements[i])
|
||||
done()
|
||||
when Protocol.RECEIVE_TEAM
|
||||
[team] = rest
|
||||
@receiveTeam(team)
|
||||
done()
|
||||
when Protocol.SPRITE_CHANGE
|
||||
[player, slot, newSpecies, newForme] = rest
|
||||
pokemon = @getPokemon(player, slot)
|
||||
pokemon.set('species', newSpecies)
|
||||
pokemon.set('forme', newForme)
|
||||
view.changeSprite(player, slot, newSpecies, newForme, done)
|
||||
when Protocol.NAME_CHANGE
|
||||
[player, slot, newName] = rest
|
||||
pokemon = @getPokemon(player, slot)
|
||||
view.changeName(player, slot, newName, done)
|
||||
when Protocol.BOOSTS
|
||||
[player, slot, deltaBoosts] = rest
|
||||
view.boost(player, slot, deltaBoosts, floatText: true)
|
||||
done()
|
||||
when Protocol.SET_BOOSTS
|
||||
[player, slot, boosts] = rest
|
||||
view.setBoosts(player, slot, boosts)
|
||||
done()
|
||||
when Protocol.RESET_BOOSTS
|
||||
[player, slot] = rest
|
||||
view.resetBoosts(player, slot)
|
||||
done()
|
||||
when Protocol.MOVESET_UPDATE
|
||||
[player, slot, movesetJSON] = rest
|
||||
pokemon = @getPokemon(player, slot)
|
||||
pokemon.set(movesetJSON)
|
||||
done()
|
||||
when Protocol.WEATHER_CHANGE
|
||||
[newWeather] = rest
|
||||
view.changeWeather(newWeather, done)
|
||||
when Protocol.TEAM_PREVIEW
|
||||
view.renderTeamPreview()
|
||||
done()
|
||||
when Protocol.ACTIVATE_ABILITY
|
||||
[player, slot, ability] = rest
|
||||
pokemon = @getPokemon(player, slot)
|
||||
pokemon.set('ability', ability)
|
||||
view.activateAbility(player, slot, ability, done)
|
||||
when Protocol.END_MOVE
|
||||
doneSpeedTimeout()
|
||||
when Protocol.ILLUSION_CHANGE
|
||||
[player, slot, change] = rest
|
||||
pokemon = @getPokemon(player, slot)
|
||||
pokemon.setIllu(change)
|
||||
done()
|
||||
when Protocol.VISIBLE_TEAM
|
||||
view.setVisibleTeam()
|
||||
else
|
||||
done()
|
||||
catch e
|
||||
console.error(e)
|
||||
console.error(e.stack)
|
||||
done()
|
||||
if wasAtBottom && !view.chatView.isAtBottom()
|
||||
view.chatView.scrollToBottom()
|
|
@ -0,0 +1,2 @@
|
|||
@PokeBattle ?= {}
|
||||
@PokeBattle.mixins ?= {}
|
|
@ -0,0 +1,85 @@
|
|||
class Teams extends Backbone.Collection
|
||||
model: Team
|
||||
|
||||
class @Battle extends Backbone.AssociatedModel
|
||||
relations: [
|
||||
type: Backbone.Many
|
||||
key: 'teams'
|
||||
relatedModel: Team
|
||||
collectionType: Teams
|
||||
]
|
||||
|
||||
defaults:
|
||||
spectating: true
|
||||
finished: false
|
||||
|
||||
_.extend(this.prototype, PokeBattle.mixins.BattleProtocolParser)
|
||||
|
||||
initialize: (attributes) =>
|
||||
@updateQueue = []
|
||||
{@numActive, spectators} = attributes
|
||||
@spectators = new UserList(spectators) unless !spectators
|
||||
@set('generation', Formats[@get('format')].generation)
|
||||
@set('notifications', 0)
|
||||
@set('turn', 0)
|
||||
@set('teams', [{hidden: true}, {hidden: true}])
|
||||
@set('spectating', !@has('index'))
|
||||
@set('index', Math.floor(2 * Math.random())) unless @has('index')
|
||||
|
||||
receiveTeams: (receivedTeams) =>
|
||||
teams = @get('teams')
|
||||
for receivedTeam, i in receivedTeams
|
||||
receivedTeam.hidden = true
|
||||
team = teams.at(i)
|
||||
team.set(receivedTeam) if team.get('hidden')
|
||||
|
||||
receiveTeam: (team) =>
|
||||
teams = @get('teams')
|
||||
teams.at(@get('index')).unset('hidden', silent: true).set(team)
|
||||
|
||||
makeMove: (moveName, forSlot, callback) =>
|
||||
pokemon = @getPokemon(@get('index'), forSlot)
|
||||
options = {}
|
||||
options['megaEvolve'] = pokemon.get('megaEvolve') if pokemon.get('megaEvolve')
|
||||
PokeBattle.primus.send(
|
||||
'sendMove', @id, moveName, forSlot,
|
||||
@get('turn'), options, callback,
|
||||
)
|
||||
|
||||
makeSwitch: (toSlot, forSlot, callback) =>
|
||||
PokeBattle.primus.send(
|
||||
'sendSwitch', @id, toSlot, forSlot, @get('turn'), callback
|
||||
)
|
||||
|
||||
makeCancel: =>
|
||||
PokeBattle.primus.send 'sendCancelAction', @id, @get('turn')
|
||||
|
||||
arrangeTeam: (arrangement) =>
|
||||
PokeBattle.primus.send 'arrangeTeam', @id, arrangement
|
||||
|
||||
switch: (fromIndex, toIndex) =>
|
||||
you = @getTeam().pokemon
|
||||
[you[fromIndex], you[toIndex]] = [you[toIndex], you[fromIndex]]
|
||||
|
||||
getTeam: (playerIndex = @get('index')) =>
|
||||
@get("teams").at(playerIndex)
|
||||
|
||||
getOpponentTeam: (playerIndex = @get('index')) =>
|
||||
@get("teams").at(1 - playerIndex)
|
||||
|
||||
getPokemon: (playerIndex, slot = 0) =>
|
||||
team = @getTeam(playerIndex)
|
||||
team.at(slot)
|
||||
|
||||
isPlaying: =>
|
||||
!@get('finished') && !@get('spectating')
|
||||
|
||||
forfeit: =>
|
||||
PokeBattle.primus.send('forfeit', @id)
|
||||
|
||||
# TODO: Opponent switch. Use some logic to determine whether the switch is
|
||||
# to a previously seen Pokemon or a new Pokemon. In the latter case, we
|
||||
# should reveal a previously unknown Pokeball if it's not a Wi-Fi battle.
|
||||
|
||||
notify: =>
|
||||
@set('notifications', @get('notifications') + 1)
|
|
@ -0,0 +1,350 @@
|
|||
class @Pokemon extends Backbone.Model
|
||||
defaults: =>
|
||||
moves: []
|
||||
percent: 100
|
||||
ivs:
|
||||
hp: 31
|
||||
attack: 31
|
||||
defense: 31
|
||||
specialAttack: 31
|
||||
specialDefense: 31
|
||||
speed: 31
|
||||
evs:
|
||||
hp: 0
|
||||
attack: 0
|
||||
defense: 0
|
||||
specialAttack: 0
|
||||
specialDefense: 0
|
||||
speed: 0
|
||||
|
||||
initialize: (attributes={}) ->
|
||||
# History lesson: We stored species under `name`. Now that we support
|
||||
# nicknames, we need the `name` freed up. However, teams are saved to the
|
||||
# server using the old scheme. Therefore we need to do a simple check for
|
||||
# the existence of `species`; if it exists, do nothing. If not, use `name`.
|
||||
@set('species', @get('name')) if !@has('species') && @has('name')
|
||||
@set('forme', 'default') unless @has('forme')
|
||||
@set('illu', false)
|
||||
@normalizeStats(@get('ivs'), 31)
|
||||
@normalizeStats(@get('evs'), 0)
|
||||
@resetBoosts()
|
||||
@isNull = false
|
||||
|
||||
# Skip teambuilder-specific properties.
|
||||
return if @get('teambuilder') != true
|
||||
|
||||
@on 'change:ivs', (model, ivs)=>
|
||||
type = HiddenPower.BW.type(ivs).toLowerCase()
|
||||
@set("hiddenPowerType", type, silent: true)
|
||||
|
||||
@on 'change:hiddenPowerType', (model, type) =>
|
||||
hpIVs = HiddenPower.BW.ivs[type.toLowerCase()]
|
||||
ivs = @get('ivs')
|
||||
for stat, iv of ivs
|
||||
ivs[stat] = hpIVs[stat] || 31
|
||||
@set('ivs', ivs, silent: true)
|
||||
|
||||
@set('ability', @getAbilities()[0]) unless attributes.ability
|
||||
@set('level', 100) unless attributes.level
|
||||
@set('happiness', 100) if isNaN(attributes.happiness)
|
||||
@set('nature', 'Hardy') unless attributes.nature
|
||||
hiddenPowerType = HiddenPower.BW.type(@get('ivs')).toLowerCase()
|
||||
@set('hiddenPowerType', hiddenPowerType, silent: true)
|
||||
|
||||
# If there is no gender set and only one possiblity, set the gender
|
||||
unless @has('gender')
|
||||
genders = @getGenders()
|
||||
@set('gender', genders[0], silent: true) if genders.length == 1
|
||||
|
||||
resetBoosts: ->
|
||||
@set 'stages',
|
||||
hp: 0
|
||||
attack: 0
|
||||
defense: 0
|
||||
specialAttack: 0
|
||||
specialDefense: 0
|
||||
speed: 0
|
||||
accuracy: 0
|
||||
evasion: 0
|
||||
|
||||
normalizeStats: (hash, defaultValue) ->
|
||||
stats = [ "hp", "attack", "defense", "specialAttack",
|
||||
"specialDefense", "speed"]
|
||||
for stat in stats
|
||||
hash[stat] ?= defaultValue
|
||||
|
||||
getName: ->
|
||||
sanitizedName = $('<div/>').text(@get('name')).html()
|
||||
sanitizedName = sanitizedName.replace(
|
||||
/[\u0300-\u036F\u20D0-\u20FF\uFE20-\uFE2F]/g, '')
|
||||
sanitizedName
|
||||
|
||||
getGeneration: (generation) ->
|
||||
gen = generation || @collection?.generation || DEFAULT_GENERATION
|
||||
gen = gen.toUpperCase()
|
||||
window.Generations[gen]
|
||||
|
||||
getSpecies: ->
|
||||
@getGeneration().SpeciesData[@get('species')]
|
||||
|
||||
getItem: ->
|
||||
@getGeneration().ItemData[@get('item')]
|
||||
|
||||
getForme: (forme, generation) ->
|
||||
forme ||= @get('forme')
|
||||
@getGeneration(generation).FormeData[@get('species')]?[forme]
|
||||
|
||||
getFormes: ->
|
||||
(forme for forme of @getGeneration().FormeData[@get('species')])
|
||||
|
||||
# Returns all non-battle only formes
|
||||
getSelectableFormes: ->
|
||||
_(@getFormes()).reject((forme) => @getForme(forme).isBattleOnly)
|
||||
|
||||
# Returns all mega formes
|
||||
getMegaFormes: ->
|
||||
_(@getFormes()).reject((forme) => forme.indexOf('mega') != 0)
|
||||
|
||||
getAbilities: ->
|
||||
forme = @getForme()
|
||||
abilities = _.clone(forme.abilities)
|
||||
abilities.push(forme.hiddenAbility) if forme.hiddenAbility
|
||||
_.unique(abilities)
|
||||
|
||||
getGenders: ->
|
||||
species = @getSpecies()
|
||||
genders = []
|
||||
switch species.genderRatio
|
||||
when -1
|
||||
genders.push("Genderless")
|
||||
when 0
|
||||
genders.push("M")
|
||||
when 8
|
||||
genders.push("F")
|
||||
else
|
||||
genders.push("M", "F")
|
||||
genders
|
||||
|
||||
hasSelectedMove: (moveName) ->
|
||||
moveName && moveName in @moves
|
||||
|
||||
getMovepool: ->
|
||||
{SpeciesData, MoveData} = @getGeneration()
|
||||
generation = GENERATION_TO_INT[@collection?.generation || DEFAULT_GENERATION]
|
||||
learnset = learnableMoves(window.Generations, @attributes, generation)
|
||||
|
||||
# Map each move name to a move object
|
||||
return _(learnset).map (moveName) ->
|
||||
console.log(moveName)
|
||||
move = _(MoveData[moveName]).clone()
|
||||
move['name'] = moveName
|
||||
move
|
||||
|
||||
getTotalEVs: (options = {}) ->
|
||||
total = 0
|
||||
for stat, value of @get("evs")
|
||||
total += value if stat != options.exclude
|
||||
total
|
||||
|
||||
getTeam: =>
|
||||
@collection?.parents[0]
|
||||
|
||||
setIv: (stat, value) ->
|
||||
ivs = _.clone(@get("ivs"))
|
||||
ivs[stat] = value
|
||||
@set("ivs", ivs) # trigger change event
|
||||
|
||||
setEv: (stat, value) ->
|
||||
evs = _.clone(@get("evs"))
|
||||
value = value - (value % 4)
|
||||
evs[stat] = value
|
||||
@set("evs", evs) # trigger change event
|
||||
value
|
||||
|
||||
iv: (stat) ->
|
||||
@get("ivs")[stat] ? 31
|
||||
|
||||
ev: (stat) ->
|
||||
@get("evs")[stat] ? 0
|
||||
|
||||
natureBoost: (stat) ->
|
||||
nature = @get('nature')?.toLowerCase()
|
||||
if nature of natures
|
||||
natures[nature][stat] || 1
|
||||
else
|
||||
1
|
||||
|
||||
base: (key) ->
|
||||
forme = @getForme()
|
||||
base = forme["stats"][key]
|
||||
|
||||
stat: (key) ->
|
||||
base = @base(key)
|
||||
return 1 if base == 1 # For Shedinja. key doesn't have to be hp.
|
||||
level = @get('level') || 100
|
||||
iv = @iv(key)
|
||||
ev = Math.floor(@ev(key) / 4)
|
||||
total = if key == 'hp'
|
||||
Math.floor((2 * base + iv + ev) * (level / 100) + level + 10)
|
||||
else
|
||||
Math.floor(((2 * base + iv + ev) * (level / 100) + 5) * @natureBoost(key))
|
||||
|
||||
# Returns the natures that this pokemon can use
|
||||
# The natures are returned as a list of [id, value] values
|
||||
# to populate a dropdown field.
|
||||
# TODO: Should this be needed in more places, return Nature objects instead
|
||||
getNatures: ->
|
||||
natureResults = []
|
||||
for nature, stats of natures
|
||||
name = nature[0].toUpperCase() + nature.substr(1)
|
||||
invertedStats = _(stats).invert()
|
||||
|
||||
label = name
|
||||
if invertedStats[PLUS]
|
||||
# This nature has an effect, so update the label
|
||||
plusStat = statAbbreviations[invertedStats[PLUS]]
|
||||
minusStat = statAbbreviations[invertedStats[MINUS]]
|
||||
label = "#{name} (+#{plusStat}, -#{minusStat})"
|
||||
|
||||
natureResults.push [name, label]
|
||||
return natureResults
|
||||
|
||||
getPBV: ->
|
||||
gen = @getGeneration()
|
||||
PokeBattle.PBV.determinePBV(gen, @attributes)
|
||||
|
||||
setPP: (moveIndex, newPP) ->
|
||||
array = _.clone(@get('pp'))
|
||||
array[moveIndex] = newPP
|
||||
@set('pp', array)
|
||||
|
||||
getPercentHP: ->
|
||||
Math.max(@get('percent'), 0)
|
||||
|
||||
getHPColor: ->
|
||||
percent = @getPercentHP()
|
||||
switch
|
||||
when percent <= 20 then 'red'
|
||||
when percent <= 50 then 'yellow'
|
||||
else 'green'
|
||||
|
||||
isFainted: ->
|
||||
@get('percent') <= 0
|
||||
|
||||
getStatus: ->
|
||||
status = @get('status')
|
||||
if status
|
||||
"#{status[0].toUpperCase()}#{status.substr(1)}"
|
||||
else
|
||||
"Healthy"
|
||||
|
||||
canMegaEvolve: ->
|
||||
# TODO: Refactor this to use getPossibleMegaForme()
|
||||
# I didn't feel like making the change and testing it while implementing getPossibleMegaForme()
|
||||
item = @getItem()
|
||||
return false if item.type != 'megastone'
|
||||
[ species, forme ] = item.mega
|
||||
return false if @get('species') != species || @get('forme') != 'default'
|
||||
return true
|
||||
|
||||
getPossibleMegaForme: ->
|
||||
item = @getItem()
|
||||
return null if item?.type != 'megastone'
|
||||
[ species, forme ] = item.mega
|
||||
return forme if @get('species') == species && @get('forme') == 'default'
|
||||
return null
|
||||
|
||||
# Returns the complete web address to the pokedex link for this pokemon.
|
||||
# For this project, this leads to our website at http://www.pokebattle.com,
|
||||
# but if you want it to lead somewhere else, edit this function.
|
||||
getPokedexUrl: ->
|
||||
# todo: move this function to /shared, or use an actual slugify library
|
||||
slugify = (str) ->
|
||||
str.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/\-{2,}/g, '-')
|
||||
|
||||
slugSpecies = slugify(@get('species'))
|
||||
slugForme = slugify(@get('forme'))
|
||||
"//pokebattle.com/dex/pokemon/#{slugSpecies}/#{slugForme}"
|
||||
|
||||
getIllu: ->
|
||||
@get('illu')
|
||||
|
||||
setIllu: (y) ->
|
||||
@set('illu', y)
|
||||
|
||||
toJSON: ->
|
||||
attributes = _.clone(@attributes)
|
||||
delete attributes.gender if attributes.gender == 'Genderless'
|
||||
delete attributes.hiddenPowerType
|
||||
delete attributes.teambuilder
|
||||
attributes
|
||||
|
||||
# TODO: These shortenings really should be stored somewhere else.
|
||||
statAbbreviations =
|
||||
'hp' : 'HP'
|
||||
'attack' : 'Atk'
|
||||
'defense' : 'Def'
|
||||
'specialAttack' : 'SAtk'
|
||||
'specialDefense' : 'SDef'
|
||||
'speed' : 'Spe'
|
||||
|
||||
# A hash that keys a nature with the stats that it boosts.
|
||||
# Neutral natures are ignored.
|
||||
# TODO: .yml-ify these.
|
||||
PLUS = 1.1
|
||||
MINUS = 0.9
|
||||
natures =
|
||||
lonely: {attack: PLUS, defense: MINUS}
|
||||
brave: {attack: PLUS, speed: MINUS}
|
||||
adamant: {attack: PLUS, specialAttack: MINUS}
|
||||
naughty: {attack: PLUS, specialDefense: MINUS}
|
||||
bold: {defense: PLUS, attack: MINUS}
|
||||
relaxed: {defense: PLUS, speed: MINUS}
|
||||
impish: {defense: PLUS, specialAttack: MINUS}
|
||||
lax: {defense: PLUS, specialDefense: MINUS}
|
||||
timid: {speed: PLUS, attack: MINUS}
|
||||
hasty: {speed: PLUS, defense: MINUS}
|
||||
jolly: {speed: PLUS, specialAttack: MINUS}
|
||||
naive: {speed: PLUS, specialDefense: MINUS}
|
||||
modest: {specialAttack: PLUS, attack: MINUS}
|
||||
mild: {specialAttack: PLUS, defense: MINUS}
|
||||
quiet: {specialAttack: PLUS, speed: MINUS}
|
||||
rash: {specialAttack: PLUS, specialDefense: MINUS}
|
||||
calm: {specialDefense: PLUS, attack: MINUS}
|
||||
gentle: {specialDefense: PLUS, defense: MINUS}
|
||||
sassy: {specialDefense: PLUS, speed: MINUS}
|
||||
careful: {specialDefense: PLUS, specialAttack: MINUS}
|
||||
hardy: {}
|
||||
docile: {}
|
||||
serious: {}
|
||||
bashful: {}
|
||||
quirky: {}
|
||||
|
||||
class @NullPokemon extends Pokemon
|
||||
initialize: ->
|
||||
@set('species', null)
|
||||
@set('forme', 'default')
|
||||
@isNull = true
|
||||
|
||||
getNatures: -> []
|
||||
getPBV: -> 0
|
||||
base: -> 0
|
||||
stat: -> null
|
||||
iv: -> null
|
||||
ev: -> null
|
||||
|
||||
getSpecies: ->
|
||||
id: 0
|
||||
genderRatio: -1
|
||||
generation: 1
|
||||
|
||||
getForme: ->
|
||||
@getFormes()['default']
|
||||
|
||||
getFormes: ->
|
||||
default:
|
||||
abilities: []
|
||||
hiddenAbility: null
|
||||
isBattleOnly: false
|
||||
learnset: {}
|
||||
types: []
|
|
@ -0,0 +1,128 @@
|
|||
class PokemonCollection extends Backbone.Collection
|
||||
model: (attrs, options) =>
|
||||
# History lesson: We stored species under `name`. Now that we support
|
||||
# nicknames, we need the `name` freed up. However, teams are saved to the
|
||||
# server using the old scheme. Therefore we need to do a simple check for
|
||||
# the existence of `species`; if it exists, do nothing. If not, use `name`.
|
||||
if attrs.name || attrs.species
|
||||
attrs.teambuilder = @parents[0].get('teambuilder')
|
||||
return new Pokemon(attrs, options)
|
||||
else
|
||||
return new NullPokemon()
|
||||
|
||||
class @Team extends Backbone.AssociatedModel
|
||||
relations: [
|
||||
type: Backbone.Many
|
||||
key: 'pokemon'
|
||||
collectionType: PokemonCollection
|
||||
]
|
||||
|
||||
initialize: (attrs={}, options={}) =>
|
||||
@owner = attrs.owner
|
||||
@set('generation', DEFAULT_GENERATION) unless attrs.generation
|
||||
@set('teambuilder', true) if options.teambuilder
|
||||
@set('pokemon', []) unless attrs.pokemon
|
||||
|
||||
getName: =>
|
||||
@get('name') || "Untitled team"
|
||||
|
||||
toJSON: =>
|
||||
json = {}
|
||||
json['id'] = @id if @id
|
||||
json['name'] = @get('name')
|
||||
json['generation'] = @get('generation')
|
||||
json['pokemon'] = @get('pokemon').toJSON()
|
||||
json
|
||||
|
||||
# Returns the pokemon at a particular index. Delegates to the internal pokemon collection
|
||||
at: (idx) => @get('pokemon').at(idx)
|
||||
|
||||
# Returns which index the pokemon is in
|
||||
indexOf: (idx) => @get('pokemon').indexOf(idx)
|
||||
|
||||
# Replace a pokemon at a particular index for another
|
||||
replace: (idx, newPokemon) =>
|
||||
@get('pokemon').remove(@get('pokemon').at(idx))
|
||||
@get('pokemon').add(newPokemon, at: idx)
|
||||
|
||||
# Equivalent to toJSON, but omits NullPokemon
|
||||
toNonNullJSON: =>
|
||||
id: @id
|
||||
name: @get('name')
|
||||
generation: @get('generation')
|
||||
pokemon: @get('pokemon')
|
||||
.reject((pokemon) -> pokemon.isNull)
|
||||
.map((pokemon) -> pokemon.toJSON())
|
||||
|
||||
clone: =>
|
||||
attrs = _(@attributes).clone()
|
||||
attrs.pokemon = @get('pokemon').toJSON()
|
||||
new Team(attrs)
|
||||
|
||||
rearrange: (arrangement) ->
|
||||
pokemon = @get('pokemon')
|
||||
pokemon.reset((pokemon.models[index] for index in arrangement))
|
||||
return true
|
||||
|
||||
getFormat: =>
|
||||
format = @get('generation') # TODO: Migrate to format
|
||||
format = DEFAULT_FORMAT if format not of Formats
|
||||
Formats[format]
|
||||
|
||||
getGeneration: (generation) ->
|
||||
gen = generation || @getFormat().generation
|
||||
gen = gen.toUpperCase()
|
||||
window.Generations[gen]
|
||||
|
||||
getPBV: =>
|
||||
gen = @getGeneration()
|
||||
pokemon = @get('pokemon').toJSON()
|
||||
PokeBattle.PBV.determinePBV(gen, pokemon)
|
||||
|
||||
getMaxPBV: =>
|
||||
{conditions} = @getFormat()
|
||||
if Conditions.PBV_1000 in conditions
|
||||
1000
|
||||
else if Conditions.PBV_500 in conditions
|
||||
500
|
||||
else
|
||||
0
|
||||
|
||||
hasPBV: =>
|
||||
@getMaxPBV() > 0
|
||||
|
||||
getNonNullPokemon: =>
|
||||
@get('pokemon').where(isNull: false)
|
||||
|
||||
hasNonNullPokemon: =>
|
||||
@get('pokemon').some((pokemon) -> not pokemon.isNull)
|
||||
|
||||
sync: (method) =>
|
||||
switch method
|
||||
when 'create', 'patch', 'update'
|
||||
@set('saving', true, silent: true)
|
||||
@trigger('remoteSync', this)
|
||||
PokeBattle.primus.send 'saveTeam', @toJSON(), (id) =>
|
||||
# Note: If this model is saved multiple times, then this won't
|
||||
# tell you if some of the saves failed.
|
||||
@set('id', id)
|
||||
@set('saving', false, silent: true)
|
||||
@trigger('remoteSync', this)
|
||||
when 'delete'
|
||||
PokeBattle.primus.send('destroyTeam', @id) if @id
|
||||
|
||||
getRandomOrder: =>
|
||||
order = @get('randomorder')
|
||||
if typeof order is 'undefined'
|
||||
@setRandomOrder()
|
||||
order = @get('randomorder')
|
||||
order
|
||||
else
|
||||
order = @get('randomorder')
|
||||
order
|
||||
|
||||
setRandomOrder: =>
|
||||
#random order for the icons with team preview when in battle
|
||||
team = @get('pokemon').toJSON()
|
||||
teamshuffled = _.shuffle(team)
|
||||
@set('randomorder', teamshuffled)
|
|
@ -0,0 +1,50 @@
|
|||
MAX_LOG_LENGTH = 50
|
||||
|
||||
class @PrivateMessage extends Backbone.Model
|
||||
initialize: =>
|
||||
@loadLog()
|
||||
@set('notifications', 0)
|
||||
|
||||
add: (username, message, opts = {}) =>
|
||||
@set('notifications', @get('notifications') + 1) if username == @id
|
||||
@trigger("receive", this, @id, username, message, opts)
|
||||
|
||||
log = @get('log')
|
||||
log.push({username, message, opts})
|
||||
|
||||
# Trim the log size. Use 2x log length to reduce how often this happens
|
||||
if log.length > (2 * MAX_LOG_LENGTH)
|
||||
log.splice(0, log.length - MAX_LOG_LENGTH)
|
||||
|
||||
@saveLog()
|
||||
|
||||
openChallenge: (args...) =>
|
||||
@trigger("openChallenge", args...)
|
||||
|
||||
cancelChallenge: (args...) =>
|
||||
@trigger("cancelChallenge", args...)
|
||||
|
||||
closeChallenge: (args...) =>
|
||||
@trigger("closeChallenge", args...)
|
||||
|
||||
getLog: =>
|
||||
log = @get('log')
|
||||
if log.length > 50
|
||||
log.splice(0, log.length - 50)
|
||||
return log
|
||||
|
||||
loadLog: =>
|
||||
try
|
||||
log = JSON.parse(window.localStorage.getItem(@logKey())) || []
|
||||
@set('log', log)
|
||||
catch
|
||||
@set('log', [])
|
||||
|
||||
saveLog: =>
|
||||
try
|
||||
window.localStorage.setItem(@logKey(), JSON.stringify(@getLog()))
|
||||
|
||||
logKey: =>
|
||||
key = [ @id, PokeBattle.username ]
|
||||
key.sort()
|
||||
key.join(':')
|
|
@ -0,0 +1,20 @@
|
|||
class @Room extends Backbone.AssociatedModel
|
||||
relations: [
|
||||
type: Backbone.Many
|
||||
key: 'users'
|
||||
relatedModel: 'User'
|
||||
collectionType: 'UserList'
|
||||
]
|
||||
|
||||
EVENTS: "userMessage rawMessage announce clear setTopic".split(/\s+/)
|
||||
|
||||
for eventName in this::EVENTS
|
||||
do (eventName) =>
|
||||
this::[eventName] = (args...) ->
|
||||
@trigger(eventName, args...)
|
||||
|
||||
sendChat: (message) ->
|
||||
return false unless message?.replace(/\s+$/).length > 0
|
||||
if !PokeBattle.commands.execute(this, message)
|
||||
PokeBattle.primus.send('sendChat', @id, message)
|
||||
return true
|
|
@ -0,0 +1,16 @@
|
|||
AuthorityMap =
|
||||
"1": ""
|
||||
"2": "+"
|
||||
"3": "%"
|
||||
"4": "@"
|
||||
"5": "~"
|
||||
|
||||
class @User extends Backbone.Model
|
||||
initialize: (attributes) =>
|
||||
|
||||
getDisplayName: =>
|
||||
authorityString = AuthorityMap[@get('authority')] ? ""
|
||||
"#{authorityString}#{@get('id')}"
|
||||
|
||||
isAlt: =>
|
||||
@get('isAlt')
|
|
@ -0,0 +1,19 @@
|
|||
class @Replay extends Backbone.Model
|
||||
urlRoot: '/replays'
|
||||
|
||||
getFormat: ->
|
||||
window.Formats[@get('format')].humanName
|
||||
|
||||
getCreatedAt: ->
|
||||
date = new Date(@get('created_at'))
|
||||
day = date.getDate()
|
||||
month = date.getMonth() + 1
|
||||
year = date.getFullYear()
|
||||
timeOfDay = 'a.m.'
|
||||
hours = date.getHours()
|
||||
minutes = "00#{date.getMinutes()}"[-2...]
|
||||
seconds = "00#{date.getSeconds()}"[-2...]
|
||||
if hours > 12
|
||||
hours = hours % 12
|
||||
timeOfDay = 'p.m.'
|
||||
"#{year}/#{month}/#{day} #{hours}:#{minutes}:#{seconds} #{timeOfDay}"
|
|
@ -0,0 +1,14 @@
|
|||
class @BattleListView extends Backbone.View
|
||||
template: JST['battle_list']
|
||||
|
||||
initialize: (attributes) =>
|
||||
@battles = []
|
||||
@render()
|
||||
|
||||
refreshList: =>
|
||||
PokeBattle.primus.send "getBattleList", (battles) =>
|
||||
@battles = _(battles).sortBy((battle) => battle[3])
|
||||
@render()
|
||||
|
||||
render: =>
|
||||
@$el.html @template(battles: @battles)
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,244 @@
|
|||
class @ChatView extends Backbone.View
|
||||
template: JST['chat']
|
||||
userListTemplate: JST['user_list']
|
||||
|
||||
events:
|
||||
'click': 'focusChat'
|
||||
'keydown .chat_input': 'handleKeys'
|
||||
'click .chat_input_send': 'sendChat'
|
||||
'scroll_to_bottom': 'scrollToBottom'
|
||||
|
||||
MAX_USERNAME_HISTORY = 10
|
||||
MAX_MESSAGES_LENGTH = 500
|
||||
|
||||
# Takes a `model`, which is a Room instance.
|
||||
initialize: (options) =>
|
||||
{@noisy} = options
|
||||
if @model
|
||||
@listenTo(@model.get('users'), 'add remove reset', @renderUserList)
|
||||
if @noisy
|
||||
@listenTo(@model.get('users'), 'add', @userJoin)
|
||||
@listenTo(@model.get('users'), 'remove', @userLeave)
|
||||
for eventName in Room::EVENTS
|
||||
callback = this[eventName] || throw new Error("ChatView must implement #{eventName}.")
|
||||
@listenTo(@model, eventName, callback)
|
||||
@chatHistory = []
|
||||
@mostRecentNames = []
|
||||
@tabCompleteIndex = -1
|
||||
@tabCompleteNames = []
|
||||
|
||||
# Sets the channel topic
|
||||
setTopic: (topic) =>
|
||||
topic = @sanitize(topic)
|
||||
@rawMessage("<div class='alert alert-info'><b>Topic:</b> #{topic}</div>")
|
||||
|
||||
render: =>
|
||||
@$el.html @template()
|
||||
if @model
|
||||
@$el.removeClass('without_spectators')
|
||||
@$el.removeClass('without_chat_input')
|
||||
@renderUserList()
|
||||
this
|
||||
|
||||
renderUserList: =>
|
||||
@$('.user_count').text "Users (#{@model.get('users').length})"
|
||||
@$('.users').html @userListTemplate(userList: @model.get('users').models)
|
||||
this
|
||||
|
||||
getSelectedText: =>
|
||||
text = ""
|
||||
if window.getSelection
|
||||
text = window.getSelection().toString()
|
||||
else if document.selection && document.selection.type != "Control"
|
||||
text = document.selection.createRange().text
|
||||
return text
|
||||
|
||||
focusChat: =>
|
||||
selectedText = @getSelectedText()
|
||||
@$('.chat_input').focus() if selectedText.length == 0
|
||||
|
||||
sendChat: =>
|
||||
$this = @$('.chat_input')
|
||||
message = $this.val()
|
||||
if @model.sendChat(message)
|
||||
@chatHistory.push(message)
|
||||
delete @chatHistoryIndex
|
||||
$this.val('')
|
||||
|
||||
tabComplete: ($input, options = {}) =>
|
||||
cursorIndex = $input.prop('selectionStart')
|
||||
text = $input.val()
|
||||
if @tabCompleteNames.length > 0 && @tabCompleteCursorIndex == cursorIndex
|
||||
if options.reverse
|
||||
@tabCompleteIndex -= 1
|
||||
if @tabCompleteIndex < 0
|
||||
@tabCompleteIndex = @tabCompleteNames.length - 1
|
||||
else
|
||||
@tabCompleteIndex = (@tabCompleteIndex + 1) % @tabCompleteNames.length
|
||||
else
|
||||
delete @tabCompleteCursorIndex
|
||||
pieces = text[0...cursorIndex].split(' ')
|
||||
possibleName = pieces.pop()
|
||||
rest = pieces.join(' ')
|
||||
rest += ' ' if pieces.length > 0 # Append a space if a word exists
|
||||
length = possibleName.length
|
||||
return if length == 0
|
||||
candidates = _.union(@mostRecentNames, @model.get('users').pluck('id'))
|
||||
candidates = candidates.filter (name) ->
|
||||
name[...length].toLowerCase() == possibleName.toLowerCase()
|
||||
return if candidates.length == 0
|
||||
if options.reverse
|
||||
@tabCompleteIndex = candidates.length - 1
|
||||
else
|
||||
@tabCompleteIndex = 0
|
||||
@tabCompleteNames = candidates
|
||||
@tabCompletePrefix = rest
|
||||
@tabCompleteCursorIndex = cursorIndex
|
||||
tabbedName = @tabCompleteNames[@tabCompleteIndex]
|
||||
newPrefix = @tabCompletePrefix + tabbedName
|
||||
newPrefixLength = newPrefix.length
|
||||
$input.val(newPrefix + text[cursorIndex...])
|
||||
$input[0].setSelectionRange(newPrefixLength, newPrefixLength)
|
||||
@tabCompleteCursorIndex = newPrefixLength
|
||||
|
||||
handleKeys: (e) =>
|
||||
$input = $(e.currentTarget)
|
||||
switch e.which
|
||||
when 13 # [Enter]
|
||||
e.preventDefault()
|
||||
@sendChat()
|
||||
when 9 # [Tab]
|
||||
e.preventDefault()
|
||||
@tabComplete($input, reverse: e.shiftKey)
|
||||
when 38 # [Up arrow]
|
||||
e.preventDefault()
|
||||
return if @chatHistory.length == 0
|
||||
if !@chatHistoryIndex?
|
||||
@chatHistoryIndex = @chatHistory.length
|
||||
@chatHistoryText = $input.val()
|
||||
if @chatHistoryIndex > 0
|
||||
@chatHistoryIndex -= 1
|
||||
$input.val(@chatHistory[@chatHistoryIndex])
|
||||
when 40 # [Down arrow]
|
||||
e.preventDefault()
|
||||
return unless @chatHistoryIndex?
|
||||
@chatHistoryIndex += 1
|
||||
if @chatHistoryIndex == @chatHistory.length
|
||||
$input.val(@chatHistoryText)
|
||||
delete @chatHistoryIndex
|
||||
else
|
||||
$input.val(@chatHistory[@chatHistoryIndex])
|
||||
|
||||
userMessage: (username, message) =>
|
||||
user = @model.get('users').get(username)
|
||||
displayName = user?.getDisplayName() || username
|
||||
yourName = PokeBattle.username
|
||||
highlight = (new RegExp("\\b#{yourName}\\b", 'i').test(message))
|
||||
|
||||
# Render the chat message
|
||||
u = "<b class='open_pm fake_link' data-user-id='#{username}'
|
||||
style='color: #{@userColor(username)}'>#{displayName}:</b>"
|
||||
@rawMessage("#{@timestamp()} #{u} #{@sanitize(message)}", {highlight})
|
||||
|
||||
# We might want to run something based on the message, e.g. !pbv from a mod.
|
||||
@handleMessage(user, message)
|
||||
|
||||
# Record last few usernames who chatted
|
||||
index = @mostRecentNames.indexOf(username)
|
||||
@mostRecentNames.splice(index, 1) if index != -1
|
||||
@mostRecentNames.push(username)
|
||||
@mostRecentNames.shift() if @mostRecentNames.length > MAX_USERNAME_HISTORY
|
||||
|
||||
userColor: (username) =>
|
||||
# Same hashing algorithm as in Java
|
||||
hash = 0
|
||||
for c, i in username
|
||||
chr = username.charCodeAt(i)
|
||||
hash = ((hash << 5) - hash) + chr
|
||||
hash |= 0
|
||||
|
||||
h = hash % 360
|
||||
hash /= 360
|
||||
s = (hash % 25) + 75
|
||||
l = 50
|
||||
"hsl(#{h}, #{s}%, #{l}%)"
|
||||
|
||||
handleMessage: (user, message) =>
|
||||
authority = user?.get('authority')
|
||||
printableCommands = ['/pbv', '/data']
|
||||
# TODO: no magic constants. '1' is a regular user.
|
||||
if authority > 1 && message.split(/\s/, 1)[0] in printableCommands
|
||||
PokeBattle.commands.execute(@model, message)
|
||||
|
||||
userJoin: (user) =>
|
||||
@rawMessage("#{@timestamp()} #{user.id} joined!")
|
||||
|
||||
userLeave: (user) =>
|
||||
@rawMessage("#{@timestamp()} #{user.id} left!")
|
||||
|
||||
rawMessage: (message, options = {}) =>
|
||||
wasAtBottom = @isAtBottom()
|
||||
klass = []
|
||||
klass.push('bg-blue') if options.highlight
|
||||
klass.push(options.class) if options.class
|
||||
@print("<p class='chat_message #{klass.join(' ')}'>#{message}</p>")
|
||||
@cleanChat()
|
||||
if wasAtBottom then @scrollToBottom()
|
||||
|
||||
cleanChat: =>
|
||||
$messages = @$('.chat_message')
|
||||
numToRemove = ($messages.length - MAX_MESSAGES_LENGTH)
|
||||
if numToRemove > 0
|
||||
$messages.slice(0, numToRemove).remove()
|
||||
|
||||
announce: (klass, message) =>
|
||||
wasAtBottom = @isAtBottom()
|
||||
message = @linkify(message)
|
||||
@print("<div class='alert alert-#{klass} clearfix'>#{message}</div>")
|
||||
if wasAtBottom then @scrollToBottom()
|
||||
|
||||
print: (message) =>
|
||||
@$('.messages').append(message)
|
||||
|
||||
clear: =>
|
||||
@$('.messages').empty()
|
||||
|
||||
timestamp: =>
|
||||
date = new Date()
|
||||
hours = date.getHours()
|
||||
minutes = date.getMinutes()
|
||||
seconds = date.getSeconds()
|
||||
|
||||
minutes = "00#{minutes}".substr(-2)
|
||||
seconds = "00#{seconds}".substr(-2)
|
||||
"<span class='monospace'>[#{hours}:#{minutes}:#{seconds}]</span>"
|
||||
|
||||
# Escapes all HTML, but also converts links to clickable links.
|
||||
sanitize: (message) =>
|
||||
sanitizedMessage = $('<div/>').text(message).html()
|
||||
@linkify(sanitizedMessage)
|
||||
|
||||
linkify: (message) =>
|
||||
message = URI.withinString message, (url) ->
|
||||
uri = URI(url)
|
||||
[host, path] = [uri.host(), uri.path()]
|
||||
battleRegex = /^\/battles\/([a-fA-F0-9]+)$/i
|
||||
$a = $("<a/>").prop('href', url).prop('target', '_blank').text(url)
|
||||
|
||||
if host == URI(window.location.href).host() && battleRegex.test(path)
|
||||
battleId = path.match(battleRegex)[1]
|
||||
$a.addClass('spectate').attr('data-battle-id', battleId)
|
||||
|
||||
return $a.wrap("<div/>").parent().html()
|
||||
message
|
||||
|
||||
# Returns true if the chat is scrolled to the bottom of the screen.
|
||||
# This also returns true if the messages are hidden.
|
||||
isAtBottom: =>
|
||||
$el = @$('.messages')
|
||||
($el[0].scrollHeight - $el.scrollTop() <= $el.outerHeight())
|
||||
|
||||
scrollToBottom: =>
|
||||
messages = @$('.messages')[0]
|
||||
messages.scrollTop = messages.scrollHeight
|
||||
false
|
|
@ -0,0 +1,259 @@
|
|||
class @PrivateMessagesView extends Backbone.View
|
||||
messageTemplate: JST['private_message']
|
||||
|
||||
events:
|
||||
"keypress .chat_input" : "keyPressEvent"
|
||||
"keyup .chat_input" : "keyUpEvent"
|
||||
"click .challenge_button, .cancel_challenge" : "toggleChallengeEvent"
|
||||
"click .popup_messages" : "focusChatEvent"
|
||||
"click .title_minimize" : "minimizePopupEvent"
|
||||
"click .title_close" : "closePopupEvent"
|
||||
"challenge .popup" : "sendChallengeEvent"
|
||||
"cancelChallenge .popup" : "challengeCanceledEvent"
|
||||
"focus .popup" : "focusPopupEvent"
|
||||
|
||||
initialize: =>
|
||||
@listenTo(@collection, 'open', @createPopup)
|
||||
@listenTo(@collection, 'focus', @focusPopup)
|
||||
@listenTo(@collection, 'receive', @receiveMessage)
|
||||
@listenTo(@collection, 'close', @closePopup)
|
||||
@listenTo(@collection, 'minimize', @minimizePopup)
|
||||
@listenTo(@collection, 'show', @showPopup)
|
||||
@listenTo(@collection, 'openChallenge', @openChallenge)
|
||||
@listenTo(@collection, 'cancelChallenge', @cancelChallenge)
|
||||
@listenTo(@collection, 'closeChallenge', @closeChallenge)
|
||||
@listenTo(@collection, 'focus show', @resetNotifications)
|
||||
|
||||
# @listenTo(PokeBattle.userList, 'add', @notifyJoin)
|
||||
# @listenTo(PokeBattle.userList, 'remove', @notifyLeave)
|
||||
|
||||
createPopup: (message) =>
|
||||
title = id = message.id
|
||||
$html = @$findPopup(id)
|
||||
if @$findPopup(id).length == 0
|
||||
$html = $(@messageTemplate({window, id, title}))
|
||||
|
||||
@$el.append($html)
|
||||
@positionPopup($html, @$(".popup:visible").length - 1)
|
||||
@addLogMessages($html, message.getLog())
|
||||
$html
|
||||
|
||||
focusPopup: (message) =>
|
||||
id = message.id
|
||||
$popup = @$findPopup(id)
|
||||
$popup.find('.chat_input').focus()
|
||||
|
||||
closePopup: (message) =>
|
||||
username = message.id
|
||||
@$findPopup(username).remove()
|
||||
@repositionPopups()
|
||||
|
||||
minimizePopup: (message) =>
|
||||
username = message.id
|
||||
$popup = @$findPopup(username)
|
||||
$popup.addClass('hidden')
|
||||
@repositionPopups()
|
||||
|
||||
showPopup: (message) =>
|
||||
username = message.id
|
||||
$popup = @$findPopup(username)
|
||||
@$el.append($popup)
|
||||
$popup.removeClass('hidden')
|
||||
@scrollToBottom($popup)
|
||||
@repositionPopups()
|
||||
|
||||
addMessage: ($popup, message) =>
|
||||
$messages = $popup.find('.popup_messages')
|
||||
$messages.append(message)
|
||||
|
||||
# todo: make this and receiveMessage construct messages from a common source
|
||||
addLogMessages: ($popup, log) =>
|
||||
messageHtml = ""
|
||||
for {username, message, opts} in log
|
||||
message = _.escape(message)
|
||||
username = "Me" if username == PokeBattle.username
|
||||
if opts.type in [ 'error', 'alert' ]
|
||||
messageHtml += "<p class='grey'>#{message}</p>"
|
||||
else
|
||||
messageHtml += "<p class='grey'><strong>#{username}:</strong> #{message}</p>"
|
||||
|
||||
@addMessage($popup, messageHtml)
|
||||
@scrollToBottom($popup)
|
||||
|
||||
# todo: make this and addLogMessages construct messages from a common source
|
||||
receiveMessage: (messageModel, messageId, username, message, options) =>
|
||||
message = _.escape(message)
|
||||
$popup = @$findOrCreatePopup(messageId)
|
||||
wasAtBottom = @isAtBottom($popup)
|
||||
username = "Me" if username == PokeBattle.username
|
||||
if options.type == 'error'
|
||||
@addMessage($popup, "<p class='red italic'>#{message}</p>")
|
||||
else if options.type == 'alert'
|
||||
@addMessage($popup, "<p class='yellow italic'>#{message}</p>")
|
||||
else
|
||||
if username != "Me" && !$popup.find('.chat_input').is(":focus")
|
||||
$popup.addClass('new_message')
|
||||
PokeBattle.notifyUser(PokeBattle.NotificationTypes.PRIVATE_MESSAGE, username)
|
||||
else
|
||||
@resetNotifications(messageModel)
|
||||
@addMessage($popup, "<p><strong>#{username}:</strong> #{message}</p>")
|
||||
if wasAtBottom then @scrollToBottom($popup)
|
||||
|
||||
openChallenge: (messageId, generation, conditions) =>
|
||||
$popup = @$findOrCreatePopup(messageId)
|
||||
$challenge = @createChallenge($popup, generation, conditions)
|
||||
if generation
|
||||
$challenge.find('.is_not_challenger').addClass('hidden')
|
||||
$challenge.find('.is_challenger').removeClass('hidden')
|
||||
|
||||
cancelChallenge: (messageId) =>
|
||||
$popup = @$findOrCreatePopup(messageId)
|
||||
$challenge = $popup.find('.challenge')
|
||||
$challenge.find('.icon-spinner').addClass('hidden')
|
||||
$challenge.find('.send_challenge, .select').removeClass('disabled')
|
||||
$challenge.find('.challenge_text').text("Challenge")
|
||||
$challenge.find(".cancel_challenge").text('Close')
|
||||
|
||||
closeChallenge: (messageId) =>
|
||||
$popup = @$findOrCreatePopup(messageId)
|
||||
$challenge = $popup.find('.challenge')
|
||||
$challenge.addClass('hidden')
|
||||
$popup.find('.popup_messages').removeClass('small')
|
||||
|
||||
resetNotifications: (message) =>
|
||||
message.set('notifications', 0)
|
||||
|
||||
notifyJoin: (user) =>
|
||||
message = @collection.get(user.id)
|
||||
return unless @isOpen(message)
|
||||
message?.add(user.id, "#{user.id} is now online!", type: "alert")
|
||||
|
||||
notifyLeave: (user) =>
|
||||
message = @collection.get(user.id)
|
||||
return unless @isOpen(message)
|
||||
message?.add(user.id, "#{user.id} is now offline.", type: "alert")
|
||||
|
||||
isOpen: (message) =>
|
||||
message && @$findPopup(message.id).length > 0
|
||||
|
||||
# Returns true if the chat is scrolled to the bottom of the screen.
|
||||
# This also returns true if the messages are hidden.
|
||||
isAtBottom: ($popup) =>
|
||||
$el = $popup.find('.popup_messages')
|
||||
($el[0].scrollHeight - $el.scrollTop() <= $el.outerHeight())
|
||||
|
||||
scrollToBottom: ($popup) =>
|
||||
messages = $popup.find('.popup_messages')[0]
|
||||
return unless messages
|
||||
messages.scrollTop = messages.scrollHeight
|
||||
false
|
||||
|
||||
positionPopup: ($popup, index) =>
|
||||
leftOffset = $('#content').position().left
|
||||
$popup.css(left: leftOffset + index * $popup.outerWidth(true))
|
||||
|
||||
repositionPopups: =>
|
||||
@$(".popup:visible").each (index, self) =>
|
||||
@positionPopup($(self), index)
|
||||
|
||||
$findPopup: (id) =>
|
||||
@$(".popup[data-user-id='#{id}']")
|
||||
|
||||
$findOrCreatePopup: (messageId) =>
|
||||
$popup = @$findPopup(messageId)
|
||||
$popup = @createPopup(@collection.get(messageId)) if $popup.length == 0
|
||||
$popup
|
||||
|
||||
$closestPopup: (target) =>
|
||||
$target = $(target)
|
||||
return $target if $target.hasClass("popup")
|
||||
return $target.closest(".popup")
|
||||
|
||||
messageFromPopup: (target) =>
|
||||
$popup = @$closestPopup(target)
|
||||
message = @collection.get($popup.data('user-id'))
|
||||
return message
|
||||
|
||||
createChallenge: ($popup, generation, conditions) =>
|
||||
$challenge = $popup.find('.challenge')
|
||||
$challenge.html(JST['challenge']())
|
||||
createChallengePane
|
||||
eventName: "challenge"
|
||||
button: $popup.find('.send_challenge')
|
||||
acceptButton: $popup.find('.accept_challenge')
|
||||
rejectButton: $popup.find('.reject_challenge')
|
||||
populate: $popup.find(".challenge_data")
|
||||
generation: generation
|
||||
personId: $popup.data('user-id')
|
||||
defaultClauses: conditions || [
|
||||
Conditions.TEAM_PREVIEW
|
||||
Conditions.PBV_1000
|
||||
Conditions.SLEEP_CLAUSE
|
||||
Conditions.EVASION_CLAUSE
|
||||
Conditions.SPECIES_CLAUSE
|
||||
Conditions.OHKO_CLAUSE
|
||||
Conditions.PRANKSTER_SWAGGER_CLAUSE
|
||||
Conditions.UNRELEASED_BAN
|
||||
]
|
||||
blockedClauses: conditions? || [Conditions.RATED_BATTLE]
|
||||
$popup.find('.popup_messages').addClass('small')
|
||||
$challenge.removeClass('hidden')
|
||||
$challenge
|
||||
|
||||
##########
|
||||
# EVENTS #
|
||||
##########
|
||||
|
||||
keyPressEvent: (e) =>
|
||||
switch e.which
|
||||
when 13 # [ Enter ]
|
||||
$input = $(e.currentTarget)
|
||||
message = @messageFromPopup(e.currentTarget)
|
||||
text = $input.val()
|
||||
return if text.length == 0
|
||||
PokeBattle.primus.send('privateMessage', message.id, text)
|
||||
$input.val('')
|
||||
|
||||
keyUpEvent: (e) =>
|
||||
switch e.which
|
||||
when 27 # [ Esc ]
|
||||
@closePopupEvent(e)
|
||||
|
||||
minimizePopupEvent: (e) =>
|
||||
message = @messageFromPopup(e.currentTarget)
|
||||
message.trigger('minimize', message)
|
||||
|
||||
closePopupEvent: (e) =>
|
||||
message = @messageFromPopup(e.currentTarget)
|
||||
message.trigger('close', message)
|
||||
|
||||
focusChatEvent: (e) =>
|
||||
@$closestPopup(e.currentTarget).find('input').focus()
|
||||
|
||||
toggleChallengeEvent: (e) =>
|
||||
$popup = @$closestPopup(e.currentTarget)
|
||||
$challenge = $popup.find('.challenge')
|
||||
wasAtBottom = @isAtBottom($popup)
|
||||
if $challenge.hasClass("hidden")
|
||||
@createChallenge($popup)
|
||||
else if $challenge.find('.cancel_challenge').text() == 'Cancel'
|
||||
$popup.find('.send_challenge').click()
|
||||
else
|
||||
@closeChallenge(@messageFromPopup($popup))
|
||||
if wasAtBottom then @scrollToBottom($popup)
|
||||
|
||||
sendChallengeEvent: (e) =>
|
||||
$popup = @$closestPopup(e.currentTarget)
|
||||
$challenge = $popup.find('.challenge')
|
||||
$challenge.find(".icon-spinner").removeClass('hidden')
|
||||
$challenge.find(".challenge_text").text('Challenging...')
|
||||
$challenge.find(".cancel_challenge").text('Cancel')
|
||||
|
||||
challengeCanceledEvent: (e) =>
|
||||
message = @messageFromPopup(e.currentTarget)
|
||||
message.trigger('cancelChallenge', message.id)
|
||||
|
||||
focusPopupEvent: (e) =>
|
||||
$popup = @$closestPopup(e.currentTarget)
|
||||
$popup.removeClass('new_message')
|
||||
@resetNotifications(@collection.get($popup.data('user-id')))
|
|
@ -0,0 +1,34 @@
|
|||
class @ReplayView extends Backbone.View
|
||||
replayTemplate: JST['replay']
|
||||
|
||||
events:
|
||||
'click .delete-replay': 'deleteReplay'
|
||||
|
||||
render: =>
|
||||
@$el.empty()
|
||||
|
||||
templates = @collection.map((replay) => @replayTemplate({window, replay}))
|
||||
groups = for x in [0...templates.length] by 3
|
||||
_.compact([ templates[x], templates[x + 1], templates[x + 2] ])
|
||||
|
||||
for groupHTML in groups
|
||||
$row = $('<div/>').addClass('row-fluid')
|
||||
$row.append(groupHTML)
|
||||
$row.appendTo(@$el)
|
||||
|
||||
if @collection.length == 0
|
||||
@$el.append($("<p/>").text("You have not saved any replays."))
|
||||
|
||||
this
|
||||
|
||||
deleteReplay: (e) =>
|
||||
return unless confirm("Do you really want to delete this replay?")
|
||||
$target = $(e.currentTarget)
|
||||
$spinner = $target.closest('.clickable-box').find('.show_spinner')
|
||||
|
||||
$spinner.removeClass('hidden')
|
||||
cid = $target.data('cid')
|
||||
replay = @collection.get(cid)
|
||||
replay
|
||||
.destroy()
|
||||
.complete(@render)
|
|
@ -0,0 +1,188 @@
|
|||
class @SidebarView extends Backbone.View
|
||||
template: JST['navigation']
|
||||
|
||||
events:
|
||||
"click .logo" : "focusLobbyEvent"
|
||||
"click .nav_rooms li" : 'focusRoomEvent'
|
||||
"click .nav_battles li" : 'focusBattleEvent'
|
||||
"click .nav_messages li": 'focusMessageEvent'
|
||||
"click .nav_battles .close" : 'leaveRoomEvent'
|
||||
"click .nav_messages .close" : 'closeMessageEvent'
|
||||
"click .nav_teambuilder": 'showTeambuilder'
|
||||
"click .nav_battle_list": 'showBattleList'
|
||||
|
||||
initialize: (attributes) =>
|
||||
@currentWindow = null
|
||||
|
||||
@listenTo(PokeBattle.battles, 'add', @addBattle)
|
||||
@listenTo(PokeBattle.battles, 'remove', @removeBattle)
|
||||
@listenTo(PokeBattle.battles, 'reset', @resetBattles)
|
||||
@listenTo(PokeBattle.battles, 'change:notifications', @renderNotifications)
|
||||
|
||||
@listenTo(PokeBattle.messages, 'open receive', @addMessage)
|
||||
@listenTo(PokeBattle.messages, 'close', @removeMessage)
|
||||
@listenTo(PokeBattle.messages, 'reset', @resetMessages)
|
||||
@listenTo(PokeBattle.messages, 'change:notifications', @renderMessageNotifications)
|
||||
|
||||
@render()
|
||||
|
||||
showTeambuilder: =>
|
||||
@changeWindowTo($("#teambuilder-section"), $(".nav_teambuilder"))
|
||||
|
||||
showBattleList: =>
|
||||
@changeWindowTo($("#battle-list-section"), $(".nav_battle_list"))
|
||||
PokeBattle.battleList.refreshList()
|
||||
|
||||
render: =>
|
||||
@$el.html @template(battles: PokeBattle.battles)
|
||||
|
||||
renderNotifications: (battle) =>
|
||||
$notifications = @$("[data-battle-id='#{battle.id}'] .notifications")
|
||||
|
||||
# We don't want to display notifications if this window is already focused.
|
||||
if @currentWindow.data('battle-id') == battle.id
|
||||
battle.set('notifications', 0, silent: true)
|
||||
$notifications.addClass('hidden')
|
||||
return
|
||||
|
||||
# Show notification count.
|
||||
notificationCount = battle.get('notifications')
|
||||
if notificationCount > 0
|
||||
$notifications.text(notificationCount)
|
||||
$notifications.removeClass('hidden')
|
||||
else
|
||||
$notifications.addClass('hidden')
|
||||
|
||||
addBattle: (battle) =>
|
||||
@$(".header_battles, .nav_battles").removeClass("hidden")
|
||||
$li = $("""<li class="nav_item fake_link" data-battle-id="#{battle.id}">
|
||||
<div class="nav_meta">
|
||||
<div class="notifications hidden">0</div>
|
||||
<div class="close">×</div>
|
||||
</div>#{battle.get('playerIds').join(' VS ')}</li>""")
|
||||
$li.appendTo(@$('.nav_battles'))
|
||||
$li.click()
|
||||
|
||||
removeBattle: (battle) =>
|
||||
$navItems = @$(".nav_item")
|
||||
$battle = @$(".nav_item[data-battle-id='#{battle.id}']")
|
||||
index = $navItems.index($battle)
|
||||
$battle.remove()
|
||||
if PokeBattle.battles.size() == 0
|
||||
@$(".header_battles, .nav_battles").addClass('hidden')
|
||||
PokeBattle.navigation.focusLobby()
|
||||
else
|
||||
$next = $navItems.eq(index).add($navItems.eq(index - 1))
|
||||
$next.first().click()
|
||||
|
||||
resetBattles: (battles) =>
|
||||
for battle in battles
|
||||
@addBattle(battle)
|
||||
|
||||
addMessage: (message) =>
|
||||
# This event can trigger on already opened messages, so we need to verify
|
||||
return if @$(".nav_item[data-message-id='#{message.id}']").length
|
||||
|
||||
@$(".header_messages, .nav_messages").removeClass("hidden")
|
||||
$li = $("""<li class="nav_item fake_link" data-message-id="#{message.id}">
|
||||
<div class="nav_meta">
|
||||
<div class="notifications hidden">0</div>
|
||||
<div class="close">×</div>
|
||||
</div>#{message.id}</li>""")
|
||||
$li.appendTo(@$('.nav_messages'))
|
||||
@renderMessageNotifications(message)
|
||||
|
||||
removeMessage: (message) =>
|
||||
@$(".nav_item[data-message-id='#{message.id}']").remove()
|
||||
|
||||
# If there are no messages, remove the header
|
||||
# Note: We can't check the collection directly since messages are never actually removed from it
|
||||
if @$('.nav_messages li').length == 0
|
||||
@$(".header_messages").addClass("hidden")
|
||||
|
||||
resetMessages: (messages) =>
|
||||
@addMessage(message) for message in messages
|
||||
|
||||
renderMessageNotifications: (message) =>
|
||||
$notifications = @$("[data-message-id='#{message.id}'] .notifications")
|
||||
|
||||
notificationCount = message.get('notifications')
|
||||
if notificationCount > 0
|
||||
$notifications.text(notificationCount)
|
||||
$notifications.removeClass('hidden')
|
||||
else
|
||||
$notifications.addClass('hidden')
|
||||
|
||||
focusLobby: =>
|
||||
# TODO: Clean this up once rooms are implemented
|
||||
# right now it duplicates part of focusRoom()
|
||||
$lobbyLink = @$(".nav_rooms li").first()
|
||||
@resetNotifications($lobbyLink)
|
||||
$room = $('.chat_window')
|
||||
@changeWindowTo($room, $lobbyLink)
|
||||
PokeBattle.router.navigate("")
|
||||
|
||||
leaveRoomEvent: (e) =>
|
||||
$navItem = $(e.currentTarget).closest('.nav_item')
|
||||
battleId = $navItem.data('battle-id')
|
||||
battle = PokeBattle.battles.get(battleId)
|
||||
if battle.isPlaying()
|
||||
return if !confirm("Are you sure you want to forfeit this battle?")
|
||||
battle.forfeit()
|
||||
PokeBattle.battles.remove(battle)
|
||||
false
|
||||
|
||||
closeMessageEvent: (e) =>
|
||||
$navItem = $(e.currentTarget).closest('.nav_item')
|
||||
messageId = $navItem.data('message-id')
|
||||
message = PokeBattle.messages.get(messageId)
|
||||
message.trigger('close', message)
|
||||
|
||||
focusBattleEvent: (e) =>
|
||||
$this = $(e.currentTarget)
|
||||
@resetNotifications($this)
|
||||
battleId = $this.data('battle-id')
|
||||
@changeWindowToBattle(battleId)
|
||||
|
||||
focusLobbyEvent: (e) =>
|
||||
@focusLobby()
|
||||
|
||||
focusRoomEvent: (e) =>
|
||||
$this = $(e.currentTarget)
|
||||
@resetNotifications($this)
|
||||
# TODO: Remove hardcoding once rooms are implemented
|
||||
$room = $('.chat_window')
|
||||
@changeWindowTo($room, $this)
|
||||
PokeBattle.router.navigate("")
|
||||
|
||||
focusMessageEvent: (e) =>
|
||||
$navItem = $(e.currentTarget).closest('.nav_item')
|
||||
messageId = $navItem.data('message-id')
|
||||
message = PokeBattle.messages.get(messageId)
|
||||
message.trigger('show', message)
|
||||
message.trigger('focus', message)
|
||||
|
||||
changeWindowTo: ($toSelector, $navItem) =>
|
||||
# Show window, hide others
|
||||
$mainContent = $('#main-section')
|
||||
$mainContent.children().addClass("hidden")
|
||||
@currentWindow = $toSelector.first()
|
||||
@currentWindow.removeClass("hidden")
|
||||
@currentWindow.find('.chat').trigger('scroll_to_bottom')
|
||||
|
||||
# Add .active to navigation, remove from others
|
||||
@$('.nav_item').removeClass('active')
|
||||
$navItem.addClass('active')
|
||||
|
||||
changeWindowToBattle: (battleId) =>
|
||||
$battle = $(""".battle_window[data-battle-id='#{battleId}']""")
|
||||
$navItem = @$("[data-battle-id='#{battleId}']")
|
||||
@changeWindowTo($battle, $navItem)
|
||||
PokeBattle.router.navigate("battles/#{battleId}")
|
||||
|
||||
resetNotifications: ($link) =>
|
||||
$link = $link.first()
|
||||
$link = $link.closest('li') if $link[0].tagName != 'li'
|
||||
if battleId = $link.data('battle-id')
|
||||
battle = PokeBattle.battles.get(battleId)
|
||||
battle.set('notifications', 0)
|
|
@ -0,0 +1,484 @@
|
|||
isMobileOrAndroid = ->
|
||||
return true if /Mobile/i.test(window.navigator.userAgent)
|
||||
return true if /Android/i.test(window.navigator.userAgent)
|
||||
return false
|
||||
|
||||
# helper which attaches selectize
|
||||
attachSelectize = ($element, options) ->
|
||||
# Block selectize on mobile and all android operating systems (All androids are blocked due to a bug)
|
||||
return if isMobileOrAndroid()
|
||||
$element.selectize(options)
|
||||
|
||||
setSelectizeValue = ($element, value) ->
|
||||
if isMobileOrAndroid()
|
||||
$element.val(value)
|
||||
else
|
||||
$element.each ->
|
||||
@selectize?.setValue(value)
|
||||
|
||||
setSelectizeDisabled = ($element, disabled) ->
|
||||
$element.filter(".selectized").each ->
|
||||
return unless @selectize
|
||||
if disabled then @selectize.disable() else @selectize.enable()
|
||||
|
||||
class @PokemonEditView extends Backbone.View
|
||||
editTemplate: JST['teambuilder/pokemon']
|
||||
speciesTemplate: JST['teambuilder/species']
|
||||
nonStatsTemplate: JST['teambuilder/non_stats']
|
||||
movesTemplate: JST['teambuilder/moves']
|
||||
|
||||
events:
|
||||
'change .sortSpecies': 'changeSort'
|
||||
'change .species_list': 'changeSpecies'
|
||||
'change .selected_nickname': 'changeNickname'
|
||||
'click .selected_shininess': 'changeShiny'
|
||||
'click .selected_happiness': 'changeHappiness'
|
||||
'change .selected-forme': 'changeForme'
|
||||
'change .selected_nature': 'changeNature'
|
||||
'change .selected_ability': 'changeAbility'
|
||||
'change .selected_item': 'changeItem'
|
||||
'change .selected_gender': 'changeGender'
|
||||
'change .selected_level': 'changeLevel'
|
||||
'change .iv-entry': 'changeIv'
|
||||
'focus .ev-entry': 'focusEv'
|
||||
'blur .ev-entry': 'changeEv'
|
||||
'change .ev-entry': 'changeEv'
|
||||
'input .ev-entry[type=range]': 'changeEv' # fix for firefox
|
||||
'change .select-hidden-power': 'changeHiddenPower'
|
||||
'keydown .selected_moves input': 'keydownMoves'
|
||||
'blur .selected_moves input': 'blurMoves'
|
||||
'click .table-moves tbody tr': 'clickMoveName'
|
||||
'mousedown .table-moves': 'preventBlurMoves'
|
||||
'click .move-button': 'clickSelectedMove'
|
||||
'click .move-button .close': 'removeSelectedMove'
|
||||
|
||||
initialize: (attributes={}) =>
|
||||
@onPokemonChange = attributes.onPokemonChange
|
||||
|
||||
setFormat: (format) =>
|
||||
format = Formats[format] || Formats[DEFAULT_FORMAT]
|
||||
@setGeneration(format.generation)
|
||||
# TODO: Set PBV limit based on conditions
|
||||
|
||||
changeSort:(e) =>
|
||||
sort = $(e.currentTarget).val()
|
||||
console.log(sort)
|
||||
if sort =="Default Sort"
|
||||
@sortSpecieslist("Default")
|
||||
else if sort == "Sort by Dexnumber"
|
||||
@sortSpecieslist("id", false)
|
||||
else if sort == "Invert by Dexnumber"
|
||||
@sortSpecieslist("id", true)
|
||||
else if sort == "Sort Alphabetically"
|
||||
@sortSpecieslist("pokename", false)
|
||||
else if sort == "Invert Alphabetically"
|
||||
@sortSpecieslist("pokename", true)
|
||||
|
||||
sortSpecieslist: (option, reverse) =>
|
||||
{MoveData, SpeciesData, ItemData} = @generation
|
||||
if option == "Default"
|
||||
sortedlist = @getSpecies
|
||||
else
|
||||
sortedlist = @sortObject(SpeciesData, option, reverse)
|
||||
@speciesList = (species for species, data of sortedlist)
|
||||
@render()
|
||||
|
||||
sortObject: (data, option, reverse) ->
|
||||
arr = []
|
||||
for key, val of data
|
||||
val.pokename = key
|
||||
arr.push(val)
|
||||
arr = _.sortBy(arr, option)
|
||||
if reverse == true
|
||||
arr.reverse()
|
||||
newobj = {}
|
||||
for thing in arr
|
||||
newobj[thing.pokename] = thing
|
||||
finished = newobj
|
||||
|
||||
setGeneration: (generation) =>
|
||||
@generation = window.Generations[generation.toUpperCase()]
|
||||
{MoveData, SpeciesData, ItemData} = @generation
|
||||
@moveData = MoveData
|
||||
@speciesList = (species for species, data of SpeciesData)
|
||||
# TODO: filter irrelevant items
|
||||
@itemList = (_(itemName for itemName, data of ItemData).sort())
|
||||
|
||||
@render()
|
||||
|
||||
setPokemon: (pokemon) =>
|
||||
# Stop listening for change events on the previously set pokemon
|
||||
@stopListening(@pokemon) if @pokemon
|
||||
|
||||
@pokemon = pokemon
|
||||
@listenTo(pokemon, 'change:level', @renderStats)
|
||||
@listenTo(pokemon, 'change:ivs', @renderStats)
|
||||
@listenTo(pokemon, 'change:evs', @renderStats)
|
||||
@listenTo(pokemon, 'change:nature', @renderStats)
|
||||
@listenTo(pokemon, 'change:hiddenPowerType', @renderStats)
|
||||
@listenTo(pokemon, 'change:shiny', @renderSpecies)
|
||||
|
||||
@renderPokemon()
|
||||
|
||||
setTeamPBV: (pbv) =>
|
||||
@teamPBV = pbv
|
||||
|
||||
changeSpecies: (e) =>
|
||||
return if not @onPokemonChange
|
||||
species = $(e.currentTarget).val()
|
||||
@pokemon = if species
|
||||
new Pokemon(teambuilder: true, species: species)
|
||||
else
|
||||
new NullPokemon()
|
||||
@onPokemonChange(@pokemon)
|
||||
|
||||
changeNickname: (e) =>
|
||||
$input = $(e.currentTarget)
|
||||
@pokemon.set("name", $input.val())
|
||||
|
||||
changeShiny: (e) =>
|
||||
$switch = $(e.currentTarget).toggleClass("selected")
|
||||
@pokemon.set("shiny", $switch.is(".selected"))
|
||||
|
||||
changeHappiness: (e) =>
|
||||
$switch = $(e.currentTarget).toggleClass("selected")
|
||||
happiness = if $switch.is(".selected") then 0 else 100
|
||||
@pokemon.set("happiness", happiness)
|
||||
|
||||
changeForme: (e) =>
|
||||
$forme = $(e.currentTarget)
|
||||
@pokemon.set('forme', $forme.val())
|
||||
# Forme changes may have different abilities, so we have to change this.
|
||||
@pokemon.set('ability', @pokemon.getAbilities()[0])
|
||||
|
||||
changeNature: (e) =>
|
||||
$list = $(e.currentTarget)
|
||||
@pokemon.set("nature", $list.val())
|
||||
|
||||
changeAbility: (e) =>
|
||||
$list = $(e.currentTarget)
|
||||
@pokemon.set("ability", $list.val())
|
||||
|
||||
changeItem: (e) =>
|
||||
$list = $(e.currentTarget)
|
||||
@pokemon.set("item", $list.val())
|
||||
|
||||
changeGender: (e) =>
|
||||
$list = $(e.currentTarget)
|
||||
@pokemon.set("gender", $list.val())
|
||||
|
||||
changeLevel: (e) =>
|
||||
$input = $(e.currentTarget)
|
||||
value = parseInt($input.val(), 10)
|
||||
value = 1120 if isNaN(value) || value > 120
|
||||
value = 1 if value < 1
|
||||
$input.val(value)
|
||||
@pokemon.set("level", value)
|
||||
|
||||
changeIv: (e) =>
|
||||
# todo: make changeIv and changeEv DRY
|
||||
$input = $(e.currentTarget)
|
||||
stat = $input.data("stat")
|
||||
value = parseInt($input.val(), 10)
|
||||
if isNaN(value) || value > 31 || value < 0
|
||||
value = 31
|
||||
|
||||
@pokemon.setIv(stat, value)
|
||||
|
||||
focusEv: (e) =>
|
||||
$input = $(e.currentTarget)
|
||||
return if $input.is("[type=range]")
|
||||
value = parseInt($input.val(), 10)
|
||||
$input.val("") if value == 0
|
||||
|
||||
changeEv: (e) =>
|
||||
# todo: make changeIv and changeEv DRY
|
||||
$input = $(e.currentTarget)
|
||||
stat = $input.data("stat")
|
||||
value = parseInt($input.val(), 10)
|
||||
value = 252 if value > 252
|
||||
value = 0 if isNaN(value) || value < 0
|
||||
|
||||
value = @pokemon.setEv(stat, value)
|
||||
$input.val(value) if not $input.is("[type=range]")
|
||||
|
||||
changeHiddenPower: (e) =>
|
||||
$input = $(e.currentTarget)
|
||||
type = $input.val()
|
||||
@pokemon.set('hiddenPowerType', type.toLowerCase())
|
||||
|
||||
# Prevents the blurMoves event from activating for the duration of
|
||||
# the remaining javascript events. This allows the click event to not fire
|
||||
# the blur event.
|
||||
preventBlurMoves: (e) =>
|
||||
@_preventBlur = true
|
||||
_.defer =>
|
||||
@_preventBlur = false
|
||||
|
||||
blurMoves: (e) =>
|
||||
$input = $(e.currentTarget)
|
||||
if @_preventBlur
|
||||
previousScrollPosition = @$el.scrollTop()
|
||||
$input.focus()
|
||||
e.preventDefault()
|
||||
@$el.scrollTop(previousScrollPosition) # prevent scroll from refocus
|
||||
return
|
||||
|
||||
$selectedMove = @$selectedMove()
|
||||
moveName = $selectedMove.data('move-id')
|
||||
|
||||
# Remove filtering and row selection
|
||||
@filterMovesBy("")
|
||||
$(".table-moves .active").removeClass("active")
|
||||
|
||||
if $input.val().length == 0
|
||||
@recordMoves()
|
||||
else
|
||||
@insertMove($input, moveName)
|
||||
|
||||
clickMoveName: (e) =>
|
||||
$this = $(e.currentTarget)
|
||||
moveName = $this.data('move-id')
|
||||
$moves = @$el.find('.selected_moves')
|
||||
$input = $moves.find('input:focus').first()
|
||||
$input = $moves.find('input').first() if $input.length == 0
|
||||
return if $input.length == 0
|
||||
@insertMove($input, moveName)
|
||||
|
||||
insertMove: ($input, moveName) =>
|
||||
currentScrollPosition = @$el.scrollTop()
|
||||
|
||||
@preventBlurMoves()
|
||||
return if !@buttonify($input, moveName)
|
||||
$moves = @$el.find('.selected_moves')
|
||||
$firstInput = $moves.find('input').first()
|
||||
if $firstInput.length > 0
|
||||
$firstInput.focus()
|
||||
@$el.scrollTop(currentScrollPosition)
|
||||
else
|
||||
@$el.scrollTop(0)
|
||||
@recordMoves()
|
||||
|
||||
recordMoves: =>
|
||||
movesArray = []
|
||||
$moves = @$el.find('.selected_moves')
|
||||
$moves.find('.move-button').each ->
|
||||
moveName = $(this).find("span").text().trim()
|
||||
if moveName != ""
|
||||
movesArray.push(moveName)
|
||||
@pokemon.set("moves", movesArray)
|
||||
|
||||
$selectedMove: =>
|
||||
$table = @$el.find('.table-moves')
|
||||
$allMoves = $table.find('tbody tr')
|
||||
$allMoves.filter('.active').first()
|
||||
|
||||
clickSelectedMove: (e) =>
|
||||
$this = $(e.currentTarget)
|
||||
moveName = $this.find('span').text()
|
||||
$input = $("<input type='text' value='#{moveName}'/>")
|
||||
$this.replaceWith($input)
|
||||
$input.focus().select()
|
||||
|
||||
# Set the current move row to active
|
||||
$(".table-moves tr[data-move-id='#{moveName}']").addClass("active")
|
||||
|
||||
removeSelectedMove: (e) =>
|
||||
$this = $(e.currentTarget).parent()
|
||||
$input = $("<input type='text'/>")
|
||||
$this.replaceWith($input)
|
||||
$input.focus()
|
||||
e.stopPropagation()
|
||||
|
||||
buttonify: ($input, moveName) =>
|
||||
return false if moveName not of @moveData
|
||||
|
||||
# The blur event may have been cancelled, so when removing the input also
|
||||
# remove the filter
|
||||
if $input.is(":focus")
|
||||
@filterMovesBy("")
|
||||
$(".table-moves .active").removeClass("active")
|
||||
|
||||
type = @moveData[moveName].type.toLowerCase()
|
||||
$input.replaceWith("""
|
||||
<div class="button move-button #{type}"><span>#{moveName}</span><div class='close'>×</div></div>
|
||||
""")
|
||||
return true
|
||||
|
||||
keydownMoves: (e) =>
|
||||
$input = $(e.currentTarget)
|
||||
$table = @$el.find('.table-moves')
|
||||
$allMoves = $table.find('tbody tr')
|
||||
switch e.which
|
||||
when 13 # [Enter]; we're selecting the active move.
|
||||
$activeMove = @$selectedMove()
|
||||
$activeMove.click()
|
||||
when 38 # [Up arrow]; selects move above
|
||||
$activeMove = $allMoves.filter('.active').first()
|
||||
$prevMove = $activeMove.prevAll(":visible").first()
|
||||
if $prevMove.length > 0
|
||||
$activeMove.removeClass('active')
|
||||
$prevMove.addClass('active')
|
||||
when 40 # [Down arrow]; selects move below
|
||||
$activeMove = $allMoves.filter('.active').first()
|
||||
$nextMove = $activeMove.nextAll(":visible").first()
|
||||
if $nextMove.length > 0
|
||||
$activeMove.removeClass('active')
|
||||
$nextMove.addClass('active')
|
||||
else
|
||||
# Otherwise we're filtering moves
|
||||
# We defer since $input may not have updated yet
|
||||
_.defer =>
|
||||
return unless $input.is(":focus")
|
||||
moveName = $input.val()
|
||||
@filterMovesBy(moveName)
|
||||
|
||||
filterMovesBy: (moveName) =>
|
||||
moveName = moveName.replace(/\s+|-/g, "")
|
||||
$table = @$el.find('.table-moves')
|
||||
$allMoves = $table.find('tbody tr')
|
||||
moveRegex = new RegExp(moveName, "i")
|
||||
$moves = $allMoves.filter ->
|
||||
$move = $(this)
|
||||
moveName = $move.data('move-search-id')
|
||||
moveRegex.test(moveName)
|
||||
$table.addClass('hidden')
|
||||
$moves.removeClass('hidden')
|
||||
$allMoves.not($moves).addClass('hidden')
|
||||
$allMoves.removeClass('active')
|
||||
$moves.first().addClass('active')
|
||||
$table.removeClass('hidden')
|
||||
|
||||
disableEventsAndExecute: (callback) =>
|
||||
isOutermost = !@_eventsDisabled
|
||||
|
||||
@_eventsDisabled = true
|
||||
@undelegateEvents() if isOutermost # disable events
|
||||
callback()
|
||||
@delegateEvents() if isOutermost
|
||||
@_eventsDisabled = false if isOutermost
|
||||
|
||||
render: =>
|
||||
@$el.html @editTemplate(window: window, speciesList: @speciesList, itemList: @itemList)
|
||||
attachSelectize(@$el.find(".species_list"),
|
||||
render:
|
||||
option: (item, escape) =>
|
||||
pbv = PokeBattle.PBV.determinePBV(@generation, species: item.value)
|
||||
return "<div class='clearfix'>#{item.text}<div class='pbv'>#{pbv}</div></div>"
|
||||
)
|
||||
attachSelectize(@$el.find(".selected_item"))
|
||||
return this
|
||||
|
||||
renderPokemon: =>
|
||||
@renderSpecies()
|
||||
@renderNonStats()
|
||||
@renderStats()
|
||||
@renderMoves()
|
||||
@renderPBV()
|
||||
|
||||
# Disable entering values if this is a NullPokemon
|
||||
$elements = @$el.find("input, select").not(".species input, .species select")
|
||||
$elements.prop("disabled", @pokemon.isNull)
|
||||
setSelectizeDisabled($elements, @pokemon.isNull)
|
||||
|
||||
return this
|
||||
|
||||
renderPBV: =>
|
||||
individualPBV = @pokemon.getPBV()
|
||||
@$(".individual-pbv").text(individualPBV)
|
||||
|
||||
team = @pokemon.getTeam()
|
||||
if team && team.hasPBV()
|
||||
pbv = team.getPBV()
|
||||
maxPBV = team.getMaxPBV()
|
||||
@$(".total-pbv").text(pbv).toggleClass("red", pbv > maxPBV)
|
||||
@$(".max-pbv").text(maxPBV)
|
||||
|
||||
renderSpecies: =>
|
||||
@disableEventsAndExecute =>
|
||||
setSelectizeValue(@$(".species_list"), @pokemon.get("species"))
|
||||
html = if @pokemon.isNull then "" else @speciesTemplate(window: window, pokemon: @pokemon)
|
||||
@$(".species-info").html(html)
|
||||
@$(".selected_shininess").toggleClass("selected", @pokemon.get('shiny') == true)
|
||||
@$(".selected_happiness").toggleClass("selected", @pokemon.get("happiness") == 0)
|
||||
|
||||
renderNonStats: =>
|
||||
$nonStats = @$el.find(".non-stats")
|
||||
|
||||
populateSelect = (searchStr, valueTextPairs, selectedValue) ->
|
||||
$select = $nonStats.find(searchStr).empty()
|
||||
for pair in valueTextPairs
|
||||
value = text = pair
|
||||
if pair instanceof Array
|
||||
value = pair[0]
|
||||
text = pair[1]
|
||||
|
||||
$select.append($("<option>").attr("value", value).text(text))
|
||||
$select.val(selectedValue)
|
||||
|
||||
displayedGenders =
|
||||
F: "Female"
|
||||
M: "Male"
|
||||
Genderless: "Genderless"
|
||||
|
||||
@disableEventsAndExecute =>
|
||||
genders = ([g, displayedGenders[g]] for g in @pokemon.getGenders())
|
||||
$nonStats.find(".selected_nickname").val(@pokemon.get("name"))
|
||||
populateSelect ".selected_ability", @pokemon.getAbilities(), @pokemon.get("ability")
|
||||
populateSelect ".selected_nature", @pokemon.getNatures(), @pokemon.get("nature")
|
||||
setSelectizeValue(@$(".selected_item"), @pokemon.get("item"))
|
||||
populateSelect ".selected_gender", genders, @pokemon.get("gender")
|
||||
$nonStats.find(".selected_level").val(@pokemon.get("level"))
|
||||
|
||||
renderStats: =>
|
||||
pokemon = @pokemon
|
||||
|
||||
@$(".iv-entry").each ->
|
||||
$input = $(this)
|
||||
stat = $input.data("stat")
|
||||
$input.val(pokemon.iv(stat))
|
||||
|
||||
@$(".ev-entry").each ->
|
||||
return if $(this).is(":focus")
|
||||
$input = $(this)
|
||||
stat = $input.data("stat")
|
||||
$input.val(pokemon.ev(stat))
|
||||
|
||||
@$('.base-stat').each ->
|
||||
$this = $(this)
|
||||
stat = $this.data("stat")
|
||||
$this.text(pokemon.base(stat))
|
||||
|
||||
@$('.stat-total').each ->
|
||||
$this = $(this)
|
||||
stat = $this.data("stat")
|
||||
$this.text(pokemon.stat(stat))
|
||||
$this.removeClass('plus-nature minus-nature')
|
||||
|
||||
if pokemon.natureBoost(stat) > 1
|
||||
$this.addClass('plus-nature')
|
||||
$this.text($this.text() + '+')
|
||||
|
||||
if pokemon.natureBoost(stat) < 1
|
||||
$this.addClass('minus-nature')
|
||||
$this.text($this.text() + '-')
|
||||
|
||||
remainingEvs = 508 - @pokemon.getTotalEVs()
|
||||
@$('.remaining-evs-amount')
|
||||
.text(remainingEvs)
|
||||
.toggleClass("over-limit", remainingEvs < 0)
|
||||
|
||||
@$('.select-hidden-power').val(@pokemon.get('hiddenPowerType'))
|
||||
|
||||
renderMoves: =>
|
||||
# TODO: Cache the resultant html
|
||||
$moveSection = @$el.find(".moves-section")
|
||||
if @pokemon.isNull
|
||||
$moveSection.html ""
|
||||
return
|
||||
|
||||
$moveSection.html @movesTemplate(window: window, pokemon: @pokemon)
|
||||
$moveSection.find('.selected_moves input').each (i, el) =>
|
||||
$this = $(el)
|
||||
moveName = $this.val()
|
||||
@buttonify($this, moveName)
|
|
@ -0,0 +1,301 @@
|
|||
class @TeambuilderView extends Backbone.View
|
||||
template: JST['teambuilder/main']
|
||||
teamTemplate: JST['teambuilder/team']
|
||||
teamsTemplate: JST['teambuilder/teams']
|
||||
pokemonListTemplate: JST['teambuilder/pokemon_list']
|
||||
|
||||
events:
|
||||
# Team view
|
||||
'click .add-new-team': 'addNewTeamEvent'
|
||||
'click .export-team': 'exportTeam'
|
||||
'click .clone-team': 'cloneTeam'
|
||||
'click .delete-team': 'deleteTeamEvent'
|
||||
'click .go-to-team': 'clickTeam'
|
||||
'click .import-team': 'renderImportTeamModal'
|
||||
|
||||
# Teambuild view
|
||||
'click .change-format-dropdown a': 'changeTeamFormat'
|
||||
'blur .team_name': 'blurTeamName'
|
||||
'keypress .team_name': 'keypressTeamName'
|
||||
'click .go_back': 'goBackToOverview'
|
||||
'click .pokemon_list li': 'clickPokemon'
|
||||
'click .add_pokemon': 'addNewPokemonEvent'
|
||||
'click .save_team': 'saveTeam'
|
||||
|
||||
initialize: (attributes) =>
|
||||
@selectedPokemon = 0
|
||||
@selectedTeam = null
|
||||
|
||||
@render()
|
||||
|
||||
@listenTo(PokeBattle.TeamStore, 'reset', @resetTeams)
|
||||
@listenTo(PokeBattle.TeamStore, 'add', @addNewTeam)
|
||||
@listenTo(PokeBattle.TeamStore, 'remove', @deleteTeam)
|
||||
@listenTo(PokeBattle.TeamStore, 'change:id', @changeTeamId)
|
||||
@listenTo(PokeBattle.TeamStore, 'reset', @renderTeams)
|
||||
@listenTo(PokeBattle.TeamStore, 'saving', @renderSaving)
|
||||
@listenTo(PokeBattle.TeamStore, 'saved', @renderSaved)
|
||||
@listenTo PokeBattle.TeamStore, 'render', (team) =>
|
||||
@renderTeams()
|
||||
if @getSelectedTeam() && team.id == @getSelectedTeam().id
|
||||
@setSelectedTeam(team)
|
||||
|
||||
@pokemonEditView = new PokemonEditView(
|
||||
el: @$('.pokemon_edit')
|
||||
onPokemonChange: (newPokemon) =>
|
||||
team = @getSelectedTeam()
|
||||
team.replace(@selectedPokemon, newPokemon)
|
||||
@renderPBV()
|
||||
)
|
||||
|
||||
clickTeam: (e) =>
|
||||
$team = $(e.currentTarget).closest('.select-team')
|
||||
team = PokeBattle.TeamStore.get($team.data('cid'))
|
||||
@setSelectedTeam(team)
|
||||
|
||||
clickPokemon: (e) =>
|
||||
$listItem = $(e.currentTarget)
|
||||
index = @$('.pokemon_list li').index($listItem)
|
||||
@setSelectedPokemonIndex(index)
|
||||
|
||||
attachEventsToTeam: (team) =>
|
||||
return if team.attachedTeambuildEvents
|
||||
|
||||
@listenTo(team, 'add:pokemon', @renderPokemon)
|
||||
|
||||
# Todo: Make this perform better
|
||||
@listenTo(team, 'change:pokemon[*].species change:pokemon[*].forme', (pokemon) =>
|
||||
@renderPokemonList()
|
||||
@renderPokemon(pokemon)
|
||||
)
|
||||
|
||||
@listenTo(team, 'add:pokemon remove:pokemon', @renderPokemonList)
|
||||
@listenTo(team, 'reset:pokemon', (=> @changeTeam(team)))
|
||||
@listenTo(team, 'change nested-change reset:pokemon add:pokemon remove:pokemon', @dirty)
|
||||
@listenTo(team, 'change:pokemon[*] reset:pokemon add:pokemon remove:pokemon', @renderPBV)
|
||||
|
||||
# A temporary flag to attach until the teambuilder view is refactored
|
||||
team.attachedTeambuildEvents = true
|
||||
|
||||
addEmptyPokemon: (team) =>
|
||||
team.get('pokemon').add(new NullPokemon())
|
||||
|
||||
addNewTeamEvent: (e) =>
|
||||
team = new Team()
|
||||
PokeBattle.TeamStore.add(team)
|
||||
team.save()
|
||||
|
||||
addNewTeam: (team) =>
|
||||
@addEmptyPokemon(team) while team.get('pokemon').length < 6
|
||||
@$('.teambuilder_teams').append @teamTemplate({team, window})
|
||||
@attachEventsToTeam(team)
|
||||
|
||||
resetTeams: (teamStore) =>
|
||||
teamStore.forEach (team) =>
|
||||
@attachEventsToTeam(team)
|
||||
|
||||
cloneTeam: (e) =>
|
||||
$team = $(e.currentTarget).closest('.select-team')
|
||||
cid = $team.data('cid')
|
||||
clone = @getTeam(cid).clone().set("id", null)
|
||||
PokeBattle.TeamStore.add(clone)
|
||||
clone.save()
|
||||
return false
|
||||
|
||||
deleteTeamEvent: (e) =>
|
||||
return false if !confirm("Do you really want to delete this team?")
|
||||
$team = $(e.currentTarget).closest('.select-team')
|
||||
team = @getTeam($team.data('cid'))
|
||||
PokeBattle.TeamStore.remove(team)
|
||||
team.destroy()
|
||||
return false
|
||||
|
||||
deleteTeam: (team) =>
|
||||
@$(".select-team[data-cid=#{team.cid}]").remove()
|
||||
|
||||
changeTeam: (team) =>
|
||||
html = $(@teamTemplate({team, window})).html()
|
||||
@$(".select-team[data-cid=#{team.cid}]").html(html)
|
||||
|
||||
changeTeamId: (team) =>
|
||||
@$(".select-team[data-cid=#{team.cid}]").attr('data-id', team.id)
|
||||
|
||||
exportTeam: (e) =>
|
||||
$team = $(e.currentTarget).closest('.select-team')
|
||||
id = $team.data('id')
|
||||
if not @getTeam(id).hasNonNullPokemon()
|
||||
alert("You cannot export empty teams. Please add some pokemon first.")
|
||||
return false
|
||||
|
||||
teamJSON = @getTeam(id).toNonNullJSON()
|
||||
teamString = PokeBattle.exportTeam(teamJSON.pokemon)
|
||||
|
||||
$modal = PokeBattle.modal('modals/export_team')
|
||||
$modal.find('.exported-team').val(teamString)
|
||||
$modal.find('textarea, input').first().focus().select()
|
||||
return false
|
||||
|
||||
addNewPokemonEvent: =>
|
||||
@addNewPokemon(@getSelectedTeam())
|
||||
|
||||
addNewPokemon: (team) =>
|
||||
@addEmptyPokemon(team)
|
||||
@$('.pokemon_list li').last().click()
|
||||
|
||||
saveTeam: =>
|
||||
clone = @getSelectedTeam()
|
||||
team = PokeBattle.TeamStore.get(clone.id)
|
||||
team.save(clone.toJSON(), silent: true)
|
||||
@resetHeaderButtons()
|
||||
|
||||
changeTeamFormat: (e) =>
|
||||
$link = $(e.currentTarget)
|
||||
format = $link.data('format')
|
||||
team = @getSelectedTeam()
|
||||
if format != team.get('generation')
|
||||
team.set('generation', format)
|
||||
@renderTeam()
|
||||
@dirty() # renderTeam() removes dirty, so call it again
|
||||
|
||||
setSelectedPokemonIndex: (index) =>
|
||||
pokemon = @getSelectedTeam().at(index)
|
||||
@selectedPokemon = index
|
||||
|
||||
# Render the pokemon
|
||||
@pokemonEditView.setPokemon(pokemon)
|
||||
@renderPokemon(pokemon)
|
||||
|
||||
# Set the correct list item to active
|
||||
@$(".navigation li").removeClass("active")
|
||||
@$(".navigation li").eq(index).addClass("active")
|
||||
|
||||
getSelectedPokemon: =>
|
||||
@getSelectedTeam().at(@selectedPokemon)
|
||||
|
||||
setSelectedTeam: (team) =>
|
||||
# Duplicate the team, so that changes don't stick until saved
|
||||
@selectedTeam = team.clone()
|
||||
@selectedTeam.id = team.id
|
||||
@selectedTeam.cid = team.cid
|
||||
@selectedPokemon = 0
|
||||
@attachEventsToTeam(@selectedTeam)
|
||||
@renderTeam()
|
||||
|
||||
getAllTeams: =>
|
||||
PokeBattle.TeamStore.models
|
||||
|
||||
getSelectedTeam: =>
|
||||
@selectedTeam
|
||||
|
||||
getTeam: (idx) =>
|
||||
PokeBattle.TeamStore.get(idx)
|
||||
|
||||
blurTeamName: =>
|
||||
teamName = @$('.team_name').text()
|
||||
@getSelectedTeam().set('name', teamName)
|
||||
|
||||
keypressTeamName: (e) =>
|
||||
if e.which == 13 # [Enter]
|
||||
@$('.team_name').blur()
|
||||
|
||||
goBackToOverview: =>
|
||||
@renderTeams()
|
||||
|
||||
dirty: =>
|
||||
@$('.go_back').text('Discard changes')
|
||||
@$('.save_team').removeClass('disabled')
|
||||
|
||||
resetHeaderButtons: =>
|
||||
@$('.go_back').text('Back')
|
||||
@$('.save_team').addClass('disabled')
|
||||
|
||||
render: =>
|
||||
@$el.html @template(pokemon: @getSelectedTeam(), selected: @selectedPokemon)
|
||||
@renderTeams()
|
||||
|
||||
renderTeams: =>
|
||||
@$('.display_teams').html @teamsTemplate(teams: @getAllTeams(), window: window)
|
||||
@$('.display_teams').removeClass('hidden')
|
||||
@$('.display_pokemon').addClass('hidden')
|
||||
this
|
||||
|
||||
renderTeam: =>
|
||||
team = @getSelectedTeam()
|
||||
@pokemonEditView.setFormat(team.get('generation') || DEFAULT_FORMAT)
|
||||
@resetHeaderButtons()
|
||||
@renderFormat()
|
||||
@renderPokemonList()
|
||||
@setSelectedPokemonIndex(@selectedPokemon)
|
||||
@$('.team_name').text(team.getName())
|
||||
@$('.display_teams').addClass('hidden')
|
||||
@$('.display_pokemon').removeClass('hidden')
|
||||
|
||||
renderPokemonList: =>
|
||||
team = @getSelectedTeam()
|
||||
$pokemon_list = @$(".pokemon_list").empty()
|
||||
$pokemon_list.html @pokemonListTemplate(window: window, pokemonList: team.get('pokemon').models)
|
||||
$pokemon_list.find("li[data-pokemon-index=#{@selectedPokemon}]").addClass("active")
|
||||
|
||||
# NOTE: this isn't be used, and just amounts to hiding the button, however
|
||||
# we may re-enable this functionality in the future
|
||||
# Hide add pokemon if there's 6 pokemon
|
||||
if team.length < 6
|
||||
@$(".add_pokemon").show()
|
||||
else
|
||||
@$(".add_pokemon").hide()
|
||||
|
||||
renderPokemon: (pokemon) =>
|
||||
@pokemonEditView.setPokemon(pokemon)
|
||||
|
||||
renderPBV: (pokemon) =>
|
||||
if pokemon
|
||||
individualPBV = pokemon.getPBV()
|
||||
$listItem = @$(".pokemon_list li[data-pokemon-cid=#{pokemon.cid}]")
|
||||
$listItem.find(".pbv-value").text(individualPBV)
|
||||
|
||||
totalPBV = @getSelectedTeam().getPBV()
|
||||
@pokemonEditView.setTeamPBV(totalPBV)
|
||||
@pokemonEditView.renderPBV()
|
||||
|
||||
renderFormat: =>
|
||||
format = @getSelectedTeam().get("generation")
|
||||
format = DEFAULT_FORMAT if format not of Formats
|
||||
text = @$(".change-format-dropdown a[data-format='#{format}']").text()
|
||||
@$(".current-format").text(text)
|
||||
|
||||
renderImportTeamModal: =>
|
||||
$modal = PokeBattle.modal 'modals/import_team', ($modal) =>
|
||||
$modal.on 'click', '.import-team-submit', (e) =>
|
||||
teamString = $modal.find('.imported-team').val()
|
||||
pokemonJSON = PokeBattle.parseTeam(teamString)
|
||||
errors = @validateImportedTeam(pokemonJSON)
|
||||
if errors.length > 0
|
||||
listErrors = errors.map((e) -> "<li>#{e}</li>").join('')
|
||||
$errors = $modal.find('.form-errors')
|
||||
$errors.html("<ul>#{listErrors}</ul>").removeClass('hidden')
|
||||
else
|
||||
team = new Team(pokemon: pokemonJSON, teambuilder: true)
|
||||
PokeBattle.TeamStore.add(team)
|
||||
team.save()
|
||||
$modal.find('.imported-team').val("")
|
||||
$modal.modal('hide')
|
||||
return false
|
||||
$modal.find('.imported-team').first().focus()
|
||||
|
||||
validateImportedTeam: (json) =>
|
||||
errors = []
|
||||
pokemonSpecies = (pokemon.species for pokemon in json)
|
||||
{SpeciesData} = window.Generations[DEFAULT_GENERATION.toUpperCase()]
|
||||
pokemonSpecies = pokemonSpecies.filter((s) -> s not of SpeciesData)
|
||||
if pokemonSpecies.length > 0
|
||||
errors.push(pokemonSpecies.map((n) -> "#{n} is not a valid Pokemon.")...)
|
||||
return errors
|
||||
return errors
|
||||
|
||||
renderSaving: (team) =>
|
||||
$team = $(".select-team[data-cid='#{team.cid}']")
|
||||
$team.find('.show_spinner').removeClass('hidden')
|
||||
|
||||
renderSaved: (team) =>
|
||||
$team = $(".select-team[data-cid='#{team.cid}']")
|
||||
$team.find('.show_spinner').addClass('hidden')
|
|
@ -0,0 +1,49 @@
|
|||
extends layout
|
||||
|
||||
block content
|
||||
#main-section.clearfix
|
||||
#chat-section.window.chat_window.hidden
|
||||
.chat
|
||||
.main_buttons
|
||||
.section
|
||||
.button.big.find_battle
|
||||
span.find-icon.icon-earth
|
||||
| Find battle
|
||||
.find_battle_select_team
|
||||
.section
|
||||
.button.teambuilder_button
|
||||
span.icon-pencil
|
||||
| Teambuilder
|
||||
.section
|
||||
.button.display_credits Credits
|
||||
#teambuilder-section.window.hidden
|
||||
#battle-list-section.window.hidden
|
||||
#messages.z2
|
||||
|
||||
block footer
|
||||
.loading-container
|
||||
.loading-message Loading...
|
||||
script(src=asset_path('js/data.js'))
|
||||
script(src=asset_path('js/vendor.js'))
|
||||
script(src=asset_path('js/templates.js'))
|
||||
script(src=asset_path('js/app.js'))
|
||||
script.
|
||||
// Autogenerated client version
|
||||
PokeBattle.CLIENT_VERSION = "#{CLIENT_VERSION}";
|
||||
// quicker click response on mobile devices
|
||||
$(document).ready(function() {
|
||||
FastClick.attach(document.body);
|
||||
});
|
||||
|
||||
//- Authentication
|
||||
(function() {
|
||||
var TOKEN = "#{user.token}";
|
||||
var ID = #{user.id};
|
||||
var USERNAME = "#{user.name}"
|
||||
|
||||
PokeBattle.userId = ID;
|
||||
PokeBattle.username = USERNAME;
|
||||
PokeBattle.primus.on('open', function() {
|
||||
PokeBattle.primus.send('login', ID, TOKEN);
|
||||
});
|
||||
}).call(this);
|
|
@ -0,0 +1,31 @@
|
|||
!!!
|
||||
html
|
||||
head
|
||||
meta(http-equiv="X-UA-Compatible", content="IE=Edge")
|
||||
meta(name="viewport", content="width=device-width, initial-scale=1.0, user-scalable=0")
|
||||
title PokeBattle
|
||||
link(rel="icon", type="image/png", href="//media.pokebattle.com/img/favicon.png")
|
||||
link(rel="stylesheet", href=asset_path("css/vendor.css"))
|
||||
link(rel="stylesheet", href=asset_path("css/main.css"))
|
||||
body(class = bodyClass)
|
||||
#navigation
|
||||
#content
|
||||
#header
|
||||
#main-nav
|
||||
#leftmenu.left
|
||||
a(href="#").show-navigation
|
||||
span.icon.icon-menu
|
||||
#logo
|
||||
img(id="logoimg", src="../Sprites/logo.png", height="120px", alt="pokebattle.com")
|
||||
nav#sections
|
||||
ul
|
||||
li
|
||||
a(href="../") Simulator
|
||||
li
|
||||
a(href="//91.121.152.74/", target="_blank") Forums
|
||||
li
|
||||
a(href="../leaderboard", target="_blank") Leaderboard
|
||||
#sub-nav
|
||||
block content
|
||||
block footer
|
||||
script.
|
|
@ -0,0 +1,58 @@
|
|||
style
|
||||
body {
|
||||
font-family: Verdana;
|
||||
font-size: 13px;
|
||||
background: #eee;
|
||||
}
|
||||
td {
|
||||
padding: 10px;
|
||||
}
|
||||
tr {
|
||||
background-color: #d3d3d3;
|
||||
}
|
||||
table {
|
||||
border: 2px solid gray;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-collapse: collapse;
|
||||
border-style: hidden;
|
||||
box-shadow: 0 0 0 1px #666;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background-color: #c3c3c3;
|
||||
}
|
||||
th{
|
||||
width: 200px;
|
||||
border-bottom: 1px solid gray;
|
||||
}
|
||||
table tr:first-child th:first-child,
|
||||
table.Info tr:first-child td:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
table tr:first-child th:last-child,
|
||||
table.Info tr:first-child td:last-child {
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
table tr:last-child td:first-child {
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
|
||||
table tr:last-child td:last-child {
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
div
|
||||
table
|
||||
thead
|
||||
tr
|
||||
th Rank
|
||||
th Player
|
||||
th Score
|
||||
tbody
|
||||
each member, i in players
|
||||
tr
|
||||
td #{i + 1}
|
||||
td #{member.username}
|
||||
td #{member.score}
|
|
@ -0,0 +1 @@
|
|||
a(href="../modify/formes") Change Pokemon Data
|
|
@ -0,0 +1,138 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Advanced JSON Editor Example</title>
|
||||
|
||||
<script src="https://raw.githubusercontent.com/jdorn/json-editor/master/dist/jsoneditor.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Advanced JSON Editor Example</h1>
|
||||
|
||||
<p>This example demonstrates the following:</p>
|
||||
<ul>
|
||||
<li>Loading external schemas via ajax (using $ref)</li>
|
||||
<li>Setting the editor's value from javascript (try the Restore to Default button)</li>
|
||||
<li>Validating the editor's contents (try setting name to an empty string)</li>
|
||||
<li>Macro templates (try changing the city or state fields and watch the citystate field update automatically)</li>
|
||||
<li>Enabling and disabling editor fields</li>
|
||||
</ul>
|
||||
|
||||
<button id='submit'>Submit (console.log)</button>
|
||||
<button id='restore'>Restore to Default</button>
|
||||
<button id='enable_disable'>Disable/Enable Form</button>
|
||||
<span id='valid_indicator'></span>
|
||||
|
||||
<div id='editor_holder'></div>
|
||||
|
||||
<script>
|
||||
// This is the starting value for the editor
|
||||
// We will use this to seed the initial editor
|
||||
// and to provide a "Restore to Default" button.
|
||||
var starting_value = [
|
||||
{
|
||||
name: "John Smith",
|
||||
age: 35,
|
||||
gender: "male",
|
||||
location: {
|
||||
city: "San Francisco",
|
||||
state: "California",
|
||||
citystate: ""
|
||||
},
|
||||
pets: [
|
||||
{
|
||||
name: "Spot",
|
||||
type: "dog",
|
||||
fixed: true
|
||||
},
|
||||
{
|
||||
name: "Whiskers",
|
||||
type: "cat",
|
||||
fixed: false
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Initialize the editor
|
||||
var editor = new JSONEditor(document.getElementById('editor_holder'),{
|
||||
// Enable fetching schemas via ajax
|
||||
ajax: true,
|
||||
|
||||
// The schema for the editor
|
||||
schema: {
|
||||
type: "array",
|
||||
title: "People",
|
||||
format: "tabs",
|
||||
items: {
|
||||
title: "Person",
|
||||
headerTemplate: "{{i}} - {{self.name}}",
|
||||
oneOf: [
|
||||
{
|
||||
$ref: "basic_person.json",
|
||||
title: "Basic Person"
|
||||
},
|
||||
{
|
||||
$ref: "person.json",
|
||||
title: "Complex Person"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Seed the form with a starting value
|
||||
startval: starting_value,
|
||||
|
||||
// Disable additional properties
|
||||
no_additional_properties: true,
|
||||
|
||||
// Require all properties by default
|
||||
required_by_default: true
|
||||
});
|
||||
|
||||
// Hook up the submit button to log to the console
|
||||
document.getElementById('submit').addEventListener('click',function() {
|
||||
// Get the value from the editor
|
||||
console.log(editor.getValue());
|
||||
});
|
||||
|
||||
// Hook up the Restore to Default button
|
||||
document.getElementById('restore').addEventListener('click',function() {
|
||||
editor.setValue(starting_value);
|
||||
});
|
||||
|
||||
// Hook up the enable/disable button
|
||||
document.getElementById('enable_disable').addEventListener('click',function() {
|
||||
// Enable form
|
||||
if(!editor.isEnabled()) {
|
||||
editor.enable();
|
||||
}
|
||||
// Disable form
|
||||
else {
|
||||
editor.disable();
|
||||
}
|
||||
});
|
||||
|
||||
// Hook up the validation indicator to update its
|
||||
// status whenever the editor changes
|
||||
editor.on('change',function() {
|
||||
// Get an array of errors from the validator
|
||||
var errors = editor.validate();
|
||||
|
||||
var indicator = document.getElementById('valid_indicator');
|
||||
|
||||
// Not valid
|
||||
if(errors.length) {
|
||||
indicator.style.color = 'red';
|
||||
indicator.textContent = "not valid";
|
||||
}
|
||||
// Valid
|
||||
else {
|
||||
indicator.style.color = 'green';
|
||||
indicator.textContent = "valid";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,17 @@
|
|||
doctype html
|
||||
html
|
||||
head
|
||||
meta(charset="utf-8")
|
||||
title Advanced JSON Editor Example
|
||||
|
||||
body
|
||||
script(src="../js/jsoneditor.js")
|
||||
button#submit Submit (console.log)
|
||||
button#restore Restore to Default
|
||||
button#enable_disable Disable/Enable Form
|
||||
span#valid_indicator
|
||||
#editor_holder
|
||||
script(type='text/javascript').
|
||||
var pokedata = !{JSON.stringify(data)};
|
||||
script(src="../js/modify.js").
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
extends ../layout
|
||||
|
||||
block content
|
||||
.m4.mt-header.pt1
|
||||
h2 Your replays
|
||||
#all-replays.row-fluid
|
||||
|
||||
block footer
|
||||
script(src=asset_path('js/data.js'))
|
||||
script(src=asset_path('js/vendor.js'))
|
||||
script(src=asset_path('js/templates.js'))
|
||||
script(src=asset_path('js/replays.js'))
|
||||
script.
|
||||
var replays = new Replays(!{JSON.stringify(replays)});
|
||||
new ReplayView({el: '#all-replays', collection: replays}).render();
|
|
@ -0,0 +1,39 @@
|
|||
extends ../layout
|
||||
|
||||
block content
|
||||
.fill.flex-center.flex-column
|
||||
unless replay
|
||||
p No replay found.
|
||||
else
|
||||
h1.mt-header.mb1
|
||||
= replay.getFormat()
|
||||
= ": " + replay.getName()
|
||||
#replay.relative(style = 'width: 100%; height: 100%;')
|
||||
|
||||
block footer
|
||||
if replay
|
||||
script(src=asset_path('js/data.js', {fingerprint: replay.version('js/data.js')}))
|
||||
script(src=asset_path('js/vendor.js', {fingerprint: replay.version('js/vendor.js')}))
|
||||
script(src=asset_path('js/templates.js', {fingerprint: replay.version('js/templates.js')}))
|
||||
script(src=asset_path('js/replays.js', {fingerprint: replay.version('js/replays.js')}))
|
||||
script.
|
||||
var replay = !{JSON.stringify(replay.toJSON())};
|
||||
var battle = new Battle({
|
||||
id: replay.id,
|
||||
format: replay.format,
|
||||
numActive: replay.numActive,
|
||||
playerIds: replay.players
|
||||
});
|
||||
var $battleWindow = $(window.JST['battle_window']({
|
||||
battle: battle,
|
||||
window: window
|
||||
}));
|
||||
|
||||
$('#replay').html($battleWindow);
|
||||
|
||||
battle.view = new BattleView({
|
||||
el: $battleWindow,
|
||||
model: battle
|
||||
});
|
||||
|
||||
battle.update(replay.contents);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,80 @@
|
|||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src:url('../fonts/icomoon.eot?-lv7lfm');
|
||||
src:url('../fonts/icomoon.eot?#iefix-lv7lfm') format('embedded-opentype'),
|
||||
url('../fonts/icomoon.woff?-lv7lfm') format('woff'),
|
||||
url('../fonts/icomoon.ttf?-lv7lfm') format('truetype'),
|
||||
url('../fonts/icomoon.svg?-lv7lfm#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
[class^="icon-"], [class*=" icon-"] {
|
||||
font-family: 'icomoon';
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
font-size: 0.9em;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-lock:before {
|
||||
content: "\e60e";
|
||||
}
|
||||
.icon-unlocked:before {
|
||||
content: "\e60f";
|
||||
}
|
||||
.icon-home:before {
|
||||
content: "\e600";
|
||||
}
|
||||
.icon-pencil:before {
|
||||
content: "\e601";
|
||||
}
|
||||
.icon-copy:before {
|
||||
content: "\e607";
|
||||
}
|
||||
.icon-upload:before {
|
||||
content: "\e608";
|
||||
}
|
||||
.icon-spinner:before {
|
||||
content: "\e60c";
|
||||
}
|
||||
.icon-search:before {
|
||||
content: "\e609";
|
||||
}
|
||||
.icon-remove:before {
|
||||
content: "\e60a";
|
||||
}
|
||||
.icon-menu:before {
|
||||
content: "\e602";
|
||||
}
|
||||
.icon-earth:before {
|
||||
content: "\e603";
|
||||
}
|
||||
.icon-eye-blocked:before {
|
||||
content: "\e604";
|
||||
}
|
||||
.icon-heart:before {
|
||||
content: "\e605";
|
||||
}
|
||||
.icon-heart-broken:before {
|
||||
content: "\e606";
|
||||
}
|
||||
.icon-notification:before {
|
||||
content: "\e60b";
|
||||
}
|
||||
.icon-checkmark-circle:before {
|
||||
content: "\e60d";
|
||||
}
|
||||
.icon-close:before {
|
||||
content: "\e6fd";
|
||||
}
|
||||
.icon-minus:before {
|
||||
content: "\e701";
|
||||
}
|
|
@ -0,0 +1,383 @@
|
|||
/**
|
||||
* selectize.default.css (v0.8.5) - Default Theme
|
||||
* Copyright (c) 2013 Brian Reavis & contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
|
||||
* file except in compliance with the License. You may obtain a copy of the License at:
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
||||
* ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*
|
||||
* @author Brian Reavis <brian@thirdroute.com>
|
||||
*/
|
||||
/* MODIFIED FOR PB */
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder {
|
||||
visibility: visible !important;
|
||||
background: #f2f2f2 !important;
|
||||
background: rgba(0, 0, 0, 0.06) !important;
|
||||
border: 0 none !important;
|
||||
-webkit-box-shadow: inset 0 0 12px 4px #ffffff;
|
||||
box-shadow: inset 0 0 12px 4px #ffffff;
|
||||
}
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after {
|
||||
content: '!';
|
||||
visibility: hidden;
|
||||
}
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-helper {
|
||||
-webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.selectize-dropdown-header {
|
||||
position: relative;
|
||||
padding: 5px 8px;
|
||||
border-bottom: 1px solid #d0d0d0;
|
||||
background: #f8f8f8;
|
||||
-webkit-border-radius: 3px 3px 0 0;
|
||||
-moz-border-radius: 3px 3px 0 0;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
.selectize-dropdown-header-close {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
color: #303030;
|
||||
opacity: 0.4;
|
||||
margin-top: -12px;
|
||||
line-height: 18px;
|
||||
font-size: 20px !important;
|
||||
}
|
||||
.selectize-dropdown-header-close:hover {
|
||||
color: #000000;
|
||||
}
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup {
|
||||
border-right: 1px solid #f2f2f2;
|
||||
border-top: 0 none;
|
||||
float: left;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {
|
||||
border-right: 0 none;
|
||||
}
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:before {
|
||||
display: none;
|
||||
}
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
.selectize-control.plugin-remove_button [data-value] {
|
||||
position: relative;
|
||||
padding-right: 24px !important;
|
||||
}
|
||||
.selectize-control.plugin-remove_button [data-value] .remove {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 17px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
padding: 2px 0 0 0;
|
||||
border-left: 1px solid #0073bb;
|
||||
-webkit-border-radius: 0 2px 2px 0;
|
||||
-moz-border-radius: 0 2px 2px 0;
|
||||
border-radius: 0 2px 2px 0;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.selectize-control.plugin-remove_button [data-value] .remove:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.selectize-control.plugin-remove_button [data-value].active .remove {
|
||||
border-left-color: #00578d;
|
||||
}
|
||||
.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover {
|
||||
background: none;
|
||||
}
|
||||
.selectize-control.plugin-remove_button .disabled [data-value] .remove {
|
||||
border-left-color: #aaaaaa;
|
||||
}
|
||||
.selectize-control {
|
||||
position: relative;
|
||||
}
|
||||
.selectize-dropdown,
|
||||
.selectize-input,
|
||||
.selectize-input input {
|
||||
color: #555;
|
||||
font-family: inherit;
|
||||
/*font-size: 13px;*/ /* overriden for pb */
|
||||
line-height: 18px;
|
||||
-webkit-font-smoothing: inherit;
|
||||
}
|
||||
.selectize-input,
|
||||
.selectize-control.single .selectize-input.input-active {
|
||||
background: #ffffff;
|
||||
cursor: text;
|
||||
display: inline-block;
|
||||
}
|
||||
.selectize-input {
|
||||
border: 1px solid #d0d0d0;
|
||||
padding: 4px 6px;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
/* overflow: hidden; */ /* removing this seemed to fix sizing issues for pb */
|
||||
height: 28px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.selectize-control.multi .selectize-input.has-items {
|
||||
padding: 5px 8px 2px;
|
||||
}
|
||||
.selectize-input.full {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.selectize-input.disabled,
|
||||
.selectize-input.disabled * {
|
||||
cursor: default !important;
|
||||
}
|
||||
.selectize-input.focus {
|
||||
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.selectize-input.dropdown-active {
|
||||
-webkit-border-radius: 3px 3px 0 0;
|
||||
-moz-border-radius: 3px 3px 0 0;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
.selectize-input > * {
|
||||
vertical-align: baseline;
|
||||
display: -moz-inline-stack;
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
}
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
cursor: pointer;
|
||||
margin: 0 3px 3px 0;
|
||||
padding: 2px 6px;
|
||||
background: #1da7ee;
|
||||
color: #ffffff;
|
||||
border: 1px solid #0073bb;
|
||||
}
|
||||
.selectize-control.multi .selectize-input > div.active {
|
||||
background: #92c836;
|
||||
color: #ffffff;
|
||||
border: 1px solid #00578d;
|
||||
}
|
||||
.selectize-control.multi .selectize-input.disabled > div,
|
||||
.selectize-control.multi .selectize-input.disabled > div.active {
|
||||
color: #ffffff;
|
||||
background: #d2d2d2;
|
||||
border: 1px solid #aaaaaa;
|
||||
}
|
||||
.selectize-input > input {
|
||||
padding: 0 !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 1px !important;
|
||||
text-indent: 0 !important;
|
||||
border: 0 none !important;
|
||||
background: none !important;
|
||||
line-height: inherit !important;
|
||||
-webkit-user-select: auto !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.selectize-input > input:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
.selectize-input::after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
clear: left;
|
||||
}
|
||||
.selectize-input.dropdown-active::before {
|
||||
content: ' ';
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: #f0f0f0;
|
||||
height: 1px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.selectize-dropdown {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
border: 1px solid #d0d0d0;
|
||||
background: #ffffff;
|
||||
margin: -1px 0 0 0;
|
||||
border-top: 0 none;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
-webkit-border-radius: 0 0 3px 3px;
|
||||
-moz-border-radius: 0 0 3px 3px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
}
|
||||
.selectize-dropdown [data-selectable] {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
.selectize-dropdown [data-selectable] .highlight {
|
||||
background: rgba(125, 168, 208, 0.2);
|
||||
-webkit-border-radius: 1px;
|
||||
-moz-border-radius: 1px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
.selectize-dropdown [data-selectable],
|
||||
.selectize-dropdown .optgroup-header {
|
||||
padding: 5px 8px;
|
||||
}
|
||||
.selectize-dropdown .optgroup:first-child .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
.selectize-dropdown .optgroup-header {
|
||||
color: #303030;
|
||||
background: #ffffff;
|
||||
cursor: default;
|
||||
}
|
||||
.selectize-dropdown .active {
|
||||
background-color: #e5eaed;
|
||||
color: #495c68;
|
||||
}
|
||||
.selectize-dropdown .active.create {
|
||||
color: #495c68;
|
||||
}
|
||||
.selectize-dropdown .create {
|
||||
color: rgba(48, 48, 48, 0.5);
|
||||
}
|
||||
.selectize-dropdown-content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 200px;
|
||||
}
|
||||
.selectize-control.single .selectize-input,
|
||||
.selectize-control.single .selectize-input input {
|
||||
cursor: pointer;
|
||||
}
|
||||
.selectize-control.single .selectize-input.input-active,
|
||||
.selectize-control.single .selectize-input.input-active input {
|
||||
cursor: text;
|
||||
}
|
||||
.selectize-control.single .selectize-input:after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 6px;
|
||||
margin-top: -3px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 6px 4px 0 4px;
|
||||
border-color: #333 transparent transparent transparent;
|
||||
}
|
||||
.selectize-control.single .selectize-input.dropdown-active:after {
|
||||
margin-top: -4px;
|
||||
border-width: 0 4px 6px 4px;
|
||||
border-color: transparent transparent #333 transparent;
|
||||
}
|
||||
.selectize-control.rtl.single .selectize-input:after {
|
||||
left: 15px;
|
||||
right: auto;
|
||||
}
|
||||
.selectize-control.rtl .selectize-input > input {
|
||||
margin: 0 4px 0 -2px !important;
|
||||
}
|
||||
.selectize-control .selectize-input.disabled {
|
||||
opacity: 0.5;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.selectize-control.multi .selectize-input.has-items {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.selectize-control.multi .selectize-input.disabled [data-value] {
|
||||
color: #999;
|
||||
text-shadow: none;
|
||||
background: none;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.selectize-control.multi .selectize-input.disabled [data-value],
|
||||
.selectize-control.multi .selectize-input.disabled [data-value] .remove {
|
||||
border-color: #e6e6e6;
|
||||
}
|
||||
.selectize-control.multi .selectize-input.disabled [data-value] .remove {
|
||||
background: none;
|
||||
}
|
||||
.selectize-control.multi .selectize-input [data-value] {
|
||||
text-shadow: 0 1px 0 rgba(0, 51, 83, 0.3);
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
background-color: #1b9dec;
|
||||
background-image: -moz-linear-gradient(top, #1da7ee, #178ee9);
|
||||
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#1da7ee), to(#178ee9));
|
||||
background-image: -webkit-linear-gradient(top, #1da7ee, #178ee9);
|
||||
background-image: -o-linear-gradient(top, #1da7ee, #178ee9);
|
||||
background-image: linear-gradient(to bottom, #1da7ee, #178ee9);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff1da7ee', endColorstr='#ff178ee9', GradientType=0);
|
||||
-webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.2),inset 0 1px rgba(255,255,255,0.03);
|
||||
box-shadow: 0 1px 0 rgba(0,0,0,0.2),inset 0 1px rgba(255,255,255,0.03);
|
||||
}
|
||||
.selectize-control.multi .selectize-input [data-value].active {
|
||||
background-color: #0085d4;
|
||||
background-image: -moz-linear-gradient(top, #008fd8, #0075cf);
|
||||
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#008fd8), to(#0075cf));
|
||||
background-image: -webkit-linear-gradient(top, #008fd8, #0075cf);
|
||||
background-image: -o-linear-gradient(top, #008fd8, #0075cf);
|
||||
background-image: linear-gradient(to bottom, #008fd8, #0075cf);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff008fd8', endColorstr='#ff0075cf', GradientType=0);
|
||||
}
|
||||
.selectize-control.single .selectize-input {
|
||||
-webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.8);
|
||||
box-shadow: 0 1px 0 rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.8);
|
||||
background-color: #f9f9f9;
|
||||
background-image: -moz-linear-gradient(top, #fefefe, #f2f2f2);
|
||||
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fefefe), to(#f2f2f2));
|
||||
background-image: -webkit-linear-gradient(top, #fefefe, #f2f2f2);
|
||||
background-image: -o-linear-gradient(top, #fefefe, #f2f2f2);
|
||||
background-image: linear-gradient(to bottom, #fefefe, #f2f2f2);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffefefe', endColorstr='#fff2f2f2', GradientType=0);
|
||||
}
|
||||
.selectize-control.single .selectize-input,
|
||||
.selectize-dropdown.single {
|
||||
border-color: #b8b8b8;
|
||||
}
|
||||
.selectize-dropdown .optgroup-header {
|
||||
padding-top: 7px;
|
||||
font-weight: bold;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.selectize-dropdown .optgroup {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
.selectize-dropdown .optgroup:first-child {
|
||||
border-top: 0 none;
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
.spinner {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.container1 > div, .container2 > div, .container3 > div {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #333;
|
||||
|
||||
border-radius: 100%;
|
||||
position: absolute;
|
||||
-webkit-animation: bouncedelay 1.2s infinite ease-in-out;
|
||||
animation: bouncedelay 1.2s infinite ease-in-out;
|
||||
/* Prevent first frame from flickering when animation starts */
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.spinner .spinner-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.container2 {
|
||||
-webkit-transform: rotateZ(45deg);
|
||||
transform: rotateZ(45deg);
|
||||
}
|
||||
|
||||
.container3 {
|
||||
-webkit-transform: rotateZ(90deg);
|
||||
transform: rotateZ(90deg);
|
||||
}
|
||||
|
||||
.circle1 { top: 0; left: 0; }
|
||||
.circle2 { top: 0; right: 0; }
|
||||
.circle3 { right: 0; bottom: 0; }
|
||||
.circle4 { left: 0; bottom: 0; }
|
||||
|
||||
.container2 .circle1 {
|
||||
-webkit-animation-delay: -1.1s;
|
||||
animation-delay: -1.1s;
|
||||
}
|
||||
|
||||
.container3 .circle1 {
|
||||
-webkit-animation-delay: -1.0s;
|
||||
animation-delay: -1.0s;
|
||||
}
|
||||
|
||||
.container1 .circle2 {
|
||||
-webkit-animation-delay: -0.9s;
|
||||
animation-delay: -0.9s;
|
||||
}
|
||||
|
||||
.container2 .circle2 {
|
||||
-webkit-animation-delay: -0.8s;
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
|
||||
.container3 .circle2 {
|
||||
-webkit-animation-delay: -0.7s;
|
||||
animation-delay: -0.7s;
|
||||
}
|
||||
|
||||
.container1 .circle3 {
|
||||
-webkit-animation-delay: -0.6s;
|
||||
animation-delay: -0.6s;
|
||||
}
|
||||
|
||||
.container2 .circle3 {
|
||||
-webkit-animation-delay: -0.5s;
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
.container3 .circle3 {
|
||||
-webkit-animation-delay: -0.4s;
|
||||
animation-delay: -0.4s;
|
||||
}
|
||||
|
||||
.container1 .circle4 {
|
||||
-webkit-animation-delay: -0.3s;
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
|
||||
.container2 .circle4 {
|
||||
-webkit-animation-delay: -0.2s;
|
||||
animation-delay: -0.2s;
|
||||
}
|
||||
|
||||
.container3 .circle4 {
|
||||
-webkit-animation-delay: -0.1s;
|
||||
animation-delay: -0.1s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes bouncedelay {
|
||||
0%, 80%, 100% { -webkit-transform: scale(0.0) }
|
||||
40% { -webkit-transform: scale(1.0) }
|
||||
}
|
||||
|
||||
@keyframes bouncedelay {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0.0);
|
||||
-webkit-transform: scale(0.0);
|
||||
} 40% {
|
||||
transform: scale(1.0);
|
||||
-webkit-transform: scale(1.0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
/* Blob.js
|
||||
* A Blob implementation.
|
||||
* 2013-06-20
|
||||
*
|
||||
* By Eli Grey, http://eligrey.com
|
||||
* By Devin Samarin, https://github.com/eboyjr
|
||||
* License: X11/MIT
|
||||
* See LICENSE.md
|
||||
*/
|
||||
|
||||
/*global self, unescape */
|
||||
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
|
||||
plusplus: true */
|
||||
|
||||
/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
|
||||
|
||||
if (!(typeof Blob === "function" || typeof Blob === "object") || typeof URL === "undefined")
|
||||
if ((typeof Blob === "function" || typeof Blob === "object") && typeof webkitURL !== "undefined") self.URL = webkitURL;
|
||||
else var Blob = (function (view) {
|
||||
"use strict";
|
||||
|
||||
var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || view.MSBlobBuilder || (function(view) {
|
||||
var
|
||||
get_class = function(object) {
|
||||
return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
|
||||
}
|
||||
, FakeBlobBuilder = function BlobBuilder() {
|
||||
this.data = [];
|
||||
}
|
||||
, FakeBlob = function Blob(data, type, encoding) {
|
||||
this.data = data;
|
||||
this.size = data.length;
|
||||
this.type = type;
|
||||
this.encoding = encoding;
|
||||
}
|
||||
, FBB_proto = FakeBlobBuilder.prototype
|
||||
, FB_proto = FakeBlob.prototype
|
||||
, FileReaderSync = view.FileReaderSync
|
||||
, FileException = function(type) {
|
||||
this.code = this[this.name = type];
|
||||
}
|
||||
, file_ex_codes = (
|
||||
"NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
|
||||
+ "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
|
||||
).split(" ")
|
||||
, file_ex_code = file_ex_codes.length
|
||||
, real_URL = view.URL || view.webkitURL || view
|
||||
, real_create_object_URL = real_URL.createObjectURL
|
||||
, real_revoke_object_URL = real_URL.revokeObjectURL
|
||||
, URL = real_URL
|
||||
, btoa = view.btoa
|
||||
, atob = view.atob
|
||||
|
||||
, ArrayBuffer = view.ArrayBuffer
|
||||
, Uint8Array = view.Uint8Array
|
||||
;
|
||||
FakeBlob.fake = FB_proto.fake = true;
|
||||
while (file_ex_code--) {
|
||||
FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
|
||||
}
|
||||
if (!real_URL.createObjectURL) {
|
||||
URL = view.URL = {};
|
||||
}
|
||||
URL.createObjectURL = function(blob) {
|
||||
var
|
||||
type = blob.type
|
||||
, data_URI_header
|
||||
;
|
||||
if (type === null) {
|
||||
type = "application/octet-stream";
|
||||
}
|
||||
if (blob instanceof FakeBlob) {
|
||||
data_URI_header = "data:" + type;
|
||||
if (blob.encoding === "base64") {
|
||||
return data_URI_header + ";base64," + blob.data;
|
||||
} else if (blob.encoding === "URI") {
|
||||
return data_URI_header + "," + decodeURIComponent(blob.data);
|
||||
} if (btoa) {
|
||||
return data_URI_header + ";base64," + btoa(blob.data);
|
||||
} else {
|
||||
return data_URI_header + "," + encodeURIComponent(blob.data);
|
||||
}
|
||||
} else if (real_create_object_URL) {
|
||||
return real_create_object_URL.call(real_URL, blob);
|
||||
}
|
||||
};
|
||||
URL.revokeObjectURL = function(object_URL) {
|
||||
if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
|
||||
real_revoke_object_URL.call(real_URL, object_URL);
|
||||
}
|
||||
};
|
||||
FBB_proto.append = function(data/*, endings*/) {
|
||||
var bb = this.data;
|
||||
// decode data to a binary string
|
||||
if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
|
||||
var
|
||||
str = ""
|
||||
, buf = new Uint8Array(data)
|
||||
, i = 0
|
||||
, buf_len = buf.length
|
||||
;
|
||||
for (; i < buf_len; i++) {
|
||||
str += String.fromCharCode(buf[i]);
|
||||
}
|
||||
bb.push(str);
|
||||
} else if (get_class(data) === "Blob" || get_class(data) === "File") {
|
||||
if (FileReaderSync) {
|
||||
var fr = new FileReaderSync;
|
||||
bb.push(fr.readAsBinaryString(data));
|
||||
} else {
|
||||
// async FileReader won't work as BlobBuilder is sync
|
||||
throw new FileException("NOT_READABLE_ERR");
|
||||
}
|
||||
} else if (data instanceof FakeBlob) {
|
||||
if (data.encoding === "base64" && atob) {
|
||||
bb.push(atob(data.data));
|
||||
} else if (data.encoding === "URI") {
|
||||
bb.push(decodeURIComponent(data.data));
|
||||
} else if (data.encoding === "raw") {
|
||||
bb.push(data.data);
|
||||
}
|
||||
} else {
|
||||
if (typeof data !== "string") {
|
||||
data += ""; // convert unsupported types to strings
|
||||
}
|
||||
// decode UTF-16 to binary string
|
||||
bb.push(unescape(encodeURIComponent(data)));
|
||||
}
|
||||
};
|
||||
FBB_proto.getBlob = function(type) {
|
||||
if (!arguments.length) {
|
||||
type = null;
|
||||
}
|
||||
return new FakeBlob(this.data.join(""), type, "raw");
|
||||
};
|
||||
FBB_proto.toString = function() {
|
||||
return "[object BlobBuilder]";
|
||||
};
|
||||
FB_proto.slice = function(start, end, type) {
|
||||
var args = arguments.length;
|
||||
if (args < 3) {
|
||||
type = null;
|
||||
}
|
||||
return new FakeBlob(
|
||||
this.data.slice(start, args > 1 ? end : this.data.length)
|
||||
, type
|
||||
, this.encoding
|
||||
);
|
||||
};
|
||||
FB_proto.toString = function() {
|
||||
return "[object Blob]";
|
||||
};
|
||||
return FakeBlobBuilder;
|
||||
}(view));
|
||||
|
||||
return function Blob(blobParts, options) {
|
||||
var type = options ? (options.type || "") : "";
|
||||
var builder = new BlobBuilder();
|
||||
if (blobParts) {
|
||||
for (var i = 0, len = blobParts.length; i < len; i++) {
|
||||
builder.append(blobParts[i]);
|
||||
}
|
||||
}
|
||||
return builder.getBlob(type);
|
||||
};
|
||||
}(self));
|
|
@ -0,0 +1,218 @@
|
|||
/* FileSaver.js
|
||||
* A saveAs() FileSaver implementation.
|
||||
* 2013-01-23
|
||||
*
|
||||
* By Eli Grey, http://eligrey.com
|
||||
* License: X11/MIT
|
||||
* See LICENSE.md
|
||||
*/
|
||||
|
||||
/*global self */
|
||||
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
|
||||
plusplus: true */
|
||||
|
||||
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
|
||||
|
||||
var saveAs = saveAs
|
||||
|| (navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator))
|
||||
|| (function(view) {
|
||||
"use strict";
|
||||
var
|
||||
doc = view.document
|
||||
// only get URL when necessary in case BlobBuilder.js hasn't overridden it yet
|
||||
, get_URL = function() {
|
||||
return view.URL || view.webkitURL || view;
|
||||
}
|
||||
, URL = view.URL || view.webkitURL || view
|
||||
, save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
|
||||
, can_use_save_link = !view.externalHost && "download" in save_link
|
||||
, click = function(node) {
|
||||
var event = doc.createEvent("MouseEvents");
|
||||
event.initMouseEvent(
|
||||
"click", true, false, view, 0, 0, 0, 0, 0
|
||||
, false, false, false, false, 0, null
|
||||
);
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
, webkit_req_fs = view.webkitRequestFileSystem
|
||||
, req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
|
||||
, throw_outside = function (ex) {
|
||||
(view.setImmediate || view.setTimeout)(function() {
|
||||
throw ex;
|
||||
}, 0);
|
||||
}
|
||||
, force_saveable_type = "application/octet-stream"
|
||||
, fs_min_size = 0
|
||||
, deletion_queue = []
|
||||
, process_deletion_queue = function() {
|
||||
var i = deletion_queue.length;
|
||||
while (i--) {
|
||||
var file = deletion_queue[i];
|
||||
if (typeof file === "string") { // file is an object URL
|
||||
URL.revokeObjectURL(file);
|
||||
} else { // file is a File
|
||||
file.remove();
|
||||
}
|
||||
}
|
||||
deletion_queue.length = 0; // clear queue
|
||||
}
|
||||
, dispatch = function(filesaver, event_types, event) {
|
||||
event_types = [].concat(event_types);
|
||||
var i = event_types.length;
|
||||
while (i--) {
|
||||
var listener = filesaver["on" + event_types[i]];
|
||||
if (typeof listener === "function") {
|
||||
try {
|
||||
listener.call(filesaver, event || filesaver);
|
||||
} catch (ex) {
|
||||
throw_outside(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
, FileSaver = function(blob, name) {
|
||||
// First try a.download, then web filesystem, then object URLs
|
||||
var
|
||||
filesaver = this
|
||||
, type = blob.type
|
||||
, blob_changed = false
|
||||
, object_url
|
||||
, target_view
|
||||
, get_object_url = function() {
|
||||
var object_url = get_URL().createObjectURL(blob);
|
||||
deletion_queue.push(object_url);
|
||||
return object_url;
|
||||
}
|
||||
, dispatch_all = function() {
|
||||
dispatch(filesaver, "writestart progress write writeend".split(" "));
|
||||
}
|
||||
// on any filesys errors revert to saving with object URLs
|
||||
, fs_error = function() {
|
||||
// don't create more object URLs than needed
|
||||
if (blob_changed || !object_url) {
|
||||
object_url = get_object_url(blob);
|
||||
}
|
||||
if (target_view) {
|
||||
target_view.location.href = object_url;
|
||||
} else {
|
||||
window.open(object_url, "_blank");
|
||||
}
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
dispatch_all();
|
||||
}
|
||||
, abortable = function(func) {
|
||||
return function() {
|
||||
if (filesaver.readyState !== filesaver.DONE) {
|
||||
return func.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
}
|
||||
, create_if_not_found = {create: true, exclusive: false}
|
||||
, slice
|
||||
;
|
||||
filesaver.readyState = filesaver.INIT;
|
||||
if (!name) {
|
||||
name = "download";
|
||||
}
|
||||
if (can_use_save_link) {
|
||||
object_url = get_object_url(blob);
|
||||
save_link.href = object_url;
|
||||
save_link.download = name;
|
||||
click(save_link);
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
dispatch_all();
|
||||
return;
|
||||
}
|
||||
// Object and web filesystem URLs have a problem saving in Google Chrome when
|
||||
// viewed in a tab, so I force save with application/octet-stream
|
||||
// http://code.google.com/p/chromium/issues/detail?id=91158
|
||||
if (view.chrome && type && type !== force_saveable_type) {
|
||||
slice = blob.slice || blob.webkitSlice;
|
||||
blob = slice.call(blob, 0, blob.size, force_saveable_type);
|
||||
blob_changed = true;
|
||||
}
|
||||
// Since I can't be sure that the guessed media type will trigger a download
|
||||
// in WebKit, I append .download to the filename.
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=65440
|
||||
if (webkit_req_fs && name !== "download") {
|
||||
name += ".download";
|
||||
}
|
||||
if (type === force_saveable_type || webkit_req_fs) {
|
||||
target_view = view;
|
||||
}
|
||||
if (!req_fs) {
|
||||
fs_error();
|
||||
return;
|
||||
}
|
||||
fs_min_size += blob.size;
|
||||
req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
|
||||
fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
|
||||
var save = function() {
|
||||
dir.getFile(name, create_if_not_found, abortable(function(file) {
|
||||
file.createWriter(abortable(function(writer) {
|
||||
writer.onwriteend = function(event) {
|
||||
target_view.location.href = file.toURL();
|
||||
deletion_queue.push(file);
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
dispatch(filesaver, "writeend", event);
|
||||
};
|
||||
writer.onerror = function() {
|
||||
var error = writer.error;
|
||||
if (error.code !== error.ABORT_ERR) {
|
||||
fs_error();
|
||||
}
|
||||
};
|
||||
"writestart progress write abort".split(" ").forEach(function(event) {
|
||||
writer["on" + event] = filesaver["on" + event];
|
||||
});
|
||||
writer.write(blob);
|
||||
filesaver.abort = function() {
|
||||
writer.abort();
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
};
|
||||
filesaver.readyState = filesaver.WRITING;
|
||||
}), fs_error);
|
||||
}), fs_error);
|
||||
};
|
||||
dir.getFile(name, {create: false}, abortable(function(file) {
|
||||
// delete file if it already exists
|
||||
file.remove();
|
||||
save();
|
||||
}), abortable(function(ex) {
|
||||
if (ex.code === ex.NOT_FOUND_ERR) {
|
||||
save();
|
||||
} else {
|
||||
fs_error();
|
||||
}
|
||||
}));
|
||||
}), fs_error);
|
||||
}), fs_error);
|
||||
}
|
||||
, FS_proto = FileSaver.prototype
|
||||
, saveAs = function(blob, name) {
|
||||
return new FileSaver(blob, name);
|
||||
}
|
||||
;
|
||||
FS_proto.abort = function() {
|
||||
var filesaver = this;
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
dispatch(filesaver, "abort");
|
||||
};
|
||||
FS_proto.readyState = FS_proto.INIT = 0;
|
||||
FS_proto.WRITING = 1;
|
||||
FS_proto.DONE = 2;
|
||||
|
||||
FS_proto.error =
|
||||
FS_proto.onwritestart =
|
||||
FS_proto.onprogress =
|
||||
FS_proto.onwrite =
|
||||
FS_proto.onabort =
|
||||
FS_proto.onerror =
|
||||
FS_proto.onwriteend =
|
||||
null;
|
||||
|
||||
view.addEventListener("unload", process_deletion_queue, false);
|
||||
return saveAs;
|
||||
}(self));
|
||||
|
||||
if (typeof module !== 'undefined') module.exports = saveAs;
|
|
@ -0,0 +1,820 @@
|
|||
//
|
||||
// Backbone-associations.js 0.6.1
|
||||
//
|
||||
// (c) 2013 Dhruva Ray, Jaynti Kanani, Persistent Systems Ltd.
|
||||
// Backbone-associations may be freely distributed under the MIT license.
|
||||
// For all details and documentation:
|
||||
// https://github.com/dhruvaray/backbone-associations/
|
||||
//
|
||||
|
||||
(function(root, factory) {
|
||||
// Set up Backbone-associations appropriately for the environment. Start with AMD.
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define(['underscore', 'backbone'], function(_, Backbone) {
|
||||
// Export global even in AMD case in case this script is loaded with
|
||||
// others that may still expect a global Backbone.
|
||||
return factory(root, Backbone, _);
|
||||
});
|
||||
|
||||
// Next for Node.js or CommonJS.
|
||||
} else if (typeof exports !== 'undefined') {
|
||||
var _ = require('underscore'),
|
||||
Backbone = require('backbone');
|
||||
factory(root, Backbone, _);
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = Backbone;
|
||||
}
|
||||
exports = Backbone;
|
||||
|
||||
// Finally, as a browser global.
|
||||
} else {
|
||||
factory(root, root.Backbone, root._);
|
||||
}
|
||||
|
||||
}(this, function(root, Backbone, _) {
|
||||
"use strict";
|
||||
|
||||
// Initial Setup
|
||||
// --------------
|
||||
|
||||
// The top-level namespace. All public Backbone classes and modules will be attached to this.
|
||||
// Exported for the browser and CommonJS.
|
||||
var BackboneModel, BackboneCollection, ModelProto, BackboneEvent,
|
||||
CollectionProto, AssociatedModel, pathChecker,
|
||||
delimiters, pathSeparator, sourceModel, sourceKey, endPoints = {};
|
||||
|
||||
// Create local reference `Model` prototype.
|
||||
BackboneModel = Backbone.Model;
|
||||
BackboneCollection = Backbone.Collection;
|
||||
ModelProto = BackboneModel.prototype;
|
||||
CollectionProto = BackboneCollection.prototype;
|
||||
BackboneEvent = Backbone.Events;
|
||||
|
||||
Backbone.Associations = {
|
||||
VERSION: "0.6.1"
|
||||
};
|
||||
|
||||
// Alternative scopes other than root
|
||||
Backbone.Associations.scopes = [];
|
||||
|
||||
// Define `getter` and `setter` for `separator`
|
||||
var getSeparator = function() {
|
||||
return pathSeparator;
|
||||
};
|
||||
// Define `setSeperator`
|
||||
var setSeparator = function(value) {
|
||||
if (!_.isString(value) || _.size(value) < 1) {
|
||||
value = ".";
|
||||
}
|
||||
// set private properties
|
||||
pathSeparator = value;
|
||||
pathChecker = new RegExp("[\\" + pathSeparator + "\\[\\]]+", "g");
|
||||
delimiters = new RegExp("[^\\" + pathSeparator + "\\[\\]]+", "g");
|
||||
};
|
||||
|
||||
try {
|
||||
// Define `SEPERATOR` property to Backbone.Associations
|
||||
Object.defineProperty(Backbone.Associations, 'SEPARATOR', {
|
||||
enumerable: true,
|
||||
get: getSeparator,
|
||||
set: setSeparator
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
// Backbone.AssociatedModel
|
||||
// --------------
|
||||
|
||||
//Add `Many` and `One` relations to Backbone Object.
|
||||
Backbone.Associations.Many = Backbone.Many = "Many";
|
||||
Backbone.Associations.One = Backbone.One = "One";
|
||||
Backbone.Associations.Self = Backbone.Self = "Self";
|
||||
// Set default separator
|
||||
Backbone.Associations.SEPARATOR = ".";
|
||||
Backbone.Associations.getSeparator = getSeparator;
|
||||
Backbone.Associations.setSeparator = setSeparator;
|
||||
|
||||
Backbone.Associations.EVENTS_BUBBLE = true;
|
||||
Backbone.Associations.EVENTS_WILDCARD = true;
|
||||
Backbone.Associations.EVENTS_NC = false;
|
||||
|
||||
|
||||
setSeparator();
|
||||
|
||||
// Define `AssociatedModel` (Extends Backbone.Model).
|
||||
AssociatedModel = Backbone.AssociatedModel = Backbone.Associations.AssociatedModel = BackboneModel.extend({
|
||||
// Define relations with Associated Model.
|
||||
relations:undefined,
|
||||
// Define `Model` property which can keep track of already fired `events`,
|
||||
// and prevent redundant event to be triggered in case of cyclic model graphs.
|
||||
_proxyCalls:undefined,
|
||||
|
||||
// Override constructor to set parents
|
||||
constructor: function (attributes, options) {
|
||||
// Set parent's opportunistically.
|
||||
options && options.__parents__ && (this.parents = [options.__parents__]);
|
||||
BackboneModel.apply(this, arguments);
|
||||
},
|
||||
|
||||
on: function (name, callback, context) {
|
||||
|
||||
var result = BackboneEvent.on.apply(this, arguments);
|
||||
|
||||
// No optimization possible if nested-events is wanted by the application
|
||||
if (Backbone.Associations.EVENTS_NC) return result;
|
||||
|
||||
// Regular expression used to split event strings.
|
||||
var eventSplitter = /\s+/;
|
||||
|
||||
// Handle atomic event names only
|
||||
if (_.isString(name) && name && (!eventSplitter.test(name)) && callback) {
|
||||
var endPoint = getPathEndPoint(name);
|
||||
if (endPoint) {
|
||||
//Increment end point counter. Represents # of nodes which listen to this end point
|
||||
endPoints[endPoint] = (typeof endPoints[endPoint] === 'undefined') ? 1 : (endPoints[endPoint] + 1);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
off: function (name, callback, context) {
|
||||
|
||||
// No optimization possible if nested-events is wanted by the application
|
||||
if (Backbone.Associations.EVENTS_NC) return BackboneEvent.off.apply(this, arguments);
|
||||
|
||||
var eventSplitter = /\s+/,
|
||||
events = this._events,
|
||||
listeners = {},
|
||||
names = events ? _.keys(events) : [],
|
||||
all = (!name && !callback && !context),
|
||||
atomic_event = (_.isString(name) && (!eventSplitter.test(name)));
|
||||
|
||||
if (all || atomic_event) {
|
||||
for (var i = 0, l = names.length; i < l; i++) {
|
||||
// Store the # of callbacks listening to the event name prior to the `off` call
|
||||
listeners[names[i]] = events[names[i]] ? events[names[i]].length : 0;
|
||||
}
|
||||
}
|
||||
// Call Backbone off implementation
|
||||
var result = BackboneEvent.off.apply(this, arguments);
|
||||
|
||||
if (all || atomic_event) {
|
||||
for (i = 0, l = names.length; i < l; i++) {
|
||||
var endPoint = getPathEndPoint(names[i]);
|
||||
if (endPoint) {
|
||||
if (events[names[i]]) {
|
||||
// Some listeners wiped out for this name for this object
|
||||
endPoints[endPoint] -= (listeners[names[i]] - events[names[i]].length);
|
||||
} else {
|
||||
// All listeners wiped out for this name for this object
|
||||
endPoints[endPoint] -= listeners[names[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
// Get the value of an attribute.
|
||||
get:function (attr) {
|
||||
var cache = this.__attributes__,
|
||||
val = ModelProto.get.call(this, attr),
|
||||
obj = cache ? val || cache[attr] : val;
|
||||
return obj ? obj : this._getAttr.apply(this, arguments);
|
||||
},
|
||||
|
||||
// Set a hash of model attributes on the Backbone Model.
|
||||
set:function (key, value, options) {
|
||||
var attributes, result;
|
||||
// Duplicate backbone's behavior to allow separate key/value parameters,
|
||||
// instead of a single 'attributes' object.
|
||||
if (_.isObject(key) || key == null) {
|
||||
attributes = key;
|
||||
options = value;
|
||||
} else {
|
||||
attributes = {};
|
||||
attributes[key] = value;
|
||||
}
|
||||
result = this._set(attributes, options);
|
||||
// Trigger events which have been blocked until the entire object graph is updated.
|
||||
this._processPendingEvents();
|
||||
return result;
|
||||
|
||||
},
|
||||
|
||||
// Works with an attribute hash and options + fully qualified paths
|
||||
_set:function (attributes, options) {
|
||||
var attr, modelMap, modelId, obj, result = this;
|
||||
if (!attributes) return this;
|
||||
|
||||
// temp cache of attributes
|
||||
this.__attributes__ = attributes;
|
||||
|
||||
for (attr in attributes) {
|
||||
//Create a map for each unique object whose attributes we want to set
|
||||
modelMap || (modelMap = {});
|
||||
if (attr.match(pathChecker)) {
|
||||
var pathTokens = getPathArray(attr), initials = _.initial(pathTokens),
|
||||
last = pathTokens[pathTokens.length - 1],
|
||||
parentModel = this.get(initials);
|
||||
if (parentModel instanceof BackboneModel) {
|
||||
obj = modelMap[parentModel.cid] ||
|
||||
(modelMap[parentModel.cid] = {'model': parentModel, 'data': {}});
|
||||
obj.data[last] = attributes[attr];
|
||||
}
|
||||
} else {
|
||||
obj = modelMap[this.cid] || (modelMap[this.cid] = {'model':this, 'data':{}});
|
||||
obj.data[attr] = attributes[attr];
|
||||
}
|
||||
}
|
||||
|
||||
if (modelMap) {
|
||||
for (modelId in modelMap) {
|
||||
obj = modelMap[modelId];
|
||||
this._setAttr.call(obj.model, obj.data, options) || (result = false);
|
||||
|
||||
}
|
||||
} else {
|
||||
result = this._setAttr.call(this, attributes, options);
|
||||
}
|
||||
delete this.__attributes__;
|
||||
|
||||
return result;
|
||||
|
||||
},
|
||||
|
||||
// Set a hash of model attributes on the object,
|
||||
// fire Backbone `event` with options.
|
||||
// It maintains relations between models during the set operation.
|
||||
// It also bubbles up child events to the parent.
|
||||
_setAttr:function (attributes, options) {
|
||||
var attr;
|
||||
// Extract attributes and options.
|
||||
options || (options = {});
|
||||
if (options.unset) for (attr in attributes) attributes[attr] = void 0;
|
||||
this.parents = this.parents || [];
|
||||
|
||||
if (this.relations) {
|
||||
// Iterate over `this.relations` and `set` model and collection values
|
||||
// if `relations` are available.
|
||||
_.each(this.relations, function (relation) {
|
||||
var relationKey = relation.key,
|
||||
activationContext = relation.scope || root,
|
||||
relatedModel = this._transformRelatedModel(relation, attributes),
|
||||
collectionType = this._transformCollectionType(relation, relatedModel, attributes),
|
||||
map = _.isString(relation.map) ? map2Scope(relation.map, activationContext) : relation.map,
|
||||
currVal = this.attributes[relationKey],
|
||||
idKey = currVal && currVal.idAttribute,
|
||||
val, relationOptions, data, relationValue, newCtx = false;
|
||||
|
||||
// Merge in `options` specific to this relation.
|
||||
relationOptions = relation.options ? _.extend({}, relation.options, options) : options;
|
||||
|
||||
if (attributes[relationKey]) {
|
||||
|
||||
// Get value of attribute with relation key in `val`.
|
||||
val = _.result(attributes, relationKey);
|
||||
|
||||
// Map `val` if a transformation function is provided.
|
||||
val = map ? map.call(this, val, collectionType ? collectionType : relatedModel) : val;
|
||||
if(!val) {
|
||||
attributes[relationKey] = val;
|
||||
return;
|
||||
}
|
||||
|
||||
// If `relation.type` is `Backbone.Many`,
|
||||
// Create `Backbone.Collection` with passed data and perform Backbone `set`.
|
||||
if (relation.type === Backbone.Many) {
|
||||
|
||||
if (currVal) {
|
||||
// Setting this flag will prevent events from firing immediately. That way clients
|
||||
// will not get events until the entire object graph is updated.
|
||||
currVal._deferEvents = true;
|
||||
|
||||
// Use Backbone.Collection's `reset` or smart `set` method
|
||||
currVal[relationOptions.reset ? 'reset' : 'set'](
|
||||
val instanceof BackboneCollection ? val.models : val, relationOptions);
|
||||
|
||||
data = currVal;
|
||||
|
||||
} else {
|
||||
newCtx = true;
|
||||
|
||||
if (val instanceof BackboneCollection) {
|
||||
data = val;
|
||||
} else {
|
||||
data = this._createCollection(
|
||||
collectionType || BackboneCollection,
|
||||
relation.collectionOptions || (relatedModel ? {model: relatedModel} : {})
|
||||
);
|
||||
data[relationOptions.reset ? 'reset' : 'set'](val, relationOptions);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (relation.type === Backbone.One) {
|
||||
|
||||
var hasOwnProperty = (val instanceof BackboneModel) ?
|
||||
val.attributes.hasOwnProperty(idKey) :
|
||||
val.hasOwnProperty(idKey);
|
||||
var newIdKey = (val instanceof BackboneModel) ?
|
||||
val.attributes[idKey] :
|
||||
val[idKey];
|
||||
|
||||
//Is the passed in data for the same key?
|
||||
if (currVal && hasOwnProperty &&
|
||||
currVal.attributes[idKey] === newIdKey) {
|
||||
// Setting this flag will prevent events from firing immediately. That way clients
|
||||
// will not get events until the entire object graph is updated.
|
||||
currVal._deferEvents = true;
|
||||
// Perform the traditional `set` operation
|
||||
currVal._set(val instanceof BackboneModel ? val.attributes : val, relationOptions);
|
||||
data = currVal;
|
||||
} else {
|
||||
newCtx = true;
|
||||
|
||||
if (val instanceof BackboneModel) {
|
||||
data = val;
|
||||
} else {
|
||||
relationOptions.__parents__ = this;
|
||||
data = new relatedModel(val, relationOptions);
|
||||
delete relationOptions.__parents__;
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
throw new Error('type attribute must be specified and ' +
|
||||
'have the values Backbone.One or Backbone.Many');
|
||||
}
|
||||
|
||||
|
||||
attributes[relationKey] = data;
|
||||
relationValue = data;
|
||||
|
||||
// Add proxy events to respective parents.
|
||||
// Only add callback if not defined or new Ctx has been identified.
|
||||
if (newCtx || (relationValue && !relationValue._proxyCallback)) {
|
||||
if(!relationValue._proxyCallback) {
|
||||
relationValue._proxyCallback = function () {
|
||||
return Backbone.Associations.EVENTS_BUBBLE &&
|
||||
this._bubbleEvent.call(this, relationKey, relationValue, arguments);
|
||||
};
|
||||
}
|
||||
relationValue.on("all", relationValue._proxyCallback, this);
|
||||
}
|
||||
|
||||
}
|
||||
//Distinguish between the value of undefined versus a set no-op
|
||||
if (attributes.hasOwnProperty(relationKey))
|
||||
this._setupParents(attributes[relationKey], this.attributes[relationKey]);
|
||||
}, this);
|
||||
}
|
||||
// Return results for `BackboneModel.set`.
|
||||
return ModelProto.set.call(this, attributes, options);
|
||||
},
|
||||
|
||||
|
||||
// Bubble-up event to `parent` Model
|
||||
_bubbleEvent:function (relationKey, relationValue, eventArguments) {
|
||||
var args = eventArguments,
|
||||
opt = args[0].split(":"),
|
||||
eventType = opt[0],
|
||||
catch_all = args[0] == "nested-change",
|
||||
isChangeEvent = eventType === "change",
|
||||
eventObject = args[1],
|
||||
indexEventObject = -1,
|
||||
_proxyCalls = relationValue._proxyCalls,
|
||||
cargs,
|
||||
eventPath = opt[1],
|
||||
eSrc = !eventPath || (eventPath.indexOf(pathSeparator) == -1),
|
||||
basecolEventPath;
|
||||
|
||||
|
||||
// Short circuit the listen in to the nested-graph event
|
||||
if (catch_all) return;
|
||||
|
||||
// Record the source of the event
|
||||
if (eSrc) sourceKey = (getPathEndPoint(args[0]) || relationKey);
|
||||
|
||||
// Short circuit the event bubbling as there are no listeners for this end point
|
||||
if (!Backbone.Associations.EVENTS_NC && !endPoints[sourceKey]) return;
|
||||
|
||||
// Short circuit the listen in to the wild-card event
|
||||
if (Backbone.Associations.EVENTS_WILDCARD) {
|
||||
if (/\[\*\]/g.test(eventPath)) return this;
|
||||
}
|
||||
|
||||
if (relationValue instanceof BackboneCollection && (isChangeEvent || eventPath)) {
|
||||
// O(n) search :(
|
||||
indexEventObject = relationValue.indexOf(sourceModel || eventObject);
|
||||
}
|
||||
|
||||
if (this instanceof BackboneModel) {
|
||||
// A quicker way to identify the model which caused an update inside the collection (while bubbling up)
|
||||
sourceModel = this;
|
||||
}
|
||||
// Manipulate `eventPath`.
|
||||
eventPath = relationKey + ((indexEventObject !== -1 && (isChangeEvent || eventPath)) ?
|
||||
"[" + indexEventObject + "]" : "") + (eventPath ? pathSeparator + eventPath : "");
|
||||
|
||||
// Short circuit collection * events
|
||||
|
||||
if (Backbone.Associations.EVENTS_WILDCARD) {
|
||||
basecolEventPath = eventPath.replace(/\[\d+\]/g, '[*]');
|
||||
}
|
||||
|
||||
cargs = [];
|
||||
cargs.push.apply(cargs, args);
|
||||
cargs[0] = eventType + ":" + eventPath;
|
||||
|
||||
// Create a collection modified event with wild-card
|
||||
if (Backbone.Associations.EVENTS_WILDCARD && eventPath !== basecolEventPath) {
|
||||
cargs[0] = cargs[0] + " " + eventType + ":" + basecolEventPath;
|
||||
}
|
||||
|
||||
// If event has been already triggered as result of same source `eventPath`,
|
||||
// no need to re-trigger event to prevent cycle.
|
||||
_proxyCalls = relationValue._proxyCalls = (_proxyCalls || {});
|
||||
if (this._isEventAvailable.call(this, _proxyCalls, eventPath)) return this;
|
||||
|
||||
// Add `eventPath` in `_proxyCalls` to keep track of already triggered `event`.
|
||||
_proxyCalls[eventPath] = true;
|
||||
|
||||
|
||||
// Set up previous attributes correctly.
|
||||
if (isChangeEvent) {
|
||||
this._previousAttributes[relationKey] = relationValue._previousAttributes;
|
||||
this.changed[relationKey] = relationValue;
|
||||
}
|
||||
|
||||
|
||||
// Bubble up event to parent `model` with new changed arguments.
|
||||
|
||||
this.trigger.apply(this, cargs);
|
||||
|
||||
//Only fire for change. Not change:attribute
|
||||
if (Backbone.Associations.EVENTS_NC && isChangeEvent && this.get(eventPath) != args[2]) {
|
||||
var ncargs = ["nested-change", eventPath, args[1]];
|
||||
args[2] && ncargs.push(args[2]); //args[2] will be options if present
|
||||
this.trigger.apply(this, ncargs);
|
||||
}
|
||||
|
||||
// Remove `eventPath` from `_proxyCalls`,
|
||||
// if `eventPath` and `_proxyCalls` are available,
|
||||
// which allow event to be triggered on for next operation of `set`.
|
||||
if (_proxyCalls && eventPath) delete _proxyCalls[eventPath];
|
||||
|
||||
sourceModel = undefined;
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
// Has event been fired from this source. Used to prevent event recursion in cyclic graphs
|
||||
_isEventAvailable:function (_proxyCalls, path) {
|
||||
return _.find(_proxyCalls, function (value, eventKey) {
|
||||
return path.indexOf(eventKey, path.length - eventKey.length) !== -1;
|
||||
});
|
||||
},
|
||||
|
||||
//Maintain reverse pointers - a.k.a parents
|
||||
_setupParents: function (updated, original) {
|
||||
// Set new parent for updated
|
||||
if (updated) {
|
||||
updated.parents = updated.parents || [];
|
||||
(_.indexOf(updated.parents, this) == -1) && updated.parents.push(this);
|
||||
}
|
||||
// Remove `this` from the earlier set value's parents (if the new value is different).
|
||||
if (original && original.parents.length > 0 && original != updated) {
|
||||
original.parents = _.difference(original.parents, [this]);
|
||||
// Don't bubble to this parent anymore
|
||||
original._proxyCallback && original.off("all", original._proxyCallback, this);
|
||||
}
|
||||
},
|
||||
|
||||
// Returns new `collection` (or derivatives) of type `options.model`.
|
||||
_createCollection: function (type, options) {
|
||||
options = _.defaults(options, {model: type.model});
|
||||
var c = new type([], _.isFunction(options) ? options.call(this) : options);
|
||||
c.parents = [this];
|
||||
return c;
|
||||
},
|
||||
|
||||
// Process all pending events after the entire object graph has been updated
|
||||
_processPendingEvents:function () {
|
||||
if (!this._processedEvents) {
|
||||
this._processedEvents = true;
|
||||
|
||||
this._deferEvents = false;
|
||||
|
||||
// Trigger all pending events
|
||||
_.each(this._pendingEvents, function (e) {
|
||||
e.c.trigger.apply(e.c, e.a);
|
||||
});
|
||||
|
||||
this._pendingEvents = [];
|
||||
|
||||
// Traverse down the object graph and call process pending events on sub-trees
|
||||
_.each(this.relations, function (relation) {
|
||||
var val = this.attributes[relation.key];
|
||||
val && val._processPendingEvents && val._processPendingEvents();
|
||||
}, this);
|
||||
|
||||
delete this._processedEvents;
|
||||
}
|
||||
},
|
||||
|
||||
// Process the raw `relatedModel` value set in a relation
|
||||
_transformRelatedModel: function (relation, attributes) {
|
||||
|
||||
var relatedModel = relation.relatedModel;
|
||||
var activationContext = relation.scope || root;
|
||||
|
||||
// Call function if relatedModel is implemented as a function
|
||||
if (relatedModel && !(relatedModel.prototype instanceof BackboneModel))
|
||||
relatedModel = _.isFunction(relatedModel) ?
|
||||
relatedModel.call(this, relation, attributes) :
|
||||
relatedModel;
|
||||
|
||||
// Get class if relation and map is stored as a string.
|
||||
if (relatedModel && _.isString(relatedModel)) {
|
||||
relatedModel = (relatedModel === Backbone.Self) ?
|
||||
this.constructor :
|
||||
map2Scope(relatedModel, activationContext);
|
||||
}
|
||||
|
||||
// Error checking
|
||||
if (relation.type === Backbone.One) {
|
||||
|
||||
if (!relatedModel)
|
||||
throw new Error('specify a relatedModel for Backbone.One type');
|
||||
|
||||
if (!(relatedModel.prototype instanceof Backbone.Model))
|
||||
throw new Error('specify an AssociatedModel or Backbone.Model for Backbone.One type');
|
||||
}
|
||||
|
||||
return relatedModel;
|
||||
},
|
||||
|
||||
// Process the raw `collectionType` value set in a relation
|
||||
_transformCollectionType: function (relation, relatedModel, attributes) {
|
||||
|
||||
var collectionType = relation.collectionType;
|
||||
var activationContext = relation.scope || root;
|
||||
|
||||
if (collectionType && _.isFunction(collectionType) &&
|
||||
(collectionType.prototype instanceof BackboneModel))
|
||||
throw new Error('type is of Backbone.Model. Specify derivatives of Backbone.Collection');
|
||||
|
||||
// Call function if collectionType is implemented as a function
|
||||
if (collectionType && !(collectionType.prototype instanceof BackboneCollection))
|
||||
collectionType = _.isFunction(collectionType) ?
|
||||
collectionType.call(this, relation, attributes) : collectionType;
|
||||
|
||||
collectionType && _.isString(collectionType) &&
|
||||
(collectionType = map2Scope(collectionType, activationContext));
|
||||
|
||||
// `collectionType` of defined `relation` should be instance of `Backbone.Collection`.
|
||||
if (collectionType && !collectionType.prototype instanceof BackboneCollection) {
|
||||
throw new Error('collectionType must inherit from Backbone.Collection');
|
||||
}
|
||||
|
||||
// Error checking
|
||||
if (relation.type === Backbone.Many) {
|
||||
if ((!relatedModel) && (!collectionType))
|
||||
throw new Error('specify either a relatedModel or collectionType');
|
||||
}
|
||||
|
||||
return collectionType;
|
||||
|
||||
},
|
||||
|
||||
// Override trigger to defer events in the object graph.
|
||||
trigger:function (name) {
|
||||
// Defer event processing
|
||||
if (this._deferEvents) {
|
||||
this._pendingEvents = this._pendingEvents || [];
|
||||
// Maintain a queue of pending events to trigger after the entire object graph is updated.
|
||||
this._pendingEvents.push({c:this, a:arguments});
|
||||
} else {
|
||||
ModelProto.trigger.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
|
||||
// The JSON representation of the model.
|
||||
toJSON:function (options) {
|
||||
var json = {}, aJson;
|
||||
json[this.idAttribute] = this.id;
|
||||
if (!this.visited) {
|
||||
this.visited = true;
|
||||
// Get json representation from `BackboneModel.toJSON`.
|
||||
json = ModelProto.toJSON.apply(this, arguments);
|
||||
|
||||
// Pick up only the keys you want to serialize
|
||||
if (options && options.serialize_keys) {
|
||||
json = _.pick(json, options.serialize_keys);
|
||||
}
|
||||
// If `this.relations` is defined, iterate through each `relation`
|
||||
// and added it's json representation to parents' json representation.
|
||||
if (this.relations) {
|
||||
_.each(this.relations, function (relation) {
|
||||
var key = relation.key,
|
||||
remoteKey = relation.remoteKey,
|
||||
attr = this.attributes[key],
|
||||
serialize = !relation.isTransient,
|
||||
serialize_keys = relation.serialize || [],
|
||||
_options = _.clone(options);
|
||||
|
||||
// Remove default Backbone serialization for associations.
|
||||
delete json[key];
|
||||
|
||||
//Assign to remoteKey if specified. Otherwise use the default key.
|
||||
//Only for non-transient relationships
|
||||
if (serialize) {
|
||||
|
||||
// Pass the keys to serialize as options to the toJSON method.
|
||||
if (serialize_keys.length) {
|
||||
_options ?
|
||||
(_options.serialize_keys = serialize_keys) :
|
||||
(_options = {serialize_keys: serialize_keys})
|
||||
}
|
||||
|
||||
aJson = attr && attr.toJSON ? attr.toJSON(_options) : attr;
|
||||
json[remoteKey || key] = _.isArray(aJson) ? _.compact(aJson) : aJson;
|
||||
}
|
||||
|
||||
}, this);
|
||||
}
|
||||
|
||||
delete this.visited;
|
||||
}
|
||||
return json;
|
||||
},
|
||||
|
||||
// Create a new model with identical attributes to this one.
|
||||
clone: function (options) {
|
||||
return new this.constructor(this.toJSON(options));
|
||||
},
|
||||
|
||||
// Call this if you want to set an `AssociatedModel` to a falsy value like undefined/null directly.
|
||||
// Not calling this will leak memory and have wrong parents.
|
||||
// See test case "parent relations"
|
||||
cleanup:function (options) {
|
||||
options = options || {};
|
||||
|
||||
_.each(this.relations, function (relation) {
|
||||
var val = this.attributes[relation.key];
|
||||
if(val) {
|
||||
val._proxyCallback && val.off("all", val._proxyCallback, this);
|
||||
val.parents = _.difference(val.parents, [this]);
|
||||
}
|
||||
}, this);
|
||||
|
||||
(!options.listen) && this.off();
|
||||
},
|
||||
|
||||
// Override destroy to perform house-keeping on `parents` collection
|
||||
destroy: function(options) {
|
||||
options = options ? _.clone(options) : {};
|
||||
options = _.defaults(options, {remove_references: true, listen: true});
|
||||
var model = this;
|
||||
|
||||
if(options.remove_references && options.wait) {
|
||||
// Proxy success implementation
|
||||
var success = options.success;
|
||||
|
||||
// Substitute with an implementation which will remove references to `model`
|
||||
options.success = function (resp) {
|
||||
if (success) success(model, resp, options);
|
||||
model.cleanup(options);
|
||||
}
|
||||
}
|
||||
// Call the base implementation
|
||||
var xhr = ModelProto.destroy.apply(this, [options]);
|
||||
|
||||
if(options.remove_references && !options.wait) {
|
||||
model.cleanup(options);
|
||||
}
|
||||
|
||||
return xhr;
|
||||
},
|
||||
|
||||
// Navigate the path to the leaf object in the path to query for the attribute value
|
||||
_getAttr:function (path) {
|
||||
|
||||
var result = this,
|
||||
cache = this.__attributes__,
|
||||
//Tokenize the path
|
||||
attrs = getPathArray(path),
|
||||
key,
|
||||
i;
|
||||
if (_.size(attrs) < 1) return;
|
||||
for (i = 0; i < attrs.length; i++) {
|
||||
key = attrs[i];
|
||||
if (!result) break;
|
||||
//Navigate the path to get to the result
|
||||
result = result instanceof BackboneCollection
|
||||
? (isNaN(key) ? undefined : result.at(key))
|
||||
: (cache ? result.attributes[key] || cache[key] : result.attributes[key]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
// Tokenize the fully qualified event path
|
||||
var getPathArray = function (path) {
|
||||
if (path === '') return [''];
|
||||
return _.isString(path) ? (path.match(delimiters)) : path || [];
|
||||
};
|
||||
|
||||
// Get the end point of the path.
|
||||
var getPathEndPoint = function (path) {
|
||||
|
||||
if (!path) return path;
|
||||
|
||||
// event_type:<path>
|
||||
var tokens = path.split(":");
|
||||
|
||||
if (tokens.length > 1) {
|
||||
path = tokens[tokens.length - 1];
|
||||
tokens = path.split(pathSeparator);
|
||||
return tokens.length > 1 ? tokens[tokens.length - 1].split('[')[0] : tokens[0].split('[')[0];
|
||||
} else {
|
||||
//path of 0 depth
|
||||
return "";
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var map2Scope = function (path, context) {
|
||||
var target,
|
||||
scopes = [context];
|
||||
|
||||
// Check global scopes after passed-in context
|
||||
scopes.push.apply(scopes, Backbone.Associations.scopes);
|
||||
|
||||
for (var ctx, i = 0, l = scopes.length; i < l; ++i) {
|
||||
if (ctx = scopes[i]) {
|
||||
target = _.reduce(path.split(pathSeparator), function (memo, elem) {
|
||||
return memo[elem];
|
||||
}, ctx);
|
||||
if (target) break;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
|
||||
// Infer the relation from the collection's parents and find the appropriate map for the passed in `models`
|
||||
var map2models = function (parents, target, models) {
|
||||
var relation, surrogate;
|
||||
//Iterate over collection's parents
|
||||
_.find(parents, function (parent) {
|
||||
//Iterate over relations
|
||||
relation = _.find(parent.relations, function (rel) {
|
||||
return parent.get(rel.key) === target;
|
||||
}, this);
|
||||
if (relation) {
|
||||
surrogate = parent;//surrogate for transformation
|
||||
return true;//break;
|
||||
}
|
||||
}, this);
|
||||
|
||||
//If we found a relation and it has a mapping function
|
||||
if (relation && relation.map) {
|
||||
return relation.map.call(surrogate, models, target);
|
||||
}
|
||||
return models;
|
||||
};
|
||||
|
||||
var proxies = {};
|
||||
// Proxy Backbone collection methods
|
||||
_.each(['set', 'remove', 'reset'], function (method) {
|
||||
proxies[method] = BackboneCollection.prototype[method];
|
||||
|
||||
CollectionProto[method] = function (models, options) {
|
||||
//Short-circuit if this collection doesn't hold `AssociatedModels`
|
||||
if (this.model.prototype instanceof AssociatedModel && this.parents) {
|
||||
//Find a map function if available and perform a transformation
|
||||
arguments[0] = map2models(this.parents, this, models);
|
||||
}
|
||||
return proxies[method].apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
// Override trigger to defer events in the object graph.
|
||||
proxies['trigger'] = CollectionProto['trigger'];
|
||||
CollectionProto['trigger'] = function (name) {
|
||||
if (this._deferEvents) {
|
||||
this._pendingEvents = this._pendingEvents || [];
|
||||
// Maintain a queue of pending events to trigger after the entire object graph is updated.
|
||||
this._pendingEvents.push({c:this, a:arguments});
|
||||
} else {
|
||||
proxies['trigger'].apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
// Attach process pending event functionality on collections as well. Re-use from `AssociatedModel`
|
||||
CollectionProto._processPendingEvents = AssociatedModel.prototype._processPendingEvents;
|
||||
CollectionProto.on = AssociatedModel.prototype.on;
|
||||
CollectionProto.off = AssociatedModel.prototype.off;
|
||||
|
||||
return Backbone;
|
||||
}));
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Copyright 2012 Tsvetan Tsvetkov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* Author: Tsvetan Tsvetkov (tsekach@gmail.com)
|
||||
*/
|
||||
(function(e){function m(t,n){var r;return e.Notification?r=new e.Notification(t,{icon:c(n.icon)?n.icon:n.icon.x32,body:n.body||u,tag:n.tag||u}):e.webkitNotifications?(r=e.webkitNotifications.createNotification(n.icon,t,n.body),r.show()):navigator.mozNotification?(r=navigator.mozNotification.createNotification(t,n.body,n.icon),r.show()):e.external&&e.external.msIsSiteMode()&&(e.external.msSiteModeClearIconOverlay(),e.external.msSiteModeSetIconOverlay(c(n.icon)?n.icon:n.icon.x16,t),e.external.msSiteModeActivate(),r={ieVerification:f+1}),r}function g(t){return{close:function(){t&&(t.close?t.close():e.external&&e.external.msIsSiteMode()&&t.ieVerification===f&&e.external.msSiteModeClearIconOverlay())}}}function y(t){if(!a)return;var n=l(t)?t:d;e.webkitNotifications&&e.webkitNotifications.checkPermission?e.webkitNotifications.requestPermission(n):e.Notification&&e.Notification.requestPermission&&e.Notification.requestPermission(n)}function b(){var r;if(!a)return;return e.Notification&&e.Notification.permissionLevel?r=e.Notification.permissionLevel():e.webkitNotifications&&e.webkitNotifications.checkPermission?r=i[e.webkitNotifications.checkPermission()]:navigator.mozNotification?r=n:e.Notification&&e.Notification.permission?r=e.Notification.permission:e.external&&e.external.msIsSiteMode()!==undefined&&(r=e.external.msIsSiteMode()?n:t),r}function w(e){return e&&h(e)&&p(v,e),v}function E(){return v.pageVisibility?document.hidden||document.msHidden||document.mozHidden||document.webkitHidden:!0}function S(t,r){var i,s;return a&&E()&&c(t)&&r&&(c(r.icon)||h(r.icon))&&b()===n&&(i=m(t,r)),s=g(i),v.autoClose&&i&&!i.ieVerification&&i.addEventListener&&i.addEventListener("show",function(){var t=s;e.setTimeout(function(){t.close()},v.autoClose)}),s}var t="default",n="granted",r="denied",i=[n,t,r],s={pageVisibility:!1,autoClose:0},o={},u="",a=function(){var t=!1;try{t=!!(e.Notification||e.webkitNotifications||navigator.mozNotification||e.external&&e.external.msIsSiteMode()!==undefined)}catch(n){}return t}(),f=Math.floor(Math.random()*10+1),l=function(e){return e&&e.constructor===Function},c=function(e){return e&&e.constructor===String},h=function(e){return e&&e.constructor===Object},p=function(e,t){var n,r;for(n in t){r=t[n];if(!(n in e)||e[n]!==r&&(!(n in o)||o[n]!==r))e[n]=r}return e},d=function(){},v=s;e.notify={PERMISSION_DEFAULT:t,PERMISSION_GRANTED:n,PERMISSION_DENIED:r,isSupported:a,config:w,createNotification:S,permissionLevel:b,requestPermission:y},l(Object.seal)&&Object.seal(e.notify)})(window);
|
|
@ -0,0 +1,788 @@
|
|||
/**
|
||||
* @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
|
||||
*
|
||||
* @version 0.6.11
|
||||
* @codingstandard ftlabs-jsv2
|
||||
* @copyright The Financial Times Limited [All Rights Reserved]
|
||||
* @license MIT License (see LICENSE.txt)
|
||||
*/
|
||||
|
||||
/*jslint browser:true, node:true*/
|
||||
/*global define, Event, Node*/
|
||||
|
||||
|
||||
/**
|
||||
* Instantiate fast-clicking listeners on the specificed layer.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Element} layer The layer to listen on
|
||||
*/
|
||||
function FastClick(layer) {
|
||||
'use strict';
|
||||
var oldOnClick, self = this;
|
||||
|
||||
|
||||
/**
|
||||
* Whether a click is currently being tracked.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
this.trackingClick = false;
|
||||
|
||||
|
||||
/**
|
||||
* Timestamp for when when click tracking started.
|
||||
*
|
||||
* @type number
|
||||
*/
|
||||
this.trackingClickStart = 0;
|
||||
|
||||
|
||||
/**
|
||||
* The element being tracked for a click.
|
||||
*
|
||||
* @type EventTarget
|
||||
*/
|
||||
this.targetElement = null;
|
||||
|
||||
|
||||
/**
|
||||
* X-coordinate of touch start event.
|
||||
*
|
||||
* @type number
|
||||
*/
|
||||
this.touchStartX = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Y-coordinate of touch start event.
|
||||
*
|
||||
* @type number
|
||||
*/
|
||||
this.touchStartY = 0;
|
||||
|
||||
|
||||
/**
|
||||
* ID of the last touch, retrieved from Touch.identifier.
|
||||
*
|
||||
* @type number
|
||||
*/
|
||||
this.lastTouchIdentifier = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Touchmove boundary, beyond which a click will be cancelled.
|
||||
*
|
||||
* @type number
|
||||
*/
|
||||
this.touchBoundary = 10;
|
||||
|
||||
|
||||
/**
|
||||
* The FastClick layer.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
this.layer = layer;
|
||||
|
||||
if (!layer || !layer.nodeType) {
|
||||
throw new TypeError('Layer must be a document node');
|
||||
}
|
||||
|
||||
/** @type function() */
|
||||
this.onClick = function() { return FastClick.prototype.onClick.apply(self, arguments); };
|
||||
|
||||
/** @type function() */
|
||||
this.onMouse = function() { return FastClick.prototype.onMouse.apply(self, arguments); };
|
||||
|
||||
/** @type function() */
|
||||
this.onTouchStart = function() { return FastClick.prototype.onTouchStart.apply(self, arguments); };
|
||||
|
||||
/** @type function() */
|
||||
this.onTouchMove = function() { return FastClick.prototype.onTouchMove.apply(self, arguments); };
|
||||
|
||||
/** @type function() */
|
||||
this.onTouchEnd = function() { return FastClick.prototype.onTouchEnd.apply(self, arguments); };
|
||||
|
||||
/** @type function() */
|
||||
this.onTouchCancel = function() { return FastClick.prototype.onTouchCancel.apply(self, arguments); };
|
||||
|
||||
if (FastClick.notNeeded(layer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up event handlers as required
|
||||
if (this.deviceIsAndroid) {
|
||||
layer.addEventListener('mouseover', this.onMouse, true);
|
||||
layer.addEventListener('mousedown', this.onMouse, true);
|
||||
layer.addEventListener('mouseup', this.onMouse, true);
|
||||
}
|
||||
|
||||
layer.addEventListener('click', this.onClick, true);
|
||||
layer.addEventListener('touchstart', this.onTouchStart, false);
|
||||
layer.addEventListener('touchmove', this.onTouchMove, false);
|
||||
layer.addEventListener('touchend', this.onTouchEnd, false);
|
||||
layer.addEventListener('touchcancel', this.onTouchCancel, false);
|
||||
|
||||
// Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
|
||||
// which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
|
||||
// layer when they are cancelled.
|
||||
if (!Event.prototype.stopImmediatePropagation) {
|
||||
layer.removeEventListener = function(type, callback, capture) {
|
||||
var rmv = Node.prototype.removeEventListener;
|
||||
if (type === 'click') {
|
||||
rmv.call(layer, type, callback.hijacked || callback, capture);
|
||||
} else {
|
||||
rmv.call(layer, type, callback, capture);
|
||||
}
|
||||
};
|
||||
|
||||
layer.addEventListener = function(type, callback, capture) {
|
||||
var adv = Node.prototype.addEventListener;
|
||||
if (type === 'click') {
|
||||
adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
|
||||
if (!event.propagationStopped) {
|
||||
callback(event);
|
||||
}
|
||||
}), capture);
|
||||
} else {
|
||||
adv.call(layer, type, callback, capture);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// If a handler is already declared in the element's onclick attribute, it will be fired before
|
||||
// FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
|
||||
// adding it as listener.
|
||||
if (typeof layer.onclick === 'function') {
|
||||
|
||||
// Android browser on at least 3.2 requires a new reference to the function in layer.onclick
|
||||
// - the old one won't work if passed to addEventListener directly.
|
||||
oldOnClick = layer.onclick;
|
||||
layer.addEventListener('click', function(event) {
|
||||
oldOnClick(event);
|
||||
}, false);
|
||||
layer.onclick = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Android requires exceptions.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
|
||||
|
||||
|
||||
/**
|
||||
* iOS requires exceptions.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
FastClick.prototype.deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
|
||||
|
||||
|
||||
/**
|
||||
* iOS 4 requires an exception for select elements.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
FastClick.prototype.deviceIsIOS4 = FastClick.prototype.deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);
|
||||
|
||||
|
||||
/**
|
||||
* iOS 6.0(+?) requires the target element to be manually derived
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
FastClick.prototype.deviceIsIOSWithBadTarget = FastClick.prototype.deviceIsIOS && (/OS ([6-9]|\d{2})_\d/).test(navigator.userAgent);
|
||||
|
||||
|
||||
/**
|
||||
* Determine whether a given element requires a native click.
|
||||
*
|
||||
* @param {EventTarget|Element} target Target DOM element
|
||||
* @returns {boolean} Returns true if the element needs a native click
|
||||
*/
|
||||
FastClick.prototype.needsClick = function(target) {
|
||||
'use strict';
|
||||
switch (target.nodeName.toLowerCase()) {
|
||||
|
||||
// Don't send a synthetic click to disabled inputs (issue #62)
|
||||
case 'button':
|
||||
case 'select':
|
||||
case 'textarea':
|
||||
if (target.disabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'input':
|
||||
|
||||
// File inputs need real clicks on iOS 6 due to a browser bug (issue #68)
|
||||
if ((this.deviceIsIOS && target.type === 'file') || target.disabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'label':
|
||||
case 'video':
|
||||
return true;
|
||||
}
|
||||
|
||||
return (/\bneedsclick\b/).test(target.className);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determine whether a given element requires a call to focus to simulate click into element.
|
||||
*
|
||||
* @param {EventTarget|Element} target Target DOM element
|
||||
* @returns {boolean} Returns true if the element requires a call to focus to simulate native click.
|
||||
*/
|
||||
FastClick.prototype.needsFocus = function(target) {
|
||||
'use strict';
|
||||
switch (target.nodeName.toLowerCase()) {
|
||||
case 'textarea':
|
||||
return true;
|
||||
case 'select':
|
||||
return !this.deviceIsAndroid;
|
||||
case 'input':
|
||||
switch (target.type) {
|
||||
case 'button':
|
||||
case 'checkbox':
|
||||
case 'file':
|
||||
case 'image':
|
||||
case 'radio':
|
||||
case 'submit':
|
||||
return false;
|
||||
}
|
||||
|
||||
// No point in attempting to focus disabled inputs
|
||||
return !target.disabled && !target.readOnly;
|
||||
default:
|
||||
return (/\bneedsfocus\b/).test(target.className);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Send a click event to the specified element.
|
||||
*
|
||||
* @param {EventTarget|Element} targetElement
|
||||
* @param {Event} event
|
||||
*/
|
||||
FastClick.prototype.sendClick = function(targetElement, event) {
|
||||
'use strict';
|
||||
var clickEvent, touch;
|
||||
|
||||
// On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
|
||||
if (document.activeElement && document.activeElement !== targetElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
touch = event.changedTouches[0];
|
||||
|
||||
// Synthesise a click event, with an extra attribute so it can be tracked
|
||||
clickEvent = document.createEvent('MouseEvents');
|
||||
clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
|
||||
clickEvent.forwardedTouchEvent = true;
|
||||
targetElement.dispatchEvent(clickEvent);
|
||||
};
|
||||
|
||||
FastClick.prototype.determineEventType = function(targetElement) {
|
||||
'use strict';
|
||||
|
||||
//Issue #159: Android Chrome Select Box does not open with a synthetic click event
|
||||
if (this.deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
|
||||
return 'mousedown';
|
||||
}
|
||||
|
||||
return 'click';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {EventTarget|Element} targetElement
|
||||
*/
|
||||
FastClick.prototype.focus = function(targetElement) {
|
||||
'use strict';
|
||||
var length;
|
||||
|
||||
// Issue #160: on iOS 7, some input elements (e.g. date datetime) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724.
|
||||
if (this.deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time') {
|
||||
length = targetElement.value.length;
|
||||
targetElement.setSelectionRange(length, length);
|
||||
} else {
|
||||
targetElement.focus();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Check whether the given target element is a child of a scrollable layer and if so, set a flag on it.
|
||||
*
|
||||
* @param {EventTarget|Element} targetElement
|
||||
*/
|
||||
FastClick.prototype.updateScrollParent = function(targetElement) {
|
||||
'use strict';
|
||||
var scrollParent, parentElement;
|
||||
|
||||
scrollParent = targetElement.fastClickScrollParent;
|
||||
|
||||
// Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the
|
||||
// target element was moved to another parent.
|
||||
if (!scrollParent || !scrollParent.contains(targetElement)) {
|
||||
parentElement = targetElement;
|
||||
do {
|
||||
if (parentElement.scrollHeight > parentElement.offsetHeight) {
|
||||
scrollParent = parentElement;
|
||||
targetElement.fastClickScrollParent = parentElement;
|
||||
break;
|
||||
}
|
||||
|
||||
parentElement = parentElement.parentElement;
|
||||
} while (parentElement);
|
||||
}
|
||||
|
||||
// Always update the scroll top tracker if possible.
|
||||
if (scrollParent) {
|
||||
scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {EventTarget} targetElement
|
||||
* @returns {Element|EventTarget}
|
||||
*/
|
||||
FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
|
||||
'use strict';
|
||||
|
||||
// On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node.
|
||||
if (eventTarget.nodeType === Node.TEXT_NODE) {
|
||||
return eventTarget.parentNode;
|
||||
}
|
||||
|
||||
return eventTarget;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* On touch start, record the position and scroll offset.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
FastClick.prototype.onTouchStart = function(event) {
|
||||
'use strict';
|
||||
var targetElement, touch, selection;
|
||||
|
||||
// Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).
|
||||
if (event.targetTouches.length > 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
targetElement = this.getTargetElementFromEventTarget(event.target);
|
||||
touch = event.targetTouches[0];
|
||||
|
||||
if (this.deviceIsIOS) {
|
||||
|
||||
// Only trusted events will deselect text on iOS (issue #49)
|
||||
selection = window.getSelection();
|
||||
if (selection.rangeCount && !selection.isCollapsed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.deviceIsIOS4) {
|
||||
|
||||
// Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
|
||||
// when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
|
||||
// with the same identifier as the touch event that previously triggered the click that triggered the alert.
|
||||
// Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an
|
||||
// immediately preceeding touch event (issue #52), so this fix is unavailable on that platform.
|
||||
if (touch.identifier === this.lastTouchIdentifier) {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.lastTouchIdentifier = touch.identifier;
|
||||
|
||||
// If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and:
|
||||
// 1) the user does a fling scroll on the scrollable layer
|
||||
// 2) the user stops the fling scroll with another tap
|
||||
// then the event.target of the last 'touchend' event will be the element that was under the user's finger
|
||||
// when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check
|
||||
// is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).
|
||||
this.updateScrollParent(targetElement);
|
||||
}
|
||||
}
|
||||
|
||||
this.trackingClick = true;
|
||||
this.trackingClickStart = event.timeStamp;
|
||||
this.targetElement = targetElement;
|
||||
|
||||
this.touchStartX = touch.pageX;
|
||||
this.touchStartY = touch.pageY;
|
||||
|
||||
// Prevent phantom clicks on fast double-tap (issue #36)
|
||||
if ((event.timeStamp - this.lastClickTime) < 200) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Based on a touchmove event object, check whether the touch has moved past a boundary since it started.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
FastClick.prototype.touchHasMoved = function(event) {
|
||||
'use strict';
|
||||
var touch = event.changedTouches[0], boundary = this.touchBoundary;
|
||||
|
||||
if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Update the last position.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
FastClick.prototype.onTouchMove = function(event) {
|
||||
'use strict';
|
||||
if (!this.trackingClick) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the touch has moved, cancel the click tracking
|
||||
if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
|
||||
this.trackingClick = false;
|
||||
this.targetElement = null;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Attempt to find the labelled control for the given label element.
|
||||
*
|
||||
* @param {EventTarget|HTMLLabelElement} labelElement
|
||||
* @returns {Element|null}
|
||||
*/
|
||||
FastClick.prototype.findControl = function(labelElement) {
|
||||
'use strict';
|
||||
|
||||
// Fast path for newer browsers supporting the HTML5 control attribute
|
||||
if (labelElement.control !== undefined) {
|
||||
return labelElement.control;
|
||||
}
|
||||
|
||||
// All browsers under test that support touch events also support the HTML5 htmlFor attribute
|
||||
if (labelElement.htmlFor) {
|
||||
return document.getElementById(labelElement.htmlFor);
|
||||
}
|
||||
|
||||
// If no for attribute exists, attempt to retrieve the first labellable descendant element
|
||||
// the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label
|
||||
return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* On touch end, determine whether to send a click event at once.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
FastClick.prototype.onTouchEnd = function(event) {
|
||||
'use strict';
|
||||
var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
|
||||
|
||||
if (!this.trackingClick) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prevent phantom clicks on fast double-tap (issue #36)
|
||||
if ((event.timeStamp - this.lastClickTime) < 200) {
|
||||
this.cancelNextClick = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reset to prevent wrong click cancel on input (issue #156).
|
||||
this.cancelNextClick = false;
|
||||
|
||||
this.lastClickTime = event.timeStamp;
|
||||
|
||||
trackingClickStart = this.trackingClickStart;
|
||||
this.trackingClick = false;
|
||||
this.trackingClickStart = 0;
|
||||
|
||||
// On some iOS devices, the targetElement supplied with the event is invalid if the layer
|
||||
// is performing a transition or scroll, and has to be re-detected manually. Note that
|
||||
// for this to function correctly, it must be called *after* the event target is checked!
|
||||
// See issue #57; also filed as rdar://13048589 .
|
||||
if (this.deviceIsIOSWithBadTarget) {
|
||||
touch = event.changedTouches[0];
|
||||
|
||||
// In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null
|
||||
targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
|
||||
targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
|
||||
}
|
||||
|
||||
targetTagName = targetElement.tagName.toLowerCase();
|
||||
if (targetTagName === 'label') {
|
||||
forElement = this.findControl(targetElement);
|
||||
if (forElement) {
|
||||
this.focus(targetElement);
|
||||
if (this.deviceIsAndroid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
targetElement = forElement;
|
||||
}
|
||||
} else if (this.needsFocus(targetElement)) {
|
||||
|
||||
// Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through.
|
||||
// Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37).
|
||||
if ((event.timeStamp - trackingClickStart) > 100 || (this.deviceIsIOS && window.top !== window && targetTagName === 'input')) {
|
||||
this.targetElement = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.focus(targetElement);
|
||||
|
||||
// Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
|
||||
if (!this.deviceIsIOS4 || targetTagName !== 'select') {
|
||||
this.targetElement = null;
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.deviceIsIOS && !this.deviceIsIOS4) {
|
||||
|
||||
// Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled
|
||||
// and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).
|
||||
scrollParent = targetElement.fastClickScrollParent;
|
||||
if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent the actual click from going though - unless the target node is marked as requiring
|
||||
// real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
|
||||
if (!this.needsClick(targetElement)) {
|
||||
event.preventDefault();
|
||||
this.sendClick(targetElement, event);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* On touch cancel, stop tracking the click.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
FastClick.prototype.onTouchCancel = function() {
|
||||
'use strict';
|
||||
this.trackingClick = false;
|
||||
this.targetElement = null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determine mouse events which should be permitted.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
FastClick.prototype.onMouse = function(event) {
|
||||
'use strict';
|
||||
|
||||
// If a target element was never set (because a touch event was never fired) allow the event
|
||||
if (!this.targetElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.forwardedTouchEvent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Programmatically generated events targeting a specific element should be permitted
|
||||
if (!event.cancelable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Derive and check the target element to see whether the mouse event needs to be permitted;
|
||||
// unless explicitly enabled, prevent non-touch click events from triggering actions,
|
||||
// to prevent ghost/doubleclicks.
|
||||
if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
|
||||
|
||||
// Prevent any user-added listeners declared on FastClick element from being fired.
|
||||
if (event.stopImmediatePropagation) {
|
||||
event.stopImmediatePropagation();
|
||||
} else {
|
||||
|
||||
// Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
|
||||
event.propagationStopped = true;
|
||||
}
|
||||
|
||||
// Cancel the event
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the mouse event is permitted, return true for the action to go through.
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* On actual clicks, determine whether this is a touch-generated click, a click action occurring
|
||||
* naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or
|
||||
* an actual click which should be permitted.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
FastClick.prototype.onClick = function(event) {
|
||||
'use strict';
|
||||
var permitted;
|
||||
|
||||
// It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early.
|
||||
if (this.trackingClick) {
|
||||
this.targetElement = null;
|
||||
this.trackingClick = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target.
|
||||
if (event.target.type === 'submit' && event.detail === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
permitted = this.onMouse(event);
|
||||
|
||||
// Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through.
|
||||
if (!permitted) {
|
||||
this.targetElement = null;
|
||||
}
|
||||
|
||||
// If clicks are permitted, return true for the action to go through.
|
||||
return permitted;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Remove all FastClick's event listeners.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
FastClick.prototype.destroy = function() {
|
||||
'use strict';
|
||||
var layer = this.layer;
|
||||
|
||||
if (this.deviceIsAndroid) {
|
||||
layer.removeEventListener('mouseover', this.onMouse, true);
|
||||
layer.removeEventListener('mousedown', this.onMouse, true);
|
||||
layer.removeEventListener('mouseup', this.onMouse, true);
|
||||
}
|
||||
|
||||
layer.removeEventListener('click', this.onClick, true);
|
||||
layer.removeEventListener('touchstart', this.onTouchStart, false);
|
||||
layer.removeEventListener('touchmove', this.onTouchMove, false);
|
||||
layer.removeEventListener('touchend', this.onTouchEnd, false);
|
||||
layer.removeEventListener('touchcancel', this.onTouchCancel, false);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Check whether FastClick is needed.
|
||||
*
|
||||
* @param {Element} layer The layer to listen on
|
||||
*/
|
||||
FastClick.notNeeded = function(layer) {
|
||||
'use strict';
|
||||
var metaViewport;
|
||||
var chromeVersion;
|
||||
|
||||
// Devices that don't support touch don't need FastClick
|
||||
if (typeof window.ontouchstart === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Chrome version - zero for other browsers
|
||||
chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
|
||||
|
||||
if (chromeVersion) {
|
||||
|
||||
if (FastClick.prototype.deviceIsAndroid) {
|
||||
metaViewport = document.querySelector('meta[name=viewport]');
|
||||
|
||||
if (metaViewport) {
|
||||
// Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89)
|
||||
if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
|
||||
return true;
|
||||
}
|
||||
// Chrome 32 and above with width=device-width or less don't need FastClick
|
||||
if (chromeVersion > 31 && window.innerWidth <= window.screen.width) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Chrome desktop doesn't need FastClick (issue #15)
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// IE10 with -ms-touch-action: none, which disables double-tap-to-zoom (issue #97)
|
||||
if (layer.style.msTouchAction === 'none') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Factory method for creating a FastClick object
|
||||
*
|
||||
* @param {Element} layer The layer to listen on
|
||||
*/
|
||||
FastClick.attach = function(layer) {
|
||||
'use strict';
|
||||
return new FastClick(layer);
|
||||
};
|
||||
|
||||
|
||||
if (typeof define !== 'undefined' && define.amd) {
|
||||
|
||||
// AMD. Register as an anonymous module.
|
||||
define(function() {
|
||||
'use strict';
|
||||
return FastClick;
|
||||
});
|
||||
} else if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = FastClick.attach;
|
||||
module.exports.FastClick = FastClick;
|
||||
} else {
|
||||
window.FastClick = FastClick;
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
(function(e){if("function"==typeof bootstrap)bootstrap("jade",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeJade=e}else"undefined"!=typeof window?window.jade=e():global.jade=e()})(function(){var define,ses,bootstrap,module,exports;
|
||||
return (function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s<n.length;s++)i(n[s]);return i})({1:[function(require,module,exports){
|
||||
|
||||
/*!
|
||||
* Jade - runtime
|
||||
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lame Array.isArray() polyfill for now.
|
||||
*/
|
||||
|
||||
if (!Array.isArray) {
|
||||
Array.isArray = function(arr){
|
||||
return '[object Array]' == Object.prototype.toString.call(arr);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lame Object.keys() polyfill for now.
|
||||
*/
|
||||
|
||||
if (!Object.keys) {
|
||||
Object.keys = function(obj){
|
||||
var arr = [];
|
||||
for (var key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
arr.push(key);
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two attribute objects giving precedence
|
||||
* to values in object `b`. Classes are special-cased
|
||||
* allowing for arrays and merging/joining appropriately
|
||||
* resulting in a string.
|
||||
*
|
||||
* @param {Object} a
|
||||
* @param {Object} b
|
||||
* @return {Object} a
|
||||
* @api private
|
||||
*/
|
||||
|
||||
exports.merge = function merge(a, b) {
|
||||
var ac = a['class'];
|
||||
var bc = b['class'];
|
||||
|
||||
if (ac || bc) {
|
||||
ac = ac || [];
|
||||
bc = bc || [];
|
||||
if (!Array.isArray(ac)) ac = [ac];
|
||||
if (!Array.isArray(bc)) bc = [bc];
|
||||
a['class'] = ac.concat(bc).filter(nulls);
|
||||
}
|
||||
|
||||
for (var key in b) {
|
||||
if (key != 'class') {
|
||||
a[key] = b[key];
|
||||
}
|
||||
}
|
||||
|
||||
return a;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter null `val`s.
|
||||
*
|
||||
* @param {*} val
|
||||
* @return {Boolean}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function nulls(val) {
|
||||
return val != null && val !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* join array as classes.
|
||||
*
|
||||
* @param {*} val
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function joinClasses(val) {
|
||||
return Array.isArray(val) ? val.map(joinClasses).filter(nulls).join(' ') : val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the given attributes object.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @param {Object} escaped
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
exports.attrs = function attrs(obj, escaped){
|
||||
var buf = []
|
||||
, terse = obj.terse;
|
||||
|
||||
delete obj.terse;
|
||||
var keys = Object.keys(obj)
|
||||
, len = keys.length;
|
||||
|
||||
if (len) {
|
||||
buf.push('');
|
||||
for (var i = 0; i < len; ++i) {
|
||||
var key = keys[i]
|
||||
, val = obj[key];
|
||||
|
||||
if ('boolean' == typeof val || null == val) {
|
||||
if (val) {
|
||||
terse
|
||||
? buf.push(key)
|
||||
: buf.push(key + '="' + key + '"');
|
||||
}
|
||||
} else if (0 == key.indexOf('data') && 'string' != typeof val) {
|
||||
buf.push(key + "='" + JSON.stringify(val) + "'");
|
||||
} else if ('class' == key) {
|
||||
if (escaped && escaped[key]){
|
||||
if (val = exports.escape(joinClasses(val))) {
|
||||
buf.push(key + '="' + val + '"');
|
||||
}
|
||||
} else {
|
||||
if (val = joinClasses(val)) {
|
||||
buf.push(key + '="' + val + '"');
|
||||
}
|
||||
}
|
||||
} else if (escaped && escaped[key]) {
|
||||
buf.push(key + '="' + exports.escape(val) + '"');
|
||||
} else {
|
||||
buf.push(key + '="' + val + '"');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buf.join(' ');
|
||||
};
|
||||
|
||||
/**
|
||||
* Escape the given string of `html`.
|
||||
*
|
||||
* @param {String} html
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
exports.escape = function escape(html){
|
||||
return String(html)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
};
|
||||
|
||||
/**
|
||||
* Re-throw the given `err` in context to the
|
||||
* the jade in `filename` at the given `lineno`.
|
||||
*
|
||||
* @param {Error} err
|
||||
* @param {String} filename
|
||||
* @param {String} lineno
|
||||
* @api private
|
||||
*/
|
||||
|
||||
exports.rethrow = function rethrow(err, filename, lineno, str){
|
||||
if (!(err instanceof Error)) throw err;
|
||||
if ((typeof window != 'undefined' || !filename) && !str) {
|
||||
err.message += ' on line ' + lineno;
|
||||
throw err;
|
||||
}
|
||||
try {
|
||||
str = str || require('fs').readFileSync(filename, 'utf8')
|
||||
} catch (ex) {
|
||||
rethrow(err, null, lineno)
|
||||
}
|
||||
var context = 3
|
||||
, lines = str.split('\n')
|
||||
, start = Math.max(lineno - context, 0)
|
||||
, end = Math.min(lines.length, lineno + context);
|
||||
|
||||
// Error context
|
||||
var context = lines.slice(start, end).map(function(line, i){
|
||||
var curr = i + start + 1;
|
||||
return (curr == lineno ? ' > ' : ' ')
|
||||
+ curr
|
||||
+ '| '
|
||||
+ line;
|
||||
}).join('\n');
|
||||
|
||||
// Alter exception message
|
||||
err.path = filename;
|
||||
err.message = (filename || 'Jade') + ':' + lineno
|
||||
+ '\n' + context + '\n\n' + err.message;
|
||||
throw err;
|
||||
};
|
||||
|
||||
},{"fs":2}],2:[function(require,module,exports){
|
||||
// nothing to see here... no file methods for the browser
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
;
|
|
@ -0,0 +1,114 @@
|
|||
/*!
|
||||
* jQuery Cookie Plugin v1.4.0
|
||||
* https://github.com/carhartl/jquery-cookie
|
||||
*
|
||||
* Copyright 2013 Klaus Hartl
|
||||
* Released under the MIT license
|
||||
*/
|
||||
(function (factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as anonymous module.
|
||||
define(['jquery'], factory);
|
||||
} else {
|
||||
// Browser globals.
|
||||
factory(jQuery);
|
||||
}
|
||||
}(function ($) {
|
||||
|
||||
var pluses = /\+/g;
|
||||
|
||||
function encode(s) {
|
||||
return config.raw ? s : encodeURIComponent(s);
|
||||
}
|
||||
|
||||
function decode(s) {
|
||||
return config.raw ? s : decodeURIComponent(s);
|
||||
}
|
||||
|
||||
function stringifyCookieValue(value) {
|
||||
return encode(config.json ? JSON.stringify(value) : String(value));
|
||||
}
|
||||
|
||||
function parseCookieValue(s) {
|
||||
if (s.indexOf('"') === 0) {
|
||||
// This is a quoted cookie as according to RFC2068, unescape...
|
||||
s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
||||
}
|
||||
|
||||
try {
|
||||
// Replace server-side written pluses with spaces.
|
||||
// If we can't decode the cookie, ignore it, it's unusable.
|
||||
// If we can't parse the cookie, ignore it, it's unusable.
|
||||
s = decodeURIComponent(s.replace(pluses, ' '));
|
||||
return config.json ? JSON.parse(s) : s;
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function read(s, converter) {
|
||||
var value = config.raw ? s : parseCookieValue(s);
|
||||
return $.isFunction(converter) ? converter(value) : value;
|
||||
}
|
||||
|
||||
var config = $.cookie = function (key, value, options) {
|
||||
|
||||
// Write
|
||||
|
||||
if (value !== undefined && !$.isFunction(value)) {
|
||||
options = $.extend({}, config.defaults, options);
|
||||
|
||||
if (typeof options.expires === 'number') {
|
||||
var days = options.expires, t = options.expires = new Date();
|
||||
t.setTime(+t + days * 864e+5);
|
||||
}
|
||||
|
||||
return (document.cookie = [
|
||||
encode(key), '=', stringifyCookieValue(value),
|
||||
options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
|
||||
options.path ? '; path=' + options.path : '',
|
||||
options.domain ? '; domain=' + options.domain : '',
|
||||
options.secure ? '; secure' : ''
|
||||
].join(''));
|
||||
}
|
||||
|
||||
// Read
|
||||
|
||||
var result = key ? undefined : {};
|
||||
|
||||
// To prevent the for loop in the first place assign an empty array
|
||||
// in case there are no cookies at all. Also prevents odd result when
|
||||
// calling $.cookie().
|
||||
var cookies = document.cookie ? document.cookie.split('; ') : [];
|
||||
|
||||
for (var i = 0, l = cookies.length; i < l; i++) {
|
||||
var parts = cookies[i].split('=');
|
||||
var name = decode(parts.shift());
|
||||
var cookie = parts.join('=');
|
||||
|
||||
if (key && key === name) {
|
||||
// If second argument (value) is a function it's a converter...
|
||||
result = read(cookie, value);
|
||||
break;
|
||||
}
|
||||
|
||||
// Prevent storing a cookie that we couldn't decode.
|
||||
if (!key && (cookie = read(cookie)) !== undefined) {
|
||||
result[name] = cookie;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
config.defaults = {};
|
||||
|
||||
$.removeCookie = function (key, options) {
|
||||
if ($.cookie(key) === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must not alter options, thus extending a fresh object...
|
||||
$.cookie(key, '', $.extend({}, options, { expires: -1 }));
|
||||
return !$.cookie(key);
|
||||
};
|
||||
|
||||
}));
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue