attache_client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +102 -0
- data/Rakefile +1 -0
- data/app/assets/javascripts/attache.js +3 -0
- data/app/assets/javascripts/attache/bootstrap3.js +105 -0
- data/app/assets/javascripts/attache/bootstrap3.js.jsx +105 -0
- data/app/assets/javascripts/attache/cors_upload.js +75 -0
- data/app/assets/javascripts/attache/ujs.js +23 -0
- data/lib/attache_client.rb +33 -0
- data/lib/attache_client/engine.rb +5 -0
- data/lib/attache_client/version.rb +3 -0
- metadata +55 -0
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,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">×</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)
|
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: []
|