garlicjs-rails 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Vasiliy Ermolovich
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # garlicjs-rails
2
+
3
+ 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.
4
+
5
+ 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.
6
+
7
+ 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!
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'garlicjs-rails'
14
+
15
+ ## Usage
16
+
17
+ Add the following directive to your Javascript manifest file (application.js):
18
+
19
+ //= require garlic
20
+
21
+ ## Versioning
22
+
23
+ garlicjs-rails 1.0.2 == Garlic.js 1.0.2
24
+
25
+ ## Contributing
26
+
27
+ 1. Fork it
28
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
29
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
30
+ 4. Push to the branch (`git push origin my-new-feature`)
31
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'garlicjs-rails/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "garlicjs-rails"
8
+ gem.version = Garlicjs::Rails::VERSION
9
+ gem.authors = ["Vasiliy Ermolovich"]
10
+ gem.email = ["younash@gmail.com"]
11
+ gem.description = %q{Garlic.js allows you to automatically persist your forms' text field values locally, until the form is submitted}
12
+ gem.summary = %q{Garlic.js allows you to automatically persist your forms' text field values locally, until the form is submitted}
13
+ gem.homepage = "http://github.com/nashby/garlicjs-rails"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.require_paths = ["lib"]
17
+ gem.add_dependency 'railties', '>= 3.1'
18
+ end
@@ -0,0 +1,8 @@
1
+ require "garlicjs-rails/version"
2
+
3
+ module Garlicjs
4
+ module Rails
5
+ class Engine < ::Rails::Engine
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ module Garlicjs
2
+ module Rails
3
+ VERSION = "1.0.2"
4
+ end
5
+ end
@@ -0,0 +1,379 @@
1
+ /*
2
+ Garlic.js allows you to automatically persist your forms' text field values locally,
3
+ until the form is submitted. This way, your users don't lose any precious data if they
4
+ accidentally close their tab or browser.
5
+
6
+ author: Guillaume Potier - @guillaumepotier
7
+ */
8
+
9
+ !function ($) {
10
+
11
+ "use strict";
12
+ /*global localStorage */
13
+ /*global document */
14
+
15
+ /* STORAGE PUBLIC CLASS DEFINITION
16
+ * =============================== */
17
+ var Storage = function ( options ) {
18
+ this.defined = 'undefined' !== typeof localStorage;
19
+ }
20
+
21
+ Storage.prototype = {
22
+
23
+ constructor: Storage
24
+
25
+ , get: function ( key, placeholder ) {
26
+ return localStorage.getItem( key ) ? localStorage.getItem( key ) : 'undefined' !== typeof placeholder ? placeholder : null;
27
+ }
28
+
29
+ , has: function ( key ) {
30
+ return localStorage.getItem( key ) ? true : false;
31
+ }
32
+
33
+ , set: function ( key, value, fn ) {
34
+ if ( 'string' === typeof value ) {
35
+
36
+ // if value is null, remove storage if exists
37
+ if ( '' === value ) {
38
+ this.destroy( key );
39
+ } else {
40
+ localStorage.setItem( key , value );
41
+ }
42
+ }
43
+
44
+ return 'function' === typeof fn ? fn() : true;
45
+ }
46
+
47
+ , destroy: function ( key, fn ) {
48
+ localStorage.removeItem( key );
49
+ return 'function' === typeof fn ? fn() : true;
50
+ }
51
+
52
+ , clean: function ( fn ) {
53
+ for ( var i = localStorage.length - 1; i >= 0; i-- ) {
54
+ if ( 'undefined' === typeof Array.indexOf && -1 !== localStorage.key(i).indexOf( 'garlic:' ) ) {
55
+ localStorage.removeItem( localStorage.key(i) );
56
+ }
57
+ }
58
+
59
+ return 'function' === typeof fn ? fn() : true;
60
+ }
61
+
62
+ , clear: function ( fn ) {
63
+ localStorage.clear();
64
+ return 'function' === typeof fn ? fn() : true;
65
+ }
66
+ }
67
+
68
+ /* GARLIC PUBLIC CLASS DEFINITION
69
+ * =============================== */
70
+
71
+ var Garlic = function ( element, storage, options ) {
72
+ this.init( 'garlic', element, storage, options );
73
+ }
74
+
75
+ Garlic.prototype = {
76
+
77
+ constructor: Garlic
78
+
79
+ /* init data, bind jQuery on() actions */
80
+ , init: function ( type, element, storage, options ) {
81
+ this.type = type;
82
+ this.$element = $( element );
83
+ this.options = this.getOptions( options );
84
+ this.storage = storage;
85
+ this.path = this.getPath();
86
+ this.parentForm = this.$element.closest( 'form' );
87
+
88
+ this.retrieve();
89
+
90
+ this.$element.on( this.options.events.join( '.' + this.type + ' ') , false, $.proxy( this.persist, this ) );
91
+
92
+ if ( this.options.destroy ) {
93
+ this.$element.closest( 'form' ).on( 'submit reset' , false, $.proxy( this.destroy, this ) );
94
+ }
95
+
96
+ this.$element.addClass('garlic-auto-save');
97
+ }
98
+
99
+ , getOptions: function ( options ) {
100
+ options = $.extend( {}, $.fn[this.type].defaults, options, this.$element.data() );
101
+
102
+ return options;
103
+ }
104
+
105
+ /* temporary store data / state in localStorage */
106
+ , persist: function () {
107
+ // for checkboxes, we need to implement an unchecked / checked behavior
108
+ if ( this.$element.is( 'input[type=checkbox]' ) ) {
109
+ return this.storage.set( this.path , this.$element.attr( 'checked' ) ? 'checked' : 'unchecked' );
110
+ }
111
+
112
+ this.storage.set( this.path , this.$element.val() );
113
+ }
114
+
115
+ /* retrieve localStorage data / state and update elem accordingly */
116
+ , retrieve: function () {
117
+ if ( this.storage.has( this.path ) ) {
118
+
119
+ // if conflictManager enabled, manage fields with already provided data, different from the one stored
120
+ if ( this.options.conflictManager.enabled && this.detectConflict() ) {
121
+ return this.conflictManager();
122
+ }
123
+
124
+ // input[type=checkbox] and input[type=radio] have a special checked / unchecked behavior
125
+ if ( this.$element.is( 'input[type=radio], input[type=checkbox]' ) ) {
126
+
127
+ // for checkboxes and radios
128
+ if ( 'checked' === this.storage.get( this.path ) || this.storage.get( this.path ) === this.$element.val() ) {
129
+ return this.$element.attr( 'checked', true );
130
+
131
+ // only needed for checkboxes
132
+ } else if ( 'unchecked' === this.storage.get( this.path ) ) {
133
+ this.$element.attr( 'checked', false );
134
+ }
135
+
136
+ return;
137
+ }
138
+
139
+ // for input[type=text], select and textarea, just set val()
140
+ this.$element.val( this.storage.get( this.path ) );
141
+ }
142
+ }
143
+
144
+ /* there is a conflict when initial data / state differs from persisted data / state */
145
+ , detectConflict: function() {
146
+ var self = this;
147
+
148
+ // radio buttons and checkboxes are yet not supported
149
+ if ( this.$element.is( 'input[type=checkbox], input[type=radio]' ) ) {
150
+ return false;
151
+ }
152
+
153
+ // there is a default not null value and we have a different one stored
154
+ if ( this.$element.val() && this.storage.get( this.path ) !== this.$element.val() ) {
155
+
156
+ // for select elements, we need to check if there is a default checked value
157
+ if ( this.$element.is( 'select' ) ) {
158
+ var selectConflictDetected = false;
159
+
160
+ // foreach each options except first one, always considered as selected, seeking for a default selected one
161
+ this.$element.find( 'option' ).each( function () {
162
+ if ( $( this ).index() !== 0 && $( this ).attr( 'selected' ) && $( this ).val() !== self.storage.get( this.path ) ) {
163
+ selectConflictDetected = true;
164
+ return;
165
+ }
166
+ });
167
+
168
+ return selectConflictDetected;
169
+ }
170
+
171
+ return true;
172
+ }
173
+
174
+ return false;
175
+ }
176
+
177
+ /* manage here the conflict, show default value depending on options.garlicPriority value */
178
+ , conflictManager: function () {
179
+
180
+ // user can define here a custom function that could stop Garlic default behavior, if returns false
181
+ if ( 'function' === typeof this.options.conflictManager.onConflictDetected
182
+ && !this.options.conflictManager.onConflictDetected( this.$element, this.storage.get( this.path ) ) ) {
183
+ return false;
184
+ }
185
+
186
+ if ( this.options.conflictManager.garlicPriority ) {
187
+ this.$element.data( 'swap-data', this.$element.val() );
188
+ this.$element.data( 'swap-state', 'garlic' );
189
+ this.$element.val( this.storage.get( this.path ) );
190
+ } else {
191
+ this.$element.data( 'swap-data', this.storage.get( this.path ) );
192
+ this.$element.data( 'swap-state', 'default' );
193
+ }
194
+
195
+ this.swapHandler();
196
+ this.$element.addClass( 'garlic-conflict-detected' );
197
+ this.$element.closest( 'input[type=submit]' ).attr( 'disabled', true );
198
+ }
199
+
200
+ /* manage swap user interface */
201
+ , swapHandler: function () {
202
+ var swapChoiceElem = $( this.options.conflictManager.template );
203
+ this.$element.after( swapChoiceElem.text( this.options.conflictManager.message ) );
204
+ swapChoiceElem.on( 'click', false, $.proxy( this.swap, this ) );
205
+ }
206
+
207
+ /* swap data / states for conflicted elements */
208
+ , swap: function () {
209
+ var val = this.$element.data( 'swap-data' );
210
+ this.$element.data( 'swap-state', 'garlic' === this.$element.data( 'swap-state' ) ? 'default' : 'garlic' );
211
+ this.$element.data( 'swap-data', this.$element.val());
212
+ $( this.$element ).val( val );
213
+ }
214
+
215
+ /* delete localStorage persistance only */
216
+ , destroy: function () {
217
+ this.storage.destroy( this.path );
218
+ }
219
+
220
+ /* remove data / reset state AND delete localStorage */
221
+ , remove: function () {
222
+ this.remove();
223
+
224
+ if ( this.$element.is( 'input[type=radio], input[type=checkbox]' ) ) {
225
+ $( this.$element ).attr( 'checked', false );
226
+ return;
227
+ }
228
+
229
+ this.$element.val( '' );
230
+ }
231
+
232
+ /* retuns an unique identifier for form elements, depending on their behaviors:
233
+ * radio buttons: domain > pathname > form.<attr.name>[:eq(x)] > input.<attr.name>
234
+ no eq(); must be all stored under the same field name inside the same form
235
+
236
+ * checkbokes: domain > pathname > form.<attr.name>[:eq(x)] > [fieldset, div, span..] > input.<attr.name>[:eq(y)]
237
+ cuz' they have the same name, must detect their exact position in the form. detect the exact hierarchy in DOM elements
238
+
239
+ * other inputs: domain > pathname > form.<attr.name>[:eq(x)] > input.<attr.name>[:eq(y)]
240
+ we just need the element name / eq() inside a given form
241
+ */
242
+ , getPath: function () {
243
+
244
+ // Requires one element.
245
+ if ( this.$element.length != 1 ) {
246
+ return false;
247
+ }
248
+
249
+ var path = ''
250
+ , fullPath = this.$element.is( 'input[type=checkbox]' )
251
+ , node = this.$element;
252
+
253
+ while ( node.length ) {
254
+ var realNode = node[0]
255
+ , name = realNode.nodeName;
256
+
257
+ if ( !name ) {
258
+ break;
259
+ }
260
+
261
+ name = name.toLowerCase();
262
+
263
+ var parent = node.parent()
264
+ , siblings = parent.children( name );
265
+
266
+ // don't need to pollute path with select, fieldsets, divs and other noisy elements,
267
+ // exept for checkboxes that need exact path, cuz have same name and sometimes same eq()!
268
+ if ( !$( realNode ).is( 'form, input, select, textarea' ) && !fullPath ) {
269
+ node = parent;
270
+ continue;
271
+ }
272
+
273
+ // set input type as name + name attr if exists
274
+ name += $( realNode ).attr( 'name' ) ? '.' + $( realNode ).attr( 'name' ) : '';
275
+
276
+ // if has sibilings, get eq(), exept for radio buttons
277
+ if ( siblings.length > 1 && !$( realNode ).is( 'input[type=radio]' ) ) {
278
+ name += ':eq(' + siblings.index( realNode ) + ')';
279
+ }
280
+
281
+ path = name + ( path ? '>' + path : '' );
282
+
283
+ // break once we came up to form:eq(x), no need to go further
284
+ if ( 'form' == realNode.nodeName.toLowerCase() ) {
285
+ break;
286
+ }
287
+
288
+ node = parent;
289
+ }
290
+
291
+ return 'garlic:' + document.domain + ( this.options.domain ? '*' : window.location.pathname ) + '>' + path;
292
+ }
293
+
294
+ , getStorage: function () {
295
+ return this.storage;
296
+ }
297
+ }
298
+
299
+ /* GARLIC PLUGIN DEFINITION
300
+ * ========================= */
301
+
302
+ $.fn.garlic = function ( option, fn ) {
303
+ var options = $.extend(true, {}, $.fn.garlic.defaults, option, this.data() )
304
+ , storage = new Storage()
305
+ , returnValue = false;
306
+
307
+ // this plugin heavily rely on local Storage. If there is no localStorage or data-storage=false, no need to go further
308
+ if ( !storage.defined ) {
309
+ return false;
310
+ }
311
+
312
+ function bind ( self ) {
313
+ var $this = $( self )
314
+ , data = $this.data( 'garlic' )
315
+ , fieldOptions = $.extend( {}, options, $this.data() );
316
+
317
+ // don't bind an elem with data-storage=false
318
+ if ( 'undefined' !== typeof fieldOptions.storage && !fieldOptions.storage ) {
319
+ return;
320
+ }
321
+
322
+ // if data never binded, bind it right now!
323
+ if ( !data ) {
324
+ $this.data( 'garlic', ( data = new Garlic( self, storage, fieldOptions ) ) );
325
+ }
326
+
327
+ // here is our garlic public function accessor, currently does not support args
328
+ if ( 'string' === typeof option && 'function' === typeof data[option] ) {
329
+ return data[option]();
330
+ }
331
+ }
332
+
333
+ // loop through every elemt we want to garlic
334
+ this.each(function () {
335
+
336
+ // if a form elem is given, bind all its input children
337
+ if ( $( this ).is( 'form' ) ) {
338
+ $( this ).find( options.inputs ).each( function () {
339
+ returnValue = bind( $( this ) );
340
+ });
341
+
342
+ // if it is a Garlic supported single element, bind it too
343
+ // add here a return instance, cuz' we could call public methods on single elems with data[option]() above
344
+ } else if ( $( this ).is( options.inputs ) ) {
345
+ returnValue = bind( $( this ) );
346
+ }
347
+ });
348
+
349
+ return 'function' === typeof fn ? fn() : returnValue;
350
+ }
351
+
352
+ /* GARLIC CONFIGS & OPTIONS
353
+ * ========================= */
354
+ $.fn.garlic.Constructor = Garlic;
355
+
356
+ $.fn.garlic.defaults = {
357
+ destroy: true // remove or not localstorage on submit & clear
358
+ , inputs: 'input, textarea, select' // Default supported inputs.
359
+ , events: [ 'DOMAttrModified', 'textInput', 'input', 'change', 'keypress', 'paste', 'focus' ] // events list that trigger a localStorage
360
+ , domain: false // store et retrieve forms data accross all domain, not just on
361
+ , conflictManager: {
362
+ enabled: true // manage default data and persisted data. If false, persisted data will always replace default ones
363
+ , garlicPriority: true // if form have default data, garlic persisted data will be shown first
364
+ , template: '<span class="garlic-swap"></span>' // template used to swap between values if conflict detected
365
+ , message: 'This is your saved data. Click here to see default one' // default message for swapping data / state
366
+ , 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
367
+ }
368
+ }
369
+
370
+ /* GARLIC DATA-API
371
+ * =============== */
372
+ $( window ).on( 'load', function () {
373
+ $( '[data-persist="garlic"]' ).each( function () {
374
+ $(this).garlic();
375
+ })
376
+ });
377
+
378
+ // This plugin works with jQuery or Zepto (with data extension builded for Zepto. See changelog 0.0.6)
379
+ }(window.jQuery || window.Zepto);
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: garlicjs-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Vasiliy Ermolovich
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: railties
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '3.1'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '3.1'
30
+ description: Garlic.js allows you to automatically persist your forms' text field
31
+ values locally, until the form is submitted
32
+ email:
33
+ - younash@gmail.com
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - .gitignore
39
+ - Gemfile
40
+ - LICENSE.txt
41
+ - README.md
42
+ - Rakefile
43
+ - garlicjs-rails.gemspec
44
+ - lib/garlicjs-rails.rb
45
+ - lib/garlicjs-rails/version.rb
46
+ - vendor/assets/javascripts/garlic.js
47
+ homepage: http://github.com/nashby/garlicjs-rails
48
+ licenses: []
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ! '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ segments:
60
+ - 0
61
+ hash: 1480325064676632536
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ segments:
69
+ - 0
70
+ hash: 1480325064676632536
71
+ requirements: []
72
+ rubyforge_project:
73
+ rubygems_version: 1.8.24
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: Garlic.js allows you to automatically persist your forms' text field values
77
+ locally, until the form is submitted
78
+ test_files: []