Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
choonkeat committed Feb 21, 2015
0 parents commit 8ab6bb8
Show file tree
Hide file tree
Showing 15 changed files with 528 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
@@ -0,0 +1,8 @@
.bundle/
log/*.log
pkg/
test/dummy/db/*.sqlite3
test/dummy/db/*.sqlite3-journal
test/dummy/log/*.log
test/dummy/tmp/
test/dummy/.sass-cache
14 changes: 14 additions & 0 deletions Gemfile
@@ -0,0 +1,14 @@
source 'https://rubygems.org'

# Declare your gem's dependencies in attache_client.gemspec.
# Bundler will treat runtime dependencies like base dependencies, and
# development dependencies will be added by default to the :development group.
gemspec

# Declare any dependencies that are still in development here instead of in
# your gemspec. These might include edge Rails or gems from your path or
# Git. Remember to move these dependencies to your gemspec before releasing
# your gem to rubygems.org.

# To use a debugger
# gem 'byebug', group: [:development, :test]
14 changes: 14 additions & 0 deletions Gemfile.lock
@@ -0,0 +1,14 @@
PATH
remote: .
specs:
attache_client (0.0.1)

GEM
remote: https://rubygems.org/
specs:

PLATFORMS
ruby

DEPENDENCIES
attache_client!
20 changes: 20 additions & 0 deletions MIT-LICENSE
@@ -0,0 +1,20 @@
Copyright 2015 choonkeat

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.
102 changes: 102 additions & 0 deletions README.md
@@ -0,0 +1,102 @@
# attache_client

Ruby on Rails integration for [attache server](https://github.com/choonkeat/attache)

## Dependencies

[React](https://github.com/reactjs/react-rails), jQuery, Bootstrap 3

## Installation

Install the attache_client package from Rubygems:

``` bash
gem install attache_client
```

Or add this to your `Gemfile`

``` ruby
gem "attache_client"
```

Add the attache javascript to your `application.js`

``` javascript
//= require attache
```

Or you can include the various scripts yourself

``` javascript
//= require attache/cors_upload
//= require attache/bootstrap3
//= require attache/ujs
```

## Usage

### Database

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

``` bash
rails generate migration AddPhotoPathToUsers photo_path:string
```

To assign **multiple** images to **one** model, you'd only need one text field

``` bash
rails generate migration AddPhotoPathToUsers photo_path:text
```

### Model

In your model, `serialize` the column

``` ruby
class User < ActiveRecord::Base
serialize :photo_path, JSON
end
```

### New or Edit form

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:

``` slim
= f.file_field :photo_path
```

Change it to

``` slim
= f.file_field :photo_path, **attache_options('64x64#', f.object.photo_path)
```

Or if you're expecting multiple files uploaded, simply add `multiple: true`

``` slim
= f.file_field :photo_path, multiple: true, **attache_options('64x64#', f.object.photo_path)
```

NOTE: `64x64#` is just an example, you should define a suitable [geometry](http://www.imagemagick.org/Usage/resize/) for your form

### Show

Use the `attache_urls` helper to obtain full urls for the values you've captured in your database.

``` slim
- attache_urls(@user.photo_path, '128x128#') do |url|
= image_tag(url)
```

Alternatively, you can get the list of urls and manipulate it however you want

``` slim
= image_tag attache_urls(@user.photo_path, '128x128#').sample
```

# License

MIT
1 change: 1 addition & 0 deletions Rakefile
@@ -0,0 +1 @@
require "bundler/gem_tasks"
3 changes: 3 additions & 0 deletions app/assets/javascripts/attache.js
@@ -0,0 +1,3 @@
//= require attache/cors_upload
//= require attache/bootstrap3
//= require attache/ujs
105 changes: 105 additions & 0 deletions app/assets/javascripts/attache/bootstrap3.js
@@ -0,0 +1,105 @@
var AttacheFileInput = React.createClass({displayName: "AttacheFileInput",

getInitialState: function() {
var files = {};
var array = ([].concat(JSON.parse(this.props['data-value'])));
$.each(array, function(uid, json) {
if (json) files[uid] = { path: json };
});
return {files: files};
},

onRemove: function(uid, e) {
delete this.state.files[uid];
this.setState(this.state);
e.preventDefault();
e.stopPropagation();
},

onChange: function() {
var file_element = this.getDOMNode().firstChild;
// user cancelled file chooser dialog. ignore
if (file_element.files.length == 0) return;
this.state.files = {};
this.setState(this.state);
// upload the file via CORS
new CORSUpload({
file_element: file_element, onComplete: this.setFileValue, onProgress: this.setFileValue,
onError: function(uid, status) { alert(status); }
});
// we don't want the file binary to be uploaded in the main form
file_element.value = '';
},

setFileValue: function(key, value) {
this.state.files[key] = value;
this.setState(this.state);
},

render: function() {
var that = this;
var previews = [];
$.each(that.state.files, function(key, result) {
var json = JSON.stringify(result);
if (result.path) {
var parts = result.path.split('/');
parts.splice(parts.length-1, 0, encodeURIComponent(that.props['data-geometry'] || '128x128#'));
result.src = that.props['data-downloadurl'] + '/' + parts.join('/');
}
previews.push(
React.createElement("div", {className: "thumbnail"},
React.createElement("input", {type: "hidden", name: that.props.name, value: result.path, readOnly: "true"}),
React.createElement(AttacheFilePreview, React.__spread({}, result, {key: key, onRemove: that.onRemove.bind(that, key)}))
)
);
});
return (
React.createElement("label", {htmlFor: this.props.id, className: "attache-file-selector"},
React.createElement("input", React.__spread({type: "file"}, this.props, {onChange: this.onChange})),
previews
)
);
}
});

var AttacheFilePreview = React.createClass({displayName: "AttacheFilePreview",

getInitialState: function() {
return { srcWas: '' };
},

removeProgressBar: function() {
this.setState({ srcWas: this.props.src });
},

render: function() {
var className = "progress-bar progress-bar-striped active" + (this.props.src ? " progress-bar-success" : "");
var pctString = (this.props.src ? 100 : this.props.percentLoaded) + "%";
var pctDesc = (this.props.src ? 'Loading...' : pctString);
var img = (this.props.src ? (React.createElement("img", {src: this.props.src, onLoad: this.removeProgressBar})) : '');
var pctStyle = { width: pctString, minWidth: '3em' };
var cptStyle = { textOverflow: "ellipsis" };
var caption = React.createElement("div", {className: "pull-left", style: cptStyle}, this.props.filename || this.props.path.split('/').pop());

if (this.state.srcWas != this.props.src) {
var progress = (
React.createElement("div", {className: "progress"},
React.createElement("div", {className: className, role: "progressbar", "aria-valuenow": this.props.percentLoaded, "aria-valuemin": "0", "aria-valuemax": "100", style: pctStyle},
pctDesc
)
)
);
}

return (
React.createElement("div", {className: "attache-file-preview"},
progress,
img,
React.createElement("div", {className: "clearfix"},
caption,
React.createElement("a", {href: "#remove", className: "pull-right", onClick: this.props.onRemove, title: "Click to remove"}, "×")
)
)
);
}
});
105 changes: 105 additions & 0 deletions app/assets/javascripts/attache/bootstrap3.js.jsx
@@ -0,0 +1,105 @@
var AttacheFileInput = React.createClass({

getInitialState: function() {
var files = {};
var array = ([].concat(JSON.parse(this.props['data-value'])));
$.each(array, function(uid, json) {
if (json) files[uid] = { path: json };
});
return {files: files};
},

onRemove: function(uid, e) {
delete this.state.files[uid];
this.setState(this.state);
e.preventDefault();
e.stopPropagation();
},

onChange: function() {
var file_element = this.getDOMNode().firstChild;
// user cancelled file chooser dialog. ignore
if (file_element.files.length == 0) return;
this.state.files = {};
this.setState(this.state);
// upload the file via CORS
new CORSUpload({
file_element: file_element, onComplete: this.setFileValue, onProgress: this.setFileValue,
onError: function(uid, status) { alert(status); }
});
// we don't want the file binary to be uploaded in the main form
file_element.value = '';
},

setFileValue: function(key, value) {
this.state.files[key] = value;
this.setState(this.state);
},

render: function() {
var that = this;
var previews = [];
$.each(that.state.files, function(key, result) {
var json = JSON.stringify(result);
if (result.path) {
var parts = result.path.split('/');
parts.splice(parts.length-1, 0, encodeURIComponent(that.props['data-geometry'] || '128x128#'));
result.src = that.props['data-downloadurl'] + '/' + parts.join('/');
}
previews.push(
<div className="thumbnail">
<input type="hidden" name={that.props.name} value={result.path} readOnly="true" />
<AttacheFilePreview {...result} key={key} onRemove={that.onRemove.bind(that, key)}/>
</div>
);
});
return (
<label htmlFor={this.props.id} className="attache-file-selector">
<input type="file" {...this.props} onChange={this.onChange}/>
{previews}
</label>
);
}
});

var AttacheFilePreview = React.createClass({

getInitialState: function() {
return { srcWas: '' };
},

removeProgressBar: function() {
this.setState({ srcWas: this.props.src });
},

render: function() {
var className = "progress-bar progress-bar-striped active" + (this.props.src ? " progress-bar-success" : "");
var pctString = (this.props.src ? 100 : this.props.percentLoaded) + "%";
var pctDesc = (this.props.src ? 'Loading...' : pctString);
var img = (this.props.src ? (<img src={this.props.src} onLoad={this.removeProgressBar} />) : '');
var pctStyle = { width: pctString, minWidth: '3em' };
var cptStyle = { textOverflow: "ellipsis" };
var caption = <div className="pull-left" style={cptStyle}>{this.props.filename || this.props.path.split('/').pop()}</div>;

if (this.state.srcWas != this.props.src) {
var progress = (
<div className="progress">
<div className={className} role="progressbar" aria-valuenow={this.props.percentLoaded} aria-valuemin="0" aria-valuemax="100" style={pctStyle}>
{pctDesc}
</div>
</div>
);
}

return (
<div className="attache-file-preview">
{progress}
{img}
<div className="clearfix">
{caption}
<a href="#remove" className="pull-right" onClick={this.props.onRemove} title="Click to remove">&times;</a>
</div>
</div>
);
}
});

0 comments on commit 8ab6bb8

Please sign in to comment.