client/users: support avatar changing
This commit is contained in:
parent
c788061976
commit
4dcee37567
|
@ -20,7 +20,7 @@ form .input {
|
|||
form .buttons {
|
||||
margin-top: 1em;
|
||||
}
|
||||
form .input li:first-child label:not(.radio):not(.checkbox),
|
||||
form .input li:first-child label:not(.radio):not(.checkbox):not(.file-dropper),
|
||||
form .input li:first-child {
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
|
@ -33,7 +33,7 @@ form.tabular ul {
|
|||
form.tabular ul li {
|
||||
display: table-row;
|
||||
}
|
||||
form.tabular ul li label:not(.radio):not(.checkbox) {
|
||||
form.tabular ul li label:not(.radio):not(.checkbox):not(.file-dropper) {
|
||||
display: table-cell;
|
||||
width: 33%;
|
||||
}
|
||||
|
@ -219,3 +219,27 @@ button::-moz-focus-inner,
|
|||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* File dropper
|
||||
*/
|
||||
.file-dropper {
|
||||
background: white;
|
||||
border: 3px dashed #eee;
|
||||
padding: 0.3em 0.5em;
|
||||
line-height: 140%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
input[type=file]:disabled+.file-dropper {
|
||||
cursor: default;
|
||||
opacity: .5;
|
||||
}
|
||||
input[type=file]:active+.file-dropper,
|
||||
input[type=file]:focus+.file-dropper,
|
||||
.file-dropper.active {
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
|
|
|
@ -64,11 +64,26 @@
|
|||
margin-right: 1em;
|
||||
}
|
||||
#user-edit form {
|
||||
width: 22.5em;
|
||||
width: 100%;
|
||||
}
|
||||
#user-edit form {
|
||||
display: flex;
|
||||
}
|
||||
#user-edit .left {
|
||||
width: 65%;
|
||||
}
|
||||
#user-edit .right {
|
||||
width: 35%;
|
||||
margin-left: 1em;
|
||||
}
|
||||
#user-edit .file-dropper-holder {
|
||||
position: relative;
|
||||
}
|
||||
#user-edit .file-dropper {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
#user-delete form {
|
||||
width: 100%;
|
||||
}
|
||||
#user-delete form label {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<div class='file-dropper-holder'>
|
||||
<input type='file' name='{{this.name}}' id='{{this.id}}'/>
|
||||
<label class='file-dropper' for='{{this.id}}'>
|
||||
{{#if this.allowMultiple}}
|
||||
Drop files here!
|
||||
{{else}}
|
||||
Drop file here!
|
||||
{{/if}}
|
||||
<br/>
|
||||
Or just click on this box.
|
||||
</label>
|
||||
</div>
|
|
@ -1,36 +1,51 @@
|
|||
<div id='user-edit'>
|
||||
<form class='tabular'>
|
||||
<div class='input'>
|
||||
<ul>
|
||||
{{#if this.canEditName}}
|
||||
<li>
|
||||
{{textInput text='User name' id='user-name' name='name' value=this.user.name}}
|
||||
</li>
|
||||
{{/if}}
|
||||
<div class='left'>
|
||||
<div class='input'>
|
||||
<ul>
|
||||
{{#if this.canEditName}}
|
||||
<li>
|
||||
{{textInput text='User name' id='user-name' name='name' value=this.user.name}}
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.canEditPassword}}
|
||||
<li>
|
||||
{{passwordInput text='Password' id='user-password' name='password' placeholder='leave blank if not changing'}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if this.canEditPassword}}
|
||||
<li>
|
||||
{{passwordInput text='Password' id='user-password' name='password' placeholder='leave blank if not changing'}}
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.canEditEmail}}
|
||||
<li>
|
||||
{{emailInput text='Email' id='user-email' name='email' value=this.user.email}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if this.canEditEmail}}
|
||||
<li>
|
||||
{{emailInput text='Email' id='user-email' name='email' value=this.user.email}}
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.canEditRank}}
|
||||
<li>
|
||||
{{select text='Rank' id='user-rank' name='rank' keyValues=this.ranks selectedKey=this.user.rank}}
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
<!-- TODO: avatar -->
|
||||
</div>
|
||||
<div class='messages'></div>
|
||||
<div class='buttons'>
|
||||
<input type='submit' value='Save settings'/>
|
||||
{{#if this.canEditRank}}
|
||||
<li>
|
||||
{{select text='Rank' id='user-rank' name='rank' keyValues=this.ranks selectedKey=this.user.rank}}
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</div>
|
||||
<div class='messages'></div>
|
||||
<div class='buttons'>
|
||||
<input type='submit' value='Save settings'/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.canEditAvatar}}
|
||||
<div class='right'>
|
||||
<ul>
|
||||
<li>
|
||||
{{radio text='Gravatar' id='gravatar-radio' name='avatar-style' value='gravatar' selectedValue=this.user.avatarStyle}}
|
||||
</li>
|
||||
<li>
|
||||
{{radio text='Manual avatar' id='avatar-radio' name='avatar-style' value='manual' selectedValue=this.user.avatarStyle}}
|
||||
<div id='avatar-content'></div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -13,28 +13,33 @@ class Api {
|
|||
}
|
||||
|
||||
get(url) {
|
||||
const fullUrl = this.getFullUrl(url);
|
||||
return this._process(fullUrl, () => request.get(fullUrl));
|
||||
return this._process(url, request.get);
|
||||
}
|
||||
|
||||
post(url, data) {
|
||||
const fullUrl = this.getFullUrl(url);
|
||||
return this._process(fullUrl, () => request.post(fullUrl).send(data));
|
||||
post(url, data, files) {
|
||||
return this._process(url, request.post, data, files);
|
||||
}
|
||||
|
||||
put(url, data) {
|
||||
const fullUrl = this.getFullUrl(url);
|
||||
return this._process(fullUrl, () => request.put(fullUrl).send(data));
|
||||
put(url, data, files) {
|
||||
return this._process(url, request.put, data, files);
|
||||
}
|
||||
|
||||
delete(url, data) {
|
||||
const fullUrl = this.getFullUrl(url);
|
||||
return this._process(fullUrl, () => request.delete(fullUrl).send(data));
|
||||
delete(url) {
|
||||
return this._process(url, request.delete);
|
||||
}
|
||||
|
||||
_process(url, requestFactory) {
|
||||
_process(url, requestFactory, data, files) {
|
||||
const fullUrl = this.getFullUrl(url);
|
||||
return new Promise((resolve, reject) => {
|
||||
let req = requestFactory();
|
||||
let req = requestFactory(fullUrl);
|
||||
if (data) {
|
||||
req.attach('metadata', new Blob([JSON.stringify(data)]));
|
||||
}
|
||||
if (files) {
|
||||
for (let key of Object.keys(files)) {
|
||||
req.attach(key, files[key]);
|
||||
}
|
||||
}
|
||||
if (this.userName && this.userPassword) {
|
||||
req.auth(this.userName, this.userPassword);
|
||||
}
|
||||
|
|
|
@ -102,27 +102,46 @@ class UsersController {
|
|||
});
|
||||
}
|
||||
|
||||
_edit(user, newName, newPassword, newEmail, newRank) {
|
||||
const data = {};
|
||||
if (newName) { data.name = newName; }
|
||||
if (newPassword) { data.password = newPassword; }
|
||||
if (newEmail) { data.email = newEmail; }
|
||||
if (newRank) { data.rank = newRank; }
|
||||
/* TODO: avatar */
|
||||
_edit(user, data) {
|
||||
let files = [];
|
||||
|
||||
if (!data.name) {
|
||||
delete data.name;
|
||||
}
|
||||
if (!data.password) {
|
||||
delete data.password;
|
||||
}
|
||||
if (!data.email) {
|
||||
delete data.email;
|
||||
}
|
||||
if (!data.rank) {
|
||||
delete data.rank;
|
||||
}
|
||||
if (!data.avatarStyle ||
|
||||
(data.avatarStyle == user.avatarStyle && !data.avatarContent)) {
|
||||
delete data.avatarStyle;
|
||||
}
|
||||
if (data.avatarContent) {
|
||||
files.avatar = data.avatarContent;
|
||||
}
|
||||
|
||||
const isLoggedIn = api.isLoggedIn() && api.user.id == user.id;
|
||||
return new Promise((resolve, reject) => {
|
||||
api.put('/user/' + user.name, data)
|
||||
api.put('/user/' + user.name, data, files)
|
||||
.then(response => {
|
||||
this.user = response.user;
|
||||
return isLoggedIn ?
|
||||
api.login(
|
||||
newName, newPassword || api.userPassword, false) :
|
||||
data.name || api.userName,
|
||||
data.password || api.userPassword,
|
||||
false) :
|
||||
Promise.fulfill();
|
||||
}, response => {
|
||||
return Promise.reject(response.description);
|
||||
}).then(() => {
|
||||
resolve();
|
||||
if (newName !== user.name) {
|
||||
page('/user/' + newName + '/edit');
|
||||
if (data.name && data.name !== user.name) {
|
||||
page('/user/' + data.name + '/edit');
|
||||
}
|
||||
events.notify(events.Success, 'Settings updated.');
|
||||
}, errorMessage => {
|
||||
|
|
|
@ -31,6 +31,7 @@ handlebars.registerHelper('radio', function(options) {
|
|||
name: options.hash.name,
|
||||
value: options.hash.value,
|
||||
type: 'radio',
|
||||
checked: options.hash.selectedValue === options.hash.value,
|
||||
required: options.hash.required,
|
||||
}),
|
||||
views.makeNonVoidElement('label', {
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
'use strict';
|
||||
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class FileDropperControl {
|
||||
constructor() {
|
||||
this.template = views.getTemplate('file-dropper');
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
const target = ctx.target;
|
||||
const source = this.template({
|
||||
id: 'file-' + Math.random().toString(36).substring(7),
|
||||
});
|
||||
|
||||
const dropper = source.querySelector('.file-dropper');
|
||||
const fileInput = source.querySelector('input');
|
||||
fileInput.style.display = 'none';
|
||||
fileInput.multiple = ctx.allowMultiple || false;
|
||||
|
||||
const resolve = files => {
|
||||
files = Array.from(files);
|
||||
if (ctx.lock) {
|
||||
dropper.innerText = files.map(file => file.name).join(', ');
|
||||
}
|
||||
ctx.resolve(files);
|
||||
};
|
||||
|
||||
let counter = 0;
|
||||
dropper.addEventListener('dragenter', e => {
|
||||
dropper.classList.add('active');
|
||||
counter++;
|
||||
});
|
||||
|
||||
dropper.addEventListener('dragleave', e => {
|
||||
counter--;
|
||||
if (counter === 0) {
|
||||
dropper.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
dropper.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
dropper.addEventListener('drop', e => {
|
||||
dropper.classList.remove('active');
|
||||
e.preventDefault();
|
||||
if (!e.dataTransfer.files.length) {
|
||||
window.alert('Only files are supported.');
|
||||
}
|
||||
if (!ctx.allowMultiple && e.dataTransfer.files.length > 1) {
|
||||
window.alert('Cannot select multiple files.');
|
||||
}
|
||||
resolve(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', e => {
|
||||
resolve(e.target.files);
|
||||
});
|
||||
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FileDropperControl;
|
|
@ -2,10 +2,12 @@
|
|||
|
||||
const config = require('../config.js');
|
||||
const views = require('../util/views.js');
|
||||
const FileDropperControl = require('./file_dropper_control.js');
|
||||
|
||||
class UserEditView {
|
||||
constructor() {
|
||||
this.template = views.getTemplate('user-edit');
|
||||
this.fileDropperControl = new FileDropperControl();
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
|
@ -16,25 +18,39 @@ class UserEditView {
|
|||
const source = this.template(ctx);
|
||||
|
||||
const form = source.querySelector('form');
|
||||
const rankField = source.querySelector('#user-rank');
|
||||
const emailField = source.querySelector('#user-email');
|
||||
const userNameField = source.querySelector('#user-name');
|
||||
const passwordField = source.querySelector('#user-password');
|
||||
const avatarStyleField = source.querySelector('#avatar-style');
|
||||
const avatarContentField = source.querySelector('#avatar-content');
|
||||
|
||||
views.decorateValidator(form);
|
||||
|
||||
/* TODO: avatar */
|
||||
let avatarContent = null;
|
||||
this.fileDropperControl.render({
|
||||
target: avatarContentField,
|
||||
lock: true,
|
||||
resolve: files => {
|
||||
source.querySelector(
|
||||
'[name=avatar-style][value=manual]').checked = true;
|
||||
avatarContent = files[0];
|
||||
},
|
||||
});
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
const rankField = source.querySelector('#user-rank');
|
||||
const emailField = source.querySelector('#user-email');
|
||||
const userNameField = source.querySelector('#user-name');
|
||||
const passwordField = source.querySelector('#user-password');
|
||||
const avatarStyleField = source.querySelector(
|
||||
'[name=avatar-style]:checked');
|
||||
|
||||
e.preventDefault();
|
||||
views.clearMessages(target);
|
||||
views.disableForm(form);
|
||||
ctx.edit(
|
||||
userNameField.value,
|
||||
passwordField.value,
|
||||
emailField.value,
|
||||
rankField.value)
|
||||
ctx.edit({
|
||||
name: userNameField.value,
|
||||
password: passwordField.value,
|
||||
email: emailField.value,
|
||||
rank: rankField.value,
|
||||
avatarStyle: avatarStyleField.value,
|
||||
avatarContent: avatarContent})
|
||||
.always(() => { views.enableForm(form); });
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue