attache_client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6d9aaa6578d9c8d8cc1d1f0035dc3c5e6e26bd37
4
+ data.tar.gz: ee8085a4cf5bd23635fc28bf7308f0adbcb1bc96
5
+ SHA512:
6
+ metadata.gz: 04641dbd477892eb63e024f6793f88fe16e0c8e925c3044924a031eae7df2ce8395564dd3af641c1e6a8da04631c58d6bc3fde7bc5b7d9a5d431b43a563c522f
7
+ data.tar.gz: 227455a48ab4155e7ca2a9ea7fbf9266e4fc7a641bbd378f7686e7832d1963a9b0cb84c780f52820faca95610fee7f363ec494057411d96897bbc6156dfb572d
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015 choonkeat
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.
data/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # attache_client
2
+
3
+ Ruby on Rails integration for [attache server](https://github.com/choonkeat/attache)
4
+
5
+ ## Dependencies
6
+
7
+ [React](https://github.com/reactjs/react-rails), jQuery, Bootstrap 3
8
+
9
+ ## Installation
10
+
11
+ Install the attache_client package from Rubygems:
12
+
13
+ ``` bash
14
+ gem install attache_client
15
+ ```
16
+
17
+ Or add this to your `Gemfile`
18
+
19
+ ``` ruby
20
+ gem "attache_client"
21
+ ```
22
+
23
+ Add the attache javascript to your `application.js`
24
+
25
+ ``` javascript
26
+ //= require attache
27
+ ```
28
+
29
+ Or you can include the various scripts yourself
30
+
31
+ ``` javascript
32
+ //= require attache/cors_upload
33
+ //= require attache/bootstrap3
34
+ //= require attache/ujs
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Database
40
+
41
+ To use `attache`, you only need to store the `path`, given to you after you've uploaded a file. So if you have an existing model, you only need to add a string, varchar or text field
42
+
43
+ ``` bash
44
+ rails generate migration AddPhotoPathToUsers photo_path:string
45
+ ```
46
+
47
+ To assign **multiple** images to **one** model, you'd only need one text field
48
+
49
+ ``` bash
50
+ rails generate migration AddPhotoPathToUsers photo_path:text
51
+ ```
52
+
53
+ ### Model
54
+
55
+ In your model, `serialize` the column
56
+
57
+ ``` ruby
58
+ class User < ActiveRecord::Base
59
+ serialize :photo_path, JSON
60
+ end
61
+ ```
62
+
63
+ ### New or Edit form
64
+
65
+ In your form, you would add some options to `file_field` using the `attache_options` helper method. For example, a regular file field may look like this:
66
+
67
+ ``` slim
68
+ = f.file_field :photo_path
69
+ ```
70
+
71
+ Change it to
72
+
73
+ ``` slim
74
+ = f.file_field :photo_path, **attache_options('64x64#', f.object.photo_path)
75
+ ```
76
+
77
+ Or if you're expecting multiple files uploaded, simply add `multiple: true`
78
+
79
+ ``` slim
80
+ = f.file_field :photo_path, multiple: true, **attache_options('64x64#', f.object.photo_path)
81
+ ```
82
+
83
+ NOTE: `64x64#` is just an example, you should define a suitable [geometry](http://www.imagemagick.org/Usage/resize/) for your form
84
+
85
+ ### Show
86
+
87
+ Use the `attache_urls` helper to obtain full urls for the values you've captured in your database.
88
+
89
+ ``` slim
90
+ - attache_urls(@user.photo_path, '128x128#') do |url|
91
+ = image_tag(url)
92
+ ```
93
+
94
+ Alternatively, you can get the list of urls and manipulate it however you want
95
+
96
+ ``` slim
97
+ = image_tag attache_urls(@user.photo_path, '128x128#').sample
98
+ ```
99
+
100
+ # License
101
+
102
+ MIT
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,3 @@
1
+ //= require attache/cors_upload
2
+ //= require attache/bootstrap3
3
+ //= require attache/ujs
@@ -0,0 +1,105 @@
1
+ var AttacheFileInput = React.createClass({displayName: "AttacheFileInput",
2
+
3
+ getInitialState: function() {
4
+ var files = {};
5
+ var array = ([].concat(JSON.parse(this.props['data-value'])));
6
+ $.each(array, function(uid, json) {
7
+ if (json) files[uid] = { path: json };
8
+ });
9
+ return {files: files};
10
+ },
11
+
12
+ onRemove: function(uid, e) {
13
+ delete this.state.files[uid];
14
+ this.setState(this.state);
15
+ e.preventDefault();
16
+ e.stopPropagation();
17
+ },
18
+
19
+ onChange: function() {
20
+ var file_element = this.getDOMNode().firstChild;
21
+ // user cancelled file chooser dialog. ignore
22
+ if (file_element.files.length == 0) return;
23
+ this.state.files = {};
24
+ this.setState(this.state);
25
+ // upload the file via CORS
26
+ new CORSUpload({
27
+ file_element: file_element, onComplete: this.setFileValue, onProgress: this.setFileValue,
28
+ onError: function(uid, status) { alert(status); }
29
+ });
30
+ // we don't want the file binary to be uploaded in the main form
31
+ file_element.value = '';
32
+ },
33
+
34
+ setFileValue: function(key, value) {
35
+ this.state.files[key] = value;
36
+ this.setState(this.state);
37
+ },
38
+
39
+ render: function() {
40
+ var that = this;
41
+ var previews = [];
42
+ $.each(that.state.files, function(key, result) {
43
+ var json = JSON.stringify(result);
44
+ if (result.path) {
45
+ var parts = result.path.split('/');
46
+ parts.splice(parts.length-1, 0, encodeURIComponent(that.props['data-geometry'] || '128x128#'));
47
+ result.src = that.props['data-downloadurl'] + '/' + parts.join('/');
48
+ }
49
+ previews.push(
50
+ React.createElement("div", {className: "thumbnail"},
51
+ React.createElement("input", {type: "hidden", name: that.props.name, value: result.path, readOnly: "true"}),
52
+ React.createElement(AttacheFilePreview, React.__spread({}, result, {key: key, onRemove: that.onRemove.bind(that, key)}))
53
+ )
54
+ );
55
+ });
56
+ return (
57
+ React.createElement("label", {htmlFor: this.props.id, className: "attache-file-selector"},
58
+ React.createElement("input", React.__spread({type: "file"}, this.props, {onChange: this.onChange})),
59
+ previews
60
+ )
61
+ );
62
+ }
63
+ });
64
+
65
+ var AttacheFilePreview = React.createClass({displayName: "AttacheFilePreview",
66
+
67
+ getInitialState: function() {
68
+ return { srcWas: '' };
69
+ },
70
+
71
+ removeProgressBar: function() {
72
+ this.setState({ srcWas: this.props.src });
73
+ },
74
+
75
+ render: function() {
76
+ var className = "progress-bar progress-bar-striped active" + (this.props.src ? " progress-bar-success" : "");
77
+ var pctString = (this.props.src ? 100 : this.props.percentLoaded) + "%";
78
+ var pctDesc = (this.props.src ? 'Loading...' : pctString);
79
+ var img = (this.props.src ? (React.createElement("img", {src: this.props.src, onLoad: this.removeProgressBar})) : '');
80
+ var pctStyle = { width: pctString, minWidth: '3em' };
81
+ var cptStyle = { textOverflow: "ellipsis" };
82
+ var caption = React.createElement("div", {className: "pull-left", style: cptStyle}, this.props.filename || this.props.path.split('/').pop());
83
+
84
+ if (this.state.srcWas != this.props.src) {
85
+ var progress = (
86
+ React.createElement("div", {className: "progress"},
87
+ React.createElement("div", {className: className, role: "progressbar", "aria-valuenow": this.props.percentLoaded, "aria-valuemin": "0", "aria-valuemax": "100", style: pctStyle},
88
+ pctDesc
89
+ )
90
+ )
91
+ );
92
+ }
93
+
94
+ return (
95
+ React.createElement("div", {className: "attache-file-preview"},
96
+ progress,
97
+ img,
98
+ React.createElement("div", {className: "clearfix"},
99
+ caption,
100
+ React.createElement("a", {href: "#remove", className: "pull-right", onClick: this.props.onRemove, title: "Click to remove"}, "×")
101
+ )
102
+ )
103
+ );
104
+ }
105
+ });
@@ -0,0 +1,105 @@
1
+ var AttacheFileInput = React.createClass({
2
+
3
+ getInitialState: function() {
4
+ var files = {};
5
+ var array = ([].concat(JSON.parse(this.props['data-value'])));
6
+ $.each(array, function(uid, json) {
7
+ if (json) files[uid] = { path: json };
8
+ });
9
+ return {files: files};
10
+ },
11
+
12
+ onRemove: function(uid, e) {
13
+ delete this.state.files[uid];
14
+ this.setState(this.state);
15
+ e.preventDefault();
16
+ e.stopPropagation();
17
+ },
18
+
19
+ onChange: function() {
20
+ var file_element = this.getDOMNode().firstChild;
21
+ // user cancelled file chooser dialog. ignore
22
+ if (file_element.files.length == 0) return;
23
+ this.state.files = {};
24
+ this.setState(this.state);
25
+ // upload the file via CORS
26
+ new CORSUpload({
27
+ file_element: file_element, onComplete: this.setFileValue, onProgress: this.setFileValue,
28
+ onError: function(uid, status) { alert(status); }
29
+ });
30
+ // we don't want the file binary to be uploaded in the main form
31
+ file_element.value = '';
32
+ },
33
+
34
+ setFileValue: function(key, value) {
35
+ this.state.files[key] = value;
36
+ this.setState(this.state);
37
+ },
38
+
39
+ render: function() {
40
+ var that = this;
41
+ var previews = [];
42
+ $.each(that.state.files, function(key, result) {
43
+ var json = JSON.stringify(result);
44
+ if (result.path) {
45
+ var parts = result.path.split('/');
46
+ parts.splice(parts.length-1, 0, encodeURIComponent(that.props['data-geometry'] || '128x128#'));
47
+ result.src = that.props['data-downloadurl'] + '/' + parts.join('/');
48
+ }
49
+ previews.push(
50
+ <div className="thumbnail">
51
+ <input type="hidden" name={that.props.name} value={result.path} readOnly="true" />
52
+ <AttacheFilePreview {...result} key={key} onRemove={that.onRemove.bind(that, key)}/>
53
+ </div>
54
+ );
55
+ });
56
+ return (
57
+ <label htmlFor={this.props.id} className="attache-file-selector">
58
+ <input type="file" {...this.props} onChange={this.onChange}/>
59
+ {previews}
60
+ </label>
61
+ );
62
+ }
63
+ });
64
+
65
+ var AttacheFilePreview = React.createClass({
66
+
67
+ getInitialState: function() {
68
+ return { srcWas: '' };
69
+ },
70
+
71
+ removeProgressBar: function() {
72
+ this.setState({ srcWas: this.props.src });
73
+ },
74
+
75
+ render: function() {
76
+ var className = "progress-bar progress-bar-striped active" + (this.props.src ? " progress-bar-success" : "");
77
+ var pctString = (this.props.src ? 100 : this.props.percentLoaded) + "%";
78
+ var pctDesc = (this.props.src ? 'Loading...' : pctString);
79
+ var img = (this.props.src ? (<img src={this.props.src} onLoad={this.removeProgressBar} />) : '');
80
+ var pctStyle = { width: pctString, minWidth: '3em' };
81
+ var cptStyle = { textOverflow: "ellipsis" };
82
+ var caption = <div className="pull-left" style={cptStyle}>{this.props.filename || this.props.path.split('/').pop()}</div>;
83
+
84
+ if (this.state.srcWas != this.props.src) {
85
+ var progress = (
86
+ <div className="progress">
87
+ <div className={className} role="progressbar" aria-valuenow={this.props.percentLoaded} aria-valuemin="0" aria-valuemax="100" style={pctStyle}>
88
+ {pctDesc}
89
+ </div>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ return (
95
+ <div className="attache-file-preview">
96
+ {progress}
97
+ {img}
98
+ <div className="clearfix">
99
+ {caption}
100
+ <a href="#remove" className="pull-right" onClick={this.props.onRemove} title="Click to remove">&times;</a>
101
+ </div>
102
+ </div>
103
+ );
104
+ }
105
+ });
@@ -0,0 +1,75 @@
1
+ var CORSUpload = (function() {
2
+ var counter = 0;
3
+
4
+ CORSUpload.prototype.onComplete = function(uid, json) { };
5
+ CORSUpload.prototype.onProgress = function(uid, json) { };
6
+ CORSUpload.prototype.onError = function(uid, status) { alert(status); };
7
+
8
+ function CORSUpload(options) {
9
+ if (options == null) options = {};
10
+ for (option in options) {
11
+ this[option] = options[option];
12
+ }
13
+ this.handleFileSelect(options.file_element);
14
+ }
15
+
16
+ CORSUpload.prototype.handleFileSelect = function(file_element) {
17
+ var f, files, output, _i, _len, _results, url;
18
+ url = $(file_element).data('uploadurl');
19
+ files = file_element.files;
20
+ output = [];
21
+ _results = [];
22
+ for (_i = 0, _len = files.length; _i < _len; _i++) {
23
+ f = files[_i];
24
+ f.uid = counter++;
25
+ this.onProgress(f.uid, { filename: f.name, percentLoaded: 0, bytesLoaded: 0, bytesTotal: f.size });
26
+ _results.push(this.performUpload(f, url));
27
+ }
28
+ return _results;
29
+ };
30
+
31
+ CORSUpload.prototype.createCORSRequest = function(method, url) {
32
+ var xhr;
33
+ xhr = new XMLHttpRequest();
34
+ if (xhr.withCredentials != null) {
35
+ xhr.open(method, url, true);
36
+ } else if (typeof XDomainRequest !== "undefined") {
37
+ xhr = new XDomainRequest();
38
+ xhr.open(method, url);
39
+ } else {
40
+ xhr = null;
41
+ }
42
+ return xhr;
43
+ };
44
+
45
+ CORSUpload.prototype.performUpload = function(file, url) {
46
+ var this_s3upload, xhr;
47
+ this_s3upload = this;
48
+ xhr = this.createCORSRequest('PUT', url + (url.indexOf('?') == -1 ? '?' : '&') + 'file=' + encodeURIComponent(file.name));
49
+ if (!xhr) {
50
+ this.onError(file.uid, 'CORS not supported');
51
+ } else {
52
+ xhr.onload = function(e) {
53
+ if (xhr.status === 200) {
54
+ this_s3upload.onComplete(file.uid, JSON.parse(e.target.responseText));
55
+ } else {
56
+ return this_s3upload.onError(file.uid, 'Upload error: ' + xhr.status);
57
+ }
58
+ };
59
+ xhr.onerror = function() {
60
+ return this_s3upload.onError(file.uid, 'XHR error.');
61
+ };
62
+ xhr.upload.onprogress = function(e) {
63
+ var percentLoaded;
64
+ if (e.lengthComputable) {
65
+ percentLoaded = Math.round((e.loaded / e.total) * 100);
66
+ return this_s3upload.onProgress(file.uid, { filename: file.name, percentLoaded: percentLoaded, bytesLoaded: e.loaded, bytesTotal: e.total });
67
+ }
68
+ };
69
+ }
70
+ return xhr.send(file);
71
+ };
72
+
73
+ return CORSUpload;
74
+
75
+ })();
@@ -0,0 +1,23 @@
1
+ (function() {
2
+ function attacheFileInputs() {
3
+ var safeWords = { 'class': 'className', 'for': 'htmlFor' };
4
+ var sel = document.getElementsByClassName('enable-attache');
5
+ var ele, attrs, name, value;
6
+ for (var i = sel.length-1; i >= 0; i--) {
7
+ ele = sel[i];
8
+ attrs = {};
9
+ for (var j = 0; j < ele.attributes.length; j++) {
10
+ name = ele.attributes[j].name;
11
+ value = ele.attributes[j].value;
12
+ if (name === 'class') value = value.replace('enable-attache', 'attache-enabled');
13
+ name = safeWords[name] || name;
14
+ attrs[name] = value;
15
+ }
16
+ var wrap = document.createElement('div');
17
+ ele.parentNode.replaceChild(wrap, ele); // ele.parentNode.insertBefore(wrap, ele.nextSibling);
18
+ React.render(React.createElement(AttacheFileInput, React.__spread({}, attrs)), wrap);
19
+ }
20
+ }
21
+
22
+ $(document).on('page:change', attacheFileInputs);
23
+ })();
@@ -0,0 +1,33 @@
1
+ require "attache_client/engine"
2
+
3
+ module AttacheClient
4
+ module ViewHelper
5
+ ATTACHE_UPLOAD_URL = ENV.fetch('ATTACHE_UPLOAD_URL') { 'http://localhost:9292/upload' }
6
+ ATTACHE_DOWNLOAD_URL = ENV.fetch('ATTACHE_DOWNLOAD_URL') { 'http://localhost:9292/view' }
7
+
8
+ def attache_urls(json, geometry)
9
+ array = json.kind_of?(Array) ? json : [json]
10
+ array.collect do |path|
11
+ download_url = ATTACHE_DOWNLOAD_URL
12
+ prefix, basename = File.split(path)
13
+ [download_url, prefix, CGI.escape(geometry), basename].join('/').tap do |url|
14
+ yield url if block_given?
15
+ end
16
+ end
17
+ end
18
+
19
+ def attache_options(geometry, current_value)
20
+ {
21
+ class: 'enable-attache',
22
+ data: {
23
+ geometry: geometry,
24
+ value: [*current_value],
25
+ uploadurl: ATTACHE_UPLOAD_URL,
26
+ downloadurl: ATTACHE_DOWNLOAD_URL,
27
+ },
28
+ }
29
+ end
30
+ end
31
+ end
32
+
33
+ ActionView::Base.send(:include, AttacheClient::ViewHelper)
@@ -0,0 +1,5 @@
1
+ module AttacheClient
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace AttacheClient
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module AttacheClient
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: attache_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - choonkeat
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-21 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - choonkeat@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - MIT-LICENSE
21
+ - README.md
22
+ - Rakefile
23
+ - app/assets/javascripts/attache.js
24
+ - app/assets/javascripts/attache/bootstrap3.js
25
+ - app/assets/javascripts/attache/bootstrap3.js.jsx
26
+ - app/assets/javascripts/attache/cors_upload.js
27
+ - app/assets/javascripts/attache/ujs.js
28
+ - lib/attache_client.rb
29
+ - lib/attache_client/engine.rb
30
+ - lib/attache_client/version.rb
31
+ homepage: https://github.com/choonkeat/attache_client
32
+ licenses:
33
+ - MIT
34
+ metadata: {}
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project:
51
+ rubygems_version: 2.4.5
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: Client lib to use attache server
55
+ test_files: []