This reverts commit 37636a6256.
Reason for revert: The googlesource.com environment is not ready for
this yet.
I didn't realize that we couldn't yet use the hybrid version of the
elements in google3 yet. They exist in the polymer2 directory, but
apparently that depends on using polymer2. This change will need to be
reverted until iron-input v1 is updated to get the polymer2 "hybrid"
version.
Change-Id: Ibeeae2458337b0a225993e12b043b1e65c3c4c04
285 lines
8.0 KiB
JavaScript
285 lines
8.0 KiB
JavaScript
// Copyright (C) 2016 The Android Open Source Project
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
(function() {
|
|
'use strict';
|
|
|
|
const VALID_EMAIL_ALERT = 'Please input a valid email.';
|
|
|
|
Polymer({
|
|
is: 'gr-account-list',
|
|
|
|
/**
|
|
* Fired when user inputs an invalid email address.
|
|
*
|
|
* @event show-alert
|
|
*/
|
|
|
|
properties: {
|
|
accounts: {
|
|
type: Array,
|
|
value() { return []; },
|
|
notify: true,
|
|
},
|
|
change: Object,
|
|
filter: Function,
|
|
placeholder: String,
|
|
pendingConfirmation: {
|
|
type: Object,
|
|
value: null,
|
|
notify: true,
|
|
},
|
|
readonly: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
|
|
/**
|
|
* When true, the account-entry autocomplete uses the account suggest API
|
|
* endpoint, which suggests any account in that Gerrit instance (and does
|
|
* not suggest groups).
|
|
*
|
|
* When false/undefined, account-entry uses the suggest_reviewers API
|
|
* endpoint, which suggests any account or group in that Gerrit instance
|
|
* that is not already a reviewer (or is not CCed) on that change.
|
|
*/
|
|
allowAnyUser: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
|
|
/**
|
|
* When true, allows for non-suggested inputs to be added.
|
|
*/
|
|
allowAnyInput: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
|
|
/**
|
|
* Array of values (groups/accounts) that are removable. When this prop is
|
|
* undefined, all values are removable.
|
|
*/
|
|
removableValues: Array,
|
|
maxCount: {
|
|
type: Number,
|
|
value: 0,
|
|
},
|
|
},
|
|
|
|
listeners: {
|
|
remove: '_handleRemove',
|
|
},
|
|
|
|
get accountChips() {
|
|
return Polymer.dom(this.root).querySelectorAll('gr-account-chip');
|
|
},
|
|
|
|
get focusStart() {
|
|
return this.$.entry.focusStart;
|
|
},
|
|
|
|
_handleAdd(e) {
|
|
this._addReviewer(e.detail.value);
|
|
},
|
|
|
|
_addReviewer(reviewer) {
|
|
// Append new account or group to the accounts property. We add our own
|
|
// internal properties to the account/group here, so we clone the object
|
|
// to avoid cluttering up the shared change object.
|
|
if (reviewer.account) {
|
|
const account =
|
|
Object.assign({}, reviewer.account, {_pendingAdd: true});
|
|
this.push('accounts', account);
|
|
} else if (reviewer.group) {
|
|
if (reviewer.confirm) {
|
|
this.pendingConfirmation = reviewer;
|
|
return;
|
|
}
|
|
const group = Object.assign({}, reviewer.group,
|
|
{_pendingAdd: true, _group: true});
|
|
this.push('accounts', group);
|
|
} else if (this.allowAnyInput) {
|
|
if (!reviewer.includes('@')) {
|
|
// Repopulate the input with what the user tried to enter and have
|
|
// a toast tell them why they can't enter it.
|
|
this.$.entry.setText(reviewer);
|
|
this.dispatchEvent(new CustomEvent('show-alert',
|
|
{detail: {message: VALID_EMAIL_ALERT}, bubbles: true}));
|
|
return false;
|
|
} else {
|
|
const account = {email: reviewer, _pendingAdd: true};
|
|
this.push('accounts', account);
|
|
}
|
|
}
|
|
this.pendingConfirmation = null;
|
|
return true;
|
|
},
|
|
|
|
confirmGroup(group) {
|
|
group = Object.assign(
|
|
{}, group, {confirmed: true, _pendingAdd: true, _group: true});
|
|
this.push('accounts', group);
|
|
this.pendingConfirmation = null;
|
|
},
|
|
|
|
_computeChipClass(account) {
|
|
const classes = [];
|
|
if (account._group) {
|
|
classes.push('group');
|
|
}
|
|
if (account._pendingAdd) {
|
|
classes.push('pendingAdd');
|
|
}
|
|
return classes.join(' ');
|
|
},
|
|
|
|
_accountMatches(a, b) {
|
|
if (a && b) {
|
|
if (a._account_id) {
|
|
return a._account_id === b._account_id;
|
|
}
|
|
if (a.email) {
|
|
return a.email === b.email;
|
|
}
|
|
}
|
|
return a === b;
|
|
},
|
|
|
|
_computeRemovable(account) {
|
|
if (this.readonly) { return false; }
|
|
if (this.removableValues) {
|
|
for (let i = 0; i < this.removableValues.length; i++) {
|
|
if (this._accountMatches(this.removableValues[i], account)) {
|
|
return true;
|
|
}
|
|
}
|
|
return !!account._pendingAdd;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
_handleRemove(e) {
|
|
const toRemove = e.detail.account;
|
|
this._removeAccount(toRemove);
|
|
this.$.entry.focus();
|
|
},
|
|
|
|
_removeAccount(toRemove) {
|
|
if (!toRemove || !this._computeRemovable(toRemove)) { return; }
|
|
for (let i = 0; i < this.accounts.length; i++) {
|
|
let matches;
|
|
const account = this.accounts[i];
|
|
if (toRemove._group) {
|
|
matches = toRemove.id === account.id;
|
|
} else {
|
|
matches = this._accountMatches(toRemove, account);
|
|
}
|
|
if (matches) {
|
|
this.splice('accounts', i, 1);
|
|
return;
|
|
}
|
|
}
|
|
console.warn('received remove event for missing account', toRemove);
|
|
},
|
|
|
|
_handleInputKeydown(e) {
|
|
const input = e.detail.input;
|
|
if (input.selectionStart !== input.selectionEnd ||
|
|
input.selectionStart !== 0) {
|
|
return;
|
|
}
|
|
switch (e.detail.keyCode) {
|
|
case 8: // Backspace
|
|
this._removeAccount(this.accounts[this.accounts.length - 1]);
|
|
break;
|
|
case 37: // Left arrow
|
|
if (this.accountChips[this.accountChips.length - 1]) {
|
|
this.accountChips[this.accountChips.length - 1].focus();
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
|
|
_handleChipKeydown(e) {
|
|
const chip = e.target;
|
|
const chips = this.accountChips;
|
|
const index = chips.indexOf(chip);
|
|
switch (e.keyCode) {
|
|
case 8: // Backspace
|
|
case 13: // Enter
|
|
case 32: // Spacebar
|
|
case 46: // Delete
|
|
this._removeAccount(chip.account);
|
|
// Splice from this array to avoid inconsistent ordering of
|
|
// event handling.
|
|
chips.splice(index, 1);
|
|
if (index < chips.length) {
|
|
chips[index].focus();
|
|
} else if (index > 0) {
|
|
chips[index - 1].focus();
|
|
} else {
|
|
this.$.entry.focus();
|
|
}
|
|
break;
|
|
case 37: // Left arrow
|
|
if (index > 0) {
|
|
chip.blur();
|
|
chips[index - 1].focus();
|
|
}
|
|
break;
|
|
case 39: // Right arrow
|
|
chip.blur();
|
|
if (index < chips.length - 1) {
|
|
chips[index + 1].focus();
|
|
} else {
|
|
this.$.entry.focus();
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Submit the text of the entry as a reviewer value, if it exists. If it is
|
|
* a successful submit of the text, clear the entry value.
|
|
*
|
|
* @return {boolean} If there is text in the entry, return true if the
|
|
* submission was successful and false if not. If there is no text,
|
|
* return true.
|
|
*/
|
|
submitEntryText() {
|
|
const text = this.$.entry.getText();
|
|
if (!text.length) { return true; }
|
|
const wasSubmitted = this._addReviewer(text);
|
|
if (wasSubmitted) { this.$.entry.clear(); }
|
|
return wasSubmitted;
|
|
},
|
|
|
|
additions() {
|
|
return this.accounts.filter(account => {
|
|
return account._pendingAdd;
|
|
}).map(account => {
|
|
if (account._group) {
|
|
return {group: account};
|
|
} else {
|
|
return {account};
|
|
}
|
|
});
|
|
},
|
|
|
|
_computeEntryHidden(maxCount, accountsRecord, readonly) {
|
|
return (maxCount && maxCount <= accountsRecord.base.length) || readonly;
|
|
},
|
|
});
|
|
})();
|