Page MenuHomestyx hydra

No OneTemporary

diff --git a/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js b/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js
index a53c567607..e4d987c425 100644
--- a/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js
+++ b/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js
@@ -1,420 +1,425 @@
/**
* @provides javelin-behavior-phabricator-remarkup-assist
* @requires javelin-behavior
* javelin-stratcom
* javelin-dom
* phabricator-phtize
* phabricator-textareautils
* javelin-workflow
* javelin-vector
* phuix-autocomplete
* javelin-mask
*/
JX.behavior('phabricator-remarkup-assist', function(config) {
var pht = JX.phtize(config.pht);
var root = JX.$(config.rootID);
var area = JX.DOM.find(root, 'textarea');
var edit_mode = 'normal';
var edit_root = null;
var preview = null;
var pinned = false;
// When we pin the comment area to the bottom of the window, we need to put
// an extra spacer element at the bottom of the document so that it is
// possible to scroll down far enough to see content at the end. Otherwise,
// the last part of the document will be hidden behind the comment area when
// the document is fully scrolled.
var pinned_spacer = JX.$N(
'div',
{className: 'remarkup-assist-pinned-spacer'});
function set_edit_mode(root, mode) {
if (mode == edit_mode) {
return;
}
// First, disable any active mode.
if (edit_root) {
if (edit_mode == 'fullscreen') {
JX.DOM.alterClass(edit_root, 'remarkup-control-fullscreen-mode', false);
JX.DOM.alterClass(document.body, 'remarkup-fullscreen-mode', false);
JX.Mask.hide('jx-light-mask');
}
area.style.height = '';
// If we're in preview mode, kick the preview back down to default
// size.
if (preview) {
JX.DOM.show(area);
resize_preview();
JX.DOM.hide(area);
}
}
edit_root = root;
edit_mode = mode;
// Now, apply the new mode.
if (mode == 'fullscreen') {
JX.DOM.alterClass(edit_root, 'remarkup-control-fullscreen-mode', true);
JX.DOM.alterClass(document.body, 'remarkup-fullscreen-mode', true);
JX.Mask.show('jx-light-mask');
// If we're in preview mode, expand the preview to full-size.
if (preview) {
JX.DOM.show(area);
}
resizearea();
if (preview) {
resize_preview();
JX.DOM.hide(area);
}
}
JX.DOM.focus(area);
}
function set_pinned_mode(root, mode) {
if (mode === pinned) {
return;
}
pinned = mode;
var container = get_pinned_container(root);
JX.DOM.alterClass(container, 'remarkup-assist-pinned', pinned);
if (pinned) {
JX.DOM.appendContent(document.body, pinned_spacer);
} else {
JX.DOM.remove(pinned_spacer);
}
resizearea();
JX.DOM.focus(area);
}
function get_pinned_container(root) {
return JX.DOM.findAbove(root, 'div', 'phui-comment-form');
}
function resizearea() {
// If we're in the pinned comment mode, resize the pinned spacer to be the
// same size as the pinned form. This allows users to scroll to the bottom
// of the document by creating extra footer space to scroll through.
if (pinned) {
var container = get_pinned_container(root);
var d = JX.Vector.getDim(container);
d.x = null;
d.setDim(pinned_spacer);
}
if (!edit_root) {
return;
}
if (edit_mode != 'fullscreen') {
return;
}
// In Firefox, a textarea with position "absolute" or "fixed", anchored
// "top" and "bottom", and height "auto" renders as two lines high. Force
// it to the correct height with Javascript.
var v = JX.Vector.getViewport();
v.x = null;
v.y -= 26;
v.setDim(area);
}
JX.Stratcom.listen('resize', null, resizearea);
JX.Stratcom.listen('keydown', null, function(e) {
if (e.getSpecialKey() != 'esc') {
return;
}
if (edit_mode != 'fullscreen') {
return;
}
e.kill();
set_edit_mode(edit_root, 'normal');
set_pinned_mode(root, false);
});
function update(area, l, m, r) {
// Replace the selection with the entire assisted text.
JX.TextAreaUtils.setSelectionText(area, l + m + r, true);
// Now, select just the middle part. For instance, if the user clicked
// "B" to create bold text, we insert '**bold**' but just select the word
// "bold" so if they type stuff they'll be editing the bold text.
var range = JX.TextAreaUtils.getSelectionRange(area);
JX.TextAreaUtils.setSelectionRange(
area,
range.start + l.length,
range.start + l.length + m.length);
}
function prepend_char_to_lines(ch, sel, def) {
if (sel) {
sel = sel.split('\n');
} else {
sel = [def];
}
if (ch === '>') {
for(var i=0; i < sel.length; i++) {
if (sel[i][0] === '>') {
ch = '>';
} else {
ch = '> ';
}
sel[i] = ch + sel[i];
}
return sel.join('\n');
}
return sel.join('\n' + ch);
}
function assist(area, action, root, button) {
// If the user has some text selected, we'll try to use that (for example,
// if they have a word selected and want to bold it). Otherwise we'll insert
// generic text.
var sel = JX.TextAreaUtils.getSelectionText(area);
var r = JX.TextAreaUtils.getSelectionRange(area);
var ch;
switch (action) {
case 'fa-bold':
update(area, '**', sel || pht('bold text'), '**');
break;
case 'fa-italic':
update(area, '//', sel || pht('italic text'), '//');
break;
case 'fa-link':
var name = pht('name');
if (/^https?:/i.test(sel)) {
update(area, '[[ ' + sel + ' | ', name, ' ]]');
} else {
update(area, '[[ ', pht('URL'), ' | ' + (sel || name) + ' ]]');
}
break;
case 'fa-text-width':
update(area, '`', sel || pht('monospaced text'), '`');
break;
case 'fa-list-ul':
case 'fa-list-ol':
ch = (action == 'fa-list-ol') ? ' # ' : ' - ';
sel = prepend_char_to_lines(ch, sel, pht('List Item'));
update(area, ((r.start === 0) ? '' : '\n\n') + ch, sel, '\n\n');
break;
case 'fa-code':
sel = sel || 'foreach ($list as $item) {\n work_miracles($item);\n}';
var code_prefix = (r.start === 0) ? '' : '\n';
update(area, code_prefix + '```\n', sel, '\n```');
break;
case 'fa-quote-right':
ch = '>';
sel = prepend_char_to_lines(ch, sel, pht('Quoted Text'));
update(area, ((r.start === 0) ? '' : '\n\n'), sel, '\n\n');
break;
case 'fa-table':
var table_prefix = (r.start === 0 ? '' : '\n\n');
update(area, table_prefix + '| ', sel || pht('data'), ' |');
break;
case 'fa-meh-o':
new JX.Workflow('/macro/meme/create/')
.setHandler(function(response) {
update(
area,
'',
sel,
(r.start === 0 ? '' : '\n\n') + response.text + '\n\n');
})
.start();
break;
case 'fa-cloud-upload':
new JX.Workflow('/file/uploaddialog/')
.setHandler(function(response) {
var files = response.files;
for (var ii = 0; ii < files.length; ii++) {
var file = files[ii];
var upload = new JX.PhabricatorFileUpload()
.setID(file.id)
.setPHID(file.phid)
.setURI(file.uri);
JX.TextAreaUtils.insertFileReference(area, upload);
}
})
.start();
break;
case 'fa-arrows-alt':
set_pinned_mode(root, false);
if (edit_mode == 'fullscreen') {
set_edit_mode(root, 'normal');
} else {
set_edit_mode(root, 'fullscreen');
}
break;
case 'fa-eye':
if (!preview) {
preview = JX.$N(
'div',
{
className: 'remarkup-inline-preview'
},
null);
area.parentNode.insertBefore(preview, area);
JX.DOM.alterClass(button, 'preview-active', true);
JX.DOM.alterClass(root, 'remarkup-preview-active', true);
resize_preview();
JX.DOM.hide(area);
update_preview();
} else {
JX.DOM.show(area);
resize_preview(true);
JX.DOM.remove(preview);
preview = null;
JX.DOM.alterClass(button, 'preview-active', false);
JX.DOM.alterClass(root, 'remarkup-preview-active', false);
}
break;
case 'fa-thumb-tack':
// If we're pinning, kick us out of fullscreen mode first.
set_edit_mode(edit_root, 'normal');
// Now pin or unpin the area.
set_pinned_mode(root, !pinned);
break;
}
}
function resize_preview(restore) {
if (!preview) {
return;
}
var src;
var dst;
if (restore) {
src = preview;
dst = area;
} else {
src = area;
dst = preview;
}
var d = JX.Vector.getDim(src);
d.x = null;
d.setDim(dst);
}
function update_preview() {
var value = area.value;
var data = {
text: value
};
var onupdate = function(r) {
if (area.value !== value) {
return;
}
if (!preview) {
return;
}
JX.DOM.setContent(preview, JX.$H(r.content).getFragment());
};
new JX.Workflow('/transactions/remarkuppreview/', data)
.setHandler(onupdate)
.start();
}
JX.DOM.listen(
root,
'click',
'remarkup-assist',
function(e) {
var data = e.getNodeData('remarkup-assist');
if (!data.action) {
return;
}
e.kill();
if (config.disabled) {
return;
}
assist(area, data.action, root, e.getNode('remarkup-assist'));
});
var autocomplete = new JX.PHUIXAutocomplete()
.setArea(area);
for (var k in config.autocompleteMap) {
autocomplete.addAutocomplete(k, config.autocompleteMap[k]);
}
autocomplete.start();
if (config.canPin) {
new JX.KeyboardShortcut('z', pht('key-help'))
.setHandler(function() {
set_pinned_mode(root, !pinned);
})
.register();
}
if (config.sendOnEnter) {
// Send on enter if the shift key is not held.
JX.DOM.listen(area, 'keydown', null,
function(e) {
if (e.getSpecialKey() != 'return') {
return;
}
+ // Let other listeners (particularly the inline autocomplete) have a
+ // chance to handle this event.
+ if (JX.Stratcom.pass()) {
+ return;
+ }
+
var raw = e.getRawEvent();
if (raw.shiftKey) {
// If the shift key is pressed, let the browser write a newline into
// the textarea.
return;
}
if (edit_mode == 'fullscreen') {
// Don't send on enter in fullscreen
return;
}
// From here on, interpret this as a "send" action, not a literal
// newline.
e.kill();
// This allows 'workflow' and similar actions to take effect.
// Such as pontificate in Conpherence
var form = e.getNode('tag:form');
- var r = JX.DOM.invoke(form, 'didSyntheticSubmit');
-
+ JX.DOM.invoke(form, 'didSyntheticSubmit');
});
}
});
diff --git a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js
index 16ed8b75f3..ac073a0e38 100644
--- a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js
+++ b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js
@@ -1,735 +1,739 @@
/**
* @provides phuix-autocomplete
* @requires javelin-install
* javelin-dom
* phuix-icon-view
* phabricator-prefab
*/
JX.install('PHUIXAutocomplete', {
construct: function() {
this._map = {};
this._datasources = {};
this._listNodes = [];
this._resultMap = {};
},
members: {
_area: null,
_active: false,
_cursorHead: null,
_cursorTail: null,
_pixelHead: null,
_pixelTail: null,
_map: null,
_datasource: null,
_datasources: null,
_value: null,
_node: null,
_echoNode: null,
_listNode: null,
_promptNode: null,
_focus: null,
_focusRef: null,
_listNodes: null,
_x: null,
_y: null,
_visible: false,
_resultMap: null,
setArea: function(area) {
this._area = area;
return this;
},
addAutocomplete: function(code, spec) {
this._map[code] = spec;
return this;
},
start: function() {
var area = this._area;
JX.DOM.listen(area, 'keypress', null, JX.bind(this, this._onkeypress));
JX.DOM.listen(
area,
['click', 'keyup', 'keydown', 'keypress'],
null,
JX.bind(this, this._update));
var select = JX.bind(this, this._onselect);
JX.DOM.listen(this._getNode(), 'mousedown', 'typeahead-result', select);
var device = JX.bind(this, this._ondevice);
JX.Stratcom.listen('phabricator-device-change', null, device);
// When the user clicks away from the textarea, deactivate.
var deactivate = JX.bind(this, this._deactivate);
JX.DOM.listen(area, 'blur', null, deactivate);
},
_getSpec: function() {
return this._map[this._active];
},
_ondevice: function() {
if (JX.Device.getDevice() != 'desktop') {
this._deactivate();
}
},
_activate: function(code) {
if (JX.Device.getDevice() != 'desktop') {
return;
}
if (!this._map[code]) {
return;
}
var area = this._area;
var range = JX.TextAreaUtils.getSelectionRange(area);
// Check the character immediately before the trigger character. We'll
// only activate the typeahead if it's something that we think a user
// might reasonably want to autocomplete after, like a space, newline,
// or open parenthesis. For example, if a user types "alincoln@",
// the prior letter will be the last "n" in "alincoln". They are probably
// typing an email address, not a username, so we don't activate the
// autocomplete.
var head = range.start;
var prior;
if (head > 1) {
prior = area.value.substring(head - 2, head - 1);
} else {
prior = '<start>';
}
switch (prior) {
case '<start>':
case ' ':
case '\n':
case '\t':
case '(': // Might be "(@username, what do you think?)".
case '-': // Might be an unnumbered list.
case '.': // Might be a numbered list.
case '|': // Might be a table cell.
case '>': // Might be a blockquote.
case '!': // Might be a blockquote attribution line.
// We'll let these autocomplete.
break;
default:
// We bail out on anything else, since the user is probably not
// typing a username or project tag.
return;
}
// Get all the text on the current line. If the line only contains
- // whitespace, don't actiavte: the user is probably typing code or a
+ // whitespace, don't activate: the user is probably typing code or a
// numbered list.
var line = area.value.substring(0, head - 1);
line = line.split('\n');
line = line[line.length - 1];
if (line.match(/^\s+$/)) {
return;
}
this._cursorHead = head;
this._cursorTail = range.end;
this._pixelHead = JX.TextAreaUtils.getPixelDimensions(
area,
range.start,
range.end);
var spec = this._map[code];
if (!this._datasources[code]) {
var datasource = new JX.TypeaheadOnDemandSource(spec.datasourceURI);
datasource.listen(
'resultsready',
JX.bind(this, this._onresults, code));
datasource.setTransformer(JX.bind(this, this._transformresult));
datasource.setSortHandler(
JX.bind(datasource, JX.Prefab.sortHandler, {}));
this._datasources[code] = datasource;
}
this._datasource = this._datasources[code];
this._active = code;
var head_icon = new JX.PHUIXIconView()
.setIcon(spec.headerIcon)
.getNode();
var head_text = spec.headerText;
var node = this._getPromptNode();
JX.DOM.setContent(node, [head_icon, head_text]);
},
_transformresult: function(fields) {
var map = JX.Prefab.transformDatasourceResults(fields);
var icon;
if (map.icon) {
icon = new JX.PHUIXIconView()
.setIcon(map.icon)
.getNode();
}
map.display = [icon, map.displayName];
return map;
},
_deactivate: function() {
var node = this._getNode();
JX.DOM.hide(node);
this._active = false;
this._visible = false;
},
_onkeypress: function(e) {
var r = e.getRawEvent();
// NOTE: We allow events to continue with "altKey", because you need
// to press Alt to type characters like "@" on a German keyboard layout.
// The cost of misfiring autocompleters is very small since we do not
// eat the keystroke. See T10252.
if (r.metaKey || r.ctrlKey) {
return;
}
var code = r.charCode;
if (this._map[code]) {
setTimeout(JX.bind(this, this._activate, code), 0);
}
},
_onresults: function(code, nodes, value, partial) {
// Even if these results are out of date, we still want to fill in the
// result map so we can terminate things later.
if (!partial) {
if (!this._resultMap[code]) {
this._resultMap[code] = {};
}
var hits = [];
for (var ii = 0; ii < nodes.length; ii++) {
var result = this._datasources[code].getResult(nodes[ii].rel);
if (!result) {
hits = null;
break;
}
if (!result.autocomplete || !result.autocomplete.length) {
hits = null;
break;
}
hits.push(result.autocomplete);
}
if (hits !== null) {
this._resultMap[code][value] = hits;
}
}
if (code !== this._active) {
return;
}
if (value !== this._value) {
return;
}
if (this._isTerminatedString(value)) {
if (this._hasUnrefinableResults(value)) {
this._deactivate();
return;
}
}
var list = this._getListNode();
JX.DOM.setContent(list, nodes);
this._listNodes = nodes;
var old_ref = this._focusRef;
this._clearFocus();
for (var ii = 0; ii < nodes.length; ii++) {
if (nodes[ii].rel == old_ref) {
this._setFocus(ii);
break;
}
}
if (this._focus === null && nodes.length) {
this._setFocus(0);
}
this._redraw();
},
_setFocus: function(idx) {
if (!this._listNodes[idx]) {
this._clearFocus();
return false;
}
if (this._focus !== null) {
JX.DOM.alterClass(this._listNodes[this._focus], 'focused', false);
}
this._focus = idx;
this._focusRef = this._listNodes[idx].rel;
JX.DOM.alterClass(this._listNodes[idx], 'focused', true);
return true;
},
_changeFocus: function(delta) {
if (this._focus === null) {
return false;
}
return this._setFocus(this._focus + delta);
},
_clearFocus: function() {
this._focus = null;
this._focusRef = null;
},
_onselect: function (e) {
if (!e.isNormalMouseEvent()) {
// Eat right clicks, control clicks, etc., on the results. These can
// not do anything meaningful and if we let them through they'll blur
// the field and dismiss the results.
e.kill();
return;
}
var target = e.getNode('typeahead-result');
for (var ii = 0; ii < this._listNodes.length; ii++) {
if (this._listNodes[ii] === target) {
this._setFocus(ii);
this._autocomplete();
break;
}
}
this._deactivate();
e.kill();
},
_getSuffixes: function() {
return [' ', ':', ',', ')'];
},
_getCancelCharacters: function() {
// The "." character does not cancel because of projects named
// "node.js" or "blog.mycompany.com".
return ['#', '@', ',', '!', '?', '{', '}'];
},
_getTerminators: function() {
return [' ', ':', ',', '.', '!', '?'];
},
_getIgnoreList: function() {
return this._map[this._active].ignore || [];
},
_isTerminatedString: function(string) {
var terminators = this._getTerminators();
for (var ii = 0; ii < terminators.length; ii++) {
var term = terminators[ii];
if (string.substring(string.length - term.length) == term) {
return true;
}
}
return false;
},
_hasUnrefinableResults: function(query) {
if (!this._resultMap[this._active]) {
return false;
}
var map = this._resultMap[this._active];
for (var ii = 1; ii < query.length; ii++) {
var prefix = query.substring(0, ii);
if (map.hasOwnProperty(prefix)) {
var results = map[prefix];
// If any prefix of the query has no results, the full query also
// has no results so we can not refine them.
if (!results.length) {
return true;
}
// If there is exactly one match and the it is a prefix of the query,
// we can safely assume the user just typed out the right result
// from memory and doesn't need to refine it.
if (results.length == 1) {
// Strip the first character off, like a "#" or "@".
var result = results[0].substring(1);
if (query.length >= result.length) {
if (query.substring(0, result.length) === result) {
return true;
}
}
}
}
}
return false;
},
_trim: function(str) {
var suffixes = this._getSuffixes();
for (var ii = 0; ii < suffixes.length; ii++) {
if (str.substring(str.length - suffixes[ii].length) == suffixes[ii]) {
str = str.substring(0, str.length - suffixes[ii].length);
}
}
return str;
},
_update: function(e) {
if (!this._active) {
return;
}
var special = e.getSpecialKey();
// Deactivate if the user types escape.
if (special == 'esc') {
this._deactivate();
e.kill();
return;
}
var area = this._area;
if (e.getType() == 'keydown') {
if (special == 'up' || special == 'down') {
var delta = (special == 'up') ? -1 : +1;
if (!this._changeFocus(delta)) {
this._deactivate();
}
e.kill();
return;
}
}
// Deactivate if the user moves the cursor to the left of the assist
// range. For example, they might press the "left" arrow to move the
// cursor to the left, or click in the textarea prior to the active
// range.
var range = JX.TextAreaUtils.getSelectionRange(area);
if (range.start < this._cursorHead) {
this._deactivate();
return;
}
if (special == 'tab' || special == 'return') {
var r = e.getRawEvent();
if (r.shiftKey && special == 'tab') {
// Don't treat "Shift + Tab" as an autocomplete action. Instead,
// let it through normally so the focus shifts to the previous
// control.
this._deactivate();
return;
}
// If the user hasn't typed any text yet after typing the character
// which can summon the autocomplete, deactivate and let the keystroke
- // through. For example, We hit this when a line ends with an
+ // through. For example, we hit this when a line ends with an
// autocomplete character and the user is trying to type a newline.
if (range.start == this._cursorHead) {
this._deactivate();
return;
}
// If we autocomplete, we're done. Otherwise, just eat the event. This
// happens if you type too fast and try to tab complete before results
// load.
if (this._autocomplete()) {
this._deactivate();
}
e.kill();
return;
}
// Deactivate if the user moves the cursor to the right of the assist
// range. For example, they might click later in the document. If the user
// is pressing the "right" arrow key, they are not allowed to move the
// cursor beyond the existing end of the text range. If they are pressing
// other keys, assume they're typing and allow the tail to move forward
// one character.
var margin;
if (special == 'right') {
margin = 0;
} else {
margin = 1;
}
var tail = this._cursorTail;
if ((range.start > tail + margin) || (range.end > tail + margin)) {
this._deactivate();
return;
}
this._cursorTail = Math.max(this._cursorTail, range.end);
var text = area.value.substring(
this._cursorHead,
this._cursorTail);
this._value = text;
var pixels = JX.TextAreaUtils.getPixelDimensions(
area,
range.start,
range.end);
var x = this._pixelHead.start.x;
var y = Math.max(this._pixelHead.end.y, pixels.end.y) + 24;
// If the first character after the trigger is a space, just deactivate
// immediately. This occurs if a user types a numbered list using "#".
if (text.length && text[0] == ' ') {
this._deactivate();
return;
}
var trim = this._trim(text);
// Deactivate immediately if a user types a character that we are
// reasonably sure means they don't want to use the autocomplete. For
// example, "##" is almost certainly a header or monospaced text, not
// a project autocompletion.
var cancels = this._getCancelCharacters();
for (var ii = 0; ii < cancels.length; ii++) {
if (trim.indexOf(cancels[ii]) !== -1) {
this._deactivate();
return;
}
}
+ // Deactivate immediately if the user types an ignored token like ":)",
+ // the smiley face emoticon. Note that we test against "text", not
+ // "trim", because the ignore list and suffix list can otherwise
+ // interact destructively.
var ignore = this._getIgnoreList();
for (ii = 0; ii < ignore.length; ii++) {
- if (trim.indexOf(ignore[ii]) === 0) {
+ if (text.indexOf(ignore[ii]) === 0) {
this._deactivate();
return;
}
}
// If the input is terminated by a space or another word-terminating
// punctuation mark, we're going to deactivate if the results can not
// be refined by addding more words.
// The idea is that if you type "@alan ab", you're allowed to keep
// editing "ab" until you type a space, period, or other terminator,
// since you might not be sure how to spell someone's last name or the
// second word of a project.
// Once you do terminate a word, if the words you have have entered match
// nothing or match only one exact match, we can safely deactivate and
// assume you're just typing text because further words could never
// refine the result set.
var force;
if (this._isTerminatedString(text)) {
if (this._hasUnrefinableResults(text)) {
this._deactivate();
return;
}
force = true;
} else {
force = false;
}
this._datasource.didChange(trim, force);
this._x = x;
this._y = y;
var hint = trim;
if (hint.length) {
// We only show the autocompleter after the user types at least one
// character. For example, "@" does not trigger it, but "@d" does.
this._visible = true;
} else {
hint = this._getSpec().hintText;
}
var echo = this._getEchoNode();
JX.DOM.setContent(echo, hint);
this._redraw();
},
_redraw: function() {
if (!this._visible) {
return;
}
var node = this._getNode();
JX.DOM.show(node);
var p = new JX.Vector(this._x, this._y);
var s = JX.Vector.getScroll();
var v = JX.Vector.getViewport();
// If the menu would run off the bottom of the screen when showing the
// maximum number of possible choices, put it above instead. We're doing
// this based on the maximum size so the menu doesn't jump up and down
// as results arrive.
var option_height = 30;
var extra_margin = 24;
if ((s.y + v.y) < (p.y + (5 * option_height) + extra_margin)) {
var d = JX.Vector.getDim(node);
p.y = p.y - d.y - 36;
}
p.setPos(node);
},
_autocomplete: function() {
if (this._focus === null) {
return false;
}
var area = this._area;
var head = this._cursorHead;
var tail = this._cursorTail;
var text = area.value;
var ref = this._focusRef;
var result = this._datasource.getResult(ref);
if (!result) {
return false;
}
ref = result.autocomplete;
if (!ref || !ref.length) {
return false;
}
// If the user types a string like "@username:" (with a trailing colon),
// then presses tab or return to pick the completion, don't destroy the
// trailing character.
var suffixes = this._getSuffixes();
var value = this._value;
var found_suffix = false;
for (var ii = 0; ii < suffixes.length; ii++) {
var last = value.substring(value.length - suffixes[ii].length);
if (last == suffixes[ii]) {
ref += suffixes[ii];
found_suffix = true;
break;
}
}
// If we didn't find an existing suffix, add a space.
if (!found_suffix) {
ref = ref + ' ';
}
area.value = text.substring(0, head - 1) + ref + text.substring(tail);
var end = head + ref.length;
JX.TextAreaUtils.setSelectionRange(area, end, end);
return true;
},
_getNode: function() {
if (!this._node) {
var head = this._getHeadNode();
var list = this._getListNode();
this._node = JX.$N(
'div',
{
className: 'phuix-autocomplete',
style: {
display: 'none'
}
},
[head, list]);
JX.DOM.hide(this._node);
document.body.appendChild(this._node);
}
return this._node;
},
_getHeadNode: function() {
if (!this._headNode) {
this._headNode = JX.$N(
'div',
{
className: 'phuix-autocomplete-head'
},
[
this._getPromptNode(),
this._getEchoNode()
]);
}
return this._headNode;
},
_getPromptNode: function() {
if (!this._promptNode) {
this._promptNode = JX.$N(
'span',
{
className: 'phuix-autocomplete-prompt',
});
}
return this._promptNode;
},
_getEchoNode: function() {
if (!this._echoNode) {
this._echoNode = JX.$N(
'span',
{
className: 'phuix-autocomplete-echo'
});
}
return this._echoNode;
},
_getListNode: function() {
if (!this._listNode) {
this._listNode = JX.$N(
'div',
{
className: 'phuix-autocomplete-list'
});
}
return this._listNode;
}
}
});

File Metadata

Mime Type
text/x-diff
Expires
Fri, Oct 31, 3:14 AM (7 h, 28 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
310817
Default Alt Text
(32 KB)

Event Timeline