├── 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;s<e.length;s++){f=e[s].value;if(t&&(f===""||f===null))continue;m=e[s].name;g=m.split(n);l=[];c=r;h="";for(o=0;o<g.length;o++){v=g[o].split("][");if(v.length>1){for(u=0;u<v.length;u++){if(u==0){v[u]=v[u]+"]"}else if(u==v.length-1){v[u]="["+v[u]}else{v[u]="["+v[u]+"]"}d=v[u].match(/([a-z_]+)?\[([a-z_][a-z0-9_]+?)\]/i);if(d){for(a=1;a<d.length;a++){if(d[a])l.push(d[a])}}else{l.push(v[u])}}}else l=l.concat(v)}for(o=0;o<l.length;o++){v=l[o];if(v.indexOf("[]")>-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(o<l.length-1){if(!c[v])c[v]={};c=c[v]}else{c[v]=f}}}}return r}function n(e,t,n,s){var o=i(e,t,n,s);return o.length>0?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<s;i++){if(r[i].selected)n.push(r[i].value)}return n}return e}) 2 | -------------------------------------------------------------------------------- /lib/nali/connection.rb: -------------------------------------------------------------------------------- 1 | module EventMachine 2 | module WebSocket 3 | class Connection 4 | 5 | attr_accessor :browser_id 6 | 7 | def all_tabs 8 | Nali::Clients.list 9 | .select { |client| client.browser_id == self.browser_id } 10 | .each{ |client| yield( client ) if block_given? } 11 | end 12 | 13 | def other_tabs 14 | Nali::Clients.list 15 | .select { |client| client != self and client.browser_id == self.browser_id } 16 | .each{ |client| yield( client ) if block_given? } 17 | end 18 | 19 | def reset 20 | @storage = {} 21 | @watches = {} 22 | self 23 | end 24 | 25 | def storage 26 | @storage ||= {} 27 | end 28 | 29 | def []( name = nil ) 30 | name ? ( storage[ name ] or nil ) : storage 31 | end 32 | 33 | def []=( name, value ) 34 | storage[ name ] = value 35 | end 36 | 37 | def watches 38 | @watches ||= {} 39 | end 40 | 41 | def watch( model ) 42 | watches[ model.class.name + model.id.to_s ] ||= 0 43 | end 44 | 45 | def unwatch( model ) 46 | watches.delete model.class.name + model.id.to_s 47 | end 48 | 49 | def watch?( model ) 50 | if watches[ model.class.name + model.id.to_s ] then true else false end 51 | end 52 | 53 | def watch_time( model ) 54 | watches[ model.class.name + model.id.to_s ] or 0 55 | end 56 | 57 | def watch_time_up( model ) 58 | watches[ model.class.name + model.id.to_s ] = model.updated_at.to_f 59 | end 60 | 61 | def send_json( hash ) 62 | send hash.to_json 63 | self 64 | end 65 | 66 | def sync( *models ) 67 | models.flatten.compact.each do |model| 68 | if watch_time( model ) < model.updated_at.to_f or model.destroyed? 69 | params, relations = model.get_sync_params( self ) 70 | unless params.empty? 71 | if model.destroyed? then unwatch( model ) else watch_time_up model end 72 | relations.each { |relation| sync relation } 73 | send_json action: :_sync, params: params 74 | end 75 | end 76 | end 77 | self 78 | end 79 | 80 | def call_method( method, model, params = nil ) 81 | model = "#{ model.class.name }.#{ model.id }" if model.is_a?( ActiveRecord::Base ) 82 | send_json action: :_callMethod, model: model, method: method, params: params 83 | self 84 | end 85 | 86 | def notice( method, params = nil ) 87 | call_method method, :Notice, params 88 | self 89 | end 90 | 91 | def info( params ) 92 | notice :info, params 93 | self 94 | end 95 | 96 | def warning( params ) 97 | notice :warning, params 98 | self 99 | end 100 | 101 | def error( params ) 102 | notice :error, params 103 | self 104 | end 105 | 106 | def app_run( method, params = nil ) 107 | send_json action: :_appRun, method: method, params: params 108 | self 109 | end 110 | 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/nali/generator.rb: -------------------------------------------------------------------------------- 1 | module Nali 2 | 3 | class Generator 4 | 5 | def initialize( args ) 6 | if args.first == 'new' 7 | if args[1] then create_application args[1] 8 | else puts 'Enter a name for the application' end 9 | elsif [ 'm', 'model' ].include?( args.first ) 10 | if args[1] then create_model args[1] 11 | else puts 'Enter a name for the model' end 12 | elsif [ 'v', 'view' ].include?( args.first ) 13 | if args[1] then create_view args[1] 14 | else puts 'Enter a name for the view' end 15 | end 16 | end 17 | 18 | def create_application( name ) 19 | source = File.join Nali.path, 'generator/application/.' 20 | target = File.join Dir.pwd, name 21 | FileUtils.cp_r source, target 22 | %w( 23 | db 24 | db/migrate 25 | lib 26 | lib/client 27 | lib/client/javascripts 28 | lib/client/stylesheets 29 | public 30 | public/client 31 | tmp 32 | vendor 33 | vendor/client 34 | vendor/client/javascripts 35 | vendor/client/stylesheets 36 | config/initializers 37 | ).each { |dir| unless Dir.exists?( path = File.join( target, dir ) ) then Dir.mkdir( path ) end } 38 | puts "Application #{ name } created" 39 | end 40 | 41 | def render( name, classname ) 42 | require 'erb' 43 | ERB.new( File.read( File.join( Nali.path, 'generator/templates', "#{ name }.tpl" ) ) ).result binding 44 | end 45 | 46 | def write( path, content, mode = 'w' ) 47 | File.open( File.join( Dir.pwd, path ), mode ) { |file| file.write( content ) } 48 | puts ( mode == 'a' ? 'Updated: ' : 'Created: ' ) + path 49 | end 50 | 51 | def clean_cache 52 | FileUtils.rm_rf File.join( Dir.pwd, 'tmp/cache' ) 53 | end 54 | 55 | def create_model( name ) 56 | if Dir.exists?( File.join( Dir.pwd, 'app' ) ) 57 | if name.scan( '_' ).size > 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, ' <assist>$1</assist>' ) 167 | unless RegExp( "^<[^>]+" + @_name ).test @template 168 | @template = "<div class=\"#{ @_name }\">#{ @template }</div>" 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 | --------------------------------------------------------------------------------