├── examples └── normas_on_rails │ ├── log │ └── .keep │ ├── tmp │ └── .keep │ ├── vendor │ └── .keep │ ├── lib │ ├── assets │ │ └── .keep │ └── tasks │ │ └── .keep │ ├── public │ ├── favicon.ico │ ├── apple-touch-icon.png │ ├── apple-touch-icon-precomposed.png │ ├── robots.txt │ ├── 500.html │ ├── 422.html │ └── 404.html │ ├── .ruby-version │ ├── app │ ├── assets │ │ ├── images │ │ │ └── .keep │ │ └── config │ │ │ └── manifest.js │ ├── models │ │ └── concerns │ │ │ └── .keep │ ├── controllers │ │ ├── concerns │ │ │ └── .keep │ │ ├── application_controller.rb │ │ └── main_controller.rb │ ├── views │ │ ├── layouts │ │ │ ├── mailer.text.haml │ │ │ ├── mailer.html.haml │ │ │ └── application.html.haml │ │ └── main │ │ │ ├── index.html.haml │ │ │ ├── split_field.html.haml │ │ │ ├── _split_row.html.haml │ │ │ ├── _react_demo.html.haml │ │ │ ├── _my_player.html.haml │ │ │ ├── _random_component.html.haml │ │ │ ├── _morpheus.html.haml │ │ │ └── _split_cell.html.haml │ ├── jobs │ │ └── application_job.rb │ ├── channels │ │ └── application_cable │ │ │ ├── channel.rb │ │ │ └── connection.rb │ ├── mailers │ │ └── application_mailer.rb │ └── helpers │ │ └── application_helper.rb │ ├── .browserslistrc │ ├── .postcssrc.yml │ ├── client │ ├── images │ │ └── morpheus.jpg │ ├── app │ │ ├── hello.js │ │ ├── components │ │ │ ├── index.js │ │ │ └── ReactDemo.jsx │ │ ├── player.js │ │ ├── split-field.js │ │ └── global │ │ │ ├── select2.js │ │ │ └── popover.js │ ├── css │ │ ├── reset.scss │ │ ├── _base.scss │ │ ├── s-box.scss │ │ ├── b-my-player.scss │ │ ├── b-morpheus.scss │ │ ├── b-split-field.scss │ │ └── s-popover.scss │ ├── packs │ │ └── application.js │ └── lib │ │ └── normas.js │ ├── config │ ├── webpack │ │ ├── test.js │ │ ├── development.js │ │ ├── production.js │ │ └── environment.js │ ├── spring.rb │ ├── boot.rb │ ├── environment.rb │ ├── initializers │ │ ├── mime_types.rb │ │ ├── filter_parameter_logging.rb │ │ ├── application_controller_renderer.rb │ │ ├── cookies_serializer.rb │ │ ├── wrap_parameters.rb │ │ ├── backtrace_silencers.rb │ │ └── inflections.rb │ ├── routes.rb │ ├── cable.yml │ ├── locales │ │ └── en.yml │ ├── application.rb │ ├── secrets.yml │ ├── webpacker.yml │ ├── environments │ │ ├── development.rb │ │ ├── test.rb │ │ └── production.rb │ └── puma.rb │ ├── bin │ ├── bundle │ ├── rake │ ├── rails │ ├── yarn │ ├── webpack │ ├── webpack-dev-server │ ├── spring │ ├── update │ └── setup │ ├── config.ru │ ├── Rakefile │ ├── .babelrc │ ├── .gitignore │ ├── package.json │ ├── Gemfile │ ├── README.md │ └── Gemfile.lock ├── src ├── .browserslistrc ├── js │ ├── .babelrc │ ├── mixins │ │ ├── base.js │ │ ├── dom.js │ │ ├── mutations.js │ │ ├── view.js │ │ ├── content.js │ │ ├── logging.js │ │ ├── views.js │ │ ├── navigation.js │ │ ├── events.js │ │ ├── elements.js │ │ └── turbolinks.js │ ├── index.js │ ├── lib │ │ ├── url.js │ │ ├── jqueryAdditions.js │ │ └── helpers.js │ └── extensions │ │ └── react.js └── scss │ ├── _font-face.scss │ ├── _media.scss │ ├── _primitives.scss │ ├── _triangle.scss │ ├── _typography.scss │ └── _sugar.scss ├── .gitignore ├── .npmignore ├── dist └── js │ ├── integrations │ ├── react.js │ ├── react.production.js │ ├── react.js.map │ ├── react.production.js.map │ ├── turbolinks.production.js │ ├── turbolinks.js │ ├── turbolinks.production.js.map │ └── turbolinks.js.map │ └── extensions │ ├── views.production.js │ ├── views.js │ ├── views.production.js.map │ └── views.js.map ├── LICENSE ├── package.json └── config └── rollup.config.js /examples/normas_on_rails/log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/normas_on_rails/tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/normas_on_rails/vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/normas_on_rails/lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/normas_on_rails/lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/normas_on_rails/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/normas_on_rails/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.0 2 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/normas_on_rails/public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 versions 2 | not IE < 11 3 | -------------------------------------------------------------------------------- /examples/normas_on_rails/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules/ 4 | 5 | yarn-error.log 6 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/views/layouts/mailer.text.haml: -------------------------------------------------------------------------------- 1 | = yield 2 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/views/main/index.html.haml: -------------------------------------------------------------------------------- 1 | %h1 Hello! 2 | -------------------------------------------------------------------------------- /examples/normas_on_rails/.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 versions 2 | not IE < 11 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gitignore 3 | 4 | /node_modules 5 | yarn.lock 6 | 7 | examples/ 8 | -------------------------------------------------------------------------------- /examples/normas_on_rails/.postcssrc.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | postcss-import: {} 3 | postcss-cssnext: {} 4 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/views/main/split_field.html.haml: -------------------------------------------------------------------------------- 1 | .b-split-field 2 | = render 'split_row' 3 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/views/main/_split_row.html.haml: -------------------------------------------------------------------------------- 1 | .b-split-field__row 2 | = render 'split_cell' 3 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/views/main/_react_demo.html.haml: -------------------------------------------------------------------------------- 1 | = react_component 'ReactDemo', demoText: 'React Demo! Click me!' 2 | -------------------------------------------------------------------------------- /examples/normas_on_rails/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/images/morpheus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evrone/normas/HEAD/examples/normas_on_rails/client/images/morpheus.jpg -------------------------------------------------------------------------------- /examples/normas_on_rails/client/app/hello.js: -------------------------------------------------------------------------------- 1 | import normas from 'lib/normas'; 2 | 3 | normas.listenEvents('click h1', () => alert('Hello from Normas!')); 4 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/webpack/test.js: -------------------------------------------------------------------------------- 1 | const environment = require('./environment'); 2 | 3 | module.exports = environment.toWebpackConfig(); 4 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | end 4 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /examples/normas_on_rails/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/css/reset.scss: -------------------------------------------------------------------------------- 1 | body { 2 | 3 | } 4 | 5 | *, 6 | *::before, 7 | *::after { 8 | position: relative; 9 | box-sizing: border-box; 10 | } 11 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/css/_base.scss: -------------------------------------------------------------------------------- 1 | @import '~normas/src/scss/sugar'; 2 | @import '~normas/src/scss/primitives'; 3 | 4 | $common-border-radius: 5px; 5 | $bounce-transition: cubic-bezier(0.42, 0.8, 0.58, 1.2); 6 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root 'main#index' 3 | get 'main/split_field' 4 | get 'main/split_field_row' 5 | get 'main/split_field_cell' 6 | get 'main/morpheus' 7 | end 8 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def react_component(component_name, props = nil) 3 | content_tag :div, '', data: { react_component: component_name, props: props } 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | channel_prefix: normas-example_production 11 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/views/main/_my_player.html.haml: -------------------------------------------------------------------------------- 1 | .b-my-player 2 | .b-my-player__full-screen= "><" 3 | 4 | .b-my-player__playback-controls 5 | .b-my-player__play= ">" 6 | .b-my-player__pause= "||" 7 | .b-my-player__stop= "[]" 8 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /examples/normas_on_rails/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /examples/normas_on_rails/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/views/layouts/mailer.html.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %meta{:content => "text/html; charset=utf-8", "http-equiv" => "Content-Type"}/ 5 | :css 6 | /* Email styles need to be inline */ 7 | /* Use premailer[-rails] */ 8 | %body 9 | = yield 10 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/webpack/development.js: -------------------------------------------------------------------------------- 1 | const environment = require('./environment'); 2 | 3 | // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 4 | // environment.plugins.append('BundleAnalyzer', new BundleAnalyzerPlugin()); 5 | 6 | module.exports = environment.toWebpackConfig(); 7 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/webpack/production.js: -------------------------------------------------------------------------------- 1 | const environment = require('./environment'); 2 | 3 | // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 4 | // environment.plugins.append('BundleAnalyzer', new BundleAnalyzerPlugin()); 5 | 6 | module.exports = environment.toWebpackConfig(); 7 | -------------------------------------------------------------------------------- /examples/normas_on_rails/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/packs/application.js: -------------------------------------------------------------------------------- 1 | import 'css/reset'; 2 | 3 | import 'jquery'; 4 | import 'jquery-ujs'; 5 | 6 | import 'app/hello'; 7 | import 'app/split-field'; 8 | 9 | import 'css/s-box'; 10 | 11 | import 'app/global/select2'; 12 | import 'app/global/popover'; 13 | 14 | import 'app/player'; 15 | import 'app/components'; 16 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/views/main/_random_component.html.haml: -------------------------------------------------------------------------------- 1 | - case rand(4) 2 | - when 0 3 | = select_tag :sample_select, options_for_select(['Free', 'Basic', 'Advanced', 'Super Platinum'], selected: 'Free'), 4 | id: "random_select_#{rand(100_000_000)}" 5 | - when 1 6 | = render 'morpheus' 7 | - when 2 8 | = render 'my_player' 9 | - when 3 10 | = render 'react_demo' 11 | -------------------------------------------------------------------------------- /examples/normas_on_rails/bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | VENDOR_PATH = File.expand_path('..', __dir__) 3 | Dir.chdir(VENDOR_PATH) do 4 | begin 5 | exec "yarnpkg #{ARGV.join(" ")}" 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/app/components/index.js: -------------------------------------------------------------------------------- 1 | import normasReact from 'normas/dist/js/integrations/react'; 2 | import normas from 'lib/normas'; // or may be you use global Normas instance 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | normasReact.init({ normas, React, ReactDOM }); 7 | 8 | import ReactDemo from './ReactDemo'; 9 | 10 | normasReact.registerComponents({ 11 | ReactDemo, 12 | }); 13 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/controllers/main_controller.rb: -------------------------------------------------------------------------------- 1 | class MainController < ApplicationController 2 | def index; end 3 | 4 | def split_field; end 5 | 6 | def split_field_row 7 | render partial: 'main/split_row' 8 | end 9 | 10 | def split_field_cell 11 | render partial: 'main/split_cell' 12 | end 13 | 14 | def morpheus 15 | render partial: 'main/morpheus', locals: { morpheus_part: 'body' } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/css/s-box.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | 3 | .s-box { 4 | & + & { 5 | margin-top: 10px; 6 | } 7 | border-radius: $common-border-radius; 8 | background: #fff; 9 | 10 | &__caption { 11 | padding: 17px 20px; 12 | //@include font(12px, 16px, medium); 13 | text-transform: uppercase; 14 | user-select: none; 15 | } 16 | 17 | &__inner { 18 | display: block; 19 | padding: 20px; 20 | word-wrap: break-word; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/normas_on_rails/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "uglify": true 9 | }, 10 | "useBuiltIns": true 11 | } 12 | ], 13 | "react" 14 | ], 15 | "plugins": [ 16 | "syntax-dynamic-import", 17 | "transform-object-rest-spread", 18 | [ 19 | "transform-class-properties", 20 | { 21 | "spec": true 22 | } 23 | ] 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /examples/normas_on_rails/bin/webpack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | # This file was generated by Bundler. 5 | # 6 | # The application 'webpack' is installed as part of a gem, and 7 | # this file is here to facilitate running it. 8 | # 9 | 10 | require "pathname" 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 12 | Pathname.new(__FILE__).realpath) 13 | 14 | require "rubygems" 15 | require "bundler/setup" 16 | 17 | load Gem.bin_path("webpacker", "webpack") 18 | -------------------------------------------------------------------------------- /src/js/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "uglify": true 9 | }, 10 | "useBuiltIns": true 11 | } 12 | ] 13 | ], 14 | "plugins": [ 15 | "external-helpers", 16 | "syntax-dynamic-import", 17 | "transform-export-extensions", 18 | "transform-object-rest-spread", 19 | [ 20 | "transform-class-properties", 21 | { 22 | "spec": true 23 | } 24 | ] 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/normas_on_rails/bin/webpack-dev-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | # This file was generated by Bundler. 5 | # 6 | # The application 'webpack-dev-server' is installed as part of a gem, and 7 | # this file is here to facilitate running it. 8 | # 9 | 10 | require "pathname" 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 12 | Pathname.new(__FILE__).realpath) 13 | 14 | require "rubygems" 15 | require "bundler/setup" 16 | 17 | load Gem.bin_path("webpacker", "webpack-dev-server") 18 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/views/main/_morpheus.html.haml: -------------------------------------------------------------------------------- 1 | - morpheus_part ||= 'head' 2 | - if morpheus_part == 'body' 3 | .b-morpheus__body 4 | = render 'morpheus', morpheus_part: 'left-hand' 5 | = render 'morpheus', morpheus_part: 'right-hand' 6 | - else 7 | .b-morpheus{ class: morpheus_part } 8 | = link_to '', main_morpheus_path, remote: true, 9 | class: "js-popover-trigger b-morpheus__#{morpheus_part}", 10 | data: { popover_selector_scope: 'next' } 11 | .s-popover.js-popover 12 | .s-box__inner 13 | Loading... 14 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/views/main/_split_cell.html.haml: -------------------------------------------------------------------------------- 1 | .b-split-field__cell 2 | %nav.b-split-field__controls 3 | = link_to '', main_split_field_cell_path, remote: true, 4 | class: 'b-split-field__control b-split-field__control_split-cell js-split-cell' 5 | = link_to '', main_split_field_row_path, remote: true, 6 | class: 'b-split-field__control b-split-field__control_split-row js-split-row' 7 | .b-split-field__control.b-split-field__control_remove.js-remove-cell 8 | 9 | .b-split-field__row 10 | = render 'random_component' 11 | -------------------------------------------------------------------------------- /examples/normas_on_rails/bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /examples/normas_on_rails/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | /.generators 11 | /.rakeTasks 12 | 13 | # Ignore all logfiles and tempfiles. 14 | /log/* 15 | /tmp/* 16 | !/log/.keep 17 | !/tmp/.keep 18 | 19 | /node_modules 20 | /yarn-error.log 21 | 22 | .byebug_history 23 | /public/packs 24 | /public/packs-test 25 | -------------------------------------------------------------------------------- /examples/normas_on_rails/app/views/layouts/application.html.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %title Normas.js example 5 | = csrf_meta_tags 6 | 7 | -#= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' 8 | -#= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' 9 | = javascript_pack_tag 'application' 10 | = stylesheet_pack_tag 'application' 11 | 12 | %body 13 | %header 14 | %nav 15 | = link_to_unless_current 'Root', root_path 16 | = link_to_unless_current 'Split field', main_split_field_path 17 | %hr 18 | 19 | .content 20 | = yield 21 | -------------------------------------------------------------------------------- /examples/normas_on_rails/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "normas-example", 3 | "private": true, 4 | "dependencies": { 5 | "@rails/webpacker": "^3.2.0", 6 | "babel-preset-react": "^6.24.1", 7 | "coffeescript": "1.12.7", 8 | "jquery": "^3.2.1", 9 | "jquery-ujs": "^1.2.2", 10 | "lodash": "^4.17.4", 11 | "normas": "file:../..", 12 | "prop-types": "^15.6.0", 13 | "react": "^16.2.0", 14 | "react-dom": "^16.2.0", 15 | "select2": "^4.0.6-rc.1", 16 | "source-map-loader": "^0.2.3", 17 | "turbolinks": "^5.0.3", 18 | "webpack-bundle-analyzer": "^2.9.1" 19 | }, 20 | "devDependencies": { 21 | "webpack-dev-server": "^2.9.7" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/normas_on_rails/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) do |repo_name| 4 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 5 | "https://github.com/#{repo_name}.git" 6 | end 7 | 8 | 9 | gem 'rails', '~> 5.1.4' 10 | gem 'haml-rails' 11 | gem 'puma', '~> 3.7' 12 | gem 'webpacker' 13 | gem 'jbuilder', '~> 2.5' 14 | 15 | # Use Capistrano for deployment 16 | # gem 'capistrano-rails', group: :development 17 | 18 | group :development, :test do 19 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] 20 | end 21 | 22 | group :development do 23 | gem 'web-console', '>= 3.3.0' 24 | gem 'listen', '>= 3.0.5', '< 3.2' 25 | gem 'spring' 26 | gem 'spring-watcher-listen', '~> 2.0.0' 27 | end 28 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/webpack/environment.js: -------------------------------------------------------------------------------- 1 | const { environment } = require('@rails/webpacker'); 2 | 3 | // User ES-next source from Normas for development 4 | const babelLoader = environment.loaders.get('babel'); 5 | babelLoader.exclude = /node_modules(?!\/normas)/; 6 | 7 | // Globalize jQuery 8 | const oldToWebpackConfig = environment.toWebpackConfig; 9 | environment.toWebpackConfig = () => { 10 | const config = oldToWebpackConfig.call(environment); 11 | config.resolve.alias = { 12 | jquery: 'jquery/src/jquery', 13 | }; 14 | return config; 15 | }; 16 | 17 | environment.loaders.append('source-map', { 18 | test: /\.js$/, 19 | use: ['source-map-loader'], 20 | enforce: 'pre' 21 | }); 22 | 23 | module.exports = environment; 24 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/app/components/ReactDemo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ReactDemo extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | demoText: this.props.demoText, 8 | }; 9 | this.handleClickDemoText = this.handleClickDemoText.bind(this); 10 | } 11 | 12 | render() { 13 | return ( 14 |
19 | {this.state.demoText} 20 |
21 | ); 22 | } 23 | 24 | handleClickDemoText() { 25 | const demoWords = this.state.demoText.split(/\s+/); 26 | demoWords.unshift(demoWords.pop()); 27 | this.setState({ demoText: demoWords.join(' ') }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /examples/normas_on_rails/README.md: -------------------------------------------------------------------------------- 1 | # Normas.js Example 2 | 3 | Example project with Normas.js and server-side rendering on Rails 5.1 4 | 5 | ### Installation 6 | 7 | `bin/setup` — should be enough 8 | 9 | ### Run 10 | 11 | Run two different processes: 12 | 13 | `bin/webpack-dev-server` — start webpack dev server 14 | 15 | `bin/rails server -b 0.0.0.0 -p 3000 -e development` — start rails server 16 | 17 | ### Contributing 18 | 19 | You can link `normas` from local setup for debugging this example with `normas`: 20 | 21 | `cd ../.. && yarn link && cd examples/normas_on_rails && yarn link "normas"` 22 | 23 | and unlink with `yarn unlink "normas"` 24 | 25 | ### Info 26 | 27 | This project bootstrapped with command: 28 | 29 | `rails new normas_on_rails --skip-coffee --skip-sprockets --skip-turbolinks --webpack=react --skip-active-record  -T` 30 | -------------------------------------------------------------------------------- /examples/normas_on_rails/bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Removing old logs and tempfiles ==" 22 | system! 'bin/rails log:clear tmp:clear' 23 | 24 | puts "\n== Restarting application server ==" 25 | system! 'bin/rails restart' 26 | end 27 | -------------------------------------------------------------------------------- /examples/normas_on_rails/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # Install JavaScript dependencies if using Yarn 22 | system('bin/yarn') 23 | 24 | 25 | puts "\n== Removing old logs and tempfiles ==" 26 | system! 'bin/rails log:clear tmp:clear' 27 | 28 | puts "\n== Restarting application server ==" 29 | system! 'bin/rails restart' 30 | end 31 | -------------------------------------------------------------------------------- /src/js/mixins/base.js: -------------------------------------------------------------------------------- 1 | import * as importedHelpers from '../lib/helpers'; 2 | import dom from './dom'; 3 | 4 | const mutableHelpers = Object.assign({}, importedHelpers); 5 | 6 | export default class Base { 7 | static version = '0.4.0-rc2'; 8 | static helpers = mutableHelpers; 9 | static dom = dom; 10 | helpers = mutableHelpers; 11 | dom = dom; 12 | 13 | constructor({ el = document, instanceName = 'NormasApp' }) { 14 | this.instanceName = instanceName; 15 | this.el = el; 16 | this.$el = $(el); 17 | } 18 | 19 | $(...args) { 20 | return this.$el.find(...args); 21 | } 22 | 23 | log(...args) { 24 | // nop 25 | } 26 | 27 | error(...args) { 28 | // nop 29 | } 30 | 31 | // protected 32 | 33 | static readOptions(dest, source, defaults) { 34 | Object.keys(defaults).forEach(key => { 35 | dest[key] = source && (key in source) ? source[key] : defaults[key]; 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Normas 3 | * 4 | * @see {@link https://github.com/evrone/normas|Github} 5 | * @license MIT 6 | * @copyright Dmitry Karpunin , 2017-2018 7 | */ 8 | 9 | import './lib/jqueryAdditions'; 10 | import NormasBase from './mixins/base'; 11 | import NormasLogging from './mixins/logging'; 12 | import normasEvents from './mixins/events'; 13 | import normasContent from './mixins/content'; 14 | import normasElements from './mixins/elements'; 15 | import normasNavigation from './mixins/navigation'; 16 | import normasMutations from './mixins/mutations'; 17 | 18 | const NormasSubBase = NORMAS_DEBUG ? NormasLogging(NormasBase) : NormasBase; 19 | 20 | const NormasCore = normasEvents(NormasSubBase); 21 | 22 | const Normas = 23 | normasMutations( 24 | normasNavigation( 25 | normasElements( 26 | normasContent( 27 | NormasCore 28 | ) 29 | ) 30 | ) 31 | ); 32 | 33 | Normas.NormasCore = NormasCore; 34 | 35 | export default Normas; 36 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/css/b-my-player.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | 3 | .b-my-player { 4 | width: 33%; 5 | margin: 0 auto; 6 | background: black; 7 | 8 | @include before { 9 | padding-bottom: 9 / 16 * 100%; 10 | } 11 | 12 | &__full-screen { 13 | @include absolute(10px, 10px); 14 | } 15 | 16 | &__playback-controls { 17 | @include absolute(false, 10px, 10px, 10px); 18 | margin: 0 auto; 19 | padding: 5px; 20 | background-color: rgba(#fff, 0.2); 21 | border-radius: 5px; 22 | display: flex; 23 | } 24 | 25 | &__play { 26 | } 27 | 28 | &__pause { 29 | display: none; 30 | 31 | } 32 | 33 | &__stop { 34 | display: none; 35 | margin-left: 5px; 36 | } 37 | 38 | &__full-screen, 39 | &__play, 40 | &__pause, 41 | &__stop { 42 | text-align: center; 43 | @include size(15px); 44 | border-radius: 3px; 45 | cursor: pointer; 46 | background-color: #394f56; 47 | 48 | 49 | &:hover { 50 | background-color: #296e7b; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /dist/js/integrations/react.js: -------------------------------------------------------------------------------- 1 | "use strict";var t={selector:"[data-react-component]",listenOptions:{}},e={normas:null,React:null,ReactDOM:null,PropTypes:null,components:{},init:function(e){var n=e.normas,o=e.app,s=e.React,r=e.ReactDOM,i=e.PropTypes,a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},m=n.helpers.deepMerge(t,a),p=m.selector,c=m.listenOptions;this.normas=n||o,this.React=s,this.ReactDOM=r,this.PropTypes=i,n.listenToElement(p,this.mountComponentToElement.bind(this),this.unmountComponentFromElement.bind(this),c)},registerComponents:function(t){Object.assign(this.components,t)},mountComponentToElement:function(t){var e=t[0],n=e.getAttribute("data-react-component");if(n){var o=this.components[n];if(o){var s=e.getAttribute("data-props"),r=s?JSON.parse(s):null,i=this.React.createElement(o,r);this.ReactDOM.render(i,e)}else this.normas.error("No registered component class with name",n)}else this.normas.error("No component name in",e)},unmountComponentFromElement:function(t){var e=t[0];this.ReactDOM.unmountComponentAtNode(e)}};module.exports=e; 2 | //# sourceMappingURL=react.js.map 3 | -------------------------------------------------------------------------------- /dist/js/integrations/react.production.js: -------------------------------------------------------------------------------- 1 | "use strict";var t={selector:"[data-react-component]",listenOptions:{}},e={normas:null,React:null,ReactDOM:null,PropTypes:null,components:{},init:function(e){var n=e.normas,o=e.app,s=e.React,r=e.ReactDOM,i=e.PropTypes,a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},m=n.helpers.deepMerge(t,a),p=m.selector,c=m.listenOptions;this.normas=n||o,this.React=s,this.ReactDOM=r,this.PropTypes=i,n.listenToElement(p,this.mountComponentToElement.bind(this),this.unmountComponentFromElement.bind(this),c)},registerComponents:function(t){Object.assign(this.components,t)},mountComponentToElement:function(t){var e=t[0],n=e.getAttribute("data-react-component");if(n){var o=this.components[n];if(o){var s=e.getAttribute("data-props"),r=s?JSON.parse(s):null,i=this.React.createElement(o,r);this.ReactDOM.render(i,e)}else this.normas.error("No registered component class with name",n)}else this.normas.error("No component name in",e)},unmountComponentFromElement:function(t){var e=t[0];this.ReactDOM.unmountComponentAtNode(e)}};module.exports=e; 2 | //# sourceMappingURL=react.production.js.map 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017-2018 Dmitry Karpunin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | # require "active_record/railtie" 8 | require "action_controller/railtie" 9 | require "action_mailer/railtie" 10 | require "action_view/railtie" 11 | require "action_cable/engine" 12 | # require "sprockets/railtie" 13 | # require "rails/test_unit/railtie" 14 | 15 | # Require the gems listed in Gemfile, including any gems 16 | # you've limited to :test, :development, or :production. 17 | Bundler.require(*Rails.groups) 18 | 19 | module NormasExample 20 | class Application < Rails::Application 21 | # Initialize configuration defaults for originally generated Rails version. 22 | config.load_defaults 5.1 23 | 24 | # Settings in config/environments/* take precedence over those specified here. 25 | # Application configuration should go into files in config/initializers 26 | # -- all .rb files in that directory are automatically loaded. 27 | 28 | # Don't generate system test files. 29 | config.generators.system_tests = nil 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/css/b-morpheus.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | 3 | .b-morpheus { 4 | margin: 0 auto; 5 | width: 450px; 6 | 7 | &__head, 8 | &__body { 9 | //&__left-hand, 10 | //&__right-hand { 11 | background: no-repeat url('../images/morpheus.jpg') center top; 12 | } 13 | 14 | &__head { 15 | @include size(130px, 160px); 16 | display: block; 17 | margin: 0 auto; 18 | } 19 | 20 | &__body { 21 | height: 430px - 160px; 22 | background-position: center -160px; 23 | } 24 | 25 | //&__body > & { 26 | // width: auto; 27 | //} 28 | 29 | &__left-hand, 30 | &__right-hand { 31 | width: 30%; 32 | display: block; 33 | //position: absolute; 34 | //bottom: 35px; 35 | height: 100%; 36 | margin: 0 auto; 37 | } 38 | 39 | &.left-hand, 40 | &.right-hand { 41 | //width: 30%; 42 | //display: block; 43 | position: absolute; 44 | bottom: 35px; 45 | height: 100px; 46 | } 47 | 48 | &.left-hand { 49 | left: -155px; 50 | } 51 | 52 | &.right-hand { 53 | right: -155px; 54 | } 55 | 56 | // 57 | //&__left-hand { 58 | // left: 0; 59 | //} 60 | // 61 | //&__right-hand { 62 | // right: 0; 63 | //} 64 | } 65 | -------------------------------------------------------------------------------- /src/scss/_font-face.scss: -------------------------------------------------------------------------------- 1 | // required typography 2 | 3 | @function font-source-declaration($font-family, $file-path, $file-formats, $base64: false) { 4 | $src: (); 5 | $formats-map: ( 6 | eot: '#{$file-path}.eot?#iefix' format('embedded-opentype'), 7 | woff2: '#{$file-path}.woff2' format('woff2'), 8 | woff: '#{$file-path}.woff' format('woff'), 9 | ttf: '#{$file-path}.ttf' format('truetype'), 10 | svg: '#{$file-path}.svg##{$font-family}' format('svg') 11 | ); 12 | @each $key, $values in $formats-map { 13 | @if contains($file-formats, $key) { 14 | $file-path: nth($values, 1); 15 | $font-format: nth($values, 2); 16 | $url: if($base64, inline($file-path), resolve($file-path)); 17 | $src: append($src, $url $font-format, comma); 18 | } 19 | } 20 | @return $src; 21 | } 22 | 23 | @mixin font-face( 24 | $font-name, 25 | $file-path, 26 | $weight: normal, 27 | $style: normal, 28 | $file-formats: eot woff2 woff ttf svg, 29 | $base64: false) { 30 | @font-face { 31 | font-family: $font-name; 32 | font-style: $style; 33 | font-weight: font-weight($weight); 34 | src: font-source-declaration($font-name, $file-path, $file-formats, $base64); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/app/player.js: -------------------------------------------------------------------------------- 1 | import normas from 'lib/normas'; 2 | // import View from 'normas/src/js/view'; 3 | 4 | 5 | import 'css/b-my-player'; 6 | 7 | class MyPlayer extends normas.View { 8 | static selector = '.b-my-player'; 9 | 10 | static events = { 11 | 'click .b-my-player__full-screen': 'gotoFullScreen', 12 | '.b-my-player__playback-controls': { 13 | 'click .b-my-player__play': 'play', 14 | 'click .b-my-player__pause': 'pause', 15 | 'click .b-my-player__stop': 'stop', 16 | }, 17 | }; 18 | 19 | gotoFullScreen() { 20 | alert('No fullscreen :)'); 21 | } 22 | 23 | play($play) { 24 | $play.hide(); 25 | this.$('.b-my-player__pause, .b-my-player__stop').show(); 26 | } 27 | 28 | pause($pause) { 29 | $pause.hide(); 30 | this.$('.b-my-player__play').show(); 31 | this.$('.b-my-player__stop').hide(); 32 | } 33 | 34 | stop($stop) { 35 | $stop.hide(); 36 | this.$('.b-my-player__play').show(); 37 | this.$('.b-my-player__pause').hide(); 38 | } 39 | } 40 | 41 | // const testPlayer0 = new normas.View({ $el: $('div:last') }); 42 | // const testPlayer = new My2Player({ $el: $('div:last') }); 43 | 44 | normas.registerView(MyPlayer); 45 | -------------------------------------------------------------------------------- /src/scss/_media.scss: -------------------------------------------------------------------------------- 1 | @mixin media($query) { 2 | $media-query: 'screen'; 3 | $tick: false; 4 | $prop: false; 5 | @each $q in $query { 6 | $tick: is-not($tick); 7 | @if $tick { 8 | $prop: $q; 9 | $media-query: $media-query + " and (#{$q}:"; 10 | } @else { 11 | @if $prop == max-width { 12 | $q: $q - 1px; 13 | } 14 | $media-query: $media-query + " #{$q})"; 15 | } 16 | } 17 | @media #{$media-query} { 18 | @content; 19 | } 20 | } 21 | 22 | $_media-variant: false; 23 | $_media-args: false; 24 | $_media-first-arg: false; 25 | $_media-last-arg: false; 26 | 27 | @mixin media-variants($media-variants, $media-prop: max-width) { 28 | $_media-first-arg: true !global; 29 | $index: 0; 30 | $length: length($media-variants); 31 | @each $args in $media-variants { 32 | $_media-args: $args !global; 33 | $_media-variant: map-get($_media-args, variant) !global; 34 | $index: $index + 1; 35 | @if $index == $length { 36 | $_media-last-arg: true !global; 37 | } 38 | @if $_media-variant == base { 39 | @content; 40 | } @else { 41 | @include media($media-prop $_media-variant) { 42 | @content; 43 | } 44 | } 45 | @if $_media-first-arg { 46 | $_media-first-arg: false !global; 47 | } 48 | } 49 | $_media-last-arg: false !global; 50 | $_media-variant: false !global; 51 | $_media-args: false !global; 52 | } 53 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | # Shared secrets are available across all environments. 14 | 15 | # shared: 16 | # api_key: a1B2c3D4e5F6 17 | 18 | # Environmental secrets are only available for that specific environment. 19 | 20 | development: 21 | secret_key_base: 33f295774989a6c95fab76264c7b135185d012aaf63d4dbc96034f81b46a895aeceabbdef50993373d44a4262e48bbccb026c866dddf83fc78e6d3a6b99ed210 22 | 23 | test: 24 | secret_key_base: 88a530e0656fdab772c2824996d1adb9be9e0b2f8176d66604db812593d99d0b2036cb2b7b25cf330c886d21dafc90023fb5bccf12bd80218e8bb4514cc5989d 25 | 26 | # Do not keep production secrets in the unencrypted secrets file. 27 | # Instead, either read values from the environment. 28 | # Or, use `bin/rails secrets:setup` to configure encrypted secrets 29 | # and move the `production:` environment over there. 30 | 31 | production: 32 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 33 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/app/split-field.js: -------------------------------------------------------------------------------- 1 | import normas from 'lib/normas'; 2 | import 'css/b-split-field'; 3 | 4 | normas.listenEvents('.b-split-field', { 5 | 'ajax:send': () => normas.sayAboutPageLoading(true), 6 | 'ajax:complete': () => normas.sayAboutPageLoading(false), 7 | 'ajax:success .js-split-row': ($splitRowControl, event, newRowHtml) => { 8 | $(newRowHtml).hide().appendTo($splitRowControl.closest('.b-split-field__cell')).fadeIn(); 9 | }, 10 | 'ajax:success .js-split-cell': ($splitCellControl, event, newCellHtml) => { 11 | $(newCellHtml).hide().insertAfter($splitCellControl.closest('.b-split-field__cell')).fadeIn(); 12 | }, 13 | 'click .js-remove-cell': $removeCellControl => { 14 | let $cell = $removeCellControl.closest('.b-split-field__cell'); 15 | if ($cell.siblings().length === 0) { 16 | $cell = $cell.closest('.b-split-field__row'); 17 | } 18 | $cell.fadeOut(() => $cell.remove()); 19 | }, 20 | 'ajax:error': ($target, event, other) => console.log(other), 21 | }); 22 | 23 | // normas.listenToContent( 24 | // ($content) => console.log('new content', $content), 25 | // ($content) => console.log('removed content', $content), 26 | // ); 27 | 28 | normas.listenToElement( 29 | '.b-split-field__cell', 30 | $cell => { 31 | //console.log('>>> $cell', $cell); 32 | if ($cell.hasClass('init-cell')) { 33 | console.error('$cell.hasClass(\'init-cell\')'); 34 | } 35 | $cell.addClass('init-cell'); 36 | }, 37 | $cell => { 38 | //console.log('--- $cell', $cell); 39 | $cell.removeClass('init-cell'); 40 | }, 41 | ); 42 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/app/global/select2.js: -------------------------------------------------------------------------------- 1 | import 'select2'; 2 | import 'select2/dist/css/select2'; 3 | import normas from 'lib/normas'; 4 | 5 | export function bindSelect2($element) { 6 | let element = $element[0]; 7 | $element.select2(); 8 | let select2 = $element.data('select2'); 9 | // this and other changes in this commit resolve Select2 and Fastclick.js conflict 10 | // by https://github.com/select2/select2/issues/3222 11 | select2.$container.find('*').addClass('needsclick'); 12 | if (element.hasAttribute('multiple')) { 13 | $element.on('change', onChangeMultipleSelect2); 14 | } 15 | let $search = select2.dropdown.$search || select2.selection.$search; 16 | $search.on('keydown', (event) => { 17 | if (event.which === 9 && select2.isOpen()) { 18 | $element.data('tabPressed', true); 19 | } 20 | }); 21 | } 22 | 23 | export function unbindSelect2($element) { 24 | normas.log('unbindSelect2'); 25 | if ($element.data('select2')) { 26 | $element.select2('destroy'); 27 | } 28 | } 29 | 30 | // normas.listenToPage(() => { 31 | // $('.select2.select2-container').remove(); 32 | // }); 33 | // 34 | normas.listenToElement('select', bindSelect2, unbindSelect2, { delay: 500 }); 35 | 36 | normas.listenEvents({ 37 | // changeMedia: debounce(resizeSelects, 100 + 10), 38 | '.select2-selection--single': { 39 | focus(event) { 40 | let $select = $(event.currentTarget).closest('.select2-container').prev('select'); 41 | // console.log('focus single, ', $select.attr('name')); 42 | if (!$select.data('closing')) { 43 | // console.log(', go open!'); 44 | $select.select2('open'); 45 | } 46 | }, 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "normas", 3 | "version": "0.4.0-rc2", 4 | "description": "Normal Ligtweight Javascript Framework for server-side render compatible with Turbolinks", 5 | "author": "Dmitry Karpunin ", 6 | "license": "MIT", 7 | "keywords": [ 8 | "lightweight", 9 | "framework", 10 | "turbolinks" 11 | ], 12 | "repository": "evrone/normas", 13 | "files": [ 14 | "src", 15 | "dist" 16 | ], 17 | "main": "dist/js/normas.js", 18 | "module": "dist/js/normas.js", 19 | "esnext": "src/js/index.js", 20 | "bundlesize": [ 21 | { 22 | "path": "dist/js/normas.js", 23 | "maxSize": "7 kB" 24 | }, 25 | { 26 | "path": "dist/js/normas.production.js", 27 | "maxSize": "4.5 kB" 28 | } 29 | ], 30 | "scripts": { 31 | "build": "rollup -c config/rollup.config.js", 32 | "test": "bundlesize" 33 | }, 34 | "devDependencies": { 35 | "babel-core": "~6.26.0", 36 | "babel-plugin-external-helpers": "~6.22.0", 37 | "babel-plugin-syntax-dynamic-import": "~6.18.0", 38 | "babel-plugin-transform-class-properties": "~6.24.1", 39 | "babel-plugin-transform-export-extensions": "~6.22.0", 40 | "babel-plugin-transform-object-rest-spread": "~6.26.0", 41 | "babel-preset-env": "~1.6.0", 42 | "bundlesize": "~0.16.0", 43 | "rollup": "~0.54.0", 44 | "rollup-config-module": "~2.0.0", 45 | "rollup-plugin-babel": "~3.0.3", 46 | "rollup-plugin-commonjs": "~8.3.0", 47 | "rollup-plugin-filesize": "~1.5.0", 48 | "rollup-plugin-node-resolve": "~3.0.2", 49 | "rollup-plugin-progress": "~0.4.0", 50 | "rollup-plugin-uglify": "~3.0.0" 51 | }, 52 | "peerDependencies": { 53 | "jquery": "^3.0.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/webpacker.yml: -------------------------------------------------------------------------------- 1 | # Note: You must restart bin/webpack-dev-server for changes to take effect 2 | 3 | default: &default 4 | source_path: client 5 | source_entry_path: packs 6 | public_output_path: packs 7 | cache_path: tmp/cache/webpacker 8 | 9 | # Additional paths webpack should lookup modules 10 | # ['app/assets', 'engine/foo/app/assets'] 11 | resolved_paths: [] 12 | 13 | # Reload manifest.json on all requests so we reload latest compiled packs 14 | cache_manifest: false 15 | 16 | extensions: 17 | # - .coffee 18 | # - .erb 19 | - .js 20 | - .jsx 21 | # - .ts 22 | # - .vue 23 | # - .sass 24 | - .scss 25 | - .css 26 | - .png 27 | - .svg 28 | - .gif 29 | - .jpeg 30 | - .jpg 31 | 32 | development: 33 | <<: *default 34 | compile: true 35 | 36 | # Reference: https://webpack.js.org/configuration/dev-server/ 37 | dev_server: 38 | https: false 39 | host: localhost 40 | port: 3035 41 | public: localhost:3035 42 | hmr: false 43 | # Inline should be set to true if using HMR 44 | inline: true 45 | overlay: true 46 | compress: true 47 | disable_host_check: true 48 | use_local_ip: false 49 | quiet: false 50 | headers: 51 | 'Access-Control-Allow-Origin': '*' 52 | watch_options: 53 | ignored: /node_modules/ 54 | 55 | 56 | test: 57 | <<: *default 58 | compile: true 59 | 60 | # Compile test packs to a separate directory 61 | public_output_path: packs-test 62 | 63 | production: 64 | <<: *default 65 | 66 | # Production depends on precompilation of packs prior to booting for performance. 67 | compile: false 68 | 69 | # Cache manifest.json for performance 70 | cache_manifest: true 71 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/css/b-split-field.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | 3 | $c-row: #008037; 4 | $c-cell: #003780; 5 | 6 | .b-split-field { 7 | &__row { 8 | display: flex; 9 | 10 | background-color: #c8f6cf; 11 | //border: 3px solid $c-row; 12 | border-radius: 5px; 13 | padding: 10px; 14 | 15 | & + & { 16 | margin-top: 10px; 17 | } 18 | } 19 | 20 | &__cell { 21 | flex: 0 1 100%; 22 | min-height: 44px; 23 | 24 | background-color: #abd4ed; 25 | //border: 3px solid $c-cell; 26 | border-radius: 5px; 27 | padding: 10px; 28 | 29 | & + & { 30 | margin-left: 10px; 31 | } 32 | } 33 | 34 | $controls-space: 2px; 35 | &__controls { 36 | @include absolute(3px, false, false, 3px); 37 | z-index: 1; 38 | width: $controls-space * 4 + 20px * 3; 39 | border-radius: 3px; 40 | padding: $controls-space; 41 | background: #999999; 42 | 43 | display: flex; 44 | } 45 | 46 | &__control { 47 | 48 | flex: 0 0 20px; 49 | height: 20px; 50 | background: #CCCCCC; 51 | border-radius: 2px; 52 | padding: 2px; 53 | cursor: pointer; 54 | 55 | &:hover { 56 | background: #EEEEEE; 57 | } 58 | 59 | & + & { 60 | margin-left: $controls-space; 61 | } 62 | 63 | display: flex; 64 | justify-content: space-around; 65 | align-items: center; 66 | 67 | &_split-row { 68 | flex-direction: column; 69 | 70 | @include both { 71 | @include size(14px, 6px); 72 | border: 2px solid $c-row; 73 | } 74 | } 75 | 76 | &_split-cell { 77 | @include both { 78 | @include size(6px, 14px); 79 | border: 2px solid $c-cell; 80 | } 81 | } 82 | 83 | &_remove { 84 | @include cross(20px, 18px, #BB5555, false, 2px, true); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Verifies that versions and hashed value of the package contents in the project's package.json 3 | config.webpacker.check_yarn_integrity = true 4 | 5 | # Settings specified here will take precedence over those in config/application.rb. 6 | 7 | # In the development environment your application's code is reloaded on 8 | # every request. This slows down response time but is perfect for development 9 | # since you don't have to restart the web server when you make code changes. 10 | config.cache_classes = false 11 | 12 | # Do not eager load code on boot. 13 | config.eager_load = false 14 | 15 | # Show full error reports. 16 | config.consider_all_requests_local = true 17 | 18 | # Enable/disable caching. By default caching is disabled. 19 | if Rails.root.join('tmp/caching-dev.txt').exist? 20 | config.action_controller.perform_caching = true 21 | 22 | config.cache_store = :memory_store 23 | config.public_file_server.headers = { 24 | 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}" 25 | } 26 | else 27 | config.action_controller.perform_caching = false 28 | 29 | config.cache_store = :null_store 30 | end 31 | 32 | # Don't care if the mailer can't send. 33 | config.action_mailer.raise_delivery_errors = false 34 | 35 | config.action_mailer.perform_caching = false 36 | 37 | # Print deprecation notices to the Rails logger. 38 | config.active_support.deprecation = :log 39 | 40 | 41 | # Raises error for missing translations 42 | # config.action_view.raise_on_missing_translations = true 43 | 44 | # Use an evented file watcher to asynchronously detect changes in source code, 45 | # routes, locales, etc. This feature depends on the listen gem. 46 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 47 | end 48 | -------------------------------------------------------------------------------- /examples/normas_on_rails/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /src/js/mixins/dom.js: -------------------------------------------------------------------------------- 1 | import { isArray, isPlainObject, mapValues, each } from '../lib/helpers'; 2 | 3 | export default { 4 | memoryData(element, key, ...value) { 5 | if (value.length > 0) { 6 | this.elementsApply(element, el => { el[key] = value[0]; }); 7 | } else { 8 | return element[key]; 9 | } 10 | }, 11 | 12 | removeMemoryData(element, key) { 13 | const value = element[key]; 14 | delete element[key]; 15 | return value; 16 | }, 17 | 18 | data(element, key, ...value) { 19 | if (value.length > 0) { 20 | const stringifiedValue = this.dataStringify(value[0]); 21 | this.elementsApply(element, el => { el.dataset[key] = stringifiedValue; }); 22 | } else { 23 | return key 24 | ? 25 | this.dataParse(element.dataset[key]) 26 | : 27 | mapValues(element.dataset, dataValue => this.dataParse(dataValue)); 28 | } 29 | }, 30 | 31 | removeData(element, key) { 32 | const value = element.dataset[key]; 33 | delete element.dataset[key]; 34 | return value; 35 | }, 36 | 37 | dataStringify(data) { 38 | return isArray(data) || isPlainObject(data) 39 | ? 40 | JSON.stringify(data) 41 | : 42 | data; 43 | }, 44 | 45 | dataParse(dataValue) { 46 | try { 47 | return JSON.parse(dataValue); 48 | } catch (e) { 49 | this.lastDataParseError = e; 50 | return dataValue; 51 | } 52 | }, 53 | 54 | elementsApply(element, iteratee) { 55 | if (this.isElement(element)) { 56 | iteratee(element); 57 | } else { 58 | each(element, iteratee); 59 | } 60 | }, 61 | 62 | isElement(element) { 63 | return element instanceof Element; 64 | }, 65 | 66 | contains(rootElement, element) { 67 | return (rootElement === document ? document.body : rootElement).contains(element); 68 | }, 69 | 70 | remove(element) { 71 | element.parentNode.removeChild(element); 72 | }, 73 | }; 74 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}" 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /examples/normas_on_rails/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /examples/normas_on_rails/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /src/js/lib/url.js: -------------------------------------------------------------------------------- 1 | import { compact, map, isArray } from '../lib/helpers'; 2 | 3 | export function filterUrl($form, filterNames = null) { 4 | let params = $form.serializeArray(); 5 | let action = $form.attr('action'); 6 | params = params.filter(param => param.name !== 'utf8'); 7 | return changeUrlParams(action, params, filterNames); 8 | } 9 | 10 | export function changeUrlParams(url, setParams, filterNames = null) { 11 | let urlParts = url.split('?'); 12 | let params = getUrlParams(url); 13 | if (!isArray(setParams)) { 14 | setParams = serializedArrayFromHash(setParams); 15 | } 16 | params = prepareParams(params.concat(setParams)); 17 | if (filterNames) { params = params.filter(({ name }) => filterNames.indexOf(name.replace(/\[\]$/, '')) > -1) } 18 | urlParts[1] = $.param(params); 19 | url = compact(urlParts).join('?'); 20 | return url; 21 | } 22 | 23 | export function getUrlParams(url) { 24 | let paramsPart = url.split('?')[1] || ''; 25 | if (!paramsPart) { 26 | return []; 27 | } 28 | let params = paramsPart.split('&'); 29 | return params.map((param) => { 30 | let [name, value] = param.split('=').map(decodeURIComponent); 31 | return { name, value }; 32 | }); 33 | } 34 | 35 | function prepareParams(serializedArray) { 36 | // collapsing value by names to last in order 37 | let params = []; 38 | let names = []; 39 | serializedArray.forEach((param) => { 40 | let index = /\[\]$/.test(param.name) ? -1 : names.indexOf(param.name); 41 | if (index < 0) { 42 | names.push(param.name); 43 | params.push(param); 44 | } else { 45 | params[index] = param; 46 | } 47 | }); 48 | return params.filter(param => param.value).sort(serializedSort); 49 | } 50 | 51 | function serializedSort(a, b) { 52 | if (a.name === b.name) { 53 | return a.value >= b.value ? 1 : -1; 54 | } 55 | return a.name > b.name ? 1 : -1; 56 | } 57 | 58 | function serializedArrayFromHash(params) { 59 | return map(params, (value, name) => ({ name, value })).sort(serializedSort); 60 | } 61 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/lib/normas.js: -------------------------------------------------------------------------------- 1 | import Normas from 'normas'; 2 | import normasTurbolinks from 'normas/dist/js/integrations/turbolinks'; 3 | import normasViews from 'normas/dist/js/extensions/views'; 4 | // import Turbolinks from 'turbolinks'; 5 | import Turbolinks from '../../vendor/turbolinks-debug'; 6 | 7 | const NormasWithTurbolinks = 8 | normasViews( 9 | normasTurbolinks( 10 | Normas 11 | ) 12 | ); 13 | 14 | const normas = new NormasWithTurbolinks({ 15 | Turbolinks, 16 | enabling: { // detailed enabling settings, each default `true` 17 | turbolinks: true, 18 | mutations: true, 19 | }, 20 | debugMode: process.env.NODE_ENV === 'development', // default `true` 21 | logging: { // detailed logging settings, mostly default `true` 22 | hideInstancesOf: [Element, NormasWithTurbolinks.View], 23 | constructGrouping: 'groupCollapsed', 24 | // content: true, 25 | // Core level options 26 | // hideInstancesOf: [], // list of constructors whose instances will be muted, ex: [Element, $, Normas.View, Normas] 27 | // construct: true, // logs about constructing, default `false`, because noisy 28 | // constructGrouping: true, // group logging about constructing 29 | // events: true, // logs about events listening 30 | // eventsDebounced: true, // events collect in debounced by 20ms batches 31 | // eventsTable: false, // events subscriptions info as table, default `false`, because massive 32 | // App level options 33 | // elements: true, // logs about element enter and leave 34 | // content: false, // logs about content enter and leave, default `false`, because noisy 35 | // contentGrouping: true, // group logging under content lifecycle 36 | // navigation: true, // logs in navigation mixin 37 | // navigationGrouping: true, // group logging under page events 38 | }, 39 | viewOptions: { 40 | logging: { 41 | }, 42 | }, 43 | }); 44 | 45 | global.normas = normas; 46 | 47 | export default normas; 48 | -------------------------------------------------------------------------------- /src/js/extensions/react.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React integration for Normas 3 | * 4 | * @see {@link https://github.com/evrone/normas#reactjs-integration|Docs} 5 | * @see {@link https://github.com/evrone/normas/blob/master/src/js/extensions/react.js|Source} 6 | * @license MIT 7 | * @copyright Dmitry Karpunin , 2017-2018 8 | */ 9 | 10 | const defaultOptions = { 11 | selector: '[data-react-component]', 12 | listenOptions: {}, 13 | }; 14 | 15 | export default { 16 | normas: null, 17 | React: null, 18 | ReactDOM: null, 19 | PropTypes: null, 20 | components: {}, 21 | 22 | init({ normas, app, React, ReactDOM, PropTypes }, options = {}) { 23 | const { selector, listenOptions } = normas.helpers.deepMerge(defaultOptions, options); 24 | this.normas = normas || app; 25 | this.React = React; 26 | this.ReactDOM = ReactDOM; 27 | this.PropTypes = PropTypes; 28 | 29 | normas.listenToElement( 30 | selector, 31 | this.mountComponentToElement.bind(this), 32 | this.unmountComponentFromElement.bind(this), 33 | listenOptions, 34 | ); 35 | }, 36 | 37 | registerComponents(components) { 38 | Object.assign(this.components, components); 39 | }, 40 | 41 | // private 42 | 43 | mountComponentToElement($element) { 44 | const domNode = $element[0]; 45 | const name = domNode.getAttribute('data-react-component'); 46 | if (!name) { 47 | this.normas.error('No component name in', domNode); 48 | return; 49 | } 50 | const componentClass = this.components[name]; 51 | if (!componentClass) { 52 | this.normas.error('No registered component class with name', name); 53 | return; 54 | } 55 | const propsString = domNode.getAttribute('data-props'); 56 | const props = propsString ? JSON.parse(propsString) : null; 57 | const component = this.React.createElement(componentClass, props); 58 | this.ReactDOM.render(component, domNode); 59 | }, 60 | 61 | unmountComponentFromElement($element) { 62 | const domNode = $element[0]; 63 | this.ReactDOM.unmountComponentAtNode(domNode); 64 | }, 65 | } 66 | -------------------------------------------------------------------------------- /src/js/mixins/mutations.js: -------------------------------------------------------------------------------- 1 | // require navigation mixin 2 | export default Base => (class extends Base { 3 | constructor(options) { 4 | super(options); 5 | if (!this.enablings) this.enablings = {}; 6 | this.constructor.readOptions(this.enablings, options.enablings, { mutations: true }); 7 | if (NORMAS_DEBUG) { 8 | this.log('info', 'construct', 9 | ...this.constructor.logColor(`🤖 "${this.instanceName}" MutationObserver %REPLACE%.`, 10 | this.enablings.mutations ? 'enabled' : 'disabled', 11 | this.enablings.mutations ? 'green' : 'blue')); 12 | } 13 | if (this.enablings.mutations) { 14 | if (MutationObserver) { 15 | this.observeMutations(); 16 | } else if (NORMAS_DEBUG) { 17 | this.log('warn', 'construct', `🤖 "${this.instanceName}" mutation observer NOT SUPPORTED!`, 18 | this.constructor.readmeLink('-content-broadcasting')); 19 | } 20 | } 21 | } 22 | 23 | observeMutations() { 24 | // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver 25 | this.mutationObserver = new MutationObserver(mutations => mutations.forEach(this.checkMutations)); 26 | this.mutationObserver.observe(this.$el[0], { childList: true, subtree: true }); 27 | } 28 | 29 | checkMutations = (mutation) => { 30 | if (!this.navigationStarted || mutation.type !== 'childList') { 31 | return; 32 | } 33 | const removedNodes = this.constructor.filterMutationNodes(mutation.removedNodes); 34 | const addedNodes = this.constructor.filterMutationNodes(mutation.addedNodes, true); 35 | 36 | if (removedNodes.length > 0) { 37 | this.sayAboutContentLeave($(removedNodes)); 38 | } 39 | if (addedNodes.length > 0) { 40 | this.sayAboutContentEnter($(addedNodes)); 41 | } 42 | }; 43 | 44 | static filterMutationNodes(nodes, checkParentElement = false) { 45 | return Array.prototype.filter.call(nodes, node => ( 46 | node.nodeType === 1 && 47 | !node.isPreview && 48 | !['TITLE', 'META'].includes(node.tagName) && 49 | node.className !== 'turbolinks-progress-bar' && 50 | !(checkParentElement && !node.parentElement) && 51 | !(node.parentElement && node.parentElement.tagName === 'HEAD') 52 | )); 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /src/js/mixins/view.js: -------------------------------------------------------------------------------- 1 | export default Base => (class extends Base { 2 | // Override it with your own initialization logic (like componentDidUnmount in react). 3 | initialize(options) { 4 | } 5 | 6 | // Override it with your own unmount logic (like componentWillUnmount in react). 7 | terminate() { 8 | } 9 | 10 | // protected 11 | 12 | constructor(options) { 13 | Object.assign(options, Base.dom.data(options.el)); 14 | super(options); 15 | this.reflectOptions(options); 16 | this.initializeEvents(options); 17 | this.initialize(options); 18 | if (NORMAS_DEBUG && this.logging.constructGrouping) { 19 | this.log('groupEnd', 'construct'); 20 | } 21 | } 22 | 23 | destructor() { 24 | if (NORMAS_DEBUG) { 25 | const [destructText, ...destructStyles] = this.constructor.logColor('destructing', 'red'); 26 | this.log(this.constructor.groupingMethod(this.logging.constructGrouping), 'construct', 27 | ...this.constructor.logBold(`${this.logging.constructPrefix} "%REPLACE%" ${destructText}`, this.instanceName), 28 | ...destructStyles, 29 | this); 30 | } 31 | this.terminate(); 32 | if (this.listenedEvents) { 33 | this.forgetEvents(this.listenedEvents); 34 | this.listenedEvents = null; 35 | } 36 | if (NORMAS_DEBUG && this.logging.constructGrouping) { 37 | this.log('groupEnd', 'construct'); 38 | } 39 | } 40 | 41 | reflectOptions(options) { 42 | if (!this.constructor.reflectOptions) { 43 | return; 44 | } 45 | Object.keys(options).forEach(attr => { 46 | if (this.constructor.reflectOptions.includes(attr)) { 47 | this[attr] = options[attr]; 48 | } 49 | }); 50 | } 51 | 52 | initializeEvents(_options) { 53 | const { events } = this.constructor; 54 | if (events) { 55 | if (!this.linkedEvents) { 56 | this.linkedEvents = this.linkEvents(this.helpers.isFunction(events) ? events() : events); 57 | } 58 | this.listenedEvents = this.listenEvents(this.linkedEvents); 59 | } 60 | } 61 | 62 | linkEvents(events) { 63 | return this.helpers.mapValues(events, handle => this.helpers.isString(handle) ? 64 | this[handle].bind(this) 65 | : 66 | (typeof this.helpers.isPlainObject(handle) ? this.linkEvents(handle) : handle) 67 | ); 68 | } 69 | 70 | data(key, ...value) { 71 | this.dom.data(this.el, key, ...value); 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /src/js/lib/jqueryAdditions.js: -------------------------------------------------------------------------------- 1 | $.fn.each$ = function (handle) { 2 | return this.each((index, element) => { 3 | handle($(element), index); 4 | }); 5 | }; 6 | 7 | $.fn.filter$ = function (handle) { 8 | return this.filter((index, element) => handle($(element), index)); 9 | }; 10 | 11 | $.fn.map$ = function (handle) { 12 | return this.map((index, element) => handle($(element), index)); 13 | }; 14 | 15 | // [showOrHide[, duration[, callback]]] 16 | $.fn.slideToggleByState = function slideToggleByState(...a) { 17 | if (this.length > 0) { 18 | if (a.length > 0) { 19 | if (a.shift()) { 20 | this.slideDown(...a); 21 | } else { 22 | this.slideUp(...a); 23 | } 24 | } else { 25 | this.slideToggle(); 26 | } 27 | } 28 | return this; 29 | }; 30 | 31 | // http://css-tricks.com/snippets/jquery/mover-cursor-to-end-of-textarea/ 32 | $.fn.focusToEnd = function focusToEnd() { 33 | let $this = this.first(); 34 | if ($this.is('select, :checkbox, :radio')) { 35 | $this.focus(); 36 | } else { 37 | let val = $this.val(); 38 | $this.focus().val('').val(val); 39 | } 40 | return this; 41 | }; 42 | 43 | $.fn.focusTo = function focusTo(caretPos) { 44 | return this.each((index, element) => { 45 | if (element.createTextRange) { 46 | let range = element.createTextRange(); 47 | range.move('character', caretPos); 48 | range.select(); 49 | } else if (element.selectionStart) { 50 | element.focus(); 51 | element.setSelectionRange(caretPos, caretPos); 52 | } else { 53 | element.focus(); 54 | } 55 | }); 56 | }; 57 | 58 | /* 59 | ** Returns the caret (cursor) position of the specified text field. 60 | ** Return value range is 0-oField.value.length. 61 | */ 62 | $.fn.caretPosition = function caretPosition() { 63 | // Initialize 64 | let iCaretPos = 0; 65 | let oField = this[0]; 66 | 67 | // IE Support 68 | if (document.selection) { 69 | // Set focus on the element 70 | oField.focus(); 71 | // To get cursor position, get empty selection range 72 | let oSel = document.selection.createRange(); 73 | // Move selection start to 0 position 74 | oSel.moveStart('character', -oField.value.length); 75 | // The caret position is selection length 76 | iCaretPos = oSel.text.length; 77 | } else if (oField.selectionStart != null) { 78 | iCaretPos = oField.selectionStart; 79 | } 80 | 81 | // Return results 82 | return iCaretPos; 83 | }; 84 | -------------------------------------------------------------------------------- /examples/normas_on_rails/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # If you are preloading your application and using Active Record, it's 36 | # recommended that you close any connections to the database before workers 37 | # are forked to prevent connection leakage. 38 | # 39 | # before_fork do 40 | # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) 41 | # end 42 | 43 | # The code in the `on_worker_boot` will be called if you are using 44 | # clustered mode by specifying a number of `workers`. After each worker 45 | # process is booted, this block will be run. If you are using the `preload_app!` 46 | # option, you will want to use this block to reconnect to any threads 47 | # or connections that may have been created at application boot, as Ruby 48 | # cannot share connections between processes. 49 | # 50 | # on_worker_boot do 51 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 52 | # end 53 | # 54 | 55 | # Allow puma to be restarted by `rails restart` command. 56 | plugin :tmp_restart 57 | -------------------------------------------------------------------------------- /config/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs'; 2 | import nodeResolve from 'rollup-plugin-node-resolve'; 3 | import babel from 'rollup-plugin-babel'; 4 | import uglify from 'rollup-plugin-uglify'; 5 | import progress from 'rollup-plugin-progress'; 6 | import filesize from 'rollup-plugin-filesize'; 7 | import colors from 'colors'; // dependency from 'rollup-plugin-filesize' 8 | 9 | const bundles = [ 10 | { 11 | name: 'normas', 12 | input: 'index.js', 13 | }, 14 | { 15 | name: 'integrations/turbolinks', 16 | input: 'mixins/turbolinks.js', 17 | }, 18 | { 19 | name: 'integrations/react', 20 | input: 'extensions/react.js', 21 | }, 22 | { 23 | name: 'extensions/views', 24 | input: 'mixins/views.js', 25 | }, 26 | ]; 27 | 28 | bundles.forEach(b => b.debug = true); 29 | 30 | bundles.push(...bundles.map(({ name, input }) => ({ 31 | name: `${name}.production`, 32 | input, 33 | debug: false, 34 | }))); 35 | 36 | export default bundles.map(({ name, input, debug }) => ({ 37 | input: `src/js/${input}`, 38 | output: { 39 | strict: true, 40 | file: `dist/js/${name}.js`, 41 | name, 42 | format: 'cjs', 43 | sourcemap: true, 44 | }, 45 | plugins: [ 46 | nodeResolve({ 47 | // module: true, 48 | // jsnext: true, 49 | // browser: true, 50 | // main: true, 51 | // jail: '/src/js', 52 | // modulesOnly: true, 53 | }), 54 | babel({ 55 | sourceMap: true, 56 | exclude: 'node_modules/**', 57 | }), 58 | commonjs(), 59 | uglify({ 60 | toplevel: true, 61 | compress: { 62 | global_defs: { 63 | NORMAS_DEBUG: debug, 64 | }, 65 | }, 66 | // output: { 67 | // // comments: 'all', 68 | // comments: function(node, comment) { 69 | // // multiline comment 70 | // // if (comment.type === 'comment2') { 71 | // return /@preserve|@license|@cc_on/i.test(comment.value); 72 | // // } 73 | // }, 74 | // }, 75 | }), 76 | progress(), 77 | filesize({ 78 | render: function(opt, size, gzip, _bundle) { 79 | const primaryColor = opt.theme === 'dark' ? 'green' : 'black'; 80 | const secondaryColor = opt.theme === 'dark' ? 'blue' : 'blue'; 81 | return ( 82 | (colors[primaryColor].bold('Bundle size: ') + colors[secondaryColor](size)) + 83 | (opt.showGzippedSize ? ', ' + colors[primaryColor].bold('Gzipped size: ') + colors[secondaryColor](gzip) : "") 84 | ); 85 | }, 86 | }), 87 | ], 88 | })); 89 | -------------------------------------------------------------------------------- /src/scss/_primitives.scss: -------------------------------------------------------------------------------- 1 | @mixin scrollable { 2 | overflow-x: hidden; 3 | overflow-y: auto; 4 | 5 | // Edge-tech property for smooth scrolling on touch devices 6 | // scss-lint:disable PropertySpelling 7 | -webkit-overflow-scrolling: touch; 8 | overflow-scrolling: touch; 9 | // scss-lint:enable PropertySpelling 10 | } 11 | 12 | @mixin chevron($size, $color, $direction: false, $border-width: 1px) { 13 | @if $size { 14 | @include size($size); 15 | } 16 | border-width: $border-width $border-width 0 0; 17 | border-style: solid; 18 | border-color: $color; 19 | @if $direction { 20 | @include chevron-direction($direction); 21 | } 22 | @content; 23 | } 24 | 25 | @mixin chevron-direction($direction) { 26 | // up || right || down || left || up-right || up-left || down-left || down-right 27 | @if $direction == up { 28 | transform: rotate(-45deg); 29 | } @else if $direction == right { 30 | transform: rotate(45deg); 31 | } @else if $direction == down { 32 | transform: rotate(135deg); 33 | } @else if $direction == left { 34 | transform: rotate(-135deg); 35 | } @else if $direction == up-right { 36 | transform: rotate(0deg); 37 | } @else if $direction == up-left { 38 | transform: rotate(-90deg); 39 | } @else if $direction == down-left { 40 | transform: rotate(-180deg); 41 | } @else if $direction == down-right { 42 | transform: rotate(-270deg); 43 | } @else { 44 | @error 'Unknown argument in chevron-direction($direction: #{$direction})'; 45 | } 46 | } 47 | 48 | @mixin cross( 49 | $element-size, $line-size, $line-color, 50 | $set-element-size: true, $line-width: 1px, 51 | $rotate: false, $hover-color: false) { 52 | @if $set-element-size { 53 | @include size($element-size); 54 | } 55 | @include both { 56 | @include size(0, $line-size); 57 | @include absolute(($element-size - $line-size) / 2, false, false, ($element-size - $line-width) / 2); 58 | border-right: $line-width solid $line-color; 59 | @content; 60 | } 61 | @if $rotate { 62 | &::before { 63 | transform: rotate(-45deg); 64 | } 65 | &::after { 66 | transform: rotate(45deg); 67 | } 68 | } @else { 69 | &::after { 70 | transform: rotate(90deg); 71 | } 72 | } 73 | @if $hover-color { 74 | &:hover { 75 | &::before, 76 | &::after { 77 | border-color: $hover-color; 78 | } 79 | } 80 | } 81 | } 82 | 83 | @mixin cross-size($element-size, $line-size) { 84 | $padding: ($element-size - $line-size) / 2; 85 | @include size($element-size); 86 | &::before, 87 | &::after { 88 | height: $line-size; 89 | @if $padding > 0 { 90 | top: $padding; 91 | } 92 | left: $element-size / 2; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/scss/_triangle.scss: -------------------------------------------------------------------------------- 1 | @mixin triangle($size, $color, $direction) { 2 | $width: nth($size, 1); 3 | $height: nth($size, length($size)); 4 | $foreground-color: nth($color, 1); 5 | $background-color: if(length($color) == 2, nth($color, 2), transparent); 6 | height: 0; 7 | width: 0; 8 | 9 | @if ($direction == up) or ($direction == down) or ($direction == right) or ($direction == left) { 10 | $width: $width / 2; 11 | $height: if(length($size) > 1, $height, $height / 2); 12 | 13 | @if $direction == up { 14 | border-bottom: $height solid $foreground-color; 15 | border-left: $width solid $background-color; 16 | border-right: $width solid $background-color; 17 | } @else if $direction == right { 18 | border-bottom: $width solid $background-color; 19 | border-left: $height solid $foreground-color; 20 | border-top: $width solid $background-color; 21 | } @else if $direction == down { 22 | border-left: $width solid $background-color; 23 | border-right: $width solid $background-color; 24 | border-top: $height solid $foreground-color; 25 | } @else if $direction == left { 26 | border-bottom: $width solid $background-color; 27 | border-right: $height solid $foreground-color; 28 | border-top: $width solid $background-color; 29 | } 30 | } @else if ($direction == up-right) or ($direction == up-left) { 31 | border-top: $height solid $foreground-color; 32 | 33 | @if $direction == up-right { 34 | border-left: $width solid $background-color; 35 | } @else if $direction == up-left { 36 | border-right: $width solid $background-color; 37 | } 38 | } @else if ($direction == down-right) or ($direction == down-left) { 39 | border-bottom: $height solid $foreground-color; 40 | 41 | @if $direction == down-right { 42 | border-left: $width solid $background-color; 43 | } @else if $direction == down-left { 44 | border-right: $width solid $background-color; 45 | } 46 | } @else if ($direction == inset-up) { 47 | border-color: $background-color $background-color $foreground-color; 48 | border-style: solid; 49 | border-width: $height $width; 50 | } @else if ($direction == inset-down) { 51 | border-color: $foreground-color $background-color $background-color; 52 | border-style: solid; 53 | border-width: $height $width; 54 | } @else if ($direction == inset-right) { 55 | border-color: $background-color $background-color $background-color $foreground-color; 56 | border-style: solid; 57 | border-width: $width $height; 58 | } @else if ($direction == inset-left) { 59 | border-color: $background-color $foreground-color $background-color $background-color; 60 | border-style: solid; 61 | border-width: $width $height; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/scss/_typography.scss: -------------------------------------------------------------------------------- 1 | @mixin font($font-size, $line-height: false, $args...) { 2 | @include _primary-font-prop(font-size, $font-size); 3 | @include _primary-font-prop(line-height, $line-height); 4 | @each $arg in $args { 5 | @include _font-prop($arg); 6 | } 7 | } 8 | 9 | @mixin _primary-font-prop($name, $arg) { 10 | @if $arg { 11 | @if type-of($arg) == number or index(inherit initial, $arg) { 12 | #{$name}: $arg; 13 | } @else { 14 | @include _font-prop($arg); 15 | } 16 | } 17 | } 18 | 19 | @mixin _font-prop($arg) { 20 | $arg-part: nth($arg, 1); 21 | @if type-of($arg-part) == color { 22 | color: $arg; 23 | } @else if index(capitalize lowercase uppercase, $arg-part) { 24 | text-transform: $arg; 25 | } @else if index(center left right, $arg-part) { 26 | text-align: $arg; 27 | } @else if index(blink line-through overline underline, $arg-part) { 28 | text-decoration: $arg; 29 | } @else if index(pre pre-line pre-wrap nowrap, $arg-part) { 30 | white-space: $arg; 31 | } @else { 32 | $normalized-font-weight: font-weight($arg-part); 33 | @if $normalized-font-weight { 34 | $arg: set-nth($arg, 1, $normalized-font-weight); 35 | font-weight: $arg; 36 | } @else { 37 | @error 'Unknown argument in @mixin font(..., #{$arg-part}:#{type-of($arg-part)}, ...)'; 38 | } 39 | } 40 | } 41 | 42 | @mixin font-weight($font-weight) { 43 | $normalized-font-weight: font-weight($font-weight); 44 | @if $normalized-font-weight { 45 | font-weight: $normalized-font-weight; 46 | } @else { 47 | @error 'Unknown argument in @function font-weight($font-weight: #{$font-weight})'; 48 | } 49 | } 50 | 51 | @function font-weight($font-weight) { 52 | @if ($font-weight == 100 or $font-weight == thin or $font-weight == hairline) { 53 | @return 100; 54 | } @else if($font-weight == 200 or $font-weight == extralight or $font-weight == ultralight) { 55 | @return 200; 56 | } @else if($font-weight == 300 or $font-weight == light) { 57 | @return 300; 58 | } @else if($font-weight == 400 or $font-weight == normal or $font-weight == regular) { 59 | @return 400; 60 | } @else if($font-weight == 500 or $font-weight == medium) { 61 | @return 500; 62 | } @else if($font-weight == 600 or $font-weight == semibold or $font-weight == demibold) { 63 | @return 600; 64 | } @else if($font-weight == 700 or $font-weight == bold) { 65 | @return 700; 66 | } @else if($font-weight == 800 or $font-weight == extrabold or $font-weight == ultrabold) { 67 | @return 800; 68 | } @else if($font-weight == 900 or $font-weight == 'black' or $font-weight == heavy) { 69 | @return 900; 70 | } 71 | @return false; 72 | } 73 | 74 | @mixin ellipsis($display: inline-block, $width: 100%) { 75 | @if $display { 76 | display: $display; 77 | } 78 | @if $width { 79 | max-width: $width; 80 | } 81 | overflow: hidden; 82 | text-overflow: ellipsis; 83 | white-space: nowrap; 84 | word-wrap: normal; 85 | } 86 | -------------------------------------------------------------------------------- /dist/js/integrations/react.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"react.js","sources":["../../../src/js/extensions/react.js"],"sourcesContent":["/**\n * React integration for Normas\n *\n * @see {@link https://github.com/evrone/normas#reactjs-integration|Docs}\n * @see {@link https://github.com/evrone/normas/blob/master/src/js/extensions/react.js|Source}\n * @license MIT\n * @copyright Dmitry Karpunin , 2017-2018\n */\n\nconst defaultOptions = {\n selector: '[data-react-component]',\n listenOptions: {},\n};\n\nexport default {\n normas: null,\n React: null,\n ReactDOM: null,\n PropTypes: null,\n components: {},\n\n init({ normas, app, React, ReactDOM, PropTypes }, options = {}) {\n const { selector, listenOptions } = normas.helpers.deepMerge(defaultOptions, options);\n this.normas = normas || app;\n this.React = React;\n this.ReactDOM = ReactDOM;\n this.PropTypes = PropTypes;\n\n normas.listenToElement(\n selector,\n this.mountComponentToElement.bind(this),\n this.unmountComponentFromElement.bind(this),\n listenOptions,\n );\n },\n\n registerComponents(components) {\n Object.assign(this.components, components);\n },\n\n // private\n\n mountComponentToElement($element) {\n const domNode = $element[0];\n const name = domNode.getAttribute('data-react-component');\n if (!name) {\n this.normas.error('No component name in', domNode);\n return;\n }\n const componentClass = this.components[name];\n if (!componentClass) {\n this.normas.error('No registered component class with name', name);\n return;\n }\n const propsString = domNode.getAttribute('data-props');\n const props = propsString ? JSON.parse(propsString) : null;\n const component = this.React.createElement(componentClass, props);\n this.ReactDOM.render(component, domNode);\n },\n\n unmountComponentFromElement($element) {\n const domNode = $element[0];\n this.ReactDOM.unmountComponentAtNode(domNode);\n },\n}\n"],"names":["defaultOptions","normas","app","React","ReactDOM","PropTypes","options","helpers","deepMerge","selector","listenOptions","listenToElement","this","mountComponentToElement","bind","unmountComponentFromElement","components","assign","$element","domNode","name","getAttribute","componentClass","propsString","props","JSON","parse","component","createElement","render","error","unmountComponentAtNode"],"mappings":"aASA,IAAMA,YACM,qDAKF,WACD,cACG,eACC,wCAGJC,IAAAA,OAAQC,IAAAA,IAAKC,IAAAA,MAAOC,IAAAA,SAAUC,IAAAA,UAAaC,8DACZL,EAAOM,QAAQC,UAAUR,EAAgBM,GAArEG,IAAAA,SAAUC,IAAAA,mBACbT,OAASA,GAAUC,OACnBC,MAAQA,OACRC,SAAWA,OACXC,UAAYA,IAEVM,gBACLF,EACAG,KAAKC,wBAAwBC,KAAKF,MAClCA,KAAKG,4BAA4BD,KAAKF,MACtCF,gCAIeM,UACVC,OAAOL,KAAKI,WAAYA,qCAKTE,OAChBC,EAAUD,EAAS,GACnBE,EAAOD,EAAQE,aAAa,2BAC7BD,OAICE,EAAiBV,KAAKI,WAAWI,MAClCE,OAICC,EAAcJ,EAAQE,aAAa,cACnCG,EAAQD,EAAcE,KAAKC,MAAMH,GAAe,KAChDI,EAAYf,KAAKT,MAAMyB,cAAcN,EAAgBE,QACtDpB,SAASyB,OAAOF,EAAWR,aANzBlB,OAAO6B,MAAM,0CAA2CV,aALxDnB,OAAO6B,MAAM,uBAAwBX,yCAclBD,OACpBC,EAAUD,EAAS,QACpBd,SAAS2B,uBAAuBZ"} -------------------------------------------------------------------------------- /dist/js/integrations/react.production.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"react.production.js","sources":["../../../src/js/extensions/react.js"],"sourcesContent":["/**\n * React integration for Normas\n *\n * @see {@link https://github.com/evrone/normas#reactjs-integration|Docs}\n * @see {@link https://github.com/evrone/normas/blob/master/src/js/extensions/react.js|Source}\n * @license MIT\n * @copyright Dmitry Karpunin , 2017-2018\n */\n\nconst defaultOptions = {\n selector: '[data-react-component]',\n listenOptions: {},\n};\n\nexport default {\n normas: null,\n React: null,\n ReactDOM: null,\n PropTypes: null,\n components: {},\n\n init({ normas, app, React, ReactDOM, PropTypes }, options = {}) {\n const { selector, listenOptions } = normas.helpers.deepMerge(defaultOptions, options);\n this.normas = normas || app;\n this.React = React;\n this.ReactDOM = ReactDOM;\n this.PropTypes = PropTypes;\n\n normas.listenToElement(\n selector,\n this.mountComponentToElement.bind(this),\n this.unmountComponentFromElement.bind(this),\n listenOptions,\n );\n },\n\n registerComponents(components) {\n Object.assign(this.components, components);\n },\n\n // private\n\n mountComponentToElement($element) {\n const domNode = $element[0];\n const name = domNode.getAttribute('data-react-component');\n if (!name) {\n this.normas.error('No component name in', domNode);\n return;\n }\n const componentClass = this.components[name];\n if (!componentClass) {\n this.normas.error('No registered component class with name', name);\n return;\n }\n const propsString = domNode.getAttribute('data-props');\n const props = propsString ? JSON.parse(propsString) : null;\n const component = this.React.createElement(componentClass, props);\n this.ReactDOM.render(component, domNode);\n },\n\n unmountComponentFromElement($element) {\n const domNode = $element[0];\n this.ReactDOM.unmountComponentAtNode(domNode);\n },\n}\n"],"names":["defaultOptions","normas","app","React","ReactDOM","PropTypes","options","helpers","deepMerge","selector","listenOptions","listenToElement","this","mountComponentToElement","bind","unmountComponentFromElement","components","assign","$element","domNode","name","getAttribute","componentClass","propsString","props","JSON","parse","component","createElement","render","error","unmountComponentAtNode"],"mappings":"aASA,IAAMA,YACM,qDAKF,WACD,cACG,eACC,wCAGJC,IAAAA,OAAQC,IAAAA,IAAKC,IAAAA,MAAOC,IAAAA,SAAUC,IAAAA,UAAaC,8DACZL,EAAOM,QAAQC,UAAUR,EAAgBM,GAArEG,IAAAA,SAAUC,IAAAA,mBACbT,OAASA,GAAUC,OACnBC,MAAQA,OACRC,SAAWA,OACXC,UAAYA,IAEVM,gBACLF,EACAG,KAAKC,wBAAwBC,KAAKF,MAClCA,KAAKG,4BAA4BD,KAAKF,MACtCF,gCAIeM,UACVC,OAAOL,KAAKI,WAAYA,qCAKTE,OAChBC,EAAUD,EAAS,GACnBE,EAAOD,EAAQE,aAAa,2BAC7BD,OAICE,EAAiBV,KAAKI,WAAWI,MAClCE,OAICC,EAAcJ,EAAQE,aAAa,cACnCG,EAAQD,EAAcE,KAAKC,MAAMH,GAAe,KAChDI,EAAYf,KAAKT,MAAMyB,cAAcN,EAAgBE,QACtDpB,SAASyB,OAAOF,EAAWR,aANzBlB,OAAO6B,MAAM,0CAA2CV,aALxDnB,OAAO6B,MAAM,uBAAwBX,yCAclBD,OACpBC,EAAUD,EAAS,QACpBd,SAAS2B,uBAAuBZ"} -------------------------------------------------------------------------------- /src/js/mixins/content.js: -------------------------------------------------------------------------------- 1 | // require events mixin 2 | export default Base => (class extends Base { 3 | static contentEnterEventName = 'content:enter'; 4 | static contentLeaveEventName = 'content:leave'; 5 | 6 | constructor(options) { 7 | super(options); 8 | if (NORMAS_DEBUG) { 9 | this.constructor.readOptions(this.logging, options.logging, { 10 | content: false, 11 | contentGrouping: true, 12 | }); 13 | this.log('info', 'construct', 14 | `📰 "${this.instanceName}" content mixin activated.`, 15 | 'logging.content =', this.logging.content); 16 | } 17 | } 18 | 19 | // subscription to content lifecycle 20 | 21 | listenToContent(enter, leave = null) { 22 | if (enter) { 23 | this.$el.on(this.constructor.contentEnterEventName, (event, $content) => enter($content, event)); 24 | } 25 | if (leave) { 26 | this.$el.on(this.constructor.contentLeaveEventName, (event, $content) => leave($content, event)); 27 | } 28 | } 29 | 30 | // manual content broadcasting 31 | 32 | sayAboutContentEnter($content) { 33 | return this.sayAboutContentMove('enter', this.constructor.contentEnterEventName, $content); 34 | } 35 | 36 | sayAboutContentLeave($content) { 37 | return this.sayAboutContentMove('leave', this.constructor.contentLeaveEventName, $content); 38 | } 39 | 40 | // private 41 | sayAboutContentMove(move, eventName, $content) { 42 | const enter = move === 'enter'; 43 | $content = this.constructor.filterContent($content, (enter ? 'normasEntered' : 'normasLeft'), enter); 44 | if ($content.length > 0) { 45 | if (NORMAS_DEBUG) { 46 | this.logContent(move, $content); 47 | } 48 | this.trigger(eventName, $content); 49 | if (NORMAS_DEBUG && this.logging.contentGrouping) { 50 | this.log('groupEnd', 'content'); 51 | } 52 | } 53 | return $content; 54 | } 55 | 56 | // helpers 57 | 58 | replaceContentInner($container, content) { 59 | this.sayAboutContentLeave($container); 60 | $container.html(content); 61 | this.sayAboutContentEnter($container); 62 | } 63 | 64 | replaceContent($content, $newContent) { 65 | if ($content.length > 1) { 66 | $content = $content.first(); 67 | } 68 | this.sayAboutContentLeave($content); 69 | $content.replaceWith($newContent); 70 | this.sayAboutContentEnter($newContent); 71 | } 72 | 73 | // private 74 | 75 | logContent(logEvent, $content) { 76 | if (!NORMAS_DEBUG) { 77 | return; 78 | } 79 | const [eventName, ...eventStyles] = this.constructor.logCycle(logEvent, logEvent === 'enter', 5); 80 | const [contentName, ...contentStyles] = this.constructor.logBold(this.constructor.contentName($content)); 81 | this.log(this.constructor.groupingMethod(this.logging.contentGrouping), 'content', 82 | `📰 content ${eventName} "${contentName}"`, 83 | ...eventStyles, ...contentStyles, 84 | $content); 85 | } 86 | 87 | static filterContent($content, elementFlagName, checkParentElement = false) { 88 | return $content.filter((_index, element) => { 89 | if (element[elementFlagName] || (checkParentElement && !element.parentElement)) { 90 | return false; 91 | } 92 | element[elementFlagName] = true; 93 | return true; 94 | }); 95 | } 96 | }); 97 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/css/s-popover.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | 3 | .js-popover-trigger { 4 | //@include cursor-pointer; 5 | //> .s-popover span { 6 | // cursor: default; 7 | //} 8 | } 9 | 10 | @mixin s-popover-style { 11 | background-color: #fff; 12 | border-radius: $common-border-radius; 13 | box-shadow: 14 | 0 0 13px rgba(#296e7b, 0.19), 15 | 0 8px 25px rgba(#394f56, 0.13); 16 | } 17 | 18 | @mixin s-popover-show-transition($show, 19 | $shift: -30px, 20 | $duration: $common-ui-transition-duration, 21 | $enter-transition: $bounce-transition) { 22 | @if (not $show) { 23 | @include transition-props(( 24 | visibility: hidden, 25 | opacity: 0, 26 | transform: scale(0.9) translate(0, $shift) 27 | ), $duration, $will-change: true); 28 | } @else { 29 | @include transition-props(( 30 | visibility: visible, 31 | opacity: 1, 32 | transform: scale(1) translate(0, 0) 33 | ), $duration, $enter-transition); 34 | } 35 | } 36 | 37 | .s-popover { 38 | //@include font($font-size-base, $line-height-base, regular, $c-base); 39 | text-align: left; 40 | white-space: normal; 41 | 42 | //@include gpu; 43 | @include absolute(100%, false, false, 0); 44 | 45 | min-width: 100%; 46 | min-height: 35px; 47 | max-height: 70vh; 48 | display: flex; 49 | flex-direction: column; 50 | z-index: 101; 51 | @include s-popover-style; 52 | @include s-popover-show-transition(false); 53 | 54 | .s-popover-hover-trigger:hover + &, 55 | .s-popover-hover-trigger:hover > &, 56 | &.show { 57 | @include s-popover-show-transition(true); 58 | } 59 | cursor: default; 60 | 61 | &.with-corner { 62 | @include before { 63 | @include s-popover-style; 64 | @include size(21px); 65 | transform: rotate(45deg); 66 | @include absolute(-2px, 0, false, 0); 67 | margin: auto; 68 | } 69 | } 70 | 71 | > .s-box__inner { 72 | background: #fff; 73 | border-radius: $common-border-radius; 74 | } 75 | 76 | &.right { 77 | right: 0; 78 | left: auto; 79 | } 80 | //@include desktop-tablet { 81 | // &.desktop-right { 82 | // right: 0; 83 | // left: auto; 84 | // } 85 | //} 86 | 87 | &__caption { 88 | @include height-with-line(50px); 89 | //border-bottom: 1px solid $c-form-border; 90 | //@include font(12px, false, medium); 91 | text-align: center; 92 | text-transform: uppercase; 93 | user-select: none; 94 | background: #fff; 95 | border-radius: $common-border-radius $common-border-radius 0 0; 96 | } 97 | 98 | &.select { 99 | margin-top: 2px; 100 | padding: 8px 0; 101 | text-align: left; 102 | > .sort_link, 103 | > .option { 104 | + .sort_link, 105 | + .option { 106 | margin-top: 4px; 107 | } 108 | padding: 9px 15px; 109 | //@include ellipsis(block); 110 | //@include font(14px, 15px, regular, $c-action-gray); 111 | @include cursor-pointer; 112 | &:hover { 113 | //color: $c-action-gray_hover; 114 | //background-color: $c-select-option_hover; 115 | } 116 | &.desc, 117 | &.asc, 118 | &.selected { 119 | color: #fff; 120 | //background-color: $c-brand; 121 | } 122 | } 123 | } 124 | } 125 | 126 | //.popover-links { 127 | // padding: 7px 0 5px; 128 | // > a { 129 | // display: block; 130 | // padding: 13px 15px; 131 | // //@include font(14px, 15px, regular); 132 | // &.with-border-top { 133 | // margin-top: 5px; 134 | // border-top: 1px solid $c-form-border; 135 | // padding-top: 17px; 136 | // } 137 | // } 138 | //} 139 | -------------------------------------------------------------------------------- /dist/js/integrations/turbolinks.production.js: -------------------------------------------------------------------------------- 1 | "use strict";var t=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},e=function(){function t(t,e){for(var o=0;o (class extends Base { 2 | constructor(options) { 3 | super(options); 4 | this.constructor.readOptions(this, options, { debugMode: true }); 5 | this.logging = {}; 6 | this.constructor.readOptions(this.logging, options.logging, { 7 | construct: true, 8 | constructGrouping: true, 9 | constructPrefix: '🏗️', 10 | hideInstancesOf: [], 11 | }); 12 | const [constructText, ...constructStyles] = this.constructor.logColor('construct', 'green'); 13 | this.log(this.constructor.groupingMethod(this.logging.constructGrouping), 'construct', 14 | ...this.constructor.logBold(`${this.logging.constructPrefix} "%REPLACE%" ${constructText}`, this.instanceName), 15 | ...constructStyles, 16 | this); 17 | } 18 | 19 | static consoleMethods = ['log', 'group', 'groupEnd', 'groupCollapsed', 'info', 'table', 'warn']; 20 | 21 | log(...args) { 22 | if (!this.debugMode) return; 23 | const method = this.helpers.includes(this.constructor.consoleMethods, args[0]) ? args.shift() : 'log'; 24 | if (this.helpers.intersection(Object.keys(this.logging), args[0]).length > 0 && !this.logging[args.shift()]) { 25 | return; 26 | } 27 | this.constructor.log(method, ...this.filterLog(args)); 28 | } 29 | 30 | error(...args) { 31 | this.constructor.log('error', ...args); 32 | } 33 | 34 | // private 35 | 36 | filterLog(args) { 37 | return this.logging.hideInstancesOf.length === 0 ? args : 38 | this.helpers.filter(args, a => !this.helpers.find(this.logging.hideInstancesOf, c => a instanceof c)); 39 | } 40 | 41 | static groupingMethod(grouping) { 42 | return (typeof grouping === 'string') ? grouping : (grouping ? 'group' : 'log'); 43 | } 44 | 45 | static log(method, ...args) { 46 | if (console && console[method]) { 47 | console[method](...args); // eslint-disable-line no-console 48 | } 49 | } 50 | 51 | static logPlur(message, count, omitOne = true) { 52 | return message.replace('%COUNT%', omitOne && count === 1 ? '' : count).replace('%S%', count === 1 ? '' : 's'); 53 | } 54 | 55 | static logCycle(moveName, enter, intensity = 2) { 56 | return this.logColor( 57 | `${moveName} ${(enter ? '>' : '<').repeat(intensity)}`, 58 | enter ? 'green' : 'red', 59 | ); 60 | } 61 | 62 | static logColor(template, colorText, color) { 63 | if (!color) { 64 | color = colorText; 65 | colorText = null; 66 | } 67 | return this.logStyle(template, colorText, { color }); 68 | } 69 | 70 | static logBold(template, boldText) { 71 | return this.logStyle(template, boldText, { 'font-weight': 'bold' }); 72 | } 73 | 74 | static logStyle(template, stylingReplace, style) { 75 | if (this.helpers.isPlainObject(stylingReplace)) { 76 | style = stylingReplace; 77 | stylingReplace = null; 78 | } 79 | if (!stylingReplace) { 80 | stylingReplace = template; 81 | template = null; 82 | } 83 | const stylePairs = Object.entries(style); 84 | const beginStyle = stylePairs.map(p => p.join(': ')).join('; '); 85 | const endStyle = stylePairs.map(([k]) => [k, 'inherit'].join(': ')).join('; '); 86 | stylingReplace = `%c${stylingReplace}%c`; 87 | if (template) { 88 | stylingReplace = template.replace('%REPLACE%', stylingReplace); 89 | } 90 | return [stylingReplace, beginStyle, endStyle]; 91 | } 92 | 93 | static contentName($content) { 94 | const contentCounts = this.helpers.countBy($content, content => { 95 | const classList = content.classList ? this.helpers.map(content.classList, className => className) : []; 96 | return [content.tagName].concat(classList).join('.'); 97 | }); 98 | return Object.keys(contentCounts).map(name => { 99 | const count = contentCounts[name]; 100 | return `${count > 1 ? count + ' ' : ''}${name}`; 101 | }).join(' + '); 102 | } 103 | 104 | static readmeLink(point) { 105 | return `Read https://github.com/evrone/normas#${point}`; 106 | } 107 | }); 108 | -------------------------------------------------------------------------------- /src/js/mixins/views.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Views system for Normas 3 | * 4 | * @see {@link https://github.com/evrone/normas#-views|Docs} 5 | * @see {@link https://github.com/evrone/normas/blob/master/src/js/mixins/views.js|Source} 6 | * @license MIT 7 | * @copyright Dmitry Karpunin , 2017-2018 8 | */ 9 | 10 | // TODO: may be rename Views, views, View, view 11 | import normasView from './view'; 12 | 13 | export default Base => normasViews(Base, normasView(Base.NormasCore)); 14 | 15 | // require content mixin 16 | // require events mixin 17 | const normasViews = (Base, View) => (class extends Base { 18 | static View = View; 19 | View = View; 20 | viewClasses = {}; 21 | viewInstances = []; 22 | 23 | constructor(options) { 24 | super(options); 25 | this.viewOptions = { 26 | debugMode: this.debugMode, 27 | ...options.viewOptions, 28 | }; 29 | if (NORMAS_DEBUG) { 30 | this.viewOptions.logging = { 31 | ...this.logging, 32 | constructGrouping: 'groupCollapsed', 33 | constructPrefix: '🏭', // private 34 | eventsDebounced: false, 35 | ...(options.viewOptions && options.viewOptions.logging), 36 | }; 37 | this.log('info', 'construct', `🏭 "${this.instanceName}" views mixin activated.`); 38 | } 39 | } 40 | 41 | registerView(viewClass, options = {}) { 42 | if (this.viewClasses[viewClass.selector]) { 43 | if (NORMAS_DEBUG) { 44 | this.error(`🏭 View class for selector \`${viewClass.selector}\` already registered`, 45 | this.viewClasses[viewClass.selector]); 46 | } 47 | return; 48 | } 49 | this.viewClasses[viewClass.selector] = viewClass; 50 | this.listenToElement( 51 | viewClass.selector, 52 | $el => this.bindView($el, viewClass, options), 53 | $el => this.unbindView($el, viewClass), 54 | { 55 | delay: viewClass.delay, 56 | silent: true, 57 | }, 58 | ); 59 | } 60 | 61 | bindView($el, viewClass, options) { 62 | if (!this.canBind($el, viewClass)) { 63 | return null; 64 | } 65 | if (viewClass.instanceIndex) { 66 | viewClass.instanceIndex += 1; 67 | } else { 68 | viewClass.instanceIndex = 1; 69 | } 70 | const view = new viewClass({ 71 | ...this.helpers.deepMerge(this.viewOptions, options), 72 | instanceName: `${viewClass.selector}_${viewClass.instanceIndex}`, 73 | el: $el[0], 74 | }); 75 | this.viewInstances.push(view); 76 | return view; 77 | } 78 | 79 | canBind($element, viewClass) { 80 | const view = this.getViewsOnElement($element, viewClass)[0]; 81 | if (view) { 82 | if (NORMAS_DEBUG) { 83 | this.log('warn', '🏭 Element already has bound view', $element, viewClass, view); 84 | } 85 | return false; 86 | } 87 | return true; 88 | } 89 | 90 | unbindView($element, viewClass) { 91 | const view = this.getViewsOnElement($element, viewClass)[0]; 92 | if (view) { 93 | view.destructor(); 94 | this.viewInstances = this.helpers.without(this.viewInstances, view); 95 | } 96 | } 97 | 98 | getViewsOnElement($element, viewClass = null) { 99 | const el = $element instanceof $ ? $element[0] : $element; 100 | const filterOptions = { el }; 101 | if (viewClass) { 102 | filterOptions.constructor = viewClass; 103 | } 104 | return this.helpers.filter(this.viewInstances, filterOptions); 105 | } 106 | 107 | getViewsInContainer($container, checkRoot = true) { 108 | return this.helpers.filter(this.viewInstances, view => 109 | view.$el.closest($container).length > 0 && (checkRoot || view.el !== $container[0]) 110 | ); 111 | } 112 | 113 | getAllViews(viewClass) { 114 | return this.helpers.filter(this.viewInstances, { constructor: viewClass }); 115 | } 116 | 117 | getFirstView(viewClass) { 118 | return this.helpers.find(this.viewInstances, { constructor: viewClass }); 119 | } 120 | 121 | getFirstChildView(viewClass) { 122 | return this.helpers.find(this.viewInstances, view => view instanceof viewClass); 123 | } 124 | }); 125 | -------------------------------------------------------------------------------- /src/scss/_sugar.scss: -------------------------------------------------------------------------------- 1 | // borrowed from https://github.com/thoughtbot/neat 2 | @function is-not($value) { 3 | @return if($value, false, true); 4 | } 5 | 6 | @function is-even($value) { 7 | @return if(round($value / 2) * 2 == $value, true, false); 8 | } 9 | 10 | @function contains($list, $values...) { 11 | @each $value in $values { 12 | @if type-of(index($list, $value)) != 'number' { 13 | @return false; 14 | } 15 | } 16 | @return true; 17 | } 18 | 19 | @function fix-line-height($line-height) { 20 | //@return if(is-even($line-height), $line-height, $line-height - 1px); 21 | @return $line-height; 22 | } 23 | 24 | @mixin height-with-line($height) { 25 | height: $height; 26 | line-height: fix-line-height($height); 27 | } 28 | 29 | @mixin presence-props($props) { 30 | @each $prop, $value in $props { 31 | @if $value { 32 | #{$prop}: #{$value}; 33 | } 34 | } 35 | } 36 | 37 | @mixin size($width, $height: $width) { 38 | @include presence-props((width: $width, height: $height)); 39 | } 40 | 41 | @mixin position($position, $top: false, $right: false, $bottom: false, $left: false) { 42 | @include presence-props((position: $position, top: $top, right: $right, bottom: $bottom, left: $left)); 43 | } 44 | 45 | @mixin absolute($top: false, $right: false, $bottom: false, $left: false) { 46 | @include position(absolute, $top, $right, $bottom, $left); 47 | } 48 | 49 | @mixin fixed($top: false, $right: false, $bottom: false, $left: false) { 50 | @include position(fixed, $top, $right, $bottom, $left); 51 | } 52 | 53 | @mixin before($display: block, $content: '') { 54 | @include pseudo('&::before', $display, $content) { @content; } 55 | } 56 | 57 | @mixin after($display: block, $content: '') { 58 | @include pseudo('&::after', $display, $content) { @content; } 59 | } 60 | 61 | @mixin both($display: block, $content: '') { 62 | @include pseudo('&::before, &::after', $display, $content) { @content; } 63 | } 64 | 65 | @mixin pseudo($selector, $display: block, $content: '') { 66 | #{$selector} { 67 | @if $display { 68 | display: $display; 69 | } 70 | content: $content; 71 | @content; 72 | } 73 | } 74 | 75 | @mixin disabled { 76 | cursor: default; 77 | pointer-events: none; 78 | } 79 | 80 | @mixin cursor-pointer($simple: false) { 81 | user-select: none; 82 | @if $simple { 83 | cursor: pointer; 84 | } @else { 85 | &, 86 | & span { 87 | cursor: pointer; 88 | } 89 | } 90 | } 91 | 92 | @mixin cursor-default($simple: false) { 93 | @if $simple { 94 | cursor: default; 95 | } @else { 96 | &, 97 | & span { 98 | cursor: default; 99 | } 100 | } 101 | } 102 | 103 | @mixin gpu { 104 | transform: translateZ(0); 105 | } 106 | 107 | @mixin ios-smooth-scroll { 108 | -webkit-overflow-scrolling: touch; 109 | } 110 | 111 | $common-ui-transition-duration: 300ms !default; 112 | 113 | @mixin transition-props($props, $duration: $common-ui-transition-duration, $transition-function: ease, $will-change: false) { 114 | $transitions: (); 115 | $will-change-props: (); 116 | @each $prop, $value in $props { 117 | #{$prop}: $value; 118 | $tf: if($prop == visibility, if($value == hidden, step-end, step-start), $transition-function); 119 | $transitions: append($transitions, $prop $duration $tf, comma); 120 | @if $will-change { 121 | $will-change-props: append($will-change-props, $prop, comma); 122 | } 123 | } 124 | transition: $transitions; 125 | @if $will-change { 126 | will-change: $will-change-props; 127 | } 128 | } 129 | 130 | @function vertical-paddings($outer-height, $inner-height) { 131 | $space: $outer-height - $inner-height; 132 | $space-bottom: round($space / 2); 133 | $space-top: $space - $space-bottom; 134 | @return $space-top $space-bottom $space; 135 | } 136 | 137 | @function list-to-string($list, $separator: '') { 138 | $result: ''; 139 | $first: true; 140 | @each $cut in $list { 141 | @if $first { 142 | $first: false; 143 | $result: $cut; 144 | } @else { 145 | $result: "#{$result}#{$separator}#{$cut}"; 146 | } 147 | } 148 | @return $result; 149 | } 150 | -------------------------------------------------------------------------------- /examples/normas_on_rails/client/app/global/popover.js: -------------------------------------------------------------------------------- 1 | import 'css/s-popover'; 2 | import 'css/b-morpheus'; 3 | 4 | import normas from 'lib/normas'; 5 | 6 | const popoverTriggerSelector = '.js-popover-trigger'; 7 | const popoverSelector = '.js-popover'; 8 | let $activePopovers = $([]); 9 | 10 | normas.listenEvents(popoverTriggerSelector, { 11 | click: clickOnTrigger, 12 | 'ajax:before': ajaxBeforeOnTrigger, 13 | 'ajax:success': ajaxSuccessOnTrigger, 14 | }); 15 | 16 | normas.listenEvents(popoverSelector, { 17 | click: clickOnPopover, 18 | 'click > .option': clickOnPopoverOption, 19 | }); 20 | 21 | normas.listenEvents('click', () => { closeActivePopovers(normas.$el) }); 22 | 23 | function clickOnTrigger($trigger, event) { 24 | const $popover = getPopoverForTrigger($trigger); 25 | const isActive = $popover.is($activePopovers); 26 | closeActivePopovers($trigger.closest($activePopovers)); 27 | if (!isActive) { 28 | $trigger.addClass('active'); 29 | $popover.addClass('show'); 30 | $popover.find(':input:not(:disabled, [readonly]):first').focus(); 31 | $activePopovers = $activePopovers.add($popover); 32 | } 33 | return false; 34 | } 35 | 36 | function ajaxBeforeOnTrigger($trigger, event) { 37 | const $popover = getPopoverForTrigger($trigger); 38 | return !$popover.is($activePopovers); 39 | // normas.visit($trigger.attr('href')); 40 | // return false; 41 | } 42 | 43 | function ajaxSuccessOnTrigger($trigger, event, content) { 44 | const $popover = getPopoverForTrigger($trigger); 45 | let $content = $(content); 46 | let $innerContent = $content.find('.js-popover-content'); 47 | $popover.html($innerContent.length > 0 ? $innerContent : $content); 48 | // normas.sayAboutContentLeave($popover).html($innerContent.length > 0 ? $innerContent : $content); 49 | // normas.sayAboutContentEnter($popover); 50 | $popover.data('ujsLoaded', true); 51 | return false; 52 | } 53 | 54 | function clickOnPopover($popover, event) { 55 | let $link = $(event.target).closest('a'); 56 | if ($link.length > 0) { 57 | let href = $link.attr('href'); 58 | if (href) { 59 | normas.visit(href); 60 | } 61 | closeActivePopovers($popover.parent()); 62 | } else { 63 | closeActivePopovers($popover); 64 | } 65 | event.stopPropagation(); 66 | return false; 67 | } 68 | 69 | function clickOnPopoverOption($option, event) { 70 | let $popover = $option.parent(popoverSelector); 71 | let $selected = $popover.prev('.selected'); 72 | if ($selected.length) { 73 | $option.addClass('selected').siblings('.selected').removeClass('selected'); 74 | $selected.html($option.html()); 75 | let $field = $selected.prev('input'); 76 | if ($field.length) { 77 | $field.val($option.data('value')); 78 | } 79 | closeActivePopovers(getTriggerForPopover($popover)); 80 | } else { 81 | console.warn('No .selected element in clickOnPopoverOption()'); // eslint-disable-line 82 | } 83 | } 84 | 85 | function closeActivePopovers($root) { 86 | if ($root === undefined || $root.length === 0) { 87 | $root = normas.$el; 88 | } 89 | let $localActivePopovers = $root.find($activePopovers); 90 | if ($localActivePopovers.length === 0) { 91 | return false; 92 | } 93 | $localActivePopovers.removeClass('show'); 94 | getTriggerForPopover($localActivePopovers).removeClass('active'); 95 | $activePopovers = $activePopovers.not($localActivePopovers); 96 | return true; 97 | } 98 | 99 | function getPopoverForTrigger($trigger) { 100 | let $popover = $trigger.data('$popover'); 101 | if (!$popover) { 102 | let customPopoverSelector = ($trigger.data('popoverSelector') || popoverSelector) + ':first'; // eslint-disable-line 103 | let popoverSelectorScope = $trigger.data('popoverSelectorScope') || 'find'; 104 | $popover = popoverSelectorScope === '$' ? 105 | $(customPopoverSelector) 106 | : 107 | $trigger[popoverSelectorScope](customPopoverSelector); 108 | $trigger.data('$popover', $popover); 109 | $popover.data('$trigger', $trigger); 110 | } 111 | return $popover; 112 | } 113 | 114 | function getTriggerForPopover($popover) { 115 | return $popover.data('$trigger'); 116 | } 117 | -------------------------------------------------------------------------------- /src/js/mixins/navigation.js: -------------------------------------------------------------------------------- 1 | // require content mixin 2 | export default Base => (class extends Base { 3 | static pageEnterEventName = 'page:enter'; 4 | static pageLeaveEventName = 'page:leave'; 5 | static navigationStartedEventName = 'navigation:started'; 6 | static pageSelector = 'body'; 7 | navigationStarted = false; 8 | 9 | constructor(options) { 10 | super(options); 11 | if (NORMAS_DEBUG) { 12 | this.constructor.readOptions(this.logging, options.logging, { 13 | navigation: true, 14 | navigationGrouping: true, 15 | }); 16 | } 17 | this.bindPageEvents(options); 18 | if (NORMAS_DEBUG) { 19 | this.log('info', 'construct', 20 | `🗺 "${this.instanceName}" navigation mixin activated. logging.navigation =`, this.logging.navigation); 21 | } 22 | } 23 | 24 | onStart(callback) { 25 | this.$el.one(this.constructor.navigationStartedEventName, (event, $page) => callback($page)); 26 | } 27 | 28 | bindPageEvents(options) { 29 | if (NORMAS_DEBUG && (options.Turbolinks || global.Turbolinks)) { 30 | this.log('warn', 31 | '🗺 You have Turbolinks, but not use integration.', 32 | this.constructor.readmeLink('turbolinks-integration')); 33 | } 34 | $(this.pageEnter.bind(this)); 35 | } 36 | 37 | listenToPage(enter, leave = null) { 38 | if (enter) { 39 | this.$el.on(this.constructor.pageEnterEventName, (event, $page) => enter($page)); 40 | } 41 | if (leave) { 42 | this.$el.on(this.constructor.pageLeaveEventName, (event, $page) => leave($page)); 43 | } 44 | } 45 | 46 | visit(location) { 47 | window.location = location; 48 | } 49 | 50 | refreshPage() { 51 | this.visit(window.location); 52 | } 53 | 54 | setHash(hash) { 55 | location.hash = hash; 56 | } 57 | 58 | back() { 59 | global.history.back(); 60 | } 61 | 62 | replaceLocation(url) { 63 | if (NORMAS_DEBUG) { 64 | this.log('warn', '🗺 `replaceLocation` works only with Turbolinks.'); 65 | } 66 | } 67 | 68 | pushLocation(url, title = null, state = null) { 69 | if (global.history) global.history.pushState(state, title, url); 70 | } 71 | 72 | sayAboutPageLoading(state) { 73 | if (NORMAS_DEBUG) { 74 | this.log('warn', '🗺 `sayAboutPageLoading` works only with Turbolinks.'); 75 | } 76 | } 77 | 78 | pageEnter() { 79 | if (!this.navigationStarted) { 80 | this.navigationStarted = true; 81 | this.trigger(this.constructor.navigationStartedEventName, $page); 82 | if (NORMAS_DEBUG && this.logging.constructGrouping) { 83 | this.log('groupEnd', 'construct'); 84 | } 85 | } 86 | const $page = this.$page(); 87 | if (NORMAS_DEBUG) { 88 | this.logPage('enter', $page); 89 | } 90 | this.trigger(this.constructor.pageEnterEventName, $page); 91 | this.sayAboutContentEnter($page); 92 | } 93 | 94 | pageLeave() { 95 | const $page = this.$page(); 96 | if (NORMAS_DEBUG) { 97 | this.logPage('leave', $page); 98 | } 99 | this.sayAboutContentLeave($page); 100 | this.trigger(this.constructor.pageLeaveEventName, $page); 101 | } 102 | 103 | $page() { 104 | return this.$(this.constructor.pageSelector); 105 | } 106 | 107 | // private 108 | 109 | logPage(logEvent, $page) { 110 | if (!NORMAS_DEBUG || !this.debugMode || !this.logging.navigation) { 111 | return; 112 | } 113 | const enter = logEvent === 'enter'; 114 | const [eventName, ...eventStyles] = this.constructor.logCycle(logEvent, enter, 10); 115 | if (this.logging.navigationGrouping) { 116 | this.logPageGroupEnd(); 117 | } 118 | this.log(this.constructor.groupingMethod(this.logging.navigationGrouping), 'navigation', 119 | `🗺 page ${eventName}`, 120 | ...eventStyles, 121 | ...(enter ? [window.location.href] : []), 122 | $page); 123 | this.navigationGroup = true; 124 | if (enter && this.logging.navigationGrouping) { 125 | setTimeout(() => this.logPageGroupEnd(), 25); 126 | } 127 | } 128 | 129 | logPageGroupEnd() { 130 | if (NORMAS_DEBUG && this.navigationGroup) { 131 | this.log('groupEnd', 'navigation'); 132 | this.navigationGroup = false; 133 | } 134 | } 135 | }); 136 | -------------------------------------------------------------------------------- /dist/js/extensions/views.production.js: -------------------------------------------------------------------------------- 1 | "use strict";var e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},n=function(){function e(e,t){for(var n=0;n1?n-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{};this.viewClasses[e.selector]||(this.viewClasses[e.selector]=e,this.listenToElement(e.selector,function(i){return t.bindView(i,e,n)},function(n){return t.unbindView(n,e)},{delay:e.delay,silent:!0}))}},{key:"bindView",value:function(e,t,n){if(!this.canBind(e,t))return null;t.instanceIndex?t.instanceIndex+=1:t.instanceIndex=1;var r=new t(i({},this.helpers.deepMerge(this.viewOptions,n),{instanceName:t.selector+"_"+t.instanceIndex,el:e[0]}));return this.viewInstances.push(r),r}},{key:"canBind",value:function(e,t){var n=this.getViewsOnElement(e,t)[0];return!n}},{key:"unbindView",value:function(e,t){var n=this.getViewsOnElement(e,t)[0];n&&(n.destructor(),this.viewInstances=this.helpers.without(this.viewInstances,n))}},{key:"getViewsOnElement",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n={el:e instanceof $?e[0]:e};return t&&(n.constructor=t),this.helpers.filter(this.viewInstances,n)}},{key:"getViewsInContainer",value:function(e){var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];return this.helpers.filter(this.viewInstances,function(n){return n.$el.closest(e).length>0&&(t||n.el!==e[0])})}},{key:"getAllViews",value:function(e){return this.helpers.filter(this.viewInstances,{constructor:e})}},{key:"getFirstView",value:function(e){return this.helpers.find(this.viewInstances,{constructor:e})}},{key:"getFirstChildView",value:function(e){return this.helpers.find(this.viewInstances,function(t){return t instanceof e})}}]),u}(),Object.defineProperty(l,"View",{enumerable:!0,writable:!0,value:o}),u};module.exports=function(e){return l(e,o(e.NormasCore))}; 2 | //# sourceMappingURL=views.production.js.map 3 | -------------------------------------------------------------------------------- /dist/js/integrations/turbolinks.js: -------------------------------------------------------------------------------- 1 | "use strict";var t=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},e=function(){function t(t,e){for(var n=0;n 2.0) 7 | websocket-driver (~> 0.6.1) 8 | actionmailer (5.1.4) 9 | actionpack (= 5.1.4) 10 | actionview (= 5.1.4) 11 | activejob (= 5.1.4) 12 | mail (~> 2.5, >= 2.5.4) 13 | rails-dom-testing (~> 2.0) 14 | actionpack (5.1.4) 15 | actionview (= 5.1.4) 16 | activesupport (= 5.1.4) 17 | rack (~> 2.0) 18 | rack-test (>= 0.6.3) 19 | rails-dom-testing (~> 2.0) 20 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 21 | actionview (5.1.4) 22 | activesupport (= 5.1.4) 23 | builder (~> 3.1) 24 | erubi (~> 1.4) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 27 | activejob (5.1.4) 28 | activesupport (= 5.1.4) 29 | globalid (>= 0.3.6) 30 | activemodel (5.1.4) 31 | activesupport (= 5.1.4) 32 | activerecord (5.1.4) 33 | activemodel (= 5.1.4) 34 | activesupport (= 5.1.4) 35 | arel (~> 8.0) 36 | activesupport (5.1.4) 37 | concurrent-ruby (~> 1.0, >= 1.0.2) 38 | i18n (~> 0.7) 39 | minitest (~> 5.1) 40 | tzinfo (~> 1.1) 41 | arel (8.0.0) 42 | bindex (0.5.0) 43 | builder (3.2.3) 44 | byebug (9.1.0) 45 | concurrent-ruby (1.0.5) 46 | crass (1.0.3) 47 | erubi (1.7.0) 48 | erubis (2.7.0) 49 | ffi (1.9.18) 50 | globalid (0.4.1) 51 | activesupport (>= 4.2.0) 52 | haml (4.0.7) 53 | tilt 54 | haml-rails (0.9.0) 55 | actionpack (>= 4.0.1) 56 | activesupport (>= 4.0.1) 57 | haml (>= 4.0.6, < 5.0) 58 | html2haml (>= 1.0.1) 59 | railties (>= 4.0.1) 60 | html2haml (2.1.0) 61 | erubis (~> 2.7.0) 62 | haml (~> 4.0) 63 | nokogiri (>= 1.6.0) 64 | ruby_parser (~> 3.5) 65 | i18n (0.9.1) 66 | concurrent-ruby (~> 1.0) 67 | jbuilder (2.7.0) 68 | activesupport (>= 4.2.0) 69 | multi_json (>= 1.2) 70 | listen (3.1.5) 71 | rb-fsevent (~> 0.9, >= 0.9.4) 72 | rb-inotify (~> 0.9, >= 0.9.7) 73 | ruby_dep (~> 1.2) 74 | loofah (2.1.1) 75 | crass (~> 1.0.2) 76 | nokogiri (>= 1.5.9) 77 | mail (2.7.0) 78 | mini_mime (>= 0.1.1) 79 | method_source (0.9.0) 80 | mini_mime (1.0.0) 81 | mini_portile2 (2.3.0) 82 | minitest (5.10.3) 83 | multi_json (1.12.2) 84 | nio4r (2.1.0) 85 | nokogiri (1.8.1) 86 | mini_portile2 (~> 2.3.0) 87 | puma (3.11.0) 88 | rack (2.0.3) 89 | rack-proxy (0.6.3) 90 | rack 91 | rack-test (0.8.2) 92 | rack (>= 1.0, < 3) 93 | rails (5.1.4) 94 | actioncable (= 5.1.4) 95 | actionmailer (= 5.1.4) 96 | actionpack (= 5.1.4) 97 | actionview (= 5.1.4) 98 | activejob (= 5.1.4) 99 | activemodel (= 5.1.4) 100 | activerecord (= 5.1.4) 101 | activesupport (= 5.1.4) 102 | bundler (>= 1.3.0) 103 | railties (= 5.1.4) 104 | sprockets-rails (>= 2.0.0) 105 | rails-dom-testing (2.0.3) 106 | activesupport (>= 4.2.0) 107 | nokogiri (>= 1.6) 108 | rails-html-sanitizer (1.0.3) 109 | loofah (~> 2.0) 110 | railties (5.1.4) 111 | actionpack (= 5.1.4) 112 | activesupport (= 5.1.4) 113 | method_source 114 | rake (>= 0.8.7) 115 | thor (>= 0.18.1, < 2.0) 116 | rake (12.3.0) 117 | rb-fsevent (0.10.2) 118 | rb-inotify (0.9.10) 119 | ffi (>= 0.5.0, < 2) 120 | ruby_dep (1.5.0) 121 | ruby_parser (3.8.4) 122 | sexp_processor (~> 4.1) 123 | sexp_processor (4.8.0) 124 | spring (2.0.2) 125 | activesupport (>= 4.2) 126 | spring-watcher-listen (2.0.1) 127 | listen (>= 2.7, < 4.0) 128 | spring (>= 1.2, < 3.0) 129 | sprockets (3.7.1) 130 | concurrent-ruby (~> 1.0) 131 | rack (> 1, < 3) 132 | sprockets-rails (3.2.1) 133 | actionpack (>= 4.0) 134 | activesupport (>= 4.0) 135 | sprockets (>= 3.0.0) 136 | thor (0.20.0) 137 | thread_safe (0.3.6) 138 | tilt (2.0.8) 139 | tzinfo (1.2.4) 140 | thread_safe (~> 0.1) 141 | web-console (3.5.1) 142 | actionview (>= 5.0) 143 | activemodel (>= 5.0) 144 | bindex (>= 0.4.0) 145 | railties (>= 5.0) 146 | webpacker (3.2.0) 147 | activesupport (>= 4.2) 148 | rack-proxy (>= 0.6.1) 149 | railties (>= 4.2) 150 | websocket-driver (0.6.5) 151 | websocket-extensions (>= 0.1.0) 152 | websocket-extensions (0.1.3) 153 | 154 | PLATFORMS 155 | ruby 156 | 157 | DEPENDENCIES 158 | byebug 159 | haml-rails 160 | jbuilder (~> 2.5) 161 | listen (>= 3.0.5, < 3.2) 162 | puma (~> 3.7) 163 | rails (~> 5.1.4) 164 | spring 165 | spring-watcher-listen (~> 2.0.0) 166 | web-console (>= 3.3.0) 167 | webpacker 168 | 169 | BUNDLED WITH 170 | 1.14.5 171 | -------------------------------------------------------------------------------- /src/js/lib/helpers.js: -------------------------------------------------------------------------------- 1 | // Sufficient for Normas implementation of functions like from lodash 2 | 3 | export function isPlainObject(value) { 4 | if (value == null || typeof value !== 'object') { 5 | return false; 6 | } 7 | const proto = Object.getPrototypeOf(value); 8 | if (proto === null) { 9 | return true; 10 | } 11 | const constructor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor; 12 | return typeof constructor === 'function' && constructor instanceof constructor; 13 | } 14 | 15 | export const isArray = Array.isArray; 16 | 17 | export function isFunction(v) { 18 | return typeof v === 'function'; 19 | } 20 | 21 | export function isString(v) { 22 | return typeof v === 'string'; 23 | } 24 | 25 | export function compact(array) { 26 | return filter(array, v => v); 27 | } 28 | 29 | export function debounce(func, wait) { 30 | let timeoutId; 31 | return (...args) => { 32 | if (timeoutId) { 33 | clearTimeout(timeoutId); 34 | } 35 | timeoutId = setTimeout(() => { 36 | func(...args); 37 | }, wait); 38 | } 39 | } 40 | 41 | export function groupBy(array, key) { 42 | return reduceBy(array, key, (grouped, groupKey, item) => { 43 | if (grouped[groupKey]) { 44 | grouped[groupKey].push(item); 45 | } else { 46 | grouped[groupKey] = [item]; 47 | } 48 | }); 49 | } 50 | 51 | export function countBy(array, key) { 52 | return reduceBy(array, key, (grouped, groupKey) => { 53 | if (grouped[groupKey]) { 54 | grouped[groupKey]++; 55 | } else { 56 | grouped[groupKey] = 1; 57 | } 58 | }); 59 | } 60 | 61 | export function groupByInArray(array, key) { 62 | return reduceBy(array, key, (grouped, groupKey, item) => { 63 | const group = find(grouped, ([k]) => k === groupKey); 64 | if (group) { 65 | group[1].push(item); 66 | } else { 67 | grouped.push([groupKey, [item]]); 68 | } 69 | }, []); 70 | } 71 | 72 | export function flatten(array, deep = false) { 73 | return array.reduce((flat, value) => { 74 | if (isArray(value)) { 75 | flat.push(...(deep ? flatten(value, true) : value)); 76 | } else { 77 | flat.push(value); 78 | } 79 | return flat; 80 | }, []); 81 | } 82 | 83 | export function intersection(a, b) { 84 | if (!isArray(b)) { 85 | b = [b]; 86 | } 87 | return (isArray(a) ? a : [a]).reduce((result, value) => { 88 | if (includes(b, value)) { 89 | result.push(value); 90 | } 91 | return result; 92 | }, []); 93 | } 94 | 95 | export function deepMerge(destination, source) { 96 | return Object.keys(source).reduce((result, key) => { 97 | if (source[key]) { 98 | if (isPlainObject(destination[key]) && isPlainObject(source[key])) { 99 | result[key] = deepMerge(destination[key], source[key]); 100 | } else { 101 | result[key] = source[key]; 102 | } 103 | return result; 104 | } 105 | }, Object.assign({}, destination)); 106 | } 107 | 108 | export function filter(collection, conditions) { 109 | return filterBase('filter', collection, conditions); 110 | } 111 | 112 | export function find(collection, conditions) { 113 | return filterBase('find', collection, conditions); 114 | } 115 | 116 | export function findIndex(collection, conditions) { 117 | return filterBase('find', collection, conditions); 118 | } 119 | 120 | export function map(collection, iteratee) { 121 | return Array.prototype.map.call(collection, iteratee); 122 | } 123 | 124 | export function each(collection, iteratee) { 125 | return Array.prototype.forEach.call(collection, iteratee); 126 | } 127 | 128 | export function mapValues(object, iteratee) { 129 | return Object.keys(object).reduce((result, key) => { 130 | result[key] = iteratee(object[key], key); 131 | return result; 132 | }, {}); 133 | } 134 | 135 | export function without(collection, ...values) { 136 | return difference(collection, values); 137 | } 138 | 139 | export function difference(arrayA, arrayB) { 140 | return filter(arrayA, a => !includes(arrayB, a)); 141 | } 142 | 143 | export function includes(collection, searchElement) { 144 | return Array.prototype.indexOf.call(collection, searchElement) !== -1; 145 | } 146 | 147 | // private 148 | 149 | function reduceBy(array, key, reducer, initial = {}) { 150 | return Array.prototype.reduce.call(array, (grouped, item) => { 151 | const groupKey = isFunction(key) ? key(item) : item[key]; 152 | reducer(grouped, groupKey, item); 153 | return grouped; 154 | }, initial); 155 | } 156 | 157 | function filterBase(baseName, collection, conditions) { 158 | return Array.prototype[baseName].call(collection, makeConditionsMatch(conditions)); 159 | } 160 | 161 | function makeConditionsMatch(conditions) { 162 | if (isFunction(conditions)) { 163 | return conditions; 164 | } else { 165 | const conditionsKeys = Object.keys(conditions); 166 | return item => filterMatch(item, conditions, conditionsKeys); 167 | } 168 | } 169 | 170 | function filterMatch(item, conditions, conditionsKeys) { 171 | return conditionsKeys.find(key => conditions[key] !== item[key]) === undefined; 172 | } 173 | -------------------------------------------------------------------------------- /dist/js/extensions/views.js: -------------------------------------------------------------------------------- 1 | "use strict";var e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},n=function(){function e(e,t){for(var n=0;n1?n-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{};this.viewClasses[e.selector]?this.error("🏭 View class for selector `"+e.selector+"` already registered",this.viewClasses[e.selector]):(this.viewClasses[e.selector]=e,this.listenToElement(e.selector,function(i){return t.bindView(i,e,n)},function(n){return t.unbindView(n,e)},{delay:e.delay,silent:!0}))}},{key:"bindView",value:function(e,t,n){if(!this.canBind(e,t))return null;t.instanceIndex?t.instanceIndex+=1:t.instanceIndex=1;var r=new t(i({},this.helpers.deepMerge(this.viewOptions,n),{instanceName:t.selector+"_"+t.instanceIndex,el:e[0]}));return this.viewInstances.push(r),r}},{key:"canBind",value:function(e,t){var n=this.getViewsOnElement(e,t)[0];return!n||(this.log("warn","🏭 Element already has bound view",e,t,n),!1)}},{key:"unbindView",value:function(e,t){var n=this.getViewsOnElement(e,t)[0];n&&(n.destructor(),this.viewInstances=this.helpers.without(this.viewInstances,n))}},{key:"getViewsOnElement",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n={el:e instanceof $?e[0]:e};return t&&(n.constructor=t),this.helpers.filter(this.viewInstances,n)}},{key:"getViewsInContainer",value:function(e){var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];return this.helpers.filter(this.viewInstances,function(n){return n.$el.closest(e).length>0&&(t||n.el!==e[0])})}},{key:"getAllViews",value:function(e){return this.helpers.filter(this.viewInstances,{constructor:e})}},{key:"getFirstView",value:function(e){return this.helpers.find(this.viewInstances,{constructor:e})}},{key:"getFirstChildView",value:function(e){return this.helpers.find(this.viewInstances,function(t){return t instanceof e})}}]),c}(),Object.defineProperty(l,"View",{enumerable:!0,writable:!0,value:o}),c};module.exports=function(e){return c(e,l(e.NormasCore))}; 2 | //# sourceMappingURL=views.js.map 3 | -------------------------------------------------------------------------------- /src/js/mixins/events.js: -------------------------------------------------------------------------------- 1 | export default Base => (class extends Base { 2 | constructor(options) { 3 | super(options); 4 | if (NORMAS_DEBUG) { 5 | this.constructor.readOptions(this.logging, options.logging, { 6 | events: true, 7 | eventsDebounced: true, 8 | eventsTable: false, 9 | }); 10 | if (this.debugMode && this.logging.events && this.logging.eventsDebounced) { 11 | this.eventsLogBuffer = []; 12 | this.logEventsDebounced = this.helpers.debounce(this.logEventsDebounced.bind(this), 20); 13 | } 14 | this.log('info', ['construct', 'events'], 15 | `🚦 "${this.instanceName}" events mixin activated.`, 16 | 'logging.events =', this.logging.events, 17 | 'logging.eventsDebounced =', this.logging.eventsDebounced, 18 | 'logging.eventsTable =', this.logging.eventsTable); 19 | } 20 | } 21 | 22 | trigger(eventName, ...args) { 23 | this.$el.trigger(eventName, args); 24 | } 25 | 26 | listenEvents(...args) { 27 | return this.listenEventsOnElement(this.$el, ...args); 28 | } 29 | 30 | listenEventsOnElement($element, ...args) { 31 | const listeningArgs = this.constructor.listeningArguments(...args); 32 | if (NORMAS_DEBUG && this.debugMode && this.logging.events) { 33 | this.logEvents($element, listeningArgs); 34 | } 35 | listeningArgs.forEach(({ events, selector, handle }) => { 36 | $element.on(events, selector, handle); 37 | }); 38 | return listeningArgs; 39 | } 40 | 41 | forgetEvents(listeningArgs) { 42 | this.forgetEventsOnElement(this.$el, listeningArgs); 43 | } 44 | 45 | forgetEventsOnElement($element, listeningArgs) { 46 | if (NORMAS_DEBUG) { 47 | this.logEventsOutput($element, listeningArgs, false); 48 | } 49 | listeningArgs.forEach(({ events, selector, handle }) => { 50 | $element.off(events, selector, handle); 51 | }); 52 | } 53 | 54 | // private 55 | 56 | logEvents($element, listeningArgs) { 57 | if (!NORMAS_DEBUG) { 58 | return; 59 | } 60 | if (this.logging.eventsDebounced) { 61 | const element = $element[0]; 62 | listeningArgs.forEach(args => { args.element = element; }); 63 | this.eventsLogBuffer = this.eventsLogBuffer.concat(listeningArgs); 64 | this.logEventsDebounced(); 65 | } else { 66 | this.logEventsOutput($element, listeningArgs, true); 67 | } 68 | } 69 | 70 | logEventsDebounced() { 71 | if (!NORMAS_DEBUG) { 72 | return; 73 | } 74 | const grouped = this.helpers.groupByInArray(this.eventsLogBuffer, 'element'); 75 | grouped.forEach(([element, listeningArgs]) => { 76 | this.logEventsOutput($(element), listeningArgs, true); 77 | }); 78 | this.eventsLogBuffer = []; 79 | } 80 | 81 | logEventsOutput($element, listeningArgs, enter) { 82 | if (!NORMAS_DEBUG || !this.logging.events) { 83 | return; 84 | }; 85 | const elementName = $element[0] === this.el ? this.instanceName : this.constructor.contentName($element); 86 | const count = listeningArgs.length; 87 | const plurEvents = this.constructor.logPlur('event%S%', count); 88 | const [styledCount, ...countStyles] = count > 1 ? this.constructor.logBold(count) : []; 89 | const [cycleName, ...cycleStyles] = this.constructor.logCycle(enter ? 'listen on' : 'forget from', enter); 90 | const [styledElementName, ...elementStyles] = this.constructor.logBold(elementName); 91 | this.log('events', 92 | `🚦 ${styledCount ? styledCount + ' ' : ''}${plurEvents} ${cycleName} "${styledElementName}"`, 93 | ...countStyles, ...cycleStyles, ...elementStyles, 94 | $element, ...(this.logging.eventsTable ? [] : [listeningArgs])); 95 | if (!this.logging.eventsTable) return; 96 | this.log('table', listeningArgs.map(({ selector, events }) => ({ selector, events }))); 97 | } 98 | 99 | static listeningArguments(selector, eventRule, handle) { 100 | if (this.helpers.isPlainObject(selector)) { 101 | eventRule = selector; 102 | selector = ''; 103 | } 104 | 105 | if (this.helpers.isFunction(eventRule)) { 106 | handle = eventRule; 107 | eventRule = selector; 108 | selector = ''; 109 | } 110 | 111 | if (this.helpers.isPlainObject(eventRule)) { 112 | return this.helpers.flatten(Object.keys(eventRule).map((key) => { 113 | let value = eventRule[key]; 114 | return this.helpers.isPlainObject(value) ? 115 | this.listeningArguments(selector ? `${selector} ${key}` : key, value) 116 | : 117 | this.listeningArguments(selector, key, value); 118 | })); 119 | } 120 | 121 | if (!this.helpers.isFunction(handle)) { 122 | if (NORMAS_DEBUG) { 123 | console.error(`handle isn't function in listening declaration! (selector: '${selector}')`); // eslint-disable-line no-console 124 | } 125 | return []; 126 | } 127 | if (!eventRule) { 128 | if (NORMAS_DEBUG) { 129 | console.error(`eventRule not defined! (selector: '${selector}')`); // eslint-disable-line no-console 130 | } 131 | return []; 132 | } 133 | 134 | const selectors = eventRule.split(/\s+/); 135 | const eventName = selectors[0]; 136 | selectors[0] = selector; 137 | 138 | if (!eventName) { 139 | if (NORMAS_DEBUG) { 140 | console.error(`bad eventName in listening declaration! (selector: '${selector}')`); // eslint-disable-line no-console 141 | } 142 | return []; 143 | } 144 | 145 | return [{ 146 | events: eventName.replace(/\//g, ' '), 147 | selector: selectors.join(' ').trim(), 148 | handle: (event, ...args) => handle($(event.currentTarget), event, ...args), 149 | }]; 150 | } 151 | }); 152 | -------------------------------------------------------------------------------- /src/js/mixins/elements.js: -------------------------------------------------------------------------------- 1 | // require events mixin 2 | // require content mixin 3 | export default Base => (class extends Base { 4 | static preventContentEventsClassName = 'js-prevent-normas'; 5 | static elementEnterTimeoutIdDataName = 'elementEnterTimeoutId'; 6 | 7 | constructor(options) { 8 | super(options); 9 | if (NORMAS_DEBUG) { 10 | this.constructor.readOptions(this.logging, options.logging, { elements: true }); 11 | this.log('info', 'construct', 12 | `💎 "${this.instanceName}" elements mixin activated.`, 13 | 'logging.elements =', this.logging.elements); 14 | } 15 | } 16 | 17 | listenToElement(selector, enter, leave = null, options = {}) { 18 | options = Object.assign({ delay: 0, silent: false }, options); 19 | this.listenToContent( 20 | this.makeElementContentEnter(selector, enter, options), 21 | this.makeElementContentLeave(selector, leave, options), 22 | ); 23 | } 24 | 25 | // private 26 | 27 | makeElementContentEnter(selector, enter, { delay, silent }) { 28 | return $content => { 29 | let $elements = this.constructor.contentElements($content, selector); 30 | if ($elements.length === 0) { 31 | return; 32 | } 33 | if (delay > 0) { 34 | this.dom.memoryData($elements, this.constructor.elementEnterTimeoutIdDataName, setTimeout(() => { 35 | $content = $content.filter((index, element) => this.dom.contains(this.el, element)); 36 | if ($content.length === 0) { 37 | return; 38 | } 39 | // check elements inclusion in $content after delay 40 | $elements = this.constructor.contentElements($content, $elements); 41 | if ($elements.length === 0) { 42 | return; 43 | } 44 | // check elements with delay data 45 | $elements = this.filterDelayedElements($elements, true); 46 | if ($elements.length === 0) { 47 | return; 48 | } 49 | this.handleElements($elements, selector, enter, 'enter', silent); 50 | }, delay)); 51 | } else { 52 | this.handleElements($elements, selector, enter, 'enter', silent); 53 | } 54 | }; 55 | } 56 | 57 | makeElementContentLeave(selector, leave, { delay, silent }) { 58 | if (!leave) { 59 | return null; 60 | } 61 | return $content => { 62 | let $elements = this.constructor.contentElements($content, selector); 63 | if ($elements.length === 0) { 64 | return; 65 | } 66 | if (delay > 0) { 67 | $elements = this.filterDelayedElements($elements, false); 68 | if ($elements.length === 0) { 69 | return; 70 | } 71 | } 72 | this.handleElements($elements, selector, leave, 'leave', silent); 73 | }; 74 | } 75 | 76 | filterDelayedElements($elements, delayState) { 77 | return $elements.filter((index, element) => { 78 | const delayed = this.dom.memoryData(element, this.constructor.elementEnterTimeoutIdDataName); 79 | if (delayed) { 80 | this.dom.removeMemoryData(element, this.constructor.elementEnterTimeoutIdDataName); 81 | } 82 | return !!delayed === delayState; 83 | }); 84 | } 85 | 86 | handleElements($elements, selector, handle, handleName, silent) { 87 | let preventedElements = []; 88 | const handledElements = this.helpers.filter($elements, element => { 89 | if (!this.canCycleElement(element, selector, handleName)) { 90 | return false; 91 | } 92 | const prevent = this.constructor.preventEventForElement(element); 93 | if (prevent) { 94 | preventedElements.push(element); 95 | return false; 96 | } 97 | handle($(element)); 98 | return true; 99 | }); 100 | if (NORMAS_DEBUG && !silent) { 101 | this.logElements(handledElements, preventedElements, selector, handleName); 102 | } 103 | } 104 | 105 | canCycleElement(element, selector, handleName) { 106 | const normasElements = this.dom.memoryData(element, '_normasElements'); 107 | const selectorIndex = normasElements ? normasElements.indexOf(selector) : -1; 108 | if (handleName === 'enter') { 109 | if (selectorIndex !== -1) { 110 | if (NORMAS_DEBUG) { 111 | this.log('warn', 'elements', 112 | ...this.constructor.logBold('💎 element "%REPLACE%" already entered.', selector), 113 | element); 114 | } 115 | return false; 116 | } 117 | if (normasElements) { 118 | normasElements.push(selector); 119 | } else { 120 | this.dom.memoryData(element, '_normasElements', [selector]); 121 | } 122 | return true; 123 | } 124 | // leave 125 | if (selectorIndex !== -1) { 126 | normasElements.splice(selectorIndex, 1); 127 | return true; 128 | } 129 | if (NORMAS_DEBUG) { 130 | this.log('warn', 'elements', 131 | ...this.constructor.logBold('💎 element "%REPLACE%" try leave, but did not enter.', selector), 132 | element); 133 | } 134 | return false; 135 | } 136 | 137 | logElements(handledElements, preventedElements, selector, handleName) { 138 | if (!NORMAS_DEBUG) { 139 | return; 140 | } 141 | const [elementName, ...elementStyles] = this.constructor.logBold(selector); 142 | if (handledElements.length > 0) { 143 | const [styledHandleName, ...handleStyles] = this.constructor.logCycle(handleName, handleName === 'enter', 3); 144 | this._logElements(handledElements, '', styledHandleName, elementName, handleStyles, elementStyles); 145 | } 146 | if (preventedElements.length > 0) { 147 | const [preventInfo, ...handleStyles] = this.constructor.logColor('prevent ', 'blue'); 148 | this._logElements(preventedElements, preventInfo, handleName, elementName, handleStyles, elementStyles); 149 | } 150 | } 151 | 152 | _logElements(elements, preventInfo, handleName, elementName, handleStyles, elementStyles) { 153 | if (!NORMAS_DEBUG) { 154 | return; 155 | } 156 | const count = elements.length; 157 | const plurElements = this.constructor.logPlur('element%S%', count); 158 | const [styledCount, ...countStyles] = count > 1 ? this.constructor.logBold(count) : []; 159 | const styles = [handleStyles]; 160 | styles[preventInfo ? 'push' : 'unshift'](countStyles); 161 | this.log('elements', 162 | `💎 ${preventInfo}${styledCount ? styledCount + ' ' : ''}${plurElements} ${handleName} "${elementName}"`, 163 | ...this.helpers.flatten(styles), ...elementStyles, 164 | elements); 165 | } 166 | 167 | static contentElements($content, selector) { 168 | return $content.filter(selector).add($content.find(selector)); 169 | } 170 | 171 | static preventEventForElement(element) { 172 | return $(element).closest(`.${this.preventContentEventsClassName}`).length > 0; 173 | } 174 | }); 175 | -------------------------------------------------------------------------------- /src/js/mixins/turbolinks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Turbolinks integration for Normas 3 | * 4 | * @see {@link https://github.com/evrone/normas#turbolinks-integration|Docs} 5 | * @see {@link https://github.com/evrone/normas/blob/master/src/js/mixins/turbolinks.js|Source} 6 | * @license MIT 7 | * @copyright Dmitry Karpunin , 2017-2018 8 | */ 9 | 10 | // require navigation mixin 11 | export default Base => (class extends Base { 12 | static turboPageEnterEventName = 'turbolinks:load'; 13 | static turboPageLeaveEventName = 'turbolinks:before-cache'; 14 | 15 | bindPageEvents(options) { 16 | this.Turbolinks = options.Turbolinks || global.Turbolinks; 17 | const turbolinksExists = !!this.Turbolinks; 18 | if (!this.enablings) this.enablings = {}; 19 | this.constructor.readOptions(this.enablings, options.enablings, { turbolinks: turbolinksExists }); 20 | if (NORMAS_DEBUG && options.enablings && options.enablings.turbolinks === true && !turbolinksExists) { 21 | this.error('🛤 Turbolinks: `options.enablings.turbolinks === true` but Turbolinks is not detected.', 22 | this.constructor.readmeLink('turbolinks-integration')); 23 | } 24 | if (NORMAS_DEBUG) { 25 | this.log('info', 'construct', 26 | ...this.constructor.logColor(`🛤 "${this.instanceName}" Turbolinks %REPLACE%.`, 27 | this.enablings.turbolinks ? 'enabled' : 'disabled', 28 | this.enablings.turbolinks ? 'green' : 'blue')); 29 | } 30 | if (this.enablings.turbolinks) { 31 | // Turbolinks connected :) 32 | // patchTurbolinks(this.Turbolinks); // TODO: check versions 33 | patchTurbolinksPreviewControl(this.Turbolinks); 34 | this.listenEvents(this.constructor.turboPageEnterEventName, this.pageEnter.bind(this)); 35 | this.listenEvents(this.constructor.turboPageLeaveEventName, this.pageLeave.bind(this)); 36 | if (options.Turbolinks) { 37 | options.Turbolinks.start(); 38 | } 39 | } else { 40 | // No Turbolinks ;( 41 | if (NORMAS_DEBUG && options.enablings && options.enablings.turbolinks === false) { 42 | this.log('warn', 'construct', 43 | `🛤 You ${this.Turbolinks ? '' : 'do not '}have a Turbolinks and use integration, but \`options.enablings.turbolinks === false\`.`); 44 | } 45 | $(this.pageEnter.bind(this)); 46 | } 47 | } 48 | 49 | visit(location) { 50 | if (this.enablings.turbolinks) { 51 | this.Turbolinks.visit(location); 52 | } else { 53 | super.visit(location); 54 | } 55 | } 56 | 57 | setHash(hash) { 58 | if (this.enablings.turbolinks) { 59 | let controller = this.Turbolinks.controller; 60 | controller.replaceHistoryWithLocationAndRestorationIdentifier(hash, controller.restorationIdentifier); 61 | } else { 62 | super.setHash(hash); 63 | } 64 | } 65 | 66 | replaceLocation(url) { 67 | if (this.enablings.turbolinks) { 68 | this.Turbolinks.controller.replaceHistoryWithLocationAndRestorationIdentifier(url); 69 | } else { 70 | super.replaceLocation(url); 71 | } 72 | } 73 | 74 | pushLocation(url) { 75 | if (this.enablings.turbolinks) { 76 | this.Turbolinks.controller.pushHistoryWithLocationAndRestorationIdentifier(url); 77 | } else { 78 | super.pushLocation(url); 79 | } 80 | } 81 | 82 | sayAboutPageLoading(state) { 83 | if (this.enablings.turbolinks) { 84 | const progressBar = this.Turbolinks.controller.adapter.progressBar; 85 | if (state) { 86 | progressBar.setValue(0); 87 | progressBar.show(); 88 | } else { 89 | progressBar.hide(); 90 | } 91 | } else { 92 | super.sayAboutPageLoading(state); 93 | } 94 | } 95 | }); 96 | 97 | function patchTurbolinksPreviewControl(Turbolinks) { 98 | class OurView extends Turbolinks.View { 99 | render({ snapshot, error, isPreview }, callback) { 100 | this.markAsPreview(isPreview); 101 | if (snapshot) { 102 | // added `isPreview` argument 103 | this.renderSnapshot(snapshot, isPreview, callback); 104 | } else { 105 | this.renderError(error, callback); 106 | } 107 | } 108 | 109 | // added `isPreview` argument 110 | renderSnapshot(snapshot, isPreview, callback) { 111 | const renderer = new OurSnapshotRenderer(this.getSnapshot(), Turbolinks.Snapshot.wrap(snapshot), isPreview); 112 | renderer.delegate = this.delegate; 113 | renderer.render(callback); 114 | } 115 | } 116 | Turbolinks.View = OurView; 117 | 118 | class OurSnapshotRenderer extends Turbolinks.SnapshotRenderer { 119 | // added `isPreview` argument 120 | constructor(currentSnapshot, newSnapshot, isPreview) { 121 | super(currentSnapshot, newSnapshot); 122 | if (isPreview) { 123 | this.newBody = this.newBody.cloneNode(true); 124 | this.newBody.isPreview = true; 125 | } 126 | } 127 | } 128 | } 129 | 130 | function patchTurbolinks(Turbolinks) { 131 | class OurHeadDetails extends Turbolinks.HeadDetails { 132 | constructor(...args) { 133 | super(...args); 134 | this.elements = {}; 135 | $(this.element).children().each((index, element) => { 136 | let key = turboElementToKey(element); 137 | if (!this.elements[key]) { 138 | this.elements[key] = { 139 | type: turboElementType(element), 140 | tracked: turboElementIsTracked(element), 141 | elements: [], 142 | }; 143 | } 144 | this.elements[key].elements.push(element); 145 | }); 146 | } 147 | // getTrackedElementSignature() { 148 | // let sign = super.getTrackedElementSignature(); 149 | // console.log('sign ', sign); 150 | // return sign; 151 | // } 152 | } 153 | Turbolinks.HeadDetails = OurHeadDetails; 154 | } 155 | 156 | // Injection in Turbolinks.HeadDetails for override this logic: 157 | function turboElementToKey(element) { 158 | let url = element.getAttribute('src') || element.getAttribute('href'); 159 | if (url) { 160 | let cuts = url.split('/'); 161 | cuts = cuts[cuts.length - 1]; 162 | if (cuts) { url = cuts } 163 | } 164 | return url || element.outerHTML; 165 | } 166 | 167 | function turboElementType(element) { 168 | if (turboElementIsScript(element)) { 169 | return 'script'; 170 | } else if (turboElementIsStylesheet(element)) { 171 | return 'stylesheet'; 172 | } 173 | return null; 174 | } 175 | 176 | function turboElementIsTracked(element) { 177 | return element.getAttribute('data-turbolinks-track') === 'reload'; 178 | } 179 | 180 | function turboElementIsScript(element) { 181 | let tagName = element.tagName.toLowerCase(); 182 | return tagName === 'script'; 183 | } 184 | 185 | function turboElementIsStylesheet(element) { 186 | let tagName = element.tagName.toLowerCase(); 187 | return tagName === 'style' || (tagName === 'link' && element.getAttribute('rel') === 'stylesheet'); 188 | } 189 | -------------------------------------------------------------------------------- /dist/js/integrations/turbolinks.production.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"turbolinks.production.js","sources":["../../../src/js/mixins/turbolinks.js"],"sourcesContent":["/**\n * Turbolinks integration for Normas\n *\n * @see {@link https://github.com/evrone/normas#turbolinks-integration|Docs}\n * @see {@link https://github.com/evrone/normas/blob/master/src/js/mixins/turbolinks.js|Source}\n * @license MIT\n * @copyright Dmitry Karpunin , 2017-2018\n */\n\n// require navigation mixin\nexport default Base => (class extends Base {\n static turboPageEnterEventName = 'turbolinks:load';\n static turboPageLeaveEventName = 'turbolinks:before-cache';\n\n bindPageEvents(options) {\n this.Turbolinks = options.Turbolinks || global.Turbolinks;\n const turbolinksExists = !!this.Turbolinks;\n if (!this.enablings) this.enablings = {};\n this.constructor.readOptions(this.enablings, options.enablings, { turbolinks: turbolinksExists });\n if (NORMAS_DEBUG && options.enablings && options.enablings.turbolinks === true && !turbolinksExists) {\n this.error('🛤 Turbolinks: `options.enablings.turbolinks === true` but Turbolinks is not detected.',\n this.constructor.readmeLink('turbolinks-integration'));\n }\n if (NORMAS_DEBUG) {\n this.log('info', 'construct',\n ...this.constructor.logColor(`🛤 \"${this.instanceName}\" Turbolinks %REPLACE%.`,\n this.enablings.turbolinks ? 'enabled' : 'disabled',\n this.enablings.turbolinks ? 'green' : 'blue'));\n }\n if (this.enablings.turbolinks) {\n // Turbolinks connected :)\n // patchTurbolinks(this.Turbolinks); // TODO: check versions\n patchTurbolinksPreviewControl(this.Turbolinks);\n this.listenEvents(this.constructor.turboPageEnterEventName, this.pageEnter.bind(this));\n this.listenEvents(this.constructor.turboPageLeaveEventName, this.pageLeave.bind(this));\n if (options.Turbolinks) {\n options.Turbolinks.start();\n }\n } else {\n // No Turbolinks ;(\n if (NORMAS_DEBUG && options.enablings && options.enablings.turbolinks === false) {\n this.log('warn', 'construct',\n `🛤 You ${this.Turbolinks ? '' : 'do not '}have a Turbolinks and use integration, but \\`options.enablings.turbolinks === false\\`.`);\n }\n $(this.pageEnter.bind(this));\n }\n }\n\n visit(location) {\n if (this.enablings.turbolinks) {\n this.Turbolinks.visit(location);\n } else {\n super.visit(location);\n }\n }\n\n setHash(hash) {\n if (this.enablings.turbolinks) {\n let controller = this.Turbolinks.controller;\n controller.replaceHistoryWithLocationAndRestorationIdentifier(hash, controller.restorationIdentifier);\n } else {\n super.setHash(hash);\n }\n }\n\n replaceLocation(url) {\n if (this.enablings.turbolinks) {\n this.Turbolinks.controller.replaceHistoryWithLocationAndRestorationIdentifier(url);\n } else {\n super.replaceLocation(url);\n }\n }\n\n pushLocation(url) {\n if (this.enablings.turbolinks) {\n this.Turbolinks.controller.pushHistoryWithLocationAndRestorationIdentifier(url);\n } else {\n super.pushLocation(url);\n }\n }\n\n sayAboutPageLoading(state) {\n if (this.enablings.turbolinks) {\n const progressBar = this.Turbolinks.controller.adapter.progressBar;\n if (state) {\n progressBar.setValue(0);\n progressBar.show();\n } else {\n progressBar.hide();\n }\n } else {\n super.sayAboutPageLoading(state);\n }\n }\n});\n\nfunction patchTurbolinksPreviewControl(Turbolinks) {\n class OurView extends Turbolinks.View {\n render({ snapshot, error, isPreview }, callback) {\n this.markAsPreview(isPreview);\n if (snapshot) {\n // added `isPreview` argument\n this.renderSnapshot(snapshot, isPreview, callback);\n } else {\n this.renderError(error, callback);\n }\n }\n\n // added `isPreview` argument\n renderSnapshot(snapshot, isPreview, callback) {\n const renderer = new OurSnapshotRenderer(this.getSnapshot(), Turbolinks.Snapshot.wrap(snapshot), isPreview);\n renderer.delegate = this.delegate;\n renderer.render(callback);\n }\n }\n Turbolinks.View = OurView;\n\n class OurSnapshotRenderer extends Turbolinks.SnapshotRenderer {\n // added `isPreview` argument\n constructor(currentSnapshot, newSnapshot, isPreview) {\n super(currentSnapshot, newSnapshot);\n if (isPreview) {\n this.newBody = this.newBody.cloneNode(true);\n this.newBody.isPreview = true;\n }\n }\n }\n}\n\nfunction patchTurbolinks(Turbolinks) {\n class OurHeadDetails extends Turbolinks.HeadDetails {\n constructor(...args) {\n super(...args);\n this.elements = {};\n $(this.element).children().each((index, element) => {\n let key = turboElementToKey(element);\n if (!this.elements[key]) {\n this.elements[key] = {\n type: turboElementType(element),\n tracked: turboElementIsTracked(element),\n elements: [],\n };\n }\n this.elements[key].elements.push(element);\n });\n }\n // getTrackedElementSignature() {\n // let sign = super.getTrackedElementSignature();\n // console.log('sign ', sign);\n // return sign;\n // }\n }\n Turbolinks.HeadDetails = OurHeadDetails;\n}\n\n// Injection in Turbolinks.HeadDetails for override this logic:\nfunction turboElementToKey(element) {\n let url = element.getAttribute('src') || element.getAttribute('href');\n if (url) {\n let cuts = url.split('/');\n cuts = cuts[cuts.length - 1];\n if (cuts) { url = cuts }\n }\n return url || element.outerHTML;\n}\n\nfunction turboElementType(element) {\n if (turboElementIsScript(element)) {\n return 'script';\n } else if (turboElementIsStylesheet(element)) {\n return 'stylesheet';\n }\n return null;\n}\n\nfunction turboElementIsTracked(element) {\n return element.getAttribute('data-turbolinks-track') === 'reload';\n}\n\nfunction turboElementIsScript(element) {\n let tagName = element.tagName.toLowerCase();\n return tagName === 'script';\n}\n\nfunction turboElementIsStylesheet(element) {\n let tagName = element.tagName.toLowerCase();\n return tagName === 'style' || (tagName === 'link' && element.getAttribute('rel') === 'stylesheet');\n}\n"],"names":["Base","options","Turbolinks","global","turbolinksExists","this","enablings","constructor","readOptions","turbolinks","OurView","callback","snapshot","error","isPreview","markAsPreview","renderSnapshot","renderError","renderer","OurSnapshotRenderer","getSnapshot","Snapshot","wrap","delegate","render","View","currentSnapshot","newSnapshot","newBody","_this3","cloneNode","SnapshotRenderer","listenEvents","turboPageEnterEventName","pageEnter","bind","turboPageLeaveEventName","pageLeave","start","location","visit","hash","controller","replaceHistoryWithLocationAndRestorationIdentifier","restorationIdentifier","url","pushHistoryWithLocationAndRestorationIdentifier","state","progressBar","adapter","setValue","show","hide"],"mappings":"0uCAUsCA,6CAIrBC,QACRC,WAAaD,EAAQC,YAAcC,OAAOD,eACzCE,IAAqBC,KAAKH,WAC3BG,KAAKC,YAAWD,KAAKC,mBACrBC,YAAYC,YAAYH,KAAKC,UAAWL,EAAQK,WAAaG,WAAYL,IAW1EC,KAAKC,UAAUG,YAmEvB,SAAuCP,OAC/BQ,oKACmCC,OAA9BC,IAAAA,SAAUC,IAAAA,MAAOC,IAAAA,eACnBC,cAAcD,GACfF,OAEGI,eAAeJ,EAAUE,EAAWH,QAEpCM,YAAYJ,EAAOF,0CAKbC,EAAUE,EAAWH,OAC5BO,EAAW,IAAIC,EAAoBd,KAAKe,cAAelB,EAAWmB,SAASC,KAAKV,GAAWE,KACxFS,SAAWlB,KAAKkB,WAChBC,OAAOb,UAfET,EAAWuB,QAkBtBA,KAAOf,MAEZS,yBAEQO,EAAiBC,EAAab,4EAClCY,EAAiBC,WACnBb,MACGc,QAAUC,EAAKD,QAAQE,WAAU,KACjCF,QAAQd,WAAY,sBANGZ,EAAW6B,mBArFX1B,KAAKH,iBAC9B8B,aAAa3B,KAAKE,YAAY0B,wBAAyB5B,KAAK6B,UAAUC,KAAK9B,YAC3E2B,aAAa3B,KAAKE,YAAY6B,wBAAyB/B,KAAKgC,UAAUF,KAAK9B,OAC5EJ,EAAQC,cACFA,WAAWoC,WAQnBjC,KAAK6B,UAAUC,KAAK9B,qCAIpBkC,GACAlC,KAAKC,UAAUG,gBACZP,WAAWsC,MAAMD,uFAEVA,mCAIRE,MACFpC,KAAKC,UAAUG,WAAY,KACzBiC,EAAarC,KAAKH,WAAWwC,aACtBC,mDAAmDF,EAAMC,EAAWE,kHAEjEH,2CAIFI,GACVxC,KAAKC,UAAUG,gBACZP,WAAWwC,WAAWC,mDAAmDE,iGAExDA,wCAIbA,GACPxC,KAAKC,UAAUG,gBACZP,WAAWwC,WAAWI,gDAAgDD,8FAExDA,+CAIHE,MACd1C,KAAKC,UAAUG,WAAY,KACvBuC,EAAc3C,KAAKH,WAAWwC,WAAWO,QAAQD,YACnDD,KACUG,SAAS,KACTC,UAEAC,8GAGYL,+FAhFG,uGACA"} -------------------------------------------------------------------------------- /dist/js/extensions/views.production.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"views.production.js","sources":["../../../src/js/mixins/view.js","../../../src/js/mixins/views.js"],"sourcesContent":["export default Base => (class extends Base {\n // Override it with your own initialization logic (like componentDidUnmount in react).\n initialize(options) {\n }\n\n // Override it with your own unmount logic (like componentWillUnmount in react).\n terminate() {\n }\n\n // protected\n\n constructor(options) {\n Object.assign(options, Base.dom.data(options.el));\n super(options);\n this.reflectOptions(options);\n this.initializeEvents(options);\n this.initialize(options);\n if (NORMAS_DEBUG && this.logging.constructGrouping) {\n this.log('groupEnd', 'construct');\n }\n }\n\n destructor() {\n if (NORMAS_DEBUG) {\n const [destructText, ...destructStyles] = this.constructor.logColor('destructing', 'red');\n this.log(this.constructor.groupingMethod(this.logging.constructGrouping), 'construct',\n ...this.constructor.logBold(`${this.logging.constructPrefix} \"%REPLACE%\" ${destructText}`, this.instanceName),\n ...destructStyles,\n this);\n }\n this.terminate();\n if (this.listenedEvents) {\n this.forgetEvents(this.listenedEvents);\n this.listenedEvents = null;\n }\n if (NORMAS_DEBUG && this.logging.constructGrouping) {\n this.log('groupEnd', 'construct');\n }\n }\n\n reflectOptions(options) {\n if (!this.constructor.reflectOptions) {\n return;\n }\n Object.keys(options).forEach(attr => {\n if (this.constructor.reflectOptions.includes(attr)) {\n this[attr] = options[attr];\n }\n });\n }\n\n initializeEvents(_options) {\n const { events } = this.constructor;\n if (events) {\n if (!this.linkedEvents) {\n this.linkedEvents = this.linkEvents(this.helpers.isFunction(events) ? events() : events);\n }\n this.listenedEvents = this.listenEvents(this.linkedEvents);\n }\n }\n\n linkEvents(events) {\n return this.helpers.mapValues(events, handle => this.helpers.isString(handle) ?\n this[handle].bind(this)\n :\n (typeof this.helpers.isPlainObject(handle) ? this.linkEvents(handle) : handle)\n );\n }\n\n data(key, ...value) {\n this.dom.data(this.el, key, ...value);\n }\n});\n","/**\n * Views system for Normas\n *\n * @see {@link https://github.com/evrone/normas#-views|Docs}\n * @see {@link https://github.com/evrone/normas/blob/master/src/js/mixins/views.js|Source}\n * @license MIT\n * @copyright Dmitry Karpunin , 2017-2018\n */\n\n// TODO: may be rename Views, views, View, view\nimport normasView from './view';\n\nexport default Base => normasViews(Base, normasView(Base.NormasCore));\n\n// require content mixin\n// require events mixin\nconst normasViews = (Base, View) => (class extends Base {\n static View = View;\n View = View;\n viewClasses = {};\n viewInstances = [];\n\n constructor(options) {\n super(options);\n this.viewOptions = {\n debugMode: this.debugMode,\n ...options.viewOptions,\n };\n if (NORMAS_DEBUG) {\n this.viewOptions.logging = {\n ...this.logging,\n constructGrouping: 'groupCollapsed',\n constructPrefix: '🏭', // private\n eventsDebounced: false,\n ...(options.viewOptions && options.viewOptions.logging),\n };\n this.log('info', 'construct', `🏭 \"${this.instanceName}\" views mixin activated.`);\n }\n }\n\n registerView(viewClass, options = {}) {\n if (this.viewClasses[viewClass.selector]) {\n if (NORMAS_DEBUG) {\n this.error(`🏭 View class for selector \\`${viewClass.selector}\\` already registered`,\n this.viewClasses[viewClass.selector]);\n }\n return;\n }\n this.viewClasses[viewClass.selector] = viewClass;\n this.listenToElement(\n viewClass.selector,\n $el => this.bindView($el, viewClass, options),\n $el => this.unbindView($el, viewClass),\n {\n delay: viewClass.delay,\n silent: true,\n },\n );\n }\n\n bindView($el, viewClass, options) {\n if (!this.canBind($el, viewClass)) {\n return null;\n }\n if (viewClass.instanceIndex) {\n viewClass.instanceIndex += 1;\n } else {\n viewClass.instanceIndex = 1;\n }\n const view = new viewClass({\n ...this.helpers.deepMerge(this.viewOptions, options),\n instanceName: `${viewClass.selector}_${viewClass.instanceIndex}`,\n el: $el[0],\n });\n this.viewInstances.push(view);\n return view;\n }\n\n canBind($element, viewClass) {\n const view = this.getViewsOnElement($element, viewClass)[0];\n if (view) {\n if (NORMAS_DEBUG) {\n this.log('warn', '🏭 Element already has bound view', $element, viewClass, view);\n }\n return false;\n }\n return true;\n }\n\n unbindView($element, viewClass) {\n const view = this.getViewsOnElement($element, viewClass)[0];\n if (view) {\n view.destructor();\n this.viewInstances = this.helpers.without(this.viewInstances, view);\n }\n }\n\n getViewsOnElement($element, viewClass = null) {\n const el = $element instanceof $ ? $element[0] : $element;\n const filterOptions = { el };\n if (viewClass) {\n filterOptions.constructor = viewClass;\n }\n return this.helpers.filter(this.viewInstances, filterOptions);\n }\n\n getViewsInContainer($container, checkRoot = true) {\n return this.helpers.filter(this.viewInstances, view =>\n view.$el.closest($container).length > 0 && (checkRoot || view.el !== $container[0])\n );\n }\n\n getAllViews(viewClass) {\n return this.helpers.filter(this.viewInstances, { constructor: viewClass });\n }\n\n getFirstView(viewClass) {\n return this.helpers.find(this.viewInstances, { constructor: viewClass });\n }\n\n getFirstChildView(viewClass) {\n return this.helpers.find(this.viewInstances, view => view instanceof viewClass);\n }\n});\n"],"names":["options","assign","Base","dom","data","el","reflectOptions","initializeEvents","initialize","terminate","this","listenedEvents","forgetEvents","constructor","keys","forEach","_this2","includes","attr","_options","events","linkedEvents","linkEvents","helpers","isFunction","listenEvents","mapValues","_this3","isString","handle","bind","babelHelpers.typeof","isPlainObject","key","value","normasViews","View","viewOptions","_this","debugMode","viewClass","viewClasses","selector","listenToElement","bindView","$el","unbindView","delay","canBind","instanceIndex","view","deepMerge","viewInstances","push","$element","getViewsOnElement","destructor","without","filterOptions","$","filter","$container","checkRoot","closest","length","find","normasView","NormasCore"],"mappings":"iuCAWcA,oBACHC,OAAOD,EAASE,EAAKC,IAAIC,KAAKJ,EAAQK,oEACvCL,aACDM,eAAeN,KACfO,iBAAiBP,KACjBQ,WAAWR,gBAhBkBE,yCAEzBF,0FA4BJS,YACDC,KAAKC,sBACFC,aAAaF,KAAKC,qBAClBA,eAAiB,6CAOXX,cACRU,KAAKG,YAAYP,uBAGfQ,KAAKd,GAASe,QAAQ,YACvBC,EAAKH,YAAYP,eAAeW,SAASC,OACtCA,GAAQlB,EAAQkB,+CAKVC,OACPC,EAAWV,KAAKG,YAAhBO,OACJA,IACGV,KAAKW,oBACHA,aAAeX,KAAKY,WAAWZ,KAAKa,QAAQC,WAAWJ,GAAUA,IAAWA,SAE9ET,eAAiBD,KAAKe,aAAaf,KAAKW,kDAItCD,qBACFV,KAAKa,QAAQG,UAAUN,EAAQ,mBAAUO,EAAKJ,QAAQK,SAASC,GACpEF,EAAKE,GAAQC,QAEZC,EAAOJ,EAAKJ,QAAQS,cAAcH,IAAUF,EAAKL,WAAWO,GAAUA,iCAItEI,gCAAQC,2DACN/B,KAAIC,cAAKM,KAAKL,GAAI4B,UAAQC,cCtD7BC,EAAc,SAACjC,EAAMkC,6CAMbpC,4EACJA,2EALDoC,gKAMAC,yBACQC,EAAKC,WACbvC,EAAQqC,0BAVkCnC,2CAwBpCsC,cAAWxC,4DAClBU,KAAK+B,YAAYD,EAAUE,iBAO1BD,YAAYD,EAAUE,UAAYF,OAClCG,gBACHH,EAAUE,SACV,mBAAO1B,EAAK4B,SAASC,EAAKL,EAAWxC,IACrC,mBAAOgB,EAAK8B,WAAWD,EAAKL,WAEnBA,EAAUO,cACT,sCAKLF,EAAKL,EAAWxC,OAClBU,KAAKsC,QAAQH,EAAKL,UACd,KAELA,EAAUS,gBACFA,eAAiB,IAEjBA,cAAgB,MAEtBC,EAAO,IAAIV,OACZ9B,KAAKa,QAAQ4B,UAAUzC,KAAK2B,YAAarC,iBAC3BwC,EAAUE,aAAYF,EAAUS,iBAC7CJ,EAAI,kBAELO,cAAcC,KAAKH,GACjBA,kCAGDI,EAAUd,OACVU,EAAOxC,KAAK6C,kBAAkBD,EAAUd,GAAW,UACrDU,qCASKI,EAAUd,OACbU,EAAOxC,KAAK6C,kBAAkBD,EAAUd,GAAW,GACrDU,MACGM,kBACAJ,cAAgB1C,KAAKa,QAAQkC,QAAQ/C,KAAK0C,cAAeF,8CAIhDI,OAAUd,yDAAY,KAEhCkB,GAAkBrD,GADbiD,aAAoBK,EAAIL,EAAS,GAAKA,UAE7Cd,MACY3B,YAAc2B,GAEvB9B,KAAKa,QAAQqC,OAAOlD,KAAK0C,cAAeM,+CAG7BG,OAAYC,oEACvBpD,KAAKa,QAAQqC,OAAOlD,KAAK0C,cAAe,mBAC7CF,EAAKL,IAAIkB,QAAQF,GAAYG,OAAS,IAAMF,GAAaZ,EAAK7C,KAAOwD,EAAW,0CAIxErB,UACH9B,KAAKa,QAAQqC,OAAOlD,KAAK0C,eAAiBvC,YAAa2B,yCAGnDA,UACJ9B,KAAKa,QAAQ0C,KAAKvD,KAAK0C,eAAiBvC,YAAa2B,8CAG5CA,UACT9B,KAAKa,QAAQ0C,KAAKvD,KAAK0C,cAAe,mBAAQF,aAAgBV,6EAxGzDJ,yCALOD,EAAYjC,EAAMgE,EAAWhE,EAAKiE"} -------------------------------------------------------------------------------- /dist/js/integrations/turbolinks.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"turbolinks.js","sources":["../../../src/js/mixins/turbolinks.js"],"sourcesContent":["/**\n * Turbolinks integration for Normas\n *\n * @see {@link https://github.com/evrone/normas#turbolinks-integration|Docs}\n * @see {@link https://github.com/evrone/normas/blob/master/src/js/mixins/turbolinks.js|Source}\n * @license MIT\n * @copyright Dmitry Karpunin , 2017-2018\n */\n\n// require navigation mixin\nexport default Base => (class extends Base {\n static turboPageEnterEventName = 'turbolinks:load';\n static turboPageLeaveEventName = 'turbolinks:before-cache';\n\n bindPageEvents(options) {\n this.Turbolinks = options.Turbolinks || global.Turbolinks;\n const turbolinksExists = !!this.Turbolinks;\n if (!this.enablings) this.enablings = {};\n this.constructor.readOptions(this.enablings, options.enablings, { turbolinks: turbolinksExists });\n if (NORMAS_DEBUG && options.enablings && options.enablings.turbolinks === true && !turbolinksExists) {\n this.error('🛤 Turbolinks: `options.enablings.turbolinks === true` but Turbolinks is not detected.',\n this.constructor.readmeLink('turbolinks-integration'));\n }\n if (NORMAS_DEBUG) {\n this.log('info', 'construct',\n ...this.constructor.logColor(`🛤 \"${this.instanceName}\" Turbolinks %REPLACE%.`,\n this.enablings.turbolinks ? 'enabled' : 'disabled',\n this.enablings.turbolinks ? 'green' : 'blue'));\n }\n if (this.enablings.turbolinks) {\n // Turbolinks connected :)\n // patchTurbolinks(this.Turbolinks); // TODO: check versions\n patchTurbolinksPreviewControl(this.Turbolinks);\n this.listenEvents(this.constructor.turboPageEnterEventName, this.pageEnter.bind(this));\n this.listenEvents(this.constructor.turboPageLeaveEventName, this.pageLeave.bind(this));\n if (options.Turbolinks) {\n options.Turbolinks.start();\n }\n } else {\n // No Turbolinks ;(\n if (NORMAS_DEBUG && options.enablings && options.enablings.turbolinks === false) {\n this.log('warn', 'construct',\n `🛤 You ${this.Turbolinks ? '' : 'do not '}have a Turbolinks and use integration, but \\`options.enablings.turbolinks === false\\`.`);\n }\n $(this.pageEnter.bind(this));\n }\n }\n\n visit(location) {\n if (this.enablings.turbolinks) {\n this.Turbolinks.visit(location);\n } else {\n super.visit(location);\n }\n }\n\n setHash(hash) {\n if (this.enablings.turbolinks) {\n let controller = this.Turbolinks.controller;\n controller.replaceHistoryWithLocationAndRestorationIdentifier(hash, controller.restorationIdentifier);\n } else {\n super.setHash(hash);\n }\n }\n\n replaceLocation(url) {\n if (this.enablings.turbolinks) {\n this.Turbolinks.controller.replaceHistoryWithLocationAndRestorationIdentifier(url);\n } else {\n super.replaceLocation(url);\n }\n }\n\n pushLocation(url) {\n if (this.enablings.turbolinks) {\n this.Turbolinks.controller.pushHistoryWithLocationAndRestorationIdentifier(url);\n } else {\n super.pushLocation(url);\n }\n }\n\n sayAboutPageLoading(state) {\n if (this.enablings.turbolinks) {\n const progressBar = this.Turbolinks.controller.adapter.progressBar;\n if (state) {\n progressBar.setValue(0);\n progressBar.show();\n } else {\n progressBar.hide();\n }\n } else {\n super.sayAboutPageLoading(state);\n }\n }\n});\n\nfunction patchTurbolinksPreviewControl(Turbolinks) {\n class OurView extends Turbolinks.View {\n render({ snapshot, error, isPreview }, callback) {\n this.markAsPreview(isPreview);\n if (snapshot) {\n // added `isPreview` argument\n this.renderSnapshot(snapshot, isPreview, callback);\n } else {\n this.renderError(error, callback);\n }\n }\n\n // added `isPreview` argument\n renderSnapshot(snapshot, isPreview, callback) {\n const renderer = new OurSnapshotRenderer(this.getSnapshot(), Turbolinks.Snapshot.wrap(snapshot), isPreview);\n renderer.delegate = this.delegate;\n renderer.render(callback);\n }\n }\n Turbolinks.View = OurView;\n\n class OurSnapshotRenderer extends Turbolinks.SnapshotRenderer {\n // added `isPreview` argument\n constructor(currentSnapshot, newSnapshot, isPreview) {\n super(currentSnapshot, newSnapshot);\n if (isPreview) {\n this.newBody = this.newBody.cloneNode(true);\n this.newBody.isPreview = true;\n }\n }\n }\n}\n\nfunction patchTurbolinks(Turbolinks) {\n class OurHeadDetails extends Turbolinks.HeadDetails {\n constructor(...args) {\n super(...args);\n this.elements = {};\n $(this.element).children().each((index, element) => {\n let key = turboElementToKey(element);\n if (!this.elements[key]) {\n this.elements[key] = {\n type: turboElementType(element),\n tracked: turboElementIsTracked(element),\n elements: [],\n };\n }\n this.elements[key].elements.push(element);\n });\n }\n // getTrackedElementSignature() {\n // let sign = super.getTrackedElementSignature();\n // console.log('sign ', sign);\n // return sign;\n // }\n }\n Turbolinks.HeadDetails = OurHeadDetails;\n}\n\n// Injection in Turbolinks.HeadDetails for override this logic:\nfunction turboElementToKey(element) {\n let url = element.getAttribute('src') || element.getAttribute('href');\n if (url) {\n let cuts = url.split('/');\n cuts = cuts[cuts.length - 1];\n if (cuts) { url = cuts }\n }\n return url || element.outerHTML;\n}\n\nfunction turboElementType(element) {\n if (turboElementIsScript(element)) {\n return 'script';\n } else if (turboElementIsStylesheet(element)) {\n return 'stylesheet';\n }\n return null;\n}\n\nfunction turboElementIsTracked(element) {\n return element.getAttribute('data-turbolinks-track') === 'reload';\n}\n\nfunction turboElementIsScript(element) {\n let tagName = element.tagName.toLowerCase();\n return tagName === 'script';\n}\n\nfunction turboElementIsStylesheet(element) {\n let tagName = element.tagName.toLowerCase();\n return tagName === 'style' || (tagName === 'link' && element.getAttribute('rel') === 'stylesheet');\n}\n"],"names":["Base","options","Turbolinks","global","turbolinksExists","this","enablings","constructor","readOptions","turbolinks","error","readmeLink","log","logColor","instanceName","OurView","callback","snapshot","isPreview","markAsPreview","renderSnapshot","renderError","renderer","OurSnapshotRenderer","getSnapshot","Snapshot","wrap","delegate","render","View","currentSnapshot","newSnapshot","newBody","_this3","cloneNode","SnapshotRenderer","listenEvents","turboPageEnterEventName","pageEnter","bind","turboPageLeaveEventName","pageLeave","start","location","visit","hash","controller","replaceHistoryWithLocationAndRestorationIdentifier","restorationIdentifier","url","pushHistoryWithLocationAndRestorationIdentifier","state","progressBar","adapter","setValue","show","hide"],"mappings":"0uCAUsCA,6CAIrBC,QACRC,WAAaD,EAAQC,YAAcC,OAAOD,eACzCE,IAAqBC,KAAKH,WAC3BG,KAAKC,YAAWD,KAAKC,mBACrBC,YAAYC,YAAYH,KAAKC,UAAWL,EAAQK,WAAaG,WAAYL,IAC1DH,EAAQK,YAA8C,IAAjCL,EAAQK,UAAUG,aAAwBL,QAC5EM,MAAM,yFACTL,KAAKE,YAAYI,WAAW,gCAGzBC,gBAAI,OAAQ,2IACZP,KAAKE,YAAYM,gBAAgBR,KAAKS,uCACvCT,KAAKC,UAAUG,WAAa,UAAY,WACxCJ,KAAKC,UAAUG,WAAa,QAAU,WAExCJ,KAAKC,UAAUG,YAmEvB,SAAuCP,OAC/Ba,oKACmCC,OAA9BC,IAAAA,SAAUP,IAAAA,MAAOQ,IAAAA,eACnBC,cAAcD,GACfD,OAEGG,eAAeH,EAAUC,EAAWF,QAEpCK,YAAYX,EAAOM,0CAKbC,EAAUC,EAAWF,OAC5BM,EAAW,IAAIC,EAAoBlB,KAAKmB,cAAetB,EAAWuB,SAASC,KAAKT,GAAWC,KACxFS,SAAWtB,KAAKsB,WAChBC,OAAOZ,UAfEd,EAAW2B,QAkBtBA,KAAOd,MAEZQ,yBAEQO,EAAiBC,EAAab,4EAClCY,EAAiBC,WACnBb,MACGc,QAAUC,EAAKD,QAAQE,WAAU,KACjCF,QAAQd,WAAY,sBANGhB,EAAWiC,mBArFX9B,KAAKH,iBAC9BkC,aAAa/B,KAAKE,YAAY8B,wBAAyBhC,KAAKiC,UAAUC,KAAKlC,YAC3E+B,aAAa/B,KAAKE,YAAYiC,wBAAyBnC,KAAKoC,UAAUF,KAAKlC,OAC5EJ,EAAQC,cACFA,WAAWwC,UAIDzC,EAAQK,YAA8C,IAAjCL,EAAQK,UAAUG,iBACpDG,IAAI,OAAQ,uBACLP,KAAKH,WAAa,GAAK,qGAEnCG,KAAKiC,UAAUC,KAAKlC,sCAIpBsC,GACAtC,KAAKC,UAAUG,gBACZP,WAAW0C,MAAMD,uFAEVA,mCAIRE,MACFxC,KAAKC,UAAUG,WAAY,KACzBqC,EAAazC,KAAKH,WAAW4C,aACtBC,mDAAmDF,EAAMC,EAAWE,kHAEjEH,2CAIFI,GACV5C,KAAKC,UAAUG,gBACZP,WAAW4C,WAAWC,mDAAmDE,iGAExDA,wCAIbA,GACP5C,KAAKC,UAAUG,gBACZP,WAAW4C,WAAWI,gDAAgDD,8FAExDA,+CAIHE,MACd9C,KAAKC,UAAUG,WAAY,KACvB2C,EAAc/C,KAAKH,WAAW4C,WAAWO,QAAQD,YACnDD,KACUG,SAAS,KACTC,UAEAC,8GAGYL,+FAhFG,uGACA"} -------------------------------------------------------------------------------- /dist/js/extensions/views.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"views.js","sources":["../../../src/js/mixins/view.js","../../../src/js/mixins/views.js"],"sourcesContent":["export default Base => (class extends Base {\n // Override it with your own initialization logic (like componentDidUnmount in react).\n initialize(options) {\n }\n\n // Override it with your own unmount logic (like componentWillUnmount in react).\n terminate() {\n }\n\n // protected\n\n constructor(options) {\n Object.assign(options, Base.dom.data(options.el));\n super(options);\n this.reflectOptions(options);\n this.initializeEvents(options);\n this.initialize(options);\n if (NORMAS_DEBUG && this.logging.constructGrouping) {\n this.log('groupEnd', 'construct');\n }\n }\n\n destructor() {\n if (NORMAS_DEBUG) {\n const [destructText, ...destructStyles] = this.constructor.logColor('destructing', 'red');\n this.log(this.constructor.groupingMethod(this.logging.constructGrouping), 'construct',\n ...this.constructor.logBold(`${this.logging.constructPrefix} \"%REPLACE%\" ${destructText}`, this.instanceName),\n ...destructStyles,\n this);\n }\n this.terminate();\n if (this.listenedEvents) {\n this.forgetEvents(this.listenedEvents);\n this.listenedEvents = null;\n }\n if (NORMAS_DEBUG && this.logging.constructGrouping) {\n this.log('groupEnd', 'construct');\n }\n }\n\n reflectOptions(options) {\n if (!this.constructor.reflectOptions) {\n return;\n }\n Object.keys(options).forEach(attr => {\n if (this.constructor.reflectOptions.includes(attr)) {\n this[attr] = options[attr];\n }\n });\n }\n\n initializeEvents(_options) {\n const { events } = this.constructor;\n if (events) {\n if (!this.linkedEvents) {\n this.linkedEvents = this.linkEvents(this.helpers.isFunction(events) ? events() : events);\n }\n this.listenedEvents = this.listenEvents(this.linkedEvents);\n }\n }\n\n linkEvents(events) {\n return this.helpers.mapValues(events, handle => this.helpers.isString(handle) ?\n this[handle].bind(this)\n :\n (typeof this.helpers.isPlainObject(handle) ? this.linkEvents(handle) : handle)\n );\n }\n\n data(key, ...value) {\n this.dom.data(this.el, key, ...value);\n }\n});\n","/**\n * Views system for Normas\n *\n * @see {@link https://github.com/evrone/normas#-views|Docs}\n * @see {@link https://github.com/evrone/normas/blob/master/src/js/mixins/views.js|Source}\n * @license MIT\n * @copyright Dmitry Karpunin , 2017-2018\n */\n\n// TODO: may be rename Views, views, View, view\nimport normasView from './view';\n\nexport default Base => normasViews(Base, normasView(Base.NormasCore));\n\n// require content mixin\n// require events mixin\nconst normasViews = (Base, View) => (class extends Base {\n static View = View;\n View = View;\n viewClasses = {};\n viewInstances = [];\n\n constructor(options) {\n super(options);\n this.viewOptions = {\n debugMode: this.debugMode,\n ...options.viewOptions,\n };\n if (NORMAS_DEBUG) {\n this.viewOptions.logging = {\n ...this.logging,\n constructGrouping: 'groupCollapsed',\n constructPrefix: '🏭', // private\n eventsDebounced: false,\n ...(options.viewOptions && options.viewOptions.logging),\n };\n this.log('info', 'construct', `🏭 \"${this.instanceName}\" views mixin activated.`);\n }\n }\n\n registerView(viewClass, options = {}) {\n if (this.viewClasses[viewClass.selector]) {\n if (NORMAS_DEBUG) {\n this.error(`🏭 View class for selector \\`${viewClass.selector}\\` already registered`,\n this.viewClasses[viewClass.selector]);\n }\n return;\n }\n this.viewClasses[viewClass.selector] = viewClass;\n this.listenToElement(\n viewClass.selector,\n $el => this.bindView($el, viewClass, options),\n $el => this.unbindView($el, viewClass),\n {\n delay: viewClass.delay,\n silent: true,\n },\n );\n }\n\n bindView($el, viewClass, options) {\n if (!this.canBind($el, viewClass)) {\n return null;\n }\n if (viewClass.instanceIndex) {\n viewClass.instanceIndex += 1;\n } else {\n viewClass.instanceIndex = 1;\n }\n const view = new viewClass({\n ...this.helpers.deepMerge(this.viewOptions, options),\n instanceName: `${viewClass.selector}_${viewClass.instanceIndex}`,\n el: $el[0],\n });\n this.viewInstances.push(view);\n return view;\n }\n\n canBind($element, viewClass) {\n const view = this.getViewsOnElement($element, viewClass)[0];\n if (view) {\n if (NORMAS_DEBUG) {\n this.log('warn', '🏭 Element already has bound view', $element, viewClass, view);\n }\n return false;\n }\n return true;\n }\n\n unbindView($element, viewClass) {\n const view = this.getViewsOnElement($element, viewClass)[0];\n if (view) {\n view.destructor();\n this.viewInstances = this.helpers.without(this.viewInstances, view);\n }\n }\n\n getViewsOnElement($element, viewClass = null) {\n const el = $element instanceof $ ? $element[0] : $element;\n const filterOptions = { el };\n if (viewClass) {\n filterOptions.constructor = viewClass;\n }\n return this.helpers.filter(this.viewInstances, filterOptions);\n }\n\n getViewsInContainer($container, checkRoot = true) {\n return this.helpers.filter(this.viewInstances, view =>\n view.$el.closest($container).length > 0 && (checkRoot || view.el !== $container[0])\n );\n }\n\n getAllViews(viewClass) {\n return this.helpers.filter(this.viewInstances, { constructor: viewClass });\n }\n\n getFirstView(viewClass) {\n return this.helpers.find(this.viewInstances, { constructor: viewClass });\n }\n\n getFirstChildView(viewClass) {\n return this.helpers.find(this.viewInstances, view => view instanceof viewClass);\n }\n});\n"],"names":["options","assign","Base","dom","data","el","reflectOptions","initializeEvents","initialize","_this","logging","constructGrouping","log","this","constructor","logColor","destructText","destructStyles","groupingMethod","logBold","constructPrefix","instanceName","terminate","listenedEvents","forgetEvents","keys","forEach","_this2","includes","attr","_options","events","linkedEvents","linkEvents","helpers","isFunction","listenEvents","mapValues","_this3","isString","handle","bind","babelHelpers.typeof","isPlainObject","key","value","normasViews","View","viewOptions","debugMode","viewClass","viewClasses","selector","error","listenToElement","bindView","$el","unbindView","delay","canBind","instanceIndex","view","deepMerge","viewInstances","push","$element","getViewsOnElement","destructor","without","filterOptions","$","filter","$container","checkRoot","closest","length","find","normasView","NormasCore"],"mappings":"01CAWcA,oBACHC,OAAOD,EAASE,EAAKC,IAAIC,KAAKJ,EAAQK,oEACvCL,aACDM,eAAeN,KACfO,iBAAiBP,KACjBQ,WAAWR,GACIS,EAAKC,QAAQC,qBAC1BC,IAAI,WAAY,0BAlBWV,yCAEzBF,6FAsBmCa,KAAKC,YAAYC,SAAS,cAAe,gDAA5EC,OAAiBC,kBACnBL,gBAAIC,KAAKC,YAAYI,eAAeL,KAAKH,QAAQC,mBAAoB,sBACrEE,KAAKC,YAAYK,QAAWN,KAAKH,QAAQU,gCAA+BJ,EAAgBH,KAAKQ,iBAC7FJ,IACHJ,aAECS,YACDT,KAAKU,sBACFC,aAAaX,KAAKU,qBAClBA,eAAiB,MAEJV,KAAKH,QAAQC,wBAC1BC,IAAI,WAAY,oDAIVZ,cACRa,KAAKC,YAAYR,uBAGfmB,KAAKzB,GAAS0B,QAAQ,YACvBC,EAAKb,YAAYR,eAAesB,SAASC,OACtCA,GAAQ7B,EAAQ6B,+CAKVC,OACPC,EAAWlB,KAAKC,YAAhBiB,OACJA,IACGlB,KAAKmB,oBACHA,aAAenB,KAAKoB,WAAWpB,KAAKqB,QAAQC,WAAWJ,GAAUA,IAAWA,SAE9ER,eAAiBV,KAAKuB,aAAavB,KAAKmB,kDAItCD,qBACFlB,KAAKqB,QAAQG,UAAUN,EAAQ,mBAAUO,EAAKJ,QAAQK,SAASC,GACpEF,EAAKE,GAAQC,QAEZC,EAAOJ,EAAKJ,QAAQS,cAAcH,IAAUF,EAAKL,WAAWO,GAAUA,iCAItEI,gCAAQC,2DACN1C,KAAIC,cAAKS,KAAKR,GAAIuC,UAAQC,cCtD7BC,EAAc,SAAC5C,EAAM6C,6CAMb/C,4EACJA,2EALD+C,gKAMAC,yBACQvC,EAAKwC,WACbjD,EAAQgD,eAGNA,YAAYtC,aACZD,EAAKC,2BACW,iCACF,sBACA,GACbV,EAAQgD,aAAehD,EAAQgD,YAAYtC,WAE5CE,IAAI,OAAQ,mBAAoBH,EAAKY,sDApBGnB,2CAwBpCgD,cAAWlD,4DAClBa,KAAKsC,YAAYD,EAAUE,eAEtBC,qCAAsCH,EAAUE,gCACnDvC,KAAKsC,YAAYD,EAAUE,iBAI5BD,YAAYD,EAAUE,UAAYF,OAClCI,gBACHJ,EAAUE,SACV,mBAAOzB,EAAK4B,SAASC,EAAKN,EAAWlD,IACrC,mBAAO2B,EAAK8B,WAAWD,EAAKN,WAEnBA,EAAUQ,cACT,sCAKLF,EAAKN,EAAWlD,OAClBa,KAAK8C,QAAQH,EAAKN,UACd,KAELA,EAAUU,gBACFA,eAAiB,IAEjBA,cAAgB,MAEtBC,EAAO,IAAIX,OACZrC,KAAKqB,QAAQ4B,UAAUjD,KAAKmC,YAAahD,iBAC3BkD,EAAUE,aAAYF,EAAUU,iBAC7CJ,EAAI,kBAELO,cAAcC,KAAKH,GACjBA,kCAGDI,EAAUf,OACVW,EAAOhD,KAAKqD,kBAAkBD,EAAUf,GAAW,UACrDW,SAEKjD,IAAI,OAAQ,oCAAqCqD,EAAUf,EAAWW,IAEtE,sCAKAI,EAAUf,OACbW,EAAOhD,KAAKqD,kBAAkBD,EAAUf,GAAW,GACrDW,MACGM,kBACAJ,cAAgBlD,KAAKqB,QAAQkC,QAAQvD,KAAKkD,cAAeF,8CAIhDI,OAAUf,yDAAY,KAEhCmB,GAAkBhE,GADb4D,aAAoBK,EAAIL,EAAS,GAAKA,UAE7Cf,MACYpC,YAAcoC,GAEvBrC,KAAKqB,QAAQqC,OAAO1D,KAAKkD,cAAeM,+CAG7BG,OAAYC,oEACvB5D,KAAKqB,QAAQqC,OAAO1D,KAAKkD,cAAe,mBAC7CF,EAAKL,IAAIkB,QAAQF,GAAYG,OAAS,IAAMF,GAAaZ,EAAKxD,KAAOmE,EAAW,0CAIxEtB,UACHrC,KAAKqB,QAAQqC,OAAO1D,KAAKkD,eAAiBjD,YAAaoC,yCAGnDA,UACJrC,KAAKqB,QAAQ0C,KAAK/D,KAAKkD,eAAiBjD,YAAaoC,8CAG5CA,UACTrC,KAAKqB,QAAQ0C,KAAK/D,KAAKkD,cAAe,mBAAQF,aAAgBX,6EAxGzDH,yCALOD,EAAY5C,EAAM2E,EAAW3E,EAAK4E"} --------------------------------------------------------------------------------