phorge/webroot/rsrc/js/core/Prefab.js
epriestley aa470d2154 Show user availability dots (red = away, orange = busy) in typeaheads, tokenizer tokens, and autocompletes
Summary:
Ref T13249. See PHI810. We currently show availability dots in some interfaces (timeline, mentions) but not others (typeheads/tokenizers).

They're potentially quite useful in tokenizers, e.g. when assigning tasks to someone or requesting reviews. Show them in more places.

(The actual rendering here isn't terribly clean, and it would be great to try to unify all these various behaviors some day.)

Test Plan:
{F6212044}

{F6212045}

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13249

Differential Revision: https://secure.phabricator.com/D20173
2019-02-19 10:57:20 -08:00

339 lines
9.3 KiB
JavaScript

/**
* @provides phabricator-prefab
* @requires javelin-install
* javelin-util
* javelin-dom
* javelin-typeahead
* javelin-tokenizer
* javelin-typeahead-preloaded-source
* javelin-typeahead-ondemand-source
* javelin-dom
* javelin-stratcom
* javelin-util
* @javelin
*/
/**
* Utilities for client-side rendering (the greatest thing in the world).
*/
JX.install('Prefab', {
statics : {
renderSelect : function(map, selected, attrs, order) {
var select = JX.$N('select', attrs || {});
// Callers may optionally pass "order" to force options into a specific
// order. Although most browsers do retain order, maps in Javascript
// aren't technically ordered. Safari, at least, will reorder maps with
// numeric keys.
order = order || JX.keys(map);
var k;
for (var ii = 0; ii < order.length; ii++) {
k = order[ii];
select.options[select.options.length] = new Option(map[k], k);
if (k == selected) {
select.value = k;
}
}
select.value = select.value || order[0];
return select;
},
newTokenizerFromTemplate: function(markup, config) {
var template = JX.$H(markup).getFragment().firstChild;
var container = JX.DOM.find(template, 'div', 'tokenizer-container');
container.id = '';
config.root = container;
var build = JX.Prefab.buildTokenizer(config);
build.node = template;
return build;
},
/**
* Build a Phabricator tokenizer out of a configuration with application
* sorting, datasource and placeholder rules.
*
* - `id` Root tokenizer ID (alternatively, pass `root`).
* - `root` Root tokenizer node (replaces `id`).
* - `src` Datasource URI.
* - `ondemand` Optional, use an ondemand source.
* - `value` Optional, initial value.
* - `limit` Optional, token limit.
* - `placeholder` Optional, placeholder text.
* - `username` Optional, username to sort first (i.e., viewer).
* - `icons` Optional, map of icons.
*
*/
buildTokenizer : function(config) {
config.icons = config.icons || {};
var root;
try {
root = config.root || JX.$(config.id);
} catch (ex) {
// If the root element does not exist, just return without building
// anything. This happens in some cases -- like Conpherence -- where we
// may load a tokenizer but not put it in the document.
return;
}
var datasource;
// Default to an ondemand source if no alternate configuration is
// provided.
var ondemand = true;
if ('ondemand' in config) {
ondemand = config.ondemand;
}
if (ondemand) {
datasource = new JX.TypeaheadOnDemandSource(config.src);
} else {
datasource = new JX.TypeaheadPreloadedSource(config.src);
}
datasource.setSortHandler(
JX.bind(datasource, JX.Prefab.sortHandler, config));
datasource.setTransformer(JX.Prefab.transformDatasourceResults);
var typeahead = new JX.Typeahead(
root,
JX.DOM.find(root, 'input', 'tokenizer-input'));
typeahead.setDatasource(datasource);
var tokenizer = new JX.Tokenizer(root);
tokenizer.setTypeahead(typeahead);
tokenizer.setRenderTokenCallback(function(value, key, container) {
var result;
if (value && (typeof value == 'object') && ('id' in value)) {
// TODO: In this case, we've been passed the decoded wire format
// dictionary directly. Token rendering is kind of a huge mess that
// should be cleaned up and made more consistent. Just force our
// way through for now.
result = value;
} else {
result = datasource.getResult(key);
}
var icon;
var type;
var color;
var availability_color;
if (result) {
icon = result.icon;
value = result.displayName;
type = result.tokenType;
color = result.color;
availability_color = result.availabilityColor;
} else {
icon = (config.icons || {})[key];
type = (config.types || {})[key];
color = (config.colors || {})[key];
availability_color = (config.availabilityColors || {})[key];
}
if (icon) {
icon = JX.Prefab._renderIcon(icon);
}
type = type || 'object';
JX.DOM.alterClass(container, 'jx-tokenizer-token-' + type, true);
if (color) {
JX.DOM.alterClass(container, color, true);
}
var dot;
if (availability_color) {
dot = JX.$N(
'span',
{
className: 'phui-tag-dot phui-tag-color-' + availability_color
});
}
return [icon, dot, value];
});
if (config.placeholder) {
tokenizer.setPlaceholder(config.placeholder);
}
if (config.limit) {
tokenizer.setLimit(config.limit);
}
if (config.value) {
tokenizer.setInitialValue(config.value);
}
if (config.browseURI) {
tokenizer.setBrowseURI(config.browseURI);
}
if (config.disabled) {
tokenizer.setDisabled(true);
}
JX.Stratcom.addData(root, {'tokenizer' : tokenizer});
return {
tokenizer: tokenizer
};
},
sortHandler: function(config, value, list, cmp) {
// Sort results so that the viewing user always comes up first; after
// that, prefer unixname matches to realname matches.
var priority_hits = {};
var self_hits = {};
// We'll put matches where the user's input is a prefix of the name
// above matches where that isn't true.
var prefix_hits = {};
var tokens = this.tokenize(value);
var normal = this.normalize(value);
for (var ii = 0; ii < list.length; ii++) {
var item = list[ii];
if (this.normalize(item.name).indexOf(normal) === 0) {
prefix_hits[item.id] = true;
}
if (!item.priority) {
continue;
}
if (config.username && item.priority == config.username) {
self_hits[item.id] = true;
}
}
list.sort(function(u, v) {
if (self_hits[u.id] != self_hits[v.id]) {
return self_hits[v.id] ? 1 : -1;
}
// If one result is open and one is closed, show the open result
// first. The "!" tricks here are because closed values are display
// strings, so the value is either `null` or some truthy string. If
// we compare the values directly, we'll apply this rule to two
// objects which are both closed but for different reasons, like
// "Archived" and "Disabled".
var u_open = !u.closed;
var v_open = !v.closed;
if (u_open != v_open) {
if (u_open) {
return -1;
} else {
return 1;
}
}
if (prefix_hits[u.id] != prefix_hits[v.id]) {
return prefix_hits[v.id] ? 1 : -1;
}
// Sort users ahead of other result types.
if (u.priorityType != v.priorityType) {
if (u.priorityType == 'user') {
return -1;
}
if (v.priorityType == 'user') {
return 1;
}
}
// Sort functions after other result types.
var uf = (u.tokenType == 'function');
var vf = (v.tokenType == 'function');
if (uf != vf) {
return uf ? 1 : -1;
}
return cmp(u, v);
});
},
/**
* Transform results from a wire format into a usable format in a standard
* way.
*/
transformDatasourceResults: function(fields) {
var closed = fields[9];
var closed_ui;
if (closed) {
closed_ui = JX.$N(
'div',
{className: 'tokenizer-closed'},
closed);
}
var icon = fields[8];
var icon_ui;
if (icon) {
icon_ui = JX.Prefab._renderIcon(icon);
}
var availability_ui;
var availability_color = fields[16];
if (availability_color) {
availability_ui = JX.$N(
'span',
{
className: 'phui-tag-dot phui-tag-color-' + availability_color
});
}
var display = JX.$N(
'div',
{className: 'tokenizer-result'},
[icon_ui, availability_ui, fields[4] || fields[0], closed_ui]);
if (closed) {
JX.DOM.alterClass(display, 'tokenizer-result-closed', true);
}
return {
name: fields[0],
displayName: fields[4] || fields[0],
display: display,
uri: fields[1],
id: fields[2],
priority: fields[3],
priorityType: fields[7],
imageURI: fields[6],
icon: icon,
closed: closed,
type: fields[5],
sprite: fields[10],
color: fields[11],
tokenType: fields[12],
unique: fields[13] || false,
autocomplete: fields[14],
sort: JX.TypeaheadNormalizer.normalize(fields[0]),
availabilityColor: availability_color
};
},
_renderIcon: function(icon) {
return JX.$N(
'span',
{className: 'phui-icon-view phui-font-fa ' + icon});
}
}
});