├── Rakefile
├── .gitignore
├── Gemfile
├── lib
├── generator
│ ├── application
│ │ ├── app
│ │ │ ├── server
│ │ │ │ ├── models
│ │ │ │ │ └── access.yml
│ │ │ │ ├── controllers
│ │ │ │ │ └── application_controller.rb
│ │ │ │ ├── routes.rb
│ │ │ │ └── clients.rb
│ │ │ └── client
│ │ │ │ ├── templates
│ │ │ │ ├── notice
│ │ │ │ │ ├── error.html
│ │ │ │ │ ├── info.html
│ │ │ │ │ └── warning.html
│ │ │ │ ├── home
│ │ │ │ │ └── index.html
│ │ │ │ └── application.html.erb
│ │ │ │ ├── javascripts
│ │ │ │ ├── models
│ │ │ │ │ └── home.js.coffee
│ │ │ │ ├── application.js.coffee
│ │ │ │ ├── controllers
│ │ │ │ │ └── homes.js.coffee
│ │ │ │ └── views
│ │ │ │ │ └── home
│ │ │ │ │ └── index.js.coffee
│ │ │ │ └── stylesheets
│ │ │ │ ├── application.css.sass
│ │ │ │ ├── notice
│ │ │ │ ├── error.css.sass
│ │ │ │ ├── info.css.sass
│ │ │ │ └── warning.css.sass
│ │ │ │ └── home
│ │ │ │ └── index.css.sass
│ │ ├── Rakefile
│ │ ├── config.ru
│ │ ├── config
│ │ │ ├── environments
│ │ │ │ ├── development.rb
│ │ │ │ ├── production.rb
│ │ │ │ └── test.rb
│ │ │ ├── application.rb
│ │ │ └── database.yml
│ │ └── Gemfile
│ └── templates
│ │ ├── client_view_styles.tpl
│ │ ├── client_model.tpl
│ │ ├── client_controller.tpl
│ │ ├── server_model_access.tpl
│ │ ├── server_controller.tpl
│ │ ├── client_view.tpl
│ │ └── server_model.tpl
├── nali
│ ├── version.rb
│ ├── path.rb
│ ├── extensions.rb
│ ├── helpers.rb
│ ├── clients.rb
│ ├── tasks.rb
│ ├── model.rb
│ ├── connection.rb
│ ├── generator.rb
│ ├── application.rb
│ └── controller.rb
├── client
│ └── javascripts
│ │ └── nali
│ │ ├── index.js
│ │ ├── extensions.js.coffee
│ │ ├── notice.js.coffee
│ │ ├── cookie.js.coffee
│ │ ├── application.js.coffee
│ │ ├── controller.js.coffee
│ │ ├── router.js.coffee
│ │ ├── connection.js.coffee
│ │ ├── form2js.min.js
│ │ ├── nali.js.coffee
│ │ ├── collection.js.coffee
│ │ ├── jbone.min.js
│ │ ├── view.js.coffee
│ │ └── model.js.coffee
└── nali.rb
├── bin
└── nali
├── LICENSE.txt
├── nali.gemspec
└── README.md
/Rakefile:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | Gemfile.lock
3 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
--------------------------------------------------------------------------------
/lib/generator/application/app/server/models/access.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/generator/templates/client_view_styles.tpl:
--------------------------------------------------------------------------------
1 | .<%= classname %>
2 |
--------------------------------------------------------------------------------
/lib/generator/application/app/client/templates/notice/error.html:
--------------------------------------------------------------------------------
1 | { @message }
2 |
--------------------------------------------------------------------------------
/lib/generator/application/app/client/templates/notice/info.html:
--------------------------------------------------------------------------------
1 | { @message }
2 |
--------------------------------------------------------------------------------
/lib/generator/application/app/client/templates/notice/warning.html:
--------------------------------------------------------------------------------
1 | { @message }
2 |
--------------------------------------------------------------------------------
/lib/nali/version.rb:
--------------------------------------------------------------------------------
1 | module Nali
2 |
3 | VERSION = '0.4.4'
4 |
5 | end
6 |
--------------------------------------------------------------------------------
/lib/generator/application/Rakefile:
--------------------------------------------------------------------------------
1 | require './config/application'
2 |
3 | Nali::Application.tasks
4 |
--------------------------------------------------------------------------------
/lib/generator/templates/client_model.tpl:
--------------------------------------------------------------------------------
1 | Nali.Model.extend <%= classname %>:
2 |
3 | attributes: {}
4 |
--------------------------------------------------------------------------------
/lib/generator/application/config.ru:
--------------------------------------------------------------------------------
1 | require './config/application'
2 |
3 | run Nali::Application.initialize!
4 |
--------------------------------------------------------------------------------
/lib/generator/templates/client_controller.tpl:
--------------------------------------------------------------------------------
1 | Nali.Controller.extend <%= classname %>s:
2 |
3 | actions: {}
4 |
--------------------------------------------------------------------------------
/lib/generator/application/app/server/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController
2 |
3 | end
4 |
--------------------------------------------------------------------------------
/lib/generator/application/app/client/javascripts/models/home.js.coffee:
--------------------------------------------------------------------------------
1 | Nali.Model.extend Home:
2 |
3 | attributes: {}
4 |
--------------------------------------------------------------------------------
/lib/generator/templates/server_model_access.tpl:
--------------------------------------------------------------------------------
1 |
2 | <%= classname %>:
3 | create:
4 | read:
5 | update:
6 | destroy:
7 |
--------------------------------------------------------------------------------
/lib/generator/application/app/client/javascripts/application.js.coffee:
--------------------------------------------------------------------------------
1 | #= require nali
2 | #= require_tree .
3 |
4 | Nali.Application.run()
5 |
--------------------------------------------------------------------------------
/lib/nali/path.rb:
--------------------------------------------------------------------------------
1 | module Nali
2 |
3 | def self.path
4 | @gem_path ||= File.expand_path '..', File.dirname( __FILE__ )
5 | end
6 |
7 | end
--------------------------------------------------------------------------------
/lib/generator/application/app/client/stylesheets/application.css.sass:
--------------------------------------------------------------------------------
1 | /*
2 | *= require_self
3 | *= require_tree .
4 | */
5 |
6 | body
7 | margin: 0
8 |
--------------------------------------------------------------------------------
/lib/generator/templates/server_controller.tpl:
--------------------------------------------------------------------------------
1 | class <%= classname %>sController < ApplicationController
2 |
3 | include Nali::Controller
4 |
5 | end
6 |
--------------------------------------------------------------------------------
/lib/generator/application/app/server/routes.rb:
--------------------------------------------------------------------------------
1 | Nali::Application.configure do |routes|
2 |
3 | # routes.get '/test' do
4 | # Your code
5 | # end
6 |
7 | end
8 |
--------------------------------------------------------------------------------
/lib/generator/application/app/client/templates/home/index.html:
--------------------------------------------------------------------------------
1 |
Welcome to Nali
2 | Framework for developing async web applications
3 |
--------------------------------------------------------------------------------
/lib/generator/templates/client_view.tpl:
--------------------------------------------------------------------------------
1 | Nali.View.extend <%= classname %>:
2 |
3 | events: []
4 |
5 | helpers: {}
6 |
7 | onDraw: ->
8 |
9 | onShow: ->
10 |
11 | onHide: ->
12 |
--------------------------------------------------------------------------------
/lib/generator/templates/server_model.tpl:
--------------------------------------------------------------------------------
1 | class <%= classname %> < ActiveRecord::Base
2 |
3 | include Nali::Model
4 |
5 | def access_level( client )
6 | :unknown
7 | end
8 |
9 | end
10 |
--------------------------------------------------------------------------------
/lib/generator/application/app/client/javascripts/controllers/homes.js.coffee:
--------------------------------------------------------------------------------
1 | Nali.Controller.extend Homes:
2 |
3 | actions:
4 | default: 'index'
5 |
6 | index: ->
7 | @collection.freeze().add @Model.Home.new()
8 |
--------------------------------------------------------------------------------
/lib/generator/application/app/client/javascripts/views/home/index.js.coffee:
--------------------------------------------------------------------------------
1 | Nali.View.extend HomeIndex:
2 |
3 | events: []
4 |
5 | helpers: {}
6 |
7 | onDraw: ->
8 |
9 | onShow: ->
10 |
11 | onHide: ->
12 |
--------------------------------------------------------------------------------
/lib/generator/application/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Nali::Application.configure :development do |config|
2 |
3 | config.client_debug = true
4 |
5 | ActiveRecord::Base.logger = false #Logger.new STDOUT
6 |
7 | end
8 |
--------------------------------------------------------------------------------
/lib/generator/application/app/server/clients.rb:
--------------------------------------------------------------------------------
1 | module Nali::Clients
2 |
3 | def client_connected( client )
4 |
5 | end
6 |
7 | def on_message( client, message )
8 |
9 | end
10 |
11 | def client_disconnected( client )
12 |
13 | end
14 |
15 | end
16 |
--------------------------------------------------------------------------------
/lib/generator/application/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'nali'
4 |
5 | # Use sqlite3 as the database for Active Record
6 | gem 'sqlite3'
7 |
8 | # Use Uglifier as compressor for JavaScript client
9 | gem 'uglifier'
10 |
11 | # Use debugger
12 | #gem 'debugger'
13 |
--------------------------------------------------------------------------------
/bin/nali:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'fileutils'
4 | require 'nali/extensions'
5 | require 'nali/version'
6 | require 'nali/generator'
7 | require 'nali/path'
8 |
9 | if ['--version', '-v'].include?(ARGV.first)
10 | puts "Nali #{Nali::VERSION}"
11 | exit(0)
12 | end
13 |
14 | Nali::Generator.new ARGV
--------------------------------------------------------------------------------
/lib/generator/application/config/application.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'bundler/setup'
3 | Bundler.require
4 |
5 | module Nali
6 |
7 | class Application
8 |
9 | configure do |config|
10 |
11 | #Your configure settings for all environments
12 |
13 | end
14 |
15 | end
16 |
17 | end
18 |
--------------------------------------------------------------------------------
/lib/generator/application/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Nali::Application.configure :production do |config|
2 |
3 | ActiveRecord::Base.logger = false
4 |
5 | config.client_digest = true
6 |
7 | config.client.js_compressor = :uglify
8 |
9 | config.client.css_compressor = :scss
10 |
11 | end
12 |
--------------------------------------------------------------------------------
/lib/client/javascripts/nali/index.js:
--------------------------------------------------------------------------------
1 | //= require ./jbone.min
2 | //= require ./form2js.min
3 | //= require ./extensions
4 | //= require ./nali
5 | //= require ./application
6 | //= require ./connection
7 | //= require ./router
8 | //= require ./view
9 | //= require ./model
10 | //= require ./collection
11 | //= require_tree .
12 |
--------------------------------------------------------------------------------
/lib/generator/application/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Nali::Application.configure :test do |config|
2 |
3 | # ActiveRecord::Base.logger = false
4 |
5 | # config.client_digest = true
6 |
7 | # config.client_debug = true
8 |
9 | # config.client.js_compressor = :uglify
10 |
11 | # config.client.css_compressor = :scss
12 |
13 | end
14 |
--------------------------------------------------------------------------------
/lib/generator/application/app/client/templates/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= stylesheet_tag 'application' %>
6 | <%= templates_tags %>
7 | <%= javascript_tag 'application' %>
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/lib/generator/application/app/client/stylesheets/notice/error.css.sass:
--------------------------------------------------------------------------------
1 | .NoticeError
2 | position: absolute
3 | z-index: 99
4 | bottom: 1rem
5 | right: 2rem
6 | width: 55%
7 | background: rgba(35, 35, 35, 0.95)
8 | border-radius: 0.3rem
9 | box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.7)
10 | color: #F75E4E
11 | padding: 1.3rem
12 |
--------------------------------------------------------------------------------
/lib/generator/application/app/client/stylesheets/notice/info.css.sass:
--------------------------------------------------------------------------------
1 | .NoticeInfo
2 | position: absolute
3 | z-index: 99
4 | bottom: 1rem
5 | right: 2rem
6 | width: 55%
7 | background: rgba(35, 35, 35, 0.95)
8 | border-radius: 0.3rem
9 | box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.7)
10 | color: #58EB96
11 | padding: 1.3rem
12 |
--------------------------------------------------------------------------------
/lib/generator/application/app/client/stylesheets/notice/warning.css.sass:
--------------------------------------------------------------------------------
1 | .NoticeWarning
2 | position: absolute
3 | z-index: 99
4 | bottom: 1rem
5 | right: 2rem
6 | width: 55%
7 | background: rgba(35, 35, 35, 0.95)
8 | border-radius: 0.3rem
9 | box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.7)
10 | color: #FED93A
11 | padding: 1.3rem
12 |
--------------------------------------------------------------------------------
/lib/generator/application/config/database.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: sqlite3
3 | database: db/development.sqlite3
4 | pool: 5
5 | timeout: 5000
6 |
7 | test:
8 | adapter: sqlite3
9 | database: db/test.sqlite3
10 | pool: 5
11 | timeout: 5000
12 |
13 | production:
14 | adapter: sqlite3
15 | database: db/production.sqlite3
16 | pool: 5
17 | timeout: 5000
18 |
--------------------------------------------------------------------------------
/lib/nali.rb:
--------------------------------------------------------------------------------
1 | require 'sinatra/base'
2 | require 'sinatra-websocket'
3 | require 'sinatra/activerecord'
4 | require 'sinatra/reloader'
5 | require 'sprockets'
6 | require 'sprockets-sass'
7 | require 'sprockets-helpers'
8 | require 'coffee-script'
9 | require 'sass'
10 |
11 | require 'nali/extensions'
12 | require 'nali/path'
13 | require 'nali/clients'
14 | require 'nali/application'
15 | require 'nali/connection'
16 | require 'nali/controller'
17 | require 'nali/model'
18 | require 'nali/helpers'
--------------------------------------------------------------------------------
/lib/generator/application/app/client/stylesheets/home/index.css.sass:
--------------------------------------------------------------------------------
1 | .HomeIndex
2 | background: #2D3E50
3 | width: 100vw
4 | height: 100vh
5 | display: table-cell
6 | vertical-align: middle
7 | font-family: Lucida Grande, Arial, tahoma, verdana, sans-serif
8 |
9 | .welcome, .title
10 | text-align: center
11 |
12 | .welcome
13 | font-size: 10vmin
14 | color: #ECF0F1
15 |
16 | .title
17 | font-size: 3vmin
18 | color: #BDC3C7
19 |
--------------------------------------------------------------------------------
/lib/nali/extensions.rb:
--------------------------------------------------------------------------------
1 | class Object
2 |
3 | def keys_to_sym!
4 | self.keys.each do |key|
5 | self[ key ].keys_to_sym! if self[ key ].is_a?( Hash )
6 | self[ ( key.to_sym rescue key ) ] = self.delete( key )
7 | end
8 | self
9 | end
10 |
11 | end
12 |
13 | class String
14 |
15 | def underscore!
16 | gsub!( /(.)([A-Z])/, '\1_\2' )
17 | downcase!
18 | end
19 |
20 | def underscore
21 | dup.tap { |s| s.underscore! }
22 | end
23 |
24 | def camelize
25 | self.split( '_' ).collect( &:capitalize ).join
26 | end
27 |
28 | end
--------------------------------------------------------------------------------
/lib/client/javascripts/nali/extensions.js.coffee:
--------------------------------------------------------------------------------
1 | String::upper = ->
2 | "#{ @toUpperCase() }"
3 |
4 | String::lower = ->
5 | "#{ @toLowerCase() }"
6 |
7 | String::capitalize = ->
8 | @charAt(0).upper() + @slice(1)
9 |
10 | String::uncapitalize = ->
11 | @charAt(0).lower() + @slice(1)
12 |
13 | String::camel = ->
14 | @replace /(_[^_]+)/g, ( match ) -> match[ 1.. ].capitalize()
15 |
16 | String::underscore = ->
17 | str = @replace /([A-Z])/g, ( match ) -> '_' + match.lower()
18 | if str[ 0...1 ] is '_' then str[ 1.. ] else str
19 |
20 | window.__ = ( args... ) -> console.log args...
21 |
--------------------------------------------------------------------------------
/lib/client/javascripts/nali/notice.js.coffee:
--------------------------------------------------------------------------------
1 | Nali.Model.extend Notice:
2 |
3 | initialize: ->
4 | @::::expand Notice: @
5 | @_addMethods()
6 |
7 | _addMethods: ->
8 | for name of @_views
9 | do ( name ) =>
10 | @[ name ] = ( params ) => @new( @_prepare params ).show name
11 |
12 | _prepare: ( params ) ->
13 | params = message: params if typeof params is 'string'
14 | params
15 |
16 |
17 |
18 | Nali.View.extend
19 |
20 | NoticeInfo:
21 | onShow: -> @hide 3000
22 |
23 | NoticeWarning:
24 | onShow: -> @hide 3000
25 |
26 | NoticeError:
27 | onShow: -> @hide 3000
28 |
--------------------------------------------------------------------------------
/lib/nali/helpers.rb:
--------------------------------------------------------------------------------
1 | module Sprockets
2 | module Helpers
3 |
4 | def templates_tags
5 | result = ''
6 | Dir[ File.join( './app/client/templates/*/*' ) ].each do |path|
7 | arr = path.split( '/' ).reverse
8 | id = arr[1] + '_' + arr[0].split( '.' )[0]
9 | asset = environment[ path ]
10 | template = asset.body.force_encoding( 'UTF-8' ).strip.gsub( /\n\s*\n/, "\n" ).gsub( "\n", "\n " )
11 | result += %Q(\n )
12 | depend_on asset.pathname
13 | end
14 | result
15 | end
16 |
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/client/javascripts/nali/cookie.js.coffee:
--------------------------------------------------------------------------------
1 | Nali.extend Cookie:
2 |
3 | set: ( name, value, options = {} ) ->
4 | set = "#{ name }=#{ escape( value ) }"
5 | if options.live? and typeof options.live is 'number'
6 | date = new Date
7 | date.setDate date.getDate() + options.live
8 | date.setMinutes date.getMinutes() - date.getTimezoneOffset()
9 | set += "; expires=#{ date.toUTCString() }"
10 | set += '; domain=' + escape options.domain if options.domain?
11 | set += '; path=' + if options.path? then escape options.path else '/'
12 | set += '; secure' if options.secure?
13 | document.cookie = set
14 | value
15 |
16 | get: ( name ) ->
17 | get = document.cookie.match "(^|;) ?#{ name }=([^;]*)(;|$)"
18 | if get then unescape( get[2] ) else null
19 |
20 | remove: ( name ) ->
21 | @set name, '', live: -1
22 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 4urbanoff
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/nali.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'nali/version'
5 |
6 | Gem::Specification.new do |s|
7 | s.name = 'nali'
8 | s.version = Nali::VERSION
9 | s.authors = ['4urbanoff']
10 | s.email = ['4urbanoff@gmail.com']
11 | s.description = 'Async web framework'
12 | s.summary = 'Framework for developing async web applications'
13 | s.homepage = 'https://github.com/4urbanoff/nali'
14 | s.license = 'MIT'
15 |
16 | s.files = Dir['lib/**/*', 'bin/**/*'] + ['LICENSE.txt', 'Rakefile', 'Gemfile', 'README.md']
17 | s.require_path = 'lib'
18 |
19 | s.bindir = 'bin'
20 | s.executables = ['nali']
21 |
22 | s.has_rdoc = false
23 |
24 | s.add_dependency 'thin', '>= 1.6'
25 | s.add_dependency 'rake', '~> 10.3'
26 | s.add_dependency 'sinatra', '>= 1.4'
27 | s.add_dependency 'sinatra-websocket', '~> 0.3'
28 | s.add_dependency 'sinatra-activerecord', '~> 2.0'
29 | s.add_dependency 'sinatra-reloader', '~> 1.0'
30 | s.add_dependency 'sprockets', '> 2.0'
31 | s.add_dependency 'sprockets-sass', '~> 1.2'
32 | s.add_dependency 'sprockets-helpers', '~> 1.1'
33 | s.add_dependency 'coffee-script', '~> 2.3'
34 | s.add_dependency 'sass', '~> 3.4'
35 |
36 | end
37 |
--------------------------------------------------------------------------------
/lib/nali/clients.rb:
--------------------------------------------------------------------------------
1 | module Nali
2 |
3 | module Clients
4 |
5 | def self.list
6 | @@list ||= []
7 | end
8 |
9 | def clients
10 | Nali::Clients.list
11 | end
12 |
13 | def on_client_connected( client )
14 | clients << client
15 | end
16 |
17 | def on_received_message( client, message )
18 | if message[ :nali_browser_id ]
19 | client.browser_id = message[ :nali_browser_id ]
20 | client_connected( client ) if respond_to?( :client_connected )
21 | client.send_json action: :_onOpen
22 | elsif message[ :ping ]
23 | client.send_json action: :_pong
24 | elsif message[ :controller ]
25 | name = message[ :controller ].capitalize + 'Controller'
26 | if Math.const_defined?( name ) and controller = Object.const_get( name )
27 | controller = controller.new( client, message )
28 | if controller.respond_to?( action = message[ :action ].to_sym )
29 | controller.runAction action
30 | else puts "Action #{ action } not exists in #{ controller }" end
31 | else puts "Controller #{ name } not exists" end
32 | on_message( client, message ) if respond_to?( :on_message )
33 | end
34 | end
35 |
36 | def on_client_disconnected( client )
37 | clients.delete client
38 | client_disconnected( client ) if respond_to?( :client_disconnected )
39 | end
40 |
41 | end
42 |
43 | end
44 |
--------------------------------------------------------------------------------
/lib/client/javascripts/nali/application.js.coffee:
--------------------------------------------------------------------------------
1 | Nali.extend Application:
2 |
3 | domEngine: jBone.noConflict()
4 | useWebSockets: true
5 | wsServer: 'ws://' + window.location.host
6 | defaultUrl: 'home'
7 | notFoundUrl: 'home'
8 | htmlContainer: 'body'
9 | title: 'Welcome to Nali'
10 | keepAliveDelay: 20
11 |
12 | run: ( options ) ->
13 | @::starting()
14 | @[ key ] = value for key, value of options
15 | @_onReadyDOM ->
16 | @::_ = @domEngine
17 | @htmlContainer = @_ @htmlContainer
18 | @setTitle @title
19 | @Router.start()
20 | @_runConnection()
21 |
22 | _onReadyDOM: ( callback ) ->
23 | document.addEventListener 'DOMContentLoaded', =>
24 | document.removeEventListener 'DOMContentLoaded', arguments.callee, false
25 | callback.call @
26 | , false
27 | @
28 |
29 | _runConnection: ->
30 | if @useWebSockets
31 | @Connection.subscribe @, 'open', @onConnectionOpen
32 | @Connection.subscribe @, 'close', @onConnectionClose
33 | @Connection.subscribe @, 'error', @onConnectionError
34 | @Connection.open()
35 | else @redirect()
36 | @
37 |
38 | onConnectionOpen: ->
39 | @redirect()
40 |
41 | onConnectionClose: ->
42 |
43 | onConnectionError: ->
44 |
45 | setTitle: ( @title ) ->
46 | @titleBox ?= if ( exists = @_ 'head title' ).lenght then exists else @_( '' ).appendTo 'head'
47 | @titleBox.html @title
48 | @trigger 'update.title'
49 | @
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Добро пожаловать в Nali
2 | **Nali** - это фреймворк для разработки асинхронных веб приложений, включающий в себя веб-сервер и инструменты для создания как клиентской, так и серверной части. Серверная часть разрабатывается на языке **Ruby**, клиентская часть на **Coffeescript** (или Javascript), **Sass** (или scss, css) и **Html** (или erb)
3 |
4 | #### Документация
5 |
6 | * [**Начало работы**](https://github.com/4urbanoff/nali/wiki/Начало-работы)
7 | * [**Структура приложения**](https://github.com/4urbanoff/nali/wiki/Структура-приложения)
8 | * **Основные компоненты**
9 | * [Модель](https://github.com/4urbanoff/nali/wiki/Модель)
10 | * [Вид](https://github.com/4urbanoff/nali/wiki/Вид)
11 | * [Контроллер](https://github.com/4urbanoff/nali/wiki/Контроллер)
12 | * [Коллекция](https://github.com/4urbanoff/nali/wiki/Коллекция)
13 | * **Клиентская часть**
14 | * [Базовый объект Nali](https://github.com/4urbanoff/nali/wiki/Nali)
15 | * [Старт приложения](https://github.com/4urbanoff/nali/wiki/Nali.Application)
16 | * [Система событий](https://github.com/4urbanoff/nali/wiki/Система-событий)
17 | * [Модель уведомлений Notice](https://github.com/4urbanoff/nali/wiki/Nali.Notice)
18 | * [Роутер](https://github.com/4urbanoff/nali/wiki/Nali.Router)
19 | * [Соединение с сервером](https://github.com/4urbanoff/nali/wiki/Nali.Connection)
20 | * [Работа с Cookie](https://github.com/4urbanoff/nali/wiki/Nali.Cookie)
21 | * **Серверная часть**
22 | * [Работа с клиентами](https://github.com/4urbanoff/nali/wiki/Работа-с-клиентами)
23 | * [Маршрутизация](https://github.com/4urbanoff/nali/wiki/Маршрутизация)
24 | * [Генератор](https://github.com/4urbanoff/nali/wiki/Генератор)
25 | * [Rake задачи](https://github.com/4urbanoff/nali/wiki/Rake-задачи)
26 | * [**Пример**](https://github.com/4urbanoff/anonim.am)
--------------------------------------------------------------------------------
/lib/client/javascripts/nali/controller.js.coffee:
--------------------------------------------------------------------------------
1 | Nali.extend Controller:
2 |
3 | extension: ->
4 | if @_name isnt 'Controller'
5 | @_prepareActions()
6 | @modelName = @_name.replace /s$/, ''
7 | @
8 |
9 | new: ( collection, filters, params ) ->
10 | @clone collection: collection, filters: filters, params: params
11 |
12 | _prepareActions: ->
13 | @_actions = {}
14 | for name, action of @actions when not ( name in [ 'default', 'before', 'after' ] )
15 | [ name, filters... ] = name.split '/'
16 | params = []
17 | for filter in filters[ 0.. ] when /^:/.test filter
18 | filters.splice filters.indexOf( filter ), 1
19 | params.push filter[ 1.. ]
20 | @_actions[ name ] = filters: filters, params: params, methods: [ action ]
21 | @_prepareBefores()
22 | @_prepareAfters()
23 | @
24 |
25 | _prepareBefores: ->
26 | if @actions?.before?
27 | list = @_analizeFilters 'before'
28 | @_actions[ name ].methods = actions.concat @_actions[ name ].methods for name, actions of list
29 | @
30 |
31 | _prepareAfters: ->
32 | if @actions?.after?
33 | list = @_analizeFilters 'after'
34 | @_actions[ name ].methods = @_actions[ name ].methods.concat actions for name, actions of list
35 | @
36 |
37 | _analizeFilters: ( type ) ->
38 | list = {}
39 | for names, action of @actions[ type ]
40 | [ invert, names ] = switch
41 | when /^!\s*/.test names then [ true, names.replace( /^!\s*/, '' ).split /\s*,\s*/ ]
42 | when names is '*' then [ true, [] ]
43 | else [ false, names.split /\s*,\s*/ ]
44 | for name of @_actions when ( invert and not ( name in names ) ) or ( not invert and name in names )
45 | ( list[ name ] ?= [] ).push action
46 | list
47 |
48 | run: ( action, filters, params ) ->
49 | collection = @Model.extensions[ @modelName ].where filters
50 | @new( collection, filters, params ).runAction action
51 | @
52 |
53 | runAction: ( name ) ->
54 | method.call @ for method in @_actions[ name ].methods when not @_stopped
55 | if @_stopped then @collection.destroy()
56 | else
57 | @collection.show name
58 | @Router.changeUrl()
59 | @
60 |
61 | stop: ->
62 | @_stopped = true
63 | @
64 |
65 | redirect: ( args... ) ->
66 | @Router.redirect args...
67 | @stop()
68 | @
69 |
--------------------------------------------------------------------------------
/lib/nali/tasks.rb:
--------------------------------------------------------------------------------
1 | module Nali
2 |
3 | class Tasks < Rake::TaskLib
4 |
5 | def initialize
6 | @settings = Nali::Application.settings
7 | define
8 | end
9 |
10 | private
11 |
12 | def define
13 |
14 | namespace :client do
15 | desc "Compile client files"
16 | task :compile do
17 | sprockets_tasks
18 | remove_cached_files
19 | remove_compiled_files
20 | Rake::Task[ 'assets' ].invoke
21 |
22 | compiled_path = File.join @settings.public_folder, 'client'
23 | Dir[ compiled_path + '/*' ]
24 | .select { |file| file =~ /.*?\.gz/ }
25 | .each { |file| File.delete file }
26 | Dir[ compiled_path + '/*' ]
27 | .select { |file| file =~ /application.*?\.html/ }
28 | .each do |file|
29 | filename = File.basename( file ).split '.'
30 | filename[0] = 'index'
31 | filename = filename.join '.'
32 | File.rename file, File.join( @settings.public_folder, filename )
33 | end
34 | remove_cached_files
35 | puts 'Client files compiled'
36 | end
37 |
38 | desc 'Remove compiled client files'
39 | task :clean do
40 | remove_compiled_files
41 | puts 'Compiled client files removed'
42 | end
43 |
44 | namespace :cache do
45 | desc 'Remove cached client files'
46 | task :clean do
47 | remove_compiled_files
48 | end
49 | end
50 |
51 | end
52 | end
53 |
54 | def remove_cached_files
55 | FileUtils.rm_rf File.join( @settings.root, 'tmp/cache' )
56 | end
57 |
58 | def remove_compiled_files
59 | FileUtils.rm_rf File.join( @settings.public_folder, 'client' )
60 | index = File.join( @settings.public_folder, 'index.html' )
61 | File.delete( index ) if File.exists?( index )
62 | end
63 |
64 | def sprockets_tasks
65 | require 'rake/sprocketstask'
66 | Sprockets::Helpers.configure do |config|
67 | config.debug = false
68 | end
69 | Rake::SprocketsTask.new do |task|
70 | task.environment = @settings.client
71 | task.output = File.join( @settings.public_folder, 'client' )
72 | task.assets = %w( application.html application.js application.css )
73 | end
74 | end
75 |
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/lib/client/javascripts/nali/router.js.coffee:
--------------------------------------------------------------------------------
1 | Nali.extend Router:
2 |
3 | initialize: ->
4 | @::expand redirect: ( args... ) => @redirect args...
5 | @
6 |
7 | _routes: {}
8 |
9 | start: ->
10 | @_scanRoutes()
11 | @_( window ).on 'popstate', ( event ) =>
12 | event.preventDefault()
13 | event.stopPropagation()
14 | @_saveHistory = false
15 | @redirect event.target.location.pathname
16 | @
17 |
18 | _scanRoutes: ->
19 | for name, controller of @Controller.extensions when controller.actions?
20 | route = '^'
21 | route += name.lower().replace /s$/, 's*(\/|$)'
22 | route += '('
23 | route += Object.keys( controller._actions ).join '|'
24 | route += ')?'
25 | @_routes[ route ] = controller
26 | @
27 |
28 | redirect: ( url = window.location.pathname, options = {} ) ->
29 | if found = @_findRoute @_prepare( url ) or @_prepare( @Application.defaultUrl )
30 | { controller, action, filters, params } = found
31 | params[ name ] = value for name, value in options
32 | controller.run action, filters, params
33 | else if @Application.notFoundUrl
34 | @redirect @Application.notFoundUrl
35 | else console.warn "Not exists route to the address %s", url
36 | @
37 |
38 | _prepare: ( url ) ->
39 | url = url.replace "http://#{ window.location.host }", ''
40 | url = url[ 1.. ] or '' if url and url[ 0...1 ] is '/'
41 | url = url[ ...-1 ] or '' if url and url[ -1.. ] is '/'
42 | url
43 |
44 | _findRoute: ( url ) ->
45 | for route, controller of @_routes when match = url.match new RegExp route, 'i'
46 | segments = ( @routedUrl = url ).split( '/' )[ 1... ]
47 | if segments[0] in Object.keys( controller._actions )
48 | action = segments.shift()
49 | else unless action = controller.actions.default
50 | console.error 'Unspecified controller action'
51 | filters = {}
52 | for name in controller._actions[ action ].filters when segments[0]?
53 | filters[ name ] = segments.shift()
54 | params = {}
55 | for name in controller._actions[ action ].params
56 | params[ name ] = if segments[0]? then segments.shift() else null
57 | return controller: controller, action: action, filters: filters, params: params
58 | false
59 |
60 | changeUrl: ( url = null ) ->
61 | if @_saveHistory
62 | @routedUrl = url if url?
63 | history.pushState null, null, '/' + ( @url = @routedUrl ) if @routedUrl isnt @url
64 | else @_saveHistory = true
65 | @
66 |
--------------------------------------------------------------------------------
/lib/nali/model.rb:
--------------------------------------------------------------------------------
1 | module Nali
2 |
3 | module Model
4 |
5 | def self.included( base )
6 | base.extend self
7 | base.class_eval do
8 | after_destroy { sync }
9 | end
10 | end
11 |
12 | def access_level( client )
13 | :unknown
14 | end
15 |
16 | def access_action( action, client )
17 | level = self.access_level client
18 | if access_levels = access_options[ action ] and access_levels.keys.include?( level )
19 | options = []
20 | ( [] << access_levels[ level ] ).flatten.compact.each { |option| options << option.to_sym }
21 | yield options
22 | end
23 | end
24 |
25 | def get_sync_params( client )
26 | params = {}
27 | relations = []
28 | if self.destroyed?
29 | sync_initial params
30 | params[ :destroyed ] = true
31 | else
32 | access_action( :read, client ) do |options|
33 | sync_initial params
34 | options.each do |option|
35 | if self.respond_to?( option )
36 | value = self.send option
37 | if value.is_a?( ActiveRecord::Relation )
38 | relations << value
39 | elsif value.is_a?( ActiveRecord::Base )
40 | relations << value
41 | if self.respond_to?( key = option.to_s + '_id' ) and self[ key ] == value.id
42 | params[ :attributes ][ key ] = value.id
43 | end
44 | else
45 | params[ :attributes ][ option ] = value
46 | end
47 | end
48 | end
49 | params[ :created ] = self.created_at.to_f
50 | params[ :updated ] = self.updated_at.to_f
51 | end
52 | end
53 | [ params, relations.flatten.compact ]
54 | end
55 |
56 | def sync_initial( params )
57 | params[ :name ] = self.class.name
58 | params[ :attributes ] = { id: self.id }
59 | end
60 |
61 | def clients
62 | Nali::Clients.list
63 | end
64 |
65 | def sync( *watches )
66 | watches.flatten.each { |client| client.watch self }
67 | clients.each { |client| client.sync self if client.watch?( self ) }
68 | end
69 |
70 | def call_method( name, params )
71 | clients.each { |client| client.call_method self, name, params if client.watch?( self ) }
72 | end
73 |
74 | private
75 |
76 | def access_options
77 | Nali::Application.access_options[ self.class.name.to_sym ] or {}
78 | end
79 |
80 | end
81 |
82 | end
83 |
--------------------------------------------------------------------------------
/lib/client/javascripts/nali/connection.js.coffee:
--------------------------------------------------------------------------------
1 | Nali.extend Connection:
2 |
3 | initialize: ->
4 | @::expand query: ( args... ) => @query args...
5 | @
6 |
7 | open: ->
8 | @_dispatcher = new WebSocket @Application.wsServer
9 | @_dispatcher.onopen = ( event ) => @_identification()
10 | @_dispatcher.onclose = ( event ) => @_onClose event
11 | @_dispatcher.onerror = ( event ) => @_onError event
12 | @_dispatcher.onmessage = ( event ) => @_onMessage JSON.parse event.data
13 | @_keepAlive()
14 | @
15 |
16 | _keepAliveTimer: null
17 | _journal: []
18 | _reconnectDelay: 0
19 |
20 | _onOpen: ->
21 | @_reconnectDelay = 0
22 | @trigger 'open'
23 |
24 | _onError: ( event ) ->
25 | console.warn 'Connection error %O', event
26 |
27 | _onClose: ( event ) ->
28 | @trigger 'close'
29 | setTimeout ( => @open() ), @_reconnectDelay * 100
30 | @_reconnectDelay += 1
31 |
32 | _onMessage: ( message ) ->
33 | @[ message.action ] message
34 |
35 | _send: ( msg ) ->
36 | @_dispatcher.send JSON.stringify msg
37 | @
38 |
39 | _keepAlive: ->
40 | clearTimeout @_keepAliveTimer if @_keepAliveTimer
41 | if @Application.keepAliveDelay
42 | @_keepAliveTimer = setTimeout =>
43 | @_keepAliveTimer = null
44 | @_send ping: true
45 | , @Application.keepAliveDelay * 1000
46 | @
47 |
48 | _pong: ->
49 | @_keepAlive()
50 | @
51 |
52 | _identification: ->
53 | @_send nali_browser_id: @Cookie.get( 'nali_browser_id' ) or @Cookie.set 'nali_browser_id', @Model.guid()
54 | @
55 |
56 | _sync: ( message ) ->
57 | @Model.sync message.params
58 | @
59 |
60 | _appRun: ( { method, params } ) ->
61 | @Application[ method ]? params
62 | @
63 |
64 | _callMethod: ( { model, method, params } ) ->
65 | if model is 'Notice' then @Notice[ method ] params
66 | else
67 | [ model, id ] = model.split '.'
68 | @Model.callStackAdd model: model, id: id, method: method, params: params
69 | @
70 |
71 | _success: ( message ) ->
72 | @_journal[ message.journal_id ].success? message.params
73 | delete @_journal[ message.journal_id ]
74 | @
75 |
76 | _failure: ( message ) ->
77 | @_journal[ message.journal_id ].failure? message.params
78 | delete @_journal[ message.journal_id ]
79 | @
80 |
81 | query: ( to, params, success, failure ) ->
82 | return success?() unless @Application.useWebSockets
83 | [ controller, action ] = to.split '.'
84 | @_journal.push callbacks = success: success, failure: failure
85 | @_send
86 | controller: controller
87 | action: action
88 | params: params
89 | journal_id: @_journal.indexOf callbacks
90 | @
91 |
--------------------------------------------------------------------------------
/lib/client/javascripts/nali/form2js.min.js:
--------------------------------------------------------------------------------
1 | (function(e,t){if(typeof define==="function"&&define.amd){define(t)}else{e.form2js=t()}})(this,function(){"use strict";function e(e,r,i,s,o,u){u=u?true:false;if(typeof i=="undefined"||i==null)i=true;if(typeof r=="undefined"||r==null)r=".";if(arguments.length<5)o=false;e=typeof e=="string"?document.getElementById(e):e;var a=[],f,l=0;if(e.constructor==Array||typeof NodeList!="undefined"&&e.constructor==NodeList){while(f=e[l++]){a=a.concat(n(f,s,o,u))}}else{a=n(e,s,o,u)}return t(a,i,r)}function t(e,t,n){var r={},i={},s,o,u,a,f,l,c,h,p,d,v,m,g;for(s=0;s1){for(u=0;u-1&&o==l.length-1){p=v.substr(0,v.indexOf("["));h+=p;if(!c[p])c[p]=[];c[p].push(f)}else if(v.indexOf("[")>-1){p=v.substr(0,v.indexOf("["));d=v.replace(/(^([a-z_]+)?\[)|(\]$)/gi,"");h+="_"+p+"_"+d;if(!i[h])i[h]={};if(p!=""&&!c[p])c[p]=[];if(o==l.length-1){if(p==""){c.push(f);i[h][d]=c[c.length-1]}else{c[p].push(f);i[h][d]=c[p][c[p].length-1]}}else{if(!i[h][d]){if(/^[0-9a-z_]+\[?/i.test(l[o+1]))c[p].push({});else c[p].push([]);i[h][d]=c[p][c[p].length-1]}}c=i[h][d]}else{h+=v;if(o0?o:r(e,t,n,s)}function r(e,t,n,r){var s=[],o=e.firstChild;while(o){s=s.concat(i(o,t,n,r));o=o.nextSibling}return s}function i(e,t,n,i){if(e.disabled&&!i)return[];var u,a,f,l=s(e,n);u=t&&t(e);if(u&&u.name){f=[u]}else if(l!=""&&e.nodeName.match(/INPUT|TEXTAREA/i)){a=o(e,i);if(null===a){f=[]}else{f=[{name:l,value:a}]}}else if(l!=""&&e.nodeName.match(/SELECT/i)){a=o(e,i);f=[{name:l.replace(/\[\]$/,""),value:a}]}else{f=r(e,t,n,i)}return f}function s(e,t){if(e.name&&e.name!="")return e.name;else if(t&&e.id&&e.id!="")return e.id;else return""}function o(e,t){if(e.disabled&&!t)return null;switch(e.nodeName){case"INPUT":case"TEXTAREA":switch(e.type.toLowerCase()){case"radio":if(e.checked&&e.value==="false")return false;case"checkbox":if(e.checked&&e.value==="true")return true;if(!e.checked&&e.value==="true")return false;if(e.checked)return e.value;break;case"button":case"reset":case"submit":case"image":return"";break;default:return e.value;break}break;case"SELECT":return u(e);break;default:break}return null}function u(e){var t=e.multiple,n=[],r,i,s;if(!t)return e.value;for(r=e.getElementsByTagName("option"),i=0,s=r.length;i 0
58 | return puts 'Please don\'t use the underscore'
59 | end
60 | clean_cache
61 | filename = name.downcase
62 | classname = name.camelize
63 | write "app/client/javascripts/models/#{ filename }.js.coffee", render( 'client_model', classname )
64 | write "app/client/javascripts/controllers/#{ filename }s.js.coffee", render( 'client_controller', classname )
65 | write "app/server/models/#{ filename }.rb", render( 'server_model', classname )
66 | write "app/server/controllers/#{ filename }s_controller.rb", render( 'server_controller', classname )
67 | write "app/server/models/access.yml", render( 'server_model_access', classname ), 'a'
68 | else puts 'Please go to the application folder' end
69 | end
70 |
71 | def create_view( name )
72 | if Dir.exists?( File.join( Dir.pwd, 'app' ) )
73 | dirname, *filename = name.underscore.split( '_' )
74 | filename = filename.join( '_' )
75 | classname = name.underscore.camelize
76 | if not dirname.empty? and not filename.empty? and not classname.empty?
77 | clean_cache
78 | [
79 | "app/client/javascripts/views/#{ dirname }",
80 | "app/client/stylesheets/#{ dirname }",
81 | "app/client/templates/#{ dirname }"
82 | ].each { |dir| unless Dir.exists?( path = File.join( Dir.pwd, dir ) ) then Dir.mkdir( path ) end }
83 | write "app/client/javascripts/views/#{ dirname }/#{ filename }.js.coffee", render( 'client_view', classname )
84 | write "app/client/stylesheets/#{ dirname }/#{ filename }.css.sass", render( 'client_view_styles', classname )
85 | write "app/client/templates/#{ dirname }/#{ filename }.html", ''
86 | else puts 'Invalid view name' end
87 | else puts 'Please go to the application folder' end
88 | end
89 |
90 | end
91 |
92 | end
93 |
--------------------------------------------------------------------------------
/lib/nali/application.rb:
--------------------------------------------------------------------------------
1 | module Nali
2 |
3 | class Application < Sinatra::Base
4 |
5 | set :root, File.expand_path( '.' )
6 | set :database_file, File.join( root, 'config/database.yml' )
7 | set :client, Sprockets::Environment.new( root )
8 | set :client_digest, false
9 | set :client_debug, false
10 | set :static, true
11 |
12 | register Sinatra::ActiveRecordExtension
13 |
14 | configure :development do
15 | register Sinatra::Reloader
16 | also_reload File.join( root, '**/*.rb' )
17 | end
18 |
19 | require File.join( root, 'config/environments', environment.to_s )
20 |
21 | configure do
22 |
23 | client.cache = Sprockets::Cache::FileStore.new File.join( root, 'tmp/cache' )
24 |
25 | client.append_path File.join( Nali.path, 'client/javascripts' )
26 |
27 | %w( app/client/templates app/client/stylesheets app/client/javascripts lib/client/stylesheets
28 | lib/client/javascripts vendor/client/stylesheets vendor/client/javascripts
29 | ).each { |path| client.append_path File.join( root, path ) }
30 |
31 | Sprockets::Helpers.configure do |config|
32 | config.environment = client
33 | config.debug = client_debug
34 | config.digest = client_digest
35 | config.prefix = '/client'
36 | end
37 |
38 | end
39 |
40 | get '/client/*.*' do |path, ext|
41 | pass if ext == 'html' or not asset = settings.client[ path + '.' + ext ]
42 | content_type asset.content_type
43 | params[ :body ] ? asset.body : asset
44 | end
45 |
46 | require File.join( root, 'app/server/routes' )
47 |
48 | include Nali::Clients
49 |
50 | get '/*' do
51 | if !request.websocket?
52 | compiled_path = File.join settings.public_folder, 'index.html'
53 | if settings.environment != :development and File.exists?( compiled_path )
54 | send_file compiled_path
55 | else
56 | settings.client[ 'application.html' ]
57 | end
58 | else
59 | request.websocket do |client|
60 | client.onopen { on_client_connected client }
61 | client.onmessage { |message| on_received_message( client, JSON.parse( message ).keys_to_sym! ) }
62 | client.onclose { on_client_disconnected client }
63 | end
64 | end
65 | end
66 |
67 | def self.access_options
68 | settings.environment == :development ? get_access_options : @access_options ||= get_access_options
69 | end
70 |
71 | def self.initialize!
72 | %w(
73 | lib/*/**/*.rb
74 | app/server/controllers/application_controller.rb
75 | app/server/**/*.rb
76 | config/application
77 | app/server/clients
78 | config/initializers/**/*.rb
79 | ).each { |path| Dir[ File.join( root, path ) ].sort.each { |file| require( file ) } }
80 | self
81 | end
82 |
83 | def self.tasks
84 | initialize!
85 | require 'rake/tasklib'
86 | require 'sinatra/activerecord/rake'
87 | require 'nali/tasks'
88 | Nali::Tasks.new
89 | end
90 |
91 | private
92 |
93 | def self.get_access_options
94 | YAML.load_file( File.join( root, 'app/server/models/access.yml' ) ).keys_to_sym!.each_value do |sections|
95 | [ :create, :read, :update ].each do |type|
96 | if section = sections[ type ]
97 | section.each_key { |level| parse_access_level section, level }
98 | end
99 | end
100 | end
101 | end
102 |
103 | def self.parse_access_level( section, level )
104 | parsed = []
105 | if section[ level ]
106 | section[ level ].each do |value|
107 | value =~ /^\+/ ? parsed += parse_access_level( section, value[ /[^\+]+/ ].to_sym ) : parsed << value
108 | end
109 | end
110 | section[ level ] = parsed
111 | end
112 |
113 | end
114 |
115 | end
116 |
--------------------------------------------------------------------------------
/lib/client/javascripts/nali/nali.js.coffee:
--------------------------------------------------------------------------------
1 | window.Nali =
2 |
3 | _name: 'Nali'
4 | extensions: {}
5 |
6 | starting: ->
7 | for name, extension of @extensions
8 | extension._runExtensions()
9 | extension.initialize() if extension.hasOwnProperty 'initialize'
10 | @starting.call extension
11 | @
12 |
13 | extend: ( params ) ->
14 | for name, param of params
15 | param._name = name
16 | @[ name ] = @extensions[ name ] = @_child param
17 | @[ name ].extensions = {}
18 |
19 | clone: ( params = {} ) ->
20 | obj = @_child params
21 | obj._runCloning()
22 | obj
23 |
24 | _child: ( params ) ->
25 | obj = Object.create @
26 | obj :: = @
27 | obj[ name ] = value for name, value of params
28 | obj._initObservation()
29 | obj
30 |
31 | expand: ( obj ) ->
32 | if obj instanceof Object then @[ key ] = value for own key, value of obj
33 | else console.error "Expand of %O error - argument is not Object", @
34 | @
35 |
36 | copy: ( obj ) ->
37 | copy = {}
38 | copy[ property ] = value for own property, value of obj
39 | copy
40 |
41 | childOf: ( parent ) ->
42 | if parent instanceof Object
43 | return false unless @::?
44 | return true if ( @:: ) is parent
45 | else
46 | return false unless @::?._name?
47 | return true if @::_name is parent
48 | @::childOf parent
49 |
50 | _runExtensions: ( context = @ ) ->
51 | @::?._runExtensions context
52 | @extension.call context if @hasOwnProperty 'extension'
53 |
54 | _runCloning: ( context = @ ) ->
55 | @::?._runCloning context
56 | @cloning.call context if @hasOwnProperty 'cloning'
57 |
58 | getter: ( property, callback ) ->
59 | @__defineGetter__ property, callback
60 | @
61 |
62 | setter: ( property, callback ) ->
63 | @__defineSetter__ property, callback
64 | @
65 |
66 | access: ( obj ) ->
67 | for own property of obj
68 | do( property ) =>
69 | @getter property, -> obj[ property ]
70 | @setter property, ( value ) -> obj[ property ] = value
71 | @
72 |
73 | _initObservation: ->
74 | @_observers = []
75 | @_observables = []
76 | @
77 |
78 | destroyObservation: ->
79 | @unsubscribeAll()
80 | @unsubscribeFromAll()
81 | @
82 |
83 | _addObservationItem: ( to, obj, event, callback ) ->
84 | return @ for item in @[ to ] when item[0] is obj and item[1] is event and item[2] in [ undefined, callback ]
85 | @[ to ].push if callback then [ obj, event, callback ] else [ obj, event ]
86 | @
87 |
88 | _removeObservationItem: ( from, obj, event ) ->
89 | for item in @[ from ][..] when item[0] is obj and ( item[1] is event or not event )
90 | @[ from ].splice @[ from ].indexOf( item ), 1
91 | @
92 |
93 | subscribe: ( observer, event, callback ) ->
94 | @_addObservationItem '_observers', observer, event, callback
95 | observer._addObservationItem '_observables', @, event, callback
96 | @
97 |
98 | subscribeTo: ( observable, event, callback ) ->
99 | observable.subscribe @, event, callback
100 | @
101 |
102 | subscribeOne: ( observer, event, callback ) ->
103 | callbackOne = ( args... ) =>
104 | callback.call observer, args...
105 | @unsubscribe observer, event
106 | @subscribe observer, event, callbackOne
107 | @
108 |
109 | subscribeOneTo: ( observable, event, callback ) ->
110 | observable.subscribeOne @, event, callback
111 | @
112 |
113 | unsubscribe: ( observer, event ) ->
114 | @_removeObservationItem '_observers', observer, event
115 | observer._removeObservationItem '_observables', @, event
116 | @
117 |
118 | unsubscribeFrom: ( observable, event ) ->
119 | observable.unsubscribe @, event
120 | @
121 |
122 | unsubscribeAll: ( event ) ->
123 | @unsubscribe item[0], event for item in @_observers[..]
124 | @
125 |
126 | unsubscribeFromAll: ( event ) ->
127 | @unsubscribeFrom item[0], event for item in @_observables[..]
128 | @
129 |
130 | trigger: ( event, args... ) ->
131 | item[2].call item[0], args... for item in @_observers[..] when item[1] is event
132 | @
133 |
--------------------------------------------------------------------------------
/lib/nali/controller.rb:
--------------------------------------------------------------------------------
1 | module Nali
2 |
3 | module Controller
4 |
5 | attr_reader :client, :params
6 |
7 | def self.included( base )
8 | base.extend self
9 | base.class_eval do
10 | self.class_variable_set :@@befores, []
11 | self.class_variable_set :@@afters, []
12 | self.class_variable_set :@@selectors, {}
13 | def self.befores
14 | self.class_variable_get :@@befores
15 | end
16 | def self.afters
17 | self.class_variable_get :@@afters
18 | end
19 | def self.selectors
20 | self.class_variable_get :@@selectors
21 | end
22 | end
23 | end
24 |
25 | def initialize( client, message )
26 | @client = client
27 | @message = message
28 | @params = message[ :params ]
29 | end
30 |
31 | def clients
32 | Nali::Clients.list
33 | end
34 |
35 | def save
36 | params[ :id ].to_i.to_s == params[ :id ].to_s ? _update : _create
37 | end
38 |
39 | def _create
40 | model = model_class.new params
41 | model.access_action( :create, client ) do |options|
42 | permit_params options
43 | if ( model = model_class.new( params ) ).save
44 | trigger_success model.get_sync_params( client )[0]
45 | client.sync model
46 | else trigger_failure end
47 | end
48 | end
49 |
50 | def _update
51 | if model = model_class.find_by_id( params[ :id ] )
52 | model.access_action( :update, client ) do |options|
53 | permit_params options
54 | if model.update( params )
55 | trigger_success model.get_sync_params( client )[0]
56 | model.sync client
57 | else trigger_failure end
58 | end
59 | end
60 | end
61 |
62 | def destroy
63 | if model = model_class.find_by_id( params[ :id ] )
64 | model.access_action( :destroy, client ) do |options|
65 | model.destroy()
66 | trigger_success model.id
67 | end
68 | else trigger_failure end
69 | end
70 |
71 | def trigger_success( params = nil )
72 | client.send_json( { action: :_success, params: params, journal_id: @message[ :journal_id ] } )
73 | end
74 |
75 | def trigger_failure( params = nil )
76 | client.send_json( { action: :_failure, params: params, journal_id: @message[ :journal_id ] } )
77 | end
78 |
79 | def before( &closure )
80 | register_before closure: closure, except: []
81 | end
82 |
83 | def before_only( *methods, &closure )
84 | register_before closure: closure, only: methods
85 | end
86 |
87 | def before_except( *methods, &closure )
88 | register_before closure: closure, except: methods
89 | end
90 |
91 | def after( &closure )
92 | register_after closure: closure, except: []
93 | end
94 |
95 | def after_only( *methods, &closure )
96 | register_after closure: closure, only: methods
97 | end
98 |
99 | def after_except( *methods, &closure )
100 | register_after closure: closure, except: methods
101 | end
102 |
103 | def selector( name, &closure )
104 | selectors[ name ] = closure
105 | end
106 |
107 | def select( name )
108 | selected = nil
109 | self.runFilters name
110 | if !@stopped and selector = self.class.selectors[ name ]
111 | selected = instance_eval( &selector )
112 | end
113 | self.runFilters name, :after
114 | if !@stopped and selected and ( selected.is_a?( ActiveRecord::Relation ) or selected.is_a?( ActiveRecord::Base ) )
115 | client.sync selected
116 | end
117 | end
118 |
119 | def runAction( name )
120 | if name == :select
121 | selector = params[ :selector ].to_sym
122 | @params = params[ :params ]
123 | self.select selector
124 | else
125 | self.runFilters name
126 | self.send( name ) unless @stopped
127 | self.runFilters name, :after
128 | end
129 | end
130 |
131 | def stop
132 | @stopped = true
133 | end
134 |
135 | protected
136 |
137 | def runFilters( name, type = :before )
138 | filters = if type == :before then self.class.befores else self.class.afters end
139 | filters.each do |obj|
140 | if !@stopped and ( ( obj[ :only ] and obj[ :only ].include?( name ) ) or ( obj[ :except ] and !obj[ :except ].include?( name ) ) )
141 | instance_eval( &obj[ :closure ] )
142 | end
143 | end
144 | end
145 |
146 | private
147 |
148 | def register_before( obj )
149 | befores.push obj
150 | end
151 |
152 | def register_after( obj )
153 | afters.push obj
154 | end
155 |
156 | def permit_params( filter )
157 | params.keys.each { |key| params.delete( key ) unless filter.include?( key ) }
158 | params
159 | end
160 |
161 | def model_class
162 | Object.const_get model_name
163 | end
164 |
165 | def model_name
166 | self.class.name.gsub( 'sController', '' ).to_sym
167 | end
168 |
169 | end
170 |
171 | end
172 |
--------------------------------------------------------------------------------
/lib/client/javascripts/nali/collection.js.coffee:
--------------------------------------------------------------------------------
1 | Nali.extend Collection:
2 |
3 | cloning: ->
4 | @subscribeTo @Model, "create.#{ @model._name.lower() }", @_onModelCreated
5 | @subscribeTo @Model, "update.#{ @model._name.lower() }", @_onModelUpdated
6 | @subscribeTo @Model, "destroy.#{ @model._name.lower() }", @_onModelDestroyed
7 | @adaptations = apply: [], cancel: []
8 | @_ordering = {}
9 | @_adaptCollection()
10 | @refilter()
11 | @
12 |
13 | _toShowViews: []
14 | _visibleViews: []
15 | length: 0
16 |
17 | refilter: ->
18 | @model.each ( model ) =>
19 | if isCorrect = model.isCorrect( @filters ) and not ( model in @ ) then @add model
20 | else if not isCorrect and model in @ then @remove model
21 | @
22 |
23 | new: ( model, filters ) ->
24 | @clone model: model, filters: filters
25 |
26 | _onModelCreated: ( model ) ->
27 | @add model if not @freezed and model.isCorrect @filters
28 | @
29 |
30 | _onModelUpdated: ( model ) ->
31 | if model.written()
32 | if model in @
33 | if @freezed or model.isCorrect @filters
34 | @_reorder()
35 | @trigger 'update.model', model
36 | else @remove model
37 | else if not @freezed and model.isCorrect @filters
38 | @add model
39 | @
40 |
41 | _onModelDestroyed: ( model ) ->
42 | @remove model if model in @ and not @freezed
43 | @
44 |
45 | _adaptCollection: ->
46 | for name, method of @model when /^__\w+$/.test( name ) and typeof method is 'function'
47 | do ( name, method ) =>
48 | @[ name = name[ 2.. ] ] = ( args... ) =>
49 | @each ( model ) -> model[ name ] args...
50 | @
51 | @
52 |
53 | _adaptModel: ( model, type = 'apply' ) ->
54 | adaptation.call @, model for adaptation in @adaptations[ type ]
55 | @
56 |
57 | adaptation: ( apply, cancel ) ->
58 | @each ( model ) -> apply.call @, model
59 | @adaptations.apply.push apply
60 | @adaptations.cancel.unshift cancel if cancel
61 | @
62 |
63 | add: ( models... ) ->
64 | for model in [].concat models...
65 | Array::push.call @, model
66 | @_adaptModel model
67 | @_reorder()
68 | @trigger 'update.length.add', model
69 | @trigger 'update.length', 'add', model
70 | @
71 |
72 | remove: ( model ) ->
73 | @_adaptModel model, 'cancel'
74 | Array::splice.call @, @indexOf( model ), 1
75 | @unsubscribeFrom model
76 | @_reorder()
77 | @trigger 'update.length.remove', model
78 | @trigger 'update.length', 'remove', model
79 | @
80 |
81 | removeAll: ->
82 | @each ( model ) -> @remove model
83 | @length = 0
84 | @
85 |
86 | each: ( callback ) ->
87 | callback.call @, model, index for model, index in @
88 | @
89 |
90 | pluck: ( property ) ->
91 | model[ property ] for model in @
92 |
93 | indexOf: ( model ) ->
94 | Array::indexOf.call @, model
95 |
96 | sort: ( sorter ) ->
97 | Array::sort.call @, sorter
98 | @
99 |
100 | toArray: ->
101 | Array::slice.call @, 0
102 |
103 | freeze: ->
104 | @freezed = true
105 | @
106 |
107 | unfreeze: ->
108 | @freezed = false
109 | @refilter()
110 | @
111 |
112 | where: ( filters ) ->
113 | filters[ name ] = value for name, value of @filters
114 | @model.where filters
115 |
116 | order: ( @_ordering ) ->
117 | @_reorder()
118 | @
119 |
120 | _reorder: ->
121 | if @_ordering.by?
122 | clearTimeout @_ordering.timer if @_ordering.timer?
123 | @_ordering.timer = setTimeout =>
124 | if typeof @_ordering.by is 'function'
125 | @sort @_ordering.by
126 | else
127 | @sort ( one, two ) =>
128 | one = one[ @_ordering.by ]
129 | two = two[ @_ordering.by ]
130 | if @_ordering.as is 'number'
131 | one = + one
132 | two = + two
133 | if @_ordering.as is 'string'
134 | one = '' + one
135 | two = '' + two
136 | ( if one > two then 1 else if one < two then -1 else 0 ) * ( if @_ordering.desc then -1 else 1 )
137 | @_orderViews()
138 | delete @_ordering.timer
139 | , 5
140 | @
141 |
142 | _orderViews: ->
143 | if @_inside
144 | children = Array::slice.call @_inside.children
145 | children.sort ( one, two ) => @indexOf( one.view.model ) - @indexOf( two.view.model )
146 | @_inside.appendChild child for child in children
147 | @
148 |
149 | show: ( viewName, insertTo, isRelation = false ) ->
150 | @adaptation ( model ) ->
151 | view = model.view viewName
152 | if isRelation
153 | view.subscribeTo @, 'reset', view.hide
154 | else unless @visible
155 | @visible = true
156 | @_prepareViewToShow view
157 | @_hideVisibleViews()
158 | else
159 | @::_visibleViews.push view
160 | view.show insertTo
161 | @_inside ?= view.element[0].parentNode
162 | , ( model ) ->
163 | model.hide viewName
164 | @
165 |
166 | hide: ( viewName, delay ) ->
167 | model.hide viewName, delay for model in @
168 | @
169 |
170 | _prepareViewToShow: ( view ) ->
171 | unless view in @::_toShowViews
172 | @::_toShowViews.push view
173 | @_prepareViewToShow layout if ( layout = view.layout() )?.childOf? 'View'
174 | @
175 |
176 | _hideVisibleViews: ->
177 | view.hide() for view in @::_visibleViews when not( view in @::_toShowViews )
178 | @::_visibleViews = @::_toShowViews
179 | @::_toShowViews = []
180 | @
181 |
182 | first: ->
183 | @[0]
184 |
185 | last: ->
186 | @[ @length - 1 ]
187 |
188 | reset: ->
189 | @_inside = null
190 | @adaptations.length = 0
191 | @trigger 'reset'
192 | @
193 |
194 | destroy: ->
195 | @trigger 'destroy'
196 | @destroyObservation()
197 | @removeAll()
198 | @reset()
199 | @
200 |
--------------------------------------------------------------------------------
/lib/client/javascripts/nali/jbone.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * jBone v1.0.18 - 2014-07-08 - Library for DOM manipulation
3 | *
4 | * https://github.com/kupriyanenko/jbone
5 | *
6 | * Copyright 2014 Alexey Kupriyanenko
7 | * Released under the MIT license.
8 | */
9 |
10 | !function(a){function b(b){var c=b.length,d=typeof b;return o(d)||b===a?!1:1===b.nodeType&&c?!0:p(d)||0===c||"number"==typeof c&&c>0&&c-1 in b}function c(a,b){var c,d;this.originalEvent=a,d=function(a,b){this[a]="preventDefault"===a?function(){return this.defaultPrevented=!0,b[a]()}:o(b[a])?function(){return b[a]()}:b[a]};for(c in a)(a[c]||"function"==typeof a[c])&&d.call(this,c,a);q.extend(this,b)}var d,e=a.$,f=a.jBone,g=/^<(\w+)\s*\/?>$/,h=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,i=[].slice,j=[].splice,k=Object.keys,l=document,m=function(a){return"string"==typeof a},n=function(a){return a instanceof Object},o=function(a){var b={};return a&&"[object Function]"===b.toString.call(a)},p=function(a){return Array.isArray(a)},q=function(a,b){return new d.init(a,b)};q.noConflict=function(){return a.$=e,a.jBone=f,q},d=q.fn=q.prototype={init:function(a,b){var c,d,e,f;if(!a)return this;if(m(a)){if(d=g.exec(a))return this[0]=l.createElement(d[1]),this.length=1,n(b)&&this.attr(b),this;if((d=h.exec(a))&&d[1]){for(f=l.createDocumentFragment(),e=l.createElement("div"),e.innerHTML=a;e.lastChild;)f.appendChild(e.firstChild);return c=i.call(f.childNodes),q.merge(this,c)}if(q.isElement(b))return q(b).find(a);try{return c=l.querySelectorAll(a),q.merge(this,c)}catch(j){return this}}return a.nodeType?(this[0]=a,this.length=1,this):o(a)?a():a instanceof q?a:q.makeArray(a,this)},pop:[].pop,push:[].push,reverse:[].reverse,shift:[].shift,sort:[].sort,splice:[].splice,slice:[].slice,indexOf:[].indexOf,forEach:[].forEach,unshift:[].unshift,concat:[].concat,join:[].join,every:[].every,some:[].some,filter:[].filter,map:[].map,reduce:[].reduce,reduceRight:[].reduceRight,length:0},d.constructor=q,d.init.prototype=d,q.setId=function(b){var c=b.jid;b===a?c="window":void 0===b.jid&&(b.jid=c=++q._cache.jid),q._cache.events[c]||(q._cache.events[c]={})},q.getData=function(b){b=b instanceof q?b[0]:b;var c=b===a?"window":b.jid;return{jid:c,events:q._cache.events[c]}},q.isElement=function(a){return a&&a instanceof q||a instanceof HTMLElement||m(a)},q._cache={events:{},jid:0},q.merge=function(a,b){for(var c=b.length,d=a.length,e=0;c>e;)a[d++]=b[e++];return a.length=d,a},q.contains=function(a,b){var c;return a.reverse().some(function(a){return a.contains(b)?c=a:void 0}),c},q.extend=function(a){var b,c,d,e;return j.call(arguments,1).forEach(function(f){if(f)for(b=k(f),c=b.length,d=0,e=a;c>d;d++)e[b[d]]=f[b[d]]}),a},q.makeArray=function(a,c){var d=c||[];return null!==a&&(b(a)?q.merge(d,m(a)?[a]:a):d.push(a)),d},q.Event=function(a,b){var c,d;return a.type&&!b&&(b=a,a=a.type),c=a.split(".").splice(1).join("."),d=a.split(".")[0],a=l.createEvent("Event"),a.initEvent(d,!0,!0),q.extend(a,{namespace:c,isDefaultPrevented:function(){return a.defaultPrevented}},b)},d.on=function(a){var b,d,e,f,g,h,i,j,k=arguments,l=this.length,m=0;for(2===k.length?b=k[1]:(d=k[1],b=k[2]),j=function(j){q.setId(j),g=q.getData(j).events,a.split(" ").forEach(function(a){h=a.split(".")[0],e=a.split(".").splice(1).join("."),g[h]=g[h]||[],f=function(a){a.namespace&&a.namespace!==e||(i=null,d?(~q(j).find(d).indexOf(a.target)||(i=q.contains(q(j).find(d),a.target)))&&(i=i||a.target,a=new c(a,{currentTarget:i}),b.call(i,a)):b.call(j,a))},g[h].push({namespace:e,fn:f,originfn:b}),j.addEventListener&&j.addEventListener(h,f,!1)})};l>m;m++)j(this[m]);return this},d.one=function(a){var b,c,d,e=arguments,f=0,g=this.length;for(2===e.length?b=e[1]:(c=e[1],b=e[2]),d=function(d){a.split(" ").forEach(function(a){var e=function(c){q(d).off(a,e),b.call(d,c)};c?q(d).on(a,c,e):q(d).on(a,e)})};g>f;f++)d(this[f]);return this},d.trigger=function(a){var b,c=[],d=0,e=this.length;if(!a)return this;for(m(a)?c=a.split(" ").map(function(a){return q.Event(a)}):(a=a instanceof Event?a:q.Event(a),c=[a]),b=function(a){c.forEach(function(b){b.type&&a.dispatchEvent&&a.dispatchEvent(b)})};e>d;d++)b(this[d]);return this},d.off=function(a,b){var c,d,e,f,g=0,h=this.length,i=function(a,c,d,e,f){var g;(b&&f.originfn===b||!b)&&(g=f.fn),a[c][d].fn===g&&(e.removeEventListener(c,g),q._cache.events[q.getData(e).jid][c].splice(d,1))};for(e=function(b){var e,g,h;return(c=q.getData(b).events)?!a&&c?k(c).forEach(function(a){for(g=c[a],e=g.length;e--;)i(c,a,e,b,g[e])}):void a.split(" ").forEach(function(a){if(f=a.split(".")[0],d=a.split(".").splice(1).join("."),c[f])for(g=c[f],e=g.length;e--;)h=g[e],(!d||d&&h.namespace===d)&&i(c,f,e,b,h);else d&&k(c).forEach(function(a){for(g=c[a],e=g.length;e--;)h=g[e],h.namespace.split(".")[0]===d.split(".")[0]&&i(c,a,e,b,h)})}):void 0};h>g;g++)e(this[g]);return this},d.find=function(a){for(var b=[],c=0,d=this.length,e=function(c){o(c.querySelectorAll)&&[].forEach.call(c.querySelectorAll(a),function(a){b.push(a)})};d>c;c++)e(this[c]);return q(b)},d.get=function(a){return this[a]},d.eq=function(a){return q(this[a])},d.parent=function(){for(var a,b=[],c=0,d=this.length;d>c;c++)!~b.indexOf(a=this[c].parentElement)&&a&&b.push(a);return q(b)},d.toArray=function(){return i.call(this)},d.is=function(){var a=arguments;return this.some(function(b){return b.tagName.toLowerCase()===a[0]})},d.has=function(){var a=arguments;return this.some(function(b){return b.querySelectorAll(a[0]).length})},d.attr=function(a,b){var c,d=arguments,e=0,f=this.length;if(m(a)&&1===d.length)return this[0]&&this[0].getAttribute(a);for(2===d.length?c=function(c){c.setAttribute(a,b)}:n(a)&&(c=function(b){k(a).forEach(function(c){b.setAttribute(c,a[c])})});f>e;e++)c(this[e]);return this},d.val=function(a){var b=0,c=this.length;if(0===arguments.length)return this[0]&&this[0].value;for(;c>b;b++)this[b].value=a;return this},d.css=function(b,c){var d,e=arguments,f=0,g=this.length;if(m(b)&&1===e.length)return this[0]&&a.getComputedStyle(this[0])[b];for(2===e.length?d=function(a){a.style[b]=c}:n(b)&&(d=function(a){k(b).forEach(function(c){a.style[c]=b[c]})});g>f;f++)d(this[f]);return this},d.data=function(a,b){var c,d=arguments,e={},f=0,g=this.length,h=function(a,b,c){n(c)?(a.jdata=a.jdata||{},a.jdata[b]=c):a.dataset[b]=c},i=function(a){return"true"===a?!0:"false"===a?!1:a};if(0===d.length)return this[0].jdata&&(e=this[0].jdata),k(this[0].dataset).forEach(function(a){e[a]=i(this[0].dataset[a])},this),e;if(1===d.length&&m(a))return this[0]&&i(this[0].dataset[a]||this[0].jdata&&this[0].jdata[a]);for(1===d.length&&n(a)?c=function(b){k(a).forEach(function(c){h(b,c,a[c])})}:2===d.length&&(c=function(c){h(c,a,b)});g>f;f++)c(this[f]);return this},d.removeData=function(a){for(var b,c,d=0,e=this.length;e>d;d++)if(b=this[d].jdata,c=this[d].dataset,a)b&&b[a]&&delete b[a],delete c[a];else{for(a in b)delete b[a];for(a in c)delete c[a]}return this},d.html=function(a){var b,c=arguments;return 1===c.length&&void 0!==a?this.empty().append(a):0===c.length&&(b=this[0])?b.innerHTML:this},d.append=function(a){var b,c=0,d=this.length;for(m(a)&&h.exec(a)?a=q(a):n(a)||(a=document.createTextNode(a)),a=a instanceof q?a:q(a),b=function(b,c){a.forEach(function(a){b.appendChild(c?a.cloneNode():a)})};d>c;c++)b(this[c],c);return this},d.appendTo=function(a){return q(a).append(this),this},d.empty=function(){for(var a,b=0,c=this.length;c>b;b++)for(a=this[b];a.lastChild;)a.removeChild(a.lastChild);return this},d.remove=function(){var a,b=0,c=this.length;for(this.off();c>b;b++)a=this[b],delete a.jdata,a.parentNode&&a.parentNode.removeChild(a);return this},"object"==typeof module&&module&&"object"==typeof module.exports?module.exports=q:"function"==typeof define&&define.amd?(define(function(){return q}),a.jBone=a.$=q):"object"==typeof a&&"object"==typeof a.document&&(a.jBone=a.$=q)}(window);
11 |
--------------------------------------------------------------------------------
/lib/client/javascripts/nali/view.js.coffee:
--------------------------------------------------------------------------------
1 | Nali.extend View:
2 |
3 | extension: ->
4 | if @_name isnt 'View'
5 | @_shortName = @_name.underscore().split( '_' )[ 1.. ].join( '_' ).camel()
6 | @_parseTemplate()
7 | @_parseEvents()
8 | @
9 |
10 | cloning: ->
11 | @my = @model
12 | @_prepareElement()
13 | @
14 |
15 | layout: -> null
16 |
17 | _onSourceUpdated: -> @_draw()
18 |
19 | _onSourceDestroyed: -> @hide()
20 |
21 | getOf: ( source, property ) ->
22 | @redrawOn source, "update.#{ property }"
23 | source[ property ]
24 |
25 | getMy: ( property ) ->
26 | @getOf @model, property
27 |
28 | redrawOn: ( source, event ) ->
29 | @subscribeTo source, event, @_onSourceUpdated
30 |
31 | insertTo: ->
32 | if ( layout = @layout() )?.childOf? 'View' then layout.show().yield
33 | else @Application.htmlContainer
34 |
35 | _draw: ->
36 | @_runAssistants 'draw'
37 | @onDraw?()
38 | @
39 |
40 | show: ( insertTo = @insertTo() ) ->
41 | unless @visible
42 | @_runModelCallback 'beforeShow'
43 | @_draw()._bindEvents()
44 | @_runAssistants 'show'
45 | @subscribeTo @model, 'destroy', @_onSourceDestroyed
46 | @element.appendTo insertTo
47 | setTimeout ( => @onShow() ), 5 if @onShow?
48 | @trigger 'show'
49 | @visible = true
50 | @_runModelCallback 'afterShow'
51 | @
52 |
53 | hide: ( delay = 0 ) ->
54 | if @visible
55 | @_runModelCallback 'beforeHide'
56 | @onHide?()
57 | @_unbindEvents()
58 | @trigger 'hide'
59 | @_runAssistants 'hide'
60 | @_hideElement if delay and typeof( delay ) is 'number' then delay else @hideDelay
61 | @destroyObservation()
62 | @visible = false
63 | @_runModelCallback 'afterHide'
64 | @
65 |
66 | _hideElement: ( delay ) ->
67 | if delay then setTimeout ( => @_removeElement() ), delay
68 | else @_removeElement()
69 | @
70 |
71 | _removeElement: ->
72 | @element[0].parentNode.removeChild @element[0]
73 | @
74 |
75 | _runModelCallback: ( type ) ->
76 | @model[ type ]?[ @_shortName ]?.call @model
77 | @
78 |
79 | _runLink: ( event ) ->
80 | event.preventDefault()
81 | @_runUrl event.currentTarget.getAttribute 'href'
82 | @
83 |
84 | _runForm: ( event ) ->
85 | event.preventDefault()
86 | @_runUrl event.currentTarget.getAttribute( 'action' ), form2js event.currentTarget, '.', false
87 | @
88 |
89 | _runUrl: ( url, params ) ->
90 | if match = url.match /^(@@?)(.+)/
91 | [ chain, segments... ] = match[2].split '/'
92 | if result = @_analizeChain chain, ( if match[1].length is 1 then @ else @model )
93 | [ source, method ] = result
94 | if method of source
95 | args = @_parseUrlSegments segments
96 | args.unshift params if params
97 | source[ method ] args...
98 | else console.warn "Method %s not exists of %O", method, source
99 | else @redirect url, params
100 | @
101 |
102 | _parseUrlSegments: ( segments ) ->
103 | params = []
104 | for segment in segments when segment isnt ''
105 | [ name, value ] = segment.split ':'
106 | if value
107 | last = params[ params.length - 1 ]
108 | params.push last = {} if typeof last isnt 'object'
109 | last[ name ] = value
110 | else params.push name
111 | params
112 |
113 | _parseEvents: ->
114 | @_eventsMap = []
115 | if @events
116 | @events = [ @events ] if typeof @events is 'string'
117 | for event in @events
118 | try
119 | [ handlers, type, other ] = event.split /\s+(on|one)\s+/
120 | [ events, selector ] = other.split /\s+at\s+/
121 | handlers = handlers.split /\s*,\s*/
122 | events = events.replace /\s*,\s*/, ' '
123 | throw true unless type and events.length and handlers.length
124 | catch
125 | console.warn "Events parsing error: \"%s\" of %O", event, @
126 | error = true
127 | if error then error = false else @_eventsMap.push [ selector, type, events, handlers ]
128 | @
129 |
130 | _bindEvents: ->
131 | @_bindRoutedElements 'a', 'href', 'click', ( event ) => @_runLink event
132 | @_bindRoutedElements 'form', 'action', 'submit', ( event ) => @_runForm event
133 | for [ selector, type, events, handlers ] in @_eventsMap
134 | for handler in handlers
135 | do ( selector, type, events, handler ) =>
136 | @element[ type ] events, selector, ( event ) => @[ handler ] event
137 | @
138 |
139 | _bindRoutedElements: ( selector, urlProp, event, callback ) ->
140 | finded = ( el for el in @element.find( selector ) when el[ urlProp ].indexOf( window.location.origin ) >= 0 )
141 | finded.push @element[0] if @element.is selector
142 | ( @_routedElements ?= {} )[ selector ] = @_( finded ).on event, callback
143 | @
144 |
145 | _unbindEvents: ->
146 | @element.off()
147 | @_routedElements.a.off()
148 | @_routedElements.form.off()
149 | @
150 |
151 | _prepareElement: ->
152 | unless @element
153 | @element = @_ @template
154 | @element[0].view = @
155 | @_addAssistants()
156 | @
157 |
158 | _getNode: ( path ) ->
159 | node = @element[0]
160 | node = node[ sub ] for sub in path
161 | node
162 |
163 | _parseTemplate: ->
164 | if container = document.querySelector '#' + @_name.underscore()
165 | @template = container.innerHTML.trim().replace( /\s+/g, ' ' )
166 | .replace( /({\s*\+.+?\s*})/g, ' $1' )
167 | unless RegExp( "^<[^>]+" + @_name ).test @template
168 | @template = "#{ @template }
"
169 | @_parseAssistants()
170 | container.parentNode.removeChild container
171 | else console.warn 'Template %s not exists', @_name
172 | @
173 |
174 | _parseAssistants: ->
175 | @_assistantsMap = []
176 | if /{\s*.+?\s*}|bind=".+?"/.test @template
177 | tmp = document.createElement 'div'
178 | tmp.innerHTML = @template
179 | @_scanAssistants tmp.children[0]
180 | @
181 |
182 | _scanAssistants: ( node, path = [] ) ->
183 | if node.nodeType is 3
184 | if /{\s*yield\s*}/.test( node.textContent.trim() ) and node.parentNode.childNodes.length is 1
185 | @_assistantsMap.push nodepath: path, type: 'Yield'
186 | else if /^{\s*\w+ of @\w*\s*}$/.test( node.textContent.trim() ) and node.parentNode.childNodes.length is 1
187 | @_assistantsMap.push nodepath: path, type: 'Relation'
188 | else if /{\s*.+?\s*}/.test node.textContent
189 | @_assistantsMap.push nodepath: path, type: 'Text'
190 | else if node.nodeName is 'ASSIST'
191 | @_assistantsMap.push nodepath: path, type: 'Html'
192 | else
193 | if node.attributes
194 | for attribute, index in node.attributes
195 | if attribute.name is 'bind'
196 | @_assistantsMap.push nodepath: path, type: 'Form'
197 | else if /{\s*.+?\s*}/.test attribute.value
198 | @_assistantsMap.push nodepath: path.concat( 'attributes', index ), type: 'Attr'
199 | @_scanAssistants child, path.concat 'childNodes', index for child, index in node.childNodes
200 | @
201 |
202 | _addAssistants: ->
203 | @_assistants = show: [], draw: [], hide: []
204 | @[ "_add#{ type }Assistant" ] @_getNode nodepath for { nodepath, type } in @_assistantsMap
205 | @
206 |
207 | _runAssistants: ( type ) ->
208 | assistant.call @ for assistant in @_assistants[ type ]
209 | @
210 |
211 | _addTextAssistant: ( node ) ->
212 | initialValue = node.textContent
213 | @_assistants[ 'draw' ].push -> node.textContent = @_analize initialValue
214 | @
215 |
216 | _addAttrAssistant: ( node ) ->
217 | initialValue = node.value
218 | @_assistants[ 'draw' ].push -> node.value = @_analize initialValue
219 | @
220 |
221 | _addHtmlAssistant: ( node ) ->
222 | parent = node.parentNode
223 | initialValue = node.innerHTML
224 | index = Array::indexOf.call parent.childNodes, node
225 | after = parent.childNodes[ index - 1 ] or null
226 | before = parent.childNodes[ index + 1 ] or null
227 | @_assistants[ 'draw' ].push ->
228 | start = if after then Array::indexOf.call( parent.childNodes, after ) + 1 else 0
229 | end = if before then Array::indexOf.call parent.childNodes, before else parent.childNodes.length
230 | parent.removeChild node for node in Array::slice.call( parent.childNodes, start, end )
231 | parent.insertBefore element, before for element in @_( @_analize initialValue )
232 | @
233 |
234 | _addFormAssistant: ( node ) ->
235 | if bind = @_analizeChain node.attributes.removeNamedItem( 'bind' ).value
236 | [ source, property ] = bind
237 | $node = @_ node
238 |
239 | updateSource = ->
240 | ( params = {} )[ property ] = if node.type is 'checkbox' and !node.checked then null else if node.value is '' then null else node.value
241 | source.update params
242 | source.save() unless node.form?
243 |
244 | [ setValue, bindChange ] = switch
245 | when node.type in [ 'text', 'textarea', 'color', 'date', 'datetime', 'datetime-local', 'email', 'number', 'range', 'search', 'tel', 'time', 'url', 'month', 'week' ]
246 | [
247 | -> node.value = source[ property ] or ''
248 | -> $node.on 'change', => updateSource.call @
249 | ]
250 | when node.type in [ 'checkbox', 'radio' ]
251 | [
252 | -> node.checked = source[ property ] + '' is node.value
253 | -> $node.on 'change', => updateSource.call @ if node.type is 'checkbox' or node.checked is true
254 | ]
255 | when node.type is 'select-one'
256 | [
257 | -> option.selected = true for option in node when source[ property ] + '' is option.value
258 | -> $node.on 'change', => updateSource.call @
259 | ]
260 |
261 | @_assistants[ 'show' ].push ->
262 | setValue.call @
263 | bindChange.call @
264 | source.subscribe @, "update.#{ property }", => setValue.call @
265 |
266 | @_assistants[ 'hide' ].push ->
267 | $node.off 'change'
268 | @
269 |
270 | _addYieldAssistant: ( node ) ->
271 | ( @yield = @_ node.parentNode )[0].removeChild node
272 |
273 | _addRelationAssistant: ( node ) ->
274 | [ match, name, chain ] = node.textContent.match /{\s*(\w+) of @(\w*)\s*}/
275 | ( insertTo = node.parentNode ).removeChild node
276 | segments = if chain.length then chain.split '.' else []
277 | @_assistants[ 'show' ].push ->
278 | if relation = @_getSource segments
279 | if relation.childOf 'Collection'
280 | relation.show name, insertTo, true
281 | relation.subscribeTo @, 'hide', relation.reset
282 | else
283 | view = relation.show name, insertTo
284 | view.subscribeTo @, 'hide', view.hide
285 |
286 | _analize: ( value ) ->
287 | value.replace /{\s*(.+?)\s*}/g, ( match, sub ) => @_analizeMatch sub
288 |
289 | _analizeMatch: ( sub ) ->
290 | if match = sub.match /^@([\w\.]+)(\?)?$/
291 | if result = @_analizeChain match[1]
292 | [ source, property ] = result
293 | source.subscribe? @, "update.#{ property }", @_onSourceUpdated
294 | if match[2] is '?'
295 | if source[ property ] then property else ''
296 | else if source[ property ]? then source[ property ] else ''
297 | else ''
298 | else if match = sub.match /^[=|\+](\w+)$/
299 | @helpers?[ match[1] ]?.call @
300 | else sub
301 |
302 | _getSource: ( segments, source = @model ) ->
303 | for segment in segments
304 | if segment of source then source = source[ segment ]
305 | else
306 | console.warn "%s: chain \"%s\" is invalid, segment \"%s\" not exists in %O", @_name, segments.join( '.' ), segment, source
307 | return null
308 | source
309 |
310 | _analizeChain: ( chain, source = @model ) ->
311 | segments = chain.split '.'
312 | property = segments.pop()
313 | return null unless source = @_getSource segments, source
314 | [ source, property ]
315 |
--------------------------------------------------------------------------------
/lib/client/javascripts/nali/model.js.coffee:
--------------------------------------------------------------------------------
1 | Nali.extend Model:
2 |
3 | extension: ->
4 | if @_name isnt 'Model'
5 | @_table = @_tables[ @_name ] ?= []
6 | @_table.index = {}
7 | @_parseRelations()
8 | @_adapt()
9 | @
10 |
11 | cloning: ->
12 | @_views = {}
13 |
14 | _tables: {}
15 | _callStack: []
16 | attributes: {}
17 | updated: 0
18 | destroyed: false
19 |
20 | _adapt: ->
21 | for name, method of @ when /^__\w+/.test name
22 | do ( name, method ) =>
23 | @[ name[ 2.. ] ] = @[ name ] if typeof method is 'function'
24 | @_adaptViews()
25 | @
26 |
27 | _adaptViews: ->
28 | @_views = {}
29 | for name, view of @View.extensions when name.indexOf( @_name ) >= 0
30 | do ( name, view ) =>
31 | @_views[ short = view._shortName ] = view
32 | shortCap = short.capitalize()
33 | unless @[ viewMethod = 'view' + shortCap ]? then @[ viewMethod ] = -> @view short
34 | unless @[ showMethod = 'show' + shortCap ]? then @[ showMethod ] = ( insertTo ) -> @show short, insertTo
35 | unless @[ hideMethod = 'hide' + shortCap ]? then @[ hideMethod ] = -> @hide short
36 | @
37 |
38 | _callStackAdd: ( params ) ->
39 | # добавляет задачу в очередь на выполнение, запускает выполнение очереди
40 | @_callStack.push params
41 | @runStack()
42 | @
43 |
44 | runStack: ->
45 | # запускает выполнение задач у существующих моделей
46 | for item, index in @_callStack[ 0.. ]
47 | if model = @extensions[ item.model ].find item.id
48 | model[ item.method ] item.params
49 | @_callStack.splice @_callStack.indexOf( item ), 1
50 | @
51 |
52 | # работа с моделями
53 |
54 | _accessing: ->
55 | # устанавливает геттеры доступа к атрибутам и связям
56 | @access @attributes
57 | @_setRelations()
58 | @
59 |
60 | save: ( success, failure ) ->
61 | # отправляет на сервер запрос на сохранение модели, вызывает success в случае успеха и failure при неудаче
62 | @beforeSave?()
63 | if @isValid()
64 | @query "#{ @_name.lower() }s.save", @attributes,
65 | ( { attributes, created, updated } ) =>
66 | @update( attributes, updated, created ).write()
67 | @afterSave?()
68 | success? @
69 | else failure? @
70 | @
71 |
72 | sync: ( { name, attributes, created, updated, destroyed } ) ->
73 | # синхронизирует пришедшую с сервера модель с локальной, либо создает новую
74 | if model = @extensions[ name ].find attributes.id
75 | if destroyed then model.remove()
76 | else if updated > @updated
77 | model.updated = updated
78 | model.created = created
79 | model.update attributes
80 | else
81 | model = @extensions[ name ].new attributes
82 | model.updated = updated
83 | model.created = created
84 | model.write()
85 | @
86 |
87 | select: ( options ) ->
88 | # отправляет на сервер запрос на выборку моделей
89 | obj = {}
90 | if typeof options is 'object'
91 | obj.selector = Object.keys( options )[0]
92 | obj.params = options[ obj.selector ]
93 | else
94 | obj.selector = options
95 | obj.params = {}
96 | @query @_name.lower() + 's.select', obj
97 | @
98 |
99 | written: ->
100 | @ in @_table
101 |
102 | write: ->
103 | # добавляет модель в локальную таблицу, генерирует событие create
104 | @_table.index[ @id ] = @ if @id and not @_table.index[ @id ]
105 | unless @written()
106 | @_table.push @
107 | @onCreate?()
108 | @Model.trigger "create.#{ @_name.lower() }", @
109 | @Model.runStack()
110 | @
111 |
112 | remove: ->
113 | # удаляет модель из локальной таблицы, генерирует событие destroy
114 | if @written()
115 | @destroyed = true
116 | delete @_table.index[ @id ]
117 | @_table.splice @_table.indexOf( @ ), 1
118 | @trigger 'destroy'
119 | @Model.trigger "destroy.#{ @_name.lower() }", @
120 | @onDestroy?()
121 | @unsubscribeAll()
122 | @
123 |
124 | new: ( attributes ) ->
125 | # создает модель, не сохраняя её на сервере
126 | model = @clone( attributes: @_defaultAttributes() )._accessing()
127 | model[ name ] = @_normalizeAttributeValue name, value for name, value of attributes
128 | model
129 |
130 | create: ( attributes, success, failure ) ->
131 | # создает модель, и сохраняет её на сервере, вызывает success в случае успеха и failure при неудаче
132 | @new( attributes ).save success, failure
133 |
134 | update: ( attributes, checkValidation = true ) ->
135 | # обновляет атрибуты модели, проверяя их валидность, генерирует событие update
136 | changed = []
137 | changed.push name for name, value of attributes when @updateProperty name, value, checkValidation
138 | if changed.length
139 | @onUpdate? changed
140 | @trigger 'update', changed
141 | @Model.trigger "update.#{ @_name.lower() }", @
142 | @
143 |
144 | updateProperty: ( name, value, checkValidation = true ) ->
145 | # обновляет один атрибут модели, проверяя его валидность, генерирует событие update.propertyName
146 | value = @_normalizeAttributeValue name, value
147 | if @[ name ] isnt value and ( not checkValidation or @isValidAttributeValue( name, value ) )
148 | @[ name ] = value
149 | @[ 'onUpdate' + name.capitalize() ]?()
150 | @trigger "update.#{ name }"
151 | true
152 | else false
153 |
154 | upgrade: ( attributes, success, failure ) ->
155 | # обновляет атрибуты модели и сохраняет её на сервер
156 | @update( attributes ).save success, failure
157 | @
158 |
159 | destroy: ( success, failure ) ->
160 | # отправляет на сервер запрос на удаление модели, вызывает success в случае успеха и failure при неудаче
161 | @query @_name.lower() + 's.destroy', @attributes, success, failure
162 | @
163 |
164 | # поиск моделей
165 |
166 | find: ( id ) ->
167 | # находит модель по её id используя индекс
168 | @_table.index[ id ]
169 |
170 | where: ( filters ) ->
171 | # возвращает коллекцию моделей соответствующих фильтру
172 | @Collection.new @, filters
173 |
174 | all: ->
175 | # возвращает коллекцию всех моделей
176 | @where id: /./
177 |
178 | each: ( callback ) ->
179 | # применяет колбек ко всем моделям
180 | callback.call @, model for model in @_table
181 | @
182 |
183 | # работа с аттрибутами
184 |
185 | guid: ->
186 | # генерирует случайный идентификатор
187 | date = new Date().getTime()
188 | 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace /[xy]/g, ( sub ) ->
189 | rand = ( date + Math.random() * 16 ) % 16 | 0
190 | date = Math.floor date / 16
191 | ( if sub is 'x' then rand else ( rand & 0x7 | 0x8 ) ).toString 16
192 |
193 | _defaultAttributes: ->
194 | # возвращает объект аттрибутов по умолчанию
195 | attributes = id: @guid()
196 | for name, value of @attributes
197 | if value instanceof Object
198 | attributes[ name ] = if value.default? then @_normalizeAttributeValue name, value.default else null
199 | else attributes[ name ] = @_normalizeAttributeValue name, value
200 | attributes
201 |
202 | _normalizeAttributeValue: ( name, value ) ->
203 | # если формат свойства number пробует привести значение к числу
204 | if @::attributes[ name ]?.format is 'number' and value is ( ( correct = + value ) + '' ) then correct else value
205 |
206 | isCorrect: ( filters = {} ) ->
207 | # проверяет соответствие аттрибутов модели определенному набору фильтров, возвращает true либо false
208 | return filters.call @ if typeof filters is 'function'
209 | return false unless Object.keys( filters ).length
210 | for name, filter of filters
211 | result = if name is 'correct' and typeof filter is 'function'
212 | filter.call @
213 | else @isCorrectAttribute @[ name ], filter
214 | return false unless result
215 | return true
216 |
217 | isCorrectAttribute: ( attribute, filter ) ->
218 | # проверяет соответствие аттрибута модели определенному фильтру, возвращает true либо false
219 | return false unless attribute
220 | if filter instanceof RegExp
221 | filter.test attribute
222 | else if typeof filter is 'string'
223 | '' + attribute is filter
224 | else if typeof filter is 'number'
225 | + attribute is filter
226 | else if typeof filter is 'boolean'
227 | attribute is filter
228 | else if filter instanceof Array
229 | '' + attribute in filter or + attribute in filter
230 | else false
231 |
232 | # работа со связями
233 |
234 | _parseRelations: ( type, options ) ->
235 | # производит разбор связей
236 | @relations = {}
237 | for type in [ 'belongsTo', 'hasOne', 'hasMany' ]
238 | for options in [].concat( @[ type ] ) when @[ type ]?
239 | params = @[ '_parse' + type.capitalize() ] @_parseInitialRelationParams options
240 | section = type + if params.through? then 'Through' else ''
241 | ( @relations[ section ] ?= [] ).push params
242 | @
243 |
244 | _parseInitialRelationParams: ( options ) ->
245 | # дает начальные параметры настроек связи
246 | if typeof options is 'object'
247 | params = name: Object.keys( options )[0]
248 | params.through = through if through = options[ params.name ].through
249 | params.key = key if key = options[ params.name ].key
250 | params.model = @Model.extensions[ model.capitalize() ] if model = options[ params.name ].model
251 | else
252 | params = name: options
253 | params
254 |
255 | _parseBelongsTo: ( params ) ->
256 | # производит разбор связей belongsTo
257 | params.model ?= @Model.extensions[ params.name.capitalize() ]
258 | params.key ?= params.name.lower() + '_id'
259 | params
260 |
261 | _parseHasOne: ( params ) ->
262 | # производит разбор связей hasOne
263 | params.model ?= @Model.extensions[ params.name.capitalize() ]
264 | params.key ?= ( if params.through then params.name else @_name + '_id' ).lower()
265 | params
266 |
267 | _parseHasMany: ( params ) ->
268 | # производит разбор связей hasMany
269 | params.model ?= @Model.extensions[ params.name[ ...-1 ].capitalize() ]
270 | params.key ?= ( if params.through then params.name[ ...-1 ] else @_name + '_id' ).lower()
271 | params
272 |
273 | _setRelations: ->
274 | # запускает установку связей у модели
275 | @_setRelationsType type for type in [ 'belongsTo', 'hasOne', 'hasMany', 'hasOneThrough', 'hasManyThrough' ]
276 | @
277 |
278 | _setRelationsType: ( type ) ->
279 | # запускает установку связей определенного типа
280 | if params = @relations[ type ]
281 | @[ '_set' + type.capitalize() ] param for param in params
282 | @
283 |
284 | _setBelongsTo: ( { key, model, name, through } ) ->
285 | # устанавливает геттер типа belongsTo возвращающий связанную модель
286 | @getter name, => model.find @[ key ]
287 | @
288 |
289 | _setHasOne: ( { key, model, name, through } ) ->
290 | # устанавливает геттер типа hasOne возвращающий связанную модель
291 | @getter name, =>
292 | delete @[ name ]
293 | ( filters = {} )[ key ] = @id
294 | collection = model.where filters
295 | @getter name, => collection.first()
296 | @[ name ]
297 | @
298 |
299 | _setHasMany: ( { key, model, name, through } ) ->
300 | # устанавливает геттер типа hasMany возвращающий коллекцию связанных моделей
301 | @getter name, =>
302 | delete @[ name ]
303 | ( filters = {} )[ key ] = @id
304 | @[ name ] = model.where filters
305 | @
306 |
307 | _setHasOneThrough: ( { key, model, name, through } ) ->
308 | # устанавливает геттер типа hasOne возвращающий модель,
309 | # связанную с текущей через модель through
310 | @getter name, =>
311 | delete @[ name ]
312 | @getter name, => @[ through ][ key ]
313 | @[ name ]
314 | @
315 |
316 | _setHasManyThrough: ( { key, model, name, through } ) ->
317 | # устанавливает геттер типа hasMany возвращающий коллекцию моделей,
318 | # связанных с текущей через модель through
319 | @getter name, =>
320 | delete @[ name ]
321 | list = @[ through ]
322 | @[ name ] = model.where correct: ->
323 | return true for model in list when model[ key ] is @
324 | false
325 | @[ name ].subscribeTo @[ through ], 'update.length.add', ( model ) -> @add model[ key ]
326 | @[ name ].subscribeTo @[ through ], 'update.length.remove', ( model ) -> @remove model[ key ]
327 | @[ name ]
328 | @
329 |
330 | # работа с видами
331 |
332 | view: ( name ) ->
333 | # возвращает объект вида, либо новый, либо ранее созданный
334 | unless ( view = @_views[ name ] )?
335 | if ( view = @::_views[ name ] )?
336 | view = @_views[ name ] = view.clone model: @
337 | else console.error 'View "%s" of model "%s" does not exist', name, @_name
338 | view
339 |
340 | show: ( name, insertTo ) ->
341 | # вставляет html-код вида в указанное место ( это может быть селектор, html-элемент или ничего - тогда
342 | # вставка произойдет в элемент указанный в самом виде либо в элемент-контейнер приложения )
343 | # функция возвращает объект вида при успехе либо null при неудаче
344 | if ( view = @view( name ) )? then view.show insertTo else null
345 |
346 | hide: ( name, delay ) ->
347 | # удаляет html-код вида со страницы
348 | # функция возвращает объект вида при успехе либо null при неудаче
349 | if ( view = @view( name ) )? then view.hide delay else null
350 |
351 | # валидации
352 |
353 | validations:
354 | # набор валидационных проверок
355 | presence: ( value, filter ) -> if filter then value? else not value?
356 | inclusion: ( value, filter ) -> not value? or value in filter
357 | exclusion: ( value, filter ) -> not value? or value not in filter
358 | length: ( value, filter ) ->
359 | if not value? then return true else value += ''
360 | return false if filter.in? and value.length not in filter.in
361 | return false if filter.min? and value.length < filter.min
362 | return false if filter.max? and value.length > filter.max
363 | return false if filter.is? and value.length isnt filter.is
364 | true
365 | format: ( value, filter ) ->
366 | return true if not value?
367 | return true if filter instanceof RegExp and filter.test value
368 | return true if filter is 'boolean' and /^true|false$/.test value
369 | return true if filter is 'number' and /^[0-9]+$/.test value
370 | return true if filter is 'letters' and /^[A-zА-я]+$/.test value
371 | return true if filter is 'email' and /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,4})+$/.test value
372 | false
373 |
374 | isValid: ->
375 | # проверяет валидна ли модель, вызывается перед сохранением модели на сервер если модель валидна,
376 | # то вызов model.isValid() вернет true, иначе false
377 | return false for name, value of @attributes when not @isValidAttributeValue( name, value )
378 | true
379 |
380 | isValidAttributeValue: ( name, value ) ->
381 | # проверяет валидно ли значение для определенного атрибута модели, вызывается при проверке
382 | # валидности модели, а также в методе updateProperty() перед изменением значения атрибута, если значение
383 | # валидно то вызов model.isValidAttributeValue( name, value )? вернет true, иначе false
384 | for validation, tester of @validations when ( filter = @::attributes[ name ]?[ validation ] )?
385 | unless tester.call @, value, filter
386 | console.warn 'Attribute %s of model %O has not validate %s', name, @, validation
387 | if notice = @::attributes[ name ].notice
388 | for type, params of notice
389 | if @Notice[ type ]? then @Notice[ type ] params
390 | else console.warn 'Unknown Notice type "%s"', type
391 | return false
392 | true
393 |
--------------------------------------------------------------------------------