Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2893455
PHUIXAutocomplete.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
17 KB
Referenced Files
None
Subscribers
None
PHUIXAutocomplete.js
View Options
/**
* @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.
case
':'
:
// Might be a "NOTE:".
// 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 begins with
// whitespace, don't activate: the user is probably typing code or a
// numbered list.
var
line
=
area
.
value
.
substring
(
0
,
head
);
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
,
{}));
datasource
.
setFilterHandler
(
JX
.
Prefab
.
filterClosedResults
);
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
();
if
(
r
.
metaKey
||
r
.
altKey
||
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
;
}
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
)
{
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
[
' '
,
':'
,
','
,
'.'
,
'!'
,
'?'
];
},
_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
;
}
}
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 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 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
;
}
// 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
;
}
}
// 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 comma),
// then presses tab or return to pick the completion, don't destroy the
// trailing character.
var
suffixes
=
this
.
_getSuffixes
();
var
value
=
this
.
_value
;
for
(
var
ii
=
0
;
ii
<
suffixes
.
length
;
ii
++
)
{
var
last
=
value
.
substring
(
value
.
length
-
suffixes
[
ii
].
length
);
if
(
last
==
suffixes
[
ii
])
{
ref
+=
suffixes
[
ii
];
break
;
}
}
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
Details
Attached
Mime Type
text/plain
Expires
Sun, Jan 19, 18:26 (2 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1127418
Default Alt Text
PHUIXAutocomplete.js (17 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment