From 1747bd49405ad1f5115b8eb2cde14df80f62146f Mon Sep 17 00:00:00 2001 From: Vasiliy Ermolovich Date: Tue, 4 Dec 2012 22:01:04 +0300 Subject: [PATCH] init --- .gitignore | 17 ++ Gemfile | 3 + LICENSE.txt | 22 ++ README.md | 31 +++ Rakefile | 1 + garlicjs-rails.gemspec | 17 ++ lib/garlicjs-rails.rb | 8 + lib/garlicjs-rails/version.rb | 5 + vendor/assets/javascripts/garlic.js | 379 ++++++++++++++++++++++++++++ 9 files changed, 483 insertions(+) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 Rakefile create mode 100644 garlicjs-rails.gemspec create mode 100644 lib/garlicjs-rails.rb create mode 100644 lib/garlicjs-rails/version.rb create mode 100644 vendor/assets/javascripts/garlic.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d87d4be --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.gem +*.rbc +.bundle +.config +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..fa75df1 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..e9c8628 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2012 Vasiliy Ermolovich + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..88b41cd --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# garlicjs-rails + +garlicjs-rails wraps the [Garlic.js](http://garlicjs.org/) library in a rails engine for simple use with the asset pipeline provided by rails 3.1. The gem includes the development (non-minified) source for ease of exploration. The asset pipeline will minify in production. + +Garlic.js allows you to automatically persist your forms' text field values locally, until the form is submitted. This way, your users don't lose any precious data if they accidentally close their tab or browser. + +It strives to have a javascript agnostic interface for UI/UX developers that might want to use it. Just add some data-persist="garlic" in your form tags, and you're good to go! + +## Installation + +Add this line to your application's Gemfile: + + gem 'garlicjs-rails' + +## Usage + +Add the following directive to your Javascript manifest file (application.js): + + //= require garlic + +## Versioning + +garlicjs-rails 1.0.2 == Garlic.js 1.0.2 + +## Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..2995527 --- /dev/null +++ b/Rakefile @@ -0,0 +1 @@ +require "bundler/gem_tasks" diff --git a/garlicjs-rails.gemspec b/garlicjs-rails.gemspec new file mode 100644 index 0000000..93689a9 --- /dev/null +++ b/garlicjs-rails.gemspec @@ -0,0 +1,17 @@ +# -*- encoding: utf-8 -*- +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'garlicjs-rails/version' + +Gem::Specification.new do |gem| + gem.name = "garlicjs-rails" + gem.version = Garlicjs::Rails::VERSION + gem.authors = ["Vasiliy Ermolovich"] + gem.email = ["younash@gmail.com"] + gem.description = %q{Garlic.js allows you to automatically persist your forms' text field values locally, until the form is submitted} + gem.summary = %q{Garlic.js allows you to automatically persist your forms' text field values locally, until the form is submitted} + gem.homepage = "http://github.com/nashby/garlicjs-rails" + + gem.files = `git ls-files`.split($/) + gem.require_paths = ["lib"] +end diff --git a/lib/garlicjs-rails.rb b/lib/garlicjs-rails.rb new file mode 100644 index 0000000..1c42099 --- /dev/null +++ b/lib/garlicjs-rails.rb @@ -0,0 +1,8 @@ +require "garlicjs-rails/version" + +module Garlicjs + module Rails + class Engine < ::Rails::Engine + end + end +end diff --git a/lib/garlicjs-rails/version.rb b/lib/garlicjs-rails/version.rb new file mode 100644 index 0000000..4ff07ac --- /dev/null +++ b/lib/garlicjs-rails/version.rb @@ -0,0 +1,5 @@ +module Garlicjs + module Rails + VERSION = "1.0.2" + end +end diff --git a/vendor/assets/javascripts/garlic.js b/vendor/assets/javascripts/garlic.js new file mode 100644 index 0000000..f24f5fa --- /dev/null +++ b/vendor/assets/javascripts/garlic.js @@ -0,0 +1,379 @@ +/* + Garlic.js allows you to automatically persist your forms' text field values locally, + until the form is submitted. This way, your users don't lose any precious data if they + accidentally close their tab or browser. + + author: Guillaume Potier - @guillaumepotier +*/ + +!function ($) { + + "use strict"; + /*global localStorage */ + /*global document */ + + /* STORAGE PUBLIC CLASS DEFINITION + * =============================== */ + var Storage = function ( options ) { + this.defined = 'undefined' !== typeof localStorage; + } + + Storage.prototype = { + + constructor: Storage + + , get: function ( key, placeholder ) { + return localStorage.getItem( key ) ? localStorage.getItem( key ) : 'undefined' !== typeof placeholder ? placeholder : null; + } + + , has: function ( key ) { + return localStorage.getItem( key ) ? true : false; + } + + , set: function ( key, value, fn ) { + if ( 'string' === typeof value ) { + + // if value is null, remove storage if exists + if ( '' === value ) { + this.destroy( key ); + } else { + localStorage.setItem( key , value ); + } + } + + return 'function' === typeof fn ? fn() : true; + } + + , destroy: function ( key, fn ) { + localStorage.removeItem( key ); + return 'function' === typeof fn ? fn() : true; + } + + , clean: function ( fn ) { + for ( var i = localStorage.length - 1; i >= 0; i-- ) { + if ( 'undefined' === typeof Array.indexOf && -1 !== localStorage.key(i).indexOf( 'garlic:' ) ) { + localStorage.removeItem( localStorage.key(i) ); + } + } + + return 'function' === typeof fn ? fn() : true; + } + + , clear: function ( fn ) { + localStorage.clear(); + return 'function' === typeof fn ? fn() : true; + } + } + + /* GARLIC PUBLIC CLASS DEFINITION + * =============================== */ + + var Garlic = function ( element, storage, options ) { + this.init( 'garlic', element, storage, options ); + } + + Garlic.prototype = { + + constructor: Garlic + + /* init data, bind jQuery on() actions */ + , init: function ( type, element, storage, options ) { + this.type = type; + this.$element = $( element ); + this.options = this.getOptions( options ); + this.storage = storage; + this.path = this.getPath(); + this.parentForm = this.$element.closest( 'form' ); + + this.retrieve(); + + this.$element.on( this.options.events.join( '.' + this.type + ' ') , false, $.proxy( this.persist, this ) ); + + if ( this.options.destroy ) { + this.$element.closest( 'form' ).on( 'submit reset' , false, $.proxy( this.destroy, this ) ); + } + + this.$element.addClass('garlic-auto-save'); + } + + , getOptions: function ( options ) { + options = $.extend( {}, $.fn[this.type].defaults, options, this.$element.data() ); + + return options; + } + + /* temporary store data / state in localStorage */ + , persist: function () { + // for checkboxes, we need to implement an unchecked / checked behavior + if ( this.$element.is( 'input[type=checkbox]' ) ) { + return this.storage.set( this.path , this.$element.attr( 'checked' ) ? 'checked' : 'unchecked' ); + } + + this.storage.set( this.path , this.$element.val() ); + } + + /* retrieve localStorage data / state and update elem accordingly */ + , retrieve: function () { + if ( this.storage.has( this.path ) ) { + + // if conflictManager enabled, manage fields with already provided data, different from the one stored + if ( this.options.conflictManager.enabled && this.detectConflict() ) { + return this.conflictManager(); + } + + // input[type=checkbox] and input[type=radio] have a special checked / unchecked behavior + if ( this.$element.is( 'input[type=radio], input[type=checkbox]' ) ) { + + // for checkboxes and radios + if ( 'checked' === this.storage.get( this.path ) || this.storage.get( this.path ) === this.$element.val() ) { + return this.$element.attr( 'checked', true ); + + // only needed for checkboxes + } else if ( 'unchecked' === this.storage.get( this.path ) ) { + this.$element.attr( 'checked', false ); + } + + return; + } + + // for input[type=text], select and textarea, just set val() + this.$element.val( this.storage.get( this.path ) ); + } + } + + /* there is a conflict when initial data / state differs from persisted data / state */ + , detectConflict: function() { + var self = this; + + // radio buttons and checkboxes are yet not supported + if ( this.$element.is( 'input[type=checkbox], input[type=radio]' ) ) { + return false; + } + + // there is a default not null value and we have a different one stored + if ( this.$element.val() && this.storage.get( this.path ) !== this.$element.val() ) { + + // for select elements, we need to check if there is a default checked value + if ( this.$element.is( 'select' ) ) { + var selectConflictDetected = false; + + // foreach each options except first one, always considered as selected, seeking for a default selected one + this.$element.find( 'option' ).each( function () { + if ( $( this ).index() !== 0 && $( this ).attr( 'selected' ) && $( this ).val() !== self.storage.get( this.path ) ) { + selectConflictDetected = true; + return; + } + }); + + return selectConflictDetected; + } + + return true; + } + + return false; + } + + /* manage here the conflict, show default value depending on options.garlicPriority value */ + , conflictManager: function () { + + // user can define here a custom function that could stop Garlic default behavior, if returns false + if ( 'function' === typeof this.options.conflictManager.onConflictDetected + && !this.options.conflictManager.onConflictDetected( this.$element, this.storage.get( this.path ) ) ) { + return false; + } + + if ( this.options.conflictManager.garlicPriority ) { + this.$element.data( 'swap-data', this.$element.val() ); + this.$element.data( 'swap-state', 'garlic' ); + this.$element.val( this.storage.get( this.path ) ); + } else { + this.$element.data( 'swap-data', this.storage.get( this.path ) ); + this.$element.data( 'swap-state', 'default' ); + } + + this.swapHandler(); + this.$element.addClass( 'garlic-conflict-detected' ); + this.$element.closest( 'input[type=submit]' ).attr( 'disabled', true ); + } + + /* manage swap user interface */ + , swapHandler: function () { + var swapChoiceElem = $( this.options.conflictManager.template ); + this.$element.after( swapChoiceElem.text( this.options.conflictManager.message ) ); + swapChoiceElem.on( 'click', false, $.proxy( this.swap, this ) ); + } + + /* swap data / states for conflicted elements */ + , swap: function () { + var val = this.$element.data( 'swap-data' ); + this.$element.data( 'swap-state', 'garlic' === this.$element.data( 'swap-state' ) ? 'default' : 'garlic' ); + this.$element.data( 'swap-data', this.$element.val()); + $( this.$element ).val( val ); + } + + /* delete localStorage persistance only */ + , destroy: function () { + this.storage.destroy( this.path ); + } + + /* remove data / reset state AND delete localStorage */ + , remove: function () { + this.remove(); + + if ( this.$element.is( 'input[type=radio], input[type=checkbox]' ) ) { + $( this.$element ).attr( 'checked', false ); + return; + } + + this.$element.val( '' ); + } + + /* retuns an unique identifier for form elements, depending on their behaviors: + * radio buttons: domain > pathname > form.[:eq(x)] > input. + no eq(); must be all stored under the same field name inside the same form + + * checkbokes: domain > pathname > form.[:eq(x)] > [fieldset, div, span..] > input.[:eq(y)] + cuz' they have the same name, must detect their exact position in the form. detect the exact hierarchy in DOM elements + + * other inputs: domain > pathname > form.[:eq(x)] > input.[:eq(y)] + we just need the element name / eq() inside a given form + */ + , getPath: function () { + + // Requires one element. + if ( this.$element.length != 1 ) { + return false; + } + + var path = '' + , fullPath = this.$element.is( 'input[type=checkbox]' ) + , node = this.$element; + + while ( node.length ) { + var realNode = node[0] + , name = realNode.nodeName; + + if ( !name ) { + break; + } + + name = name.toLowerCase(); + + var parent = node.parent() + , siblings = parent.children( name ); + + // don't need to pollute path with select, fieldsets, divs and other noisy elements, + // exept for checkboxes that need exact path, cuz have same name and sometimes same eq()! + if ( !$( realNode ).is( 'form, input, select, textarea' ) && !fullPath ) { + node = parent; + continue; + } + + // set input type as name + name attr if exists + name += $( realNode ).attr( 'name' ) ? '.' + $( realNode ).attr( 'name' ) : ''; + + // if has sibilings, get eq(), exept for radio buttons + if ( siblings.length > 1 && !$( realNode ).is( 'input[type=radio]' ) ) { + name += ':eq(' + siblings.index( realNode ) + ')'; + } + + path = name + ( path ? '>' + path : '' ); + + // break once we came up to form:eq(x), no need to go further + if ( 'form' == realNode.nodeName.toLowerCase() ) { + break; + } + + node = parent; + } + + return 'garlic:' + document.domain + ( this.options.domain ? '*' : window.location.pathname ) + '>' + path; + } + + , getStorage: function () { + return this.storage; + } + } + + /* GARLIC PLUGIN DEFINITION + * ========================= */ + + $.fn.garlic = function ( option, fn ) { + var options = $.extend(true, {}, $.fn.garlic.defaults, option, this.data() ) + , storage = new Storage() + , returnValue = false; + + // this plugin heavily rely on local Storage. If there is no localStorage or data-storage=false, no need to go further + if ( !storage.defined ) { + return false; + } + + function bind ( self ) { + var $this = $( self ) + , data = $this.data( 'garlic' ) + , fieldOptions = $.extend( {}, options, $this.data() ); + + // don't bind an elem with data-storage=false + if ( 'undefined' !== typeof fieldOptions.storage && !fieldOptions.storage ) { + return; + } + + // if data never binded, bind it right now! + if ( !data ) { + $this.data( 'garlic', ( data = new Garlic( self, storage, fieldOptions ) ) ); + } + + // here is our garlic public function accessor, currently does not support args + if ( 'string' === typeof option && 'function' === typeof data[option] ) { + return data[option](); + } + } + + // loop through every elemt we want to garlic + this.each(function () { + + // if a form elem is given, bind all its input children + if ( $( this ).is( 'form' ) ) { + $( this ).find( options.inputs ).each( function () { + returnValue = bind( $( this ) ); + }); + + // if it is a Garlic supported single element, bind it too + // add here a return instance, cuz' we could call public methods on single elems with data[option]() above + } else if ( $( this ).is( options.inputs ) ) { + returnValue = bind( $( this ) ); + } + }); + + return 'function' === typeof fn ? fn() : returnValue; + } + + /* GARLIC CONFIGS & OPTIONS + * ========================= */ + $.fn.garlic.Constructor = Garlic; + + $.fn.garlic.defaults = { + destroy: true // remove or not localstorage on submit & clear + , inputs: 'input, textarea, select' // Default supported inputs. + , events: [ 'DOMAttrModified', 'textInput', 'input', 'change', 'keypress', 'paste', 'focus' ] // events list that trigger a localStorage + , domain: false // store et retrieve forms data accross all domain, not just on + , conflictManager: { + enabled: true // manage default data and persisted data. If false, persisted data will always replace default ones + , garlicPriority: true // if form have default data, garlic persisted data will be shown first + , template: '' // template used to swap between values if conflict detected + , message: 'This is your saved data. Click here to see default one' // default message for swapping data / state + , onConflictDetected: function ( item, storedVal ) { return true; } // This function will be triggered if a conflict is detected on an item. Return true if you want Garlic behavior, return false if you want to override it + } + } + + /* GARLIC DATA-API + * =============== */ + $( window ).on( 'load', function () { + $( '[data-persist="garlic"]' ).each( function () { + $(this).garlic(); + }) + }); + +// This plugin works with jQuery or Zepto (with data extension builded for Zepto. See changelog 0.0.6) +}(window.jQuery || window.Zepto);