Page MenuHomestyx hydra

No OneTemporary

diff --git a/webroot/rsrc/css/core/z-index.css b/webroot/rsrc/css/core/z-index.css
index 6f5ce7ab33..94d636298f 100644
--- a/webroot/rsrc/css/core/z-index.css
+++ b/webroot/rsrc/css/core/z-index.css
@@ -1,154 +1,155 @@
/**
* @provides phabricator-zindex-css
*/
.keyboard-focus-focus-reticle {
z-index: 1;
}
.device-desktop .phui-timeline-minor-event .phui-timeline-image {
z-index: 2;
}
.differential-reticle {
z-index: 2;
}
.differential-changeset {
z-index: 2;
}
.pholio-new-inline-comment {
z-index: 2;
}
.slowvote-bar {
z-index: 2;
}
.slowvote-above-the-bar {
z-index: 3;
}
.phui-timeline-icon-fill {
z-index: 3;
}
.phabricator-nav-column-background {
z-index: 3;
}
.phabricator-crumbs-view {
z-index: 3;
}
.phabricator-nav-local {
z-index: 4;
}
.conpherence-layout .conpherence-no-threads {
z-index: 4;
}
.conpherence-menu-pane {
z-index: 4;
}
.phabricator-nav-drag {
z-index: 4;
}
.loading .messages-loading-mask,
.loading .widgets-loading-mask {
z-index: 5;
}
.dark-console {
z-index: 5;
}
.drag-dragging {
z-index: 5;
}
.phui-calendar-date-number {
z-index: 5;
}
.phabricator-main-menu {
z-index: 6;
}
.setup-warning-callout,
.aphront-developer-error-callout {
z-index: 6;
}
.jx-notification-container {
z-index: 7;
}
.fancy-datepicker {
z-index: 7;
}
.calendar-button {
z-index: 8;
}
div.jx-typeahead-results {
z-index: 8;
}
.differential-haunt-mode-1 .differential-add-comment-panel,
.differential-haunt-mode-2 .differential-add-comment-panel {
z-index: 8;
}
.device-desktop .phabricator-notification-menu {
z-index: 9;
}
.jx-mask {
z-index: 10;
}
.phabricator-global-upload-instructions {
z-index: 11;
}
.lightbox-attachment {
z-index: 12;
}
.jx-client-dialog {
z-index: 14;
}
.jx-hovercard-container {
z-index: 16;
}
.pholio-device-lightbox {
z-index: 20;
}
-.dropdown-menu-frame {
+.dropdown-menu-frame,
+.phuix-dropdown-menu {
z-index: 32;
}
.busy {
z-index: 40;
}
.remarkup-control-fullscreen-mode {
z-index: 50;
}
.jx-tooltip-container {
z-index: 51;
}
.audible .aural-only {
z-index: 100;
}
diff --git a/webroot/rsrc/css/phui/phui-button.css b/webroot/rsrc/css/phui/phui-button.css
index 9969b33d16..a504457b3a 100644
--- a/webroot/rsrc/css/phui/phui-button.css
+++ b/webroot/rsrc/css/phui/phui-button.css
@@ -1,362 +1,363 @@
/**
* @provides phui-button-css
*/
button,
a.button {
font: 13px/1.231 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
button,
a.button,
a.button:visited,
input[type="submit"] {
background-color: #3477ad;
color: white;
text-shadow: 0 -1px rgba(0,0,0,0.75);
border: 1px solid #19558D;
cursor: pointer;
font-weight: bold;
font-size: 13px;
display: inline-block;
padding: 3px 10px 4px;
text-align: center;
white-space: nowrap;
border-radius: 3px;
background-image: linear-gradient(to bottom, #3b86c4, #2b628f);
background-image: -webkit-linear-gradient(top, #3b86c4, #2b628f);
}
/* Buttons with images (full size only) */
button.icon,
a.icon,
a.icon:visited {
padding-left: 0;
position: relative;
text-indent: 29px;
}
button.black,
a.black,
a.black:visited {
background-color: #383838;
background-image: linear-gradient(to bottom, #505d65, #2d373c);
background-image: -webkit-linear-gradient(top, #505d65, #2d373c);
border: 1px solid {$darkgreytext};
border-bottom-color: #000;
}
button.green,
a.green,
a.green:visited {
background-color: #348e20;
background-image: linear-gradient(to bottom, #4e9b33, #158009);
background-image: -webkit-linear-gradient(top, #4e9b33, #158009);
border: 1px solid #3b6e22;
border-bottom-color: #2c5a15;
}
button.grey,
input[type="submit"].grey,
a.grey,
a.grey:visited {
background-color: {$lightgreybackground};
background-image: linear-gradient(to bottom, #ffffff, #e6e6e6);
background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
border-color: {$lightgreyborder};
color: {$darkgreytext};
border-bottom-color: {$greyborder};
text-shadow: none;
}
button.simple,
input[type="submit"].simple,
a.simple,
a.simple:visited {
background-color: transparent;
background-image: none;
border: 1px solid transparent;
color: {$bluetext};
text-shadow: 0 1px #fff;
}
a.disabled,
button.disabled,
button[disabled] {
filter:alpha(opacity=50);
-moz-opacity: 0.5;
-khtml-opacity: 0.5;
opacity: 0.5;
}
body button:active,
body a.button:active {
box-shadow: inset 0 0 8px rgba(0,0,0,.6);
}
button.grey:active,
a.grey:active,
button.grey_active,
a.button.dropdown-open {
background-color: #7d7d7d;
box-shadow: inset 0 0 4px rgba(0,0,0,.2);
}
a.dropdown-open {
color: {$greytext};
}
a.button:hover,
button:hover {
text-decoration: none;
box-shadow: inset 0 0 5px rgba(0,0,0,.4);
}
a.button.simple:hover,
button.simple:hover {
background-color: #fff;
border: 1px solid {$lightgreyborder};
background-image: none;
border-bottom: 1px solid {$greyborder};
text-shadow: none;
box-shadow: none;
}
a.button.grey:hover,
button.grey:hover {
text-decoration: none;
box-shadow: inset 0 0 4px rgba(0,0,0,.2);
}
body a.button.disabled:hover,
body button.disabled:hover,
body a.button.disabled:active,
body button.disabled:active {
box-shadow: none;
}
button.small,
a.small,
a.small:visited {
padding: 2px 7px;
height: auto;
font-size: 11px;
line-height: 16px;
}
button.link {
display: inline;
border: none;
background: transparent;
font-weight: normal;
padding: 0;
margin: 0;
font-size: inherit;
border-bottom: none;
text-decoration: none;
text-shadow: none;
color: #19558D;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
button.link:hover {
text-decoration: underline;
}
-.dropdown-menu-frame {
+.dropdown-menu-frame,
+.phuix-dropdown-menu {
position: absolute;
width: 240px;
background: #fff;
margin-top: -1px;
padding: 5px 0;
box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.3);
border: 1px solid {$lightgreyborder};
border-bottom-color: {$greyborder};
}
.dropdown-menu-frame .dropdown-menu-item {
display: block;
padding: 2px 10px;
clear: both;
line-height: 20px;
color: {$darkgreytext};
white-space: nowrap;
}
.dropdown-menu-frame .dropdown-menu-item-disabled {
color: {$lightgreytext};
}
.dropdown-menu-frame .phui-icon-view {
display: inline-block;
padding: 0;
margin: 2px 6px -2px 4px;
}
a.policy-control {
width: 240px;
text-align: left;
}
a.policy-control .caret {
float: right;
}
a.policy-control span.phui-icon-view {
/* NOTE: Nudge these icons a little bit. Should this be for all
dropdown buttons? */
top: 4px;
left: 7px;
}
.dropdown-menu-frame .dropdown-menu-item-selected {
background: {$lightblue};
}
.dropdown-menu-frame a:hover {
background: {$blue};
color: white;
cursor: pointer;
text-decoration: none;
}
a.toggle {
display: inline-block;
padding: 4px 8px;
font-size: 12px;
font-weight: bold;
color: #555;
text-decoration: none;
white-space: nowrap;
vertical-align: baseline;
background-color: {$lightgreybackground};
margin: 0 6px 0 0;
border-radius: 3px;
box-shadow: inset 0 0 3px rgba(0,0,0,.4);
}
a.toggle:hover {
background-color: #14568e;
color: #fff
}
a.toggle-selected {
background-color: #14568e;
color: #fff
}
a.toggle-fixed {
cursor: default;
}
.caret {
display: inline-block;
width: 0;
height: 0;
vertical-align: top;
border-top: 5px solid #fff;
border-right: 5px solid transparent;
border-left: 5px solid transparent;
content: "";
}
.caret-right {
display: inline-block;
width: 0;
height: 0;
vertical-align: middle;
border-left: 7px solid {$greytext};
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
content: "";
margin-bottom: 4px;
}
.caret-left {
display: inline-block;
width: 0;
height: 0;
vertical-align: middle;
border-right: 7px solid {$greytext};
border-bottom: 5px solid transparent;
border-top: 5px solid transparent;
content: "";
margin-bottom: 4px;
}
.dropdown .caret {
margin-top: 7px;
margin-left: 4px;
}
.small.dropdown .caret {
margin-top: 6px;
}
.grey.dropdown .caret {
border-top-color: #000;
}
/* Icons */
.button.has-icon {
position: relative;
}
.button .phui-icon-view {
display: inline-block;
position: absolute;
top: 5px;
left: 8px;
}
.button.has-icon .phui-button-text {
margin-left: 16px;
}
/* Login Buttons */
.button.big.has-icon {
padding: 6px 20px 6px 12px;
border-radius: 4px;
text-align: left;
}
.button.big.has-icon .phui-button-text {
margin-left: 36px;
font-size: 14px;
display: block;
}
.button.big.has-icon .phui-button-subtext {
color: {$lightgreytext};
font-size: 12px;
line-height: 15px;
font-weight: normal;
}
/* PHUI Button Bar */
.phui-button-bar a.button {
display: inline-block;
height: 16px;
width: 12px;
}
.phui-button-bar .phui-button-bar-first {
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}
.phui-button-bar .phui-button-bar-middle {
border-radius: 0;
border-left: none;
}
.phui-button-bar .phui-button-bar-last {
border-left: none;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
diff --git a/webroot/rsrc/externals/javelin/lib/DOM.js b/webroot/rsrc/externals/javelin/lib/DOM.js
index ca017a8f5d..dea3d880f6 100644
--- a/webroot/rsrc/externals/javelin/lib/DOM.js
+++ b/webroot/rsrc/externals/javelin/lib/DOM.js
@@ -1,964 +1,965 @@
/**
* @requires javelin-magical-init
* javelin-install
* javelin-util
* javelin-vector
* javelin-stratcom
* @provides javelin-dom
*
* @javelin-installs JX.$
* @javelin-installs JX.$N
* @javelin-installs JX.$H
*
* @javelin
*/
/**
* Select an element by its "id" attribute, like ##document.getElementById()##.
* For example:
*
* var node = JX.$('some_id');
*
* This will select the node with the specified "id" attribute:
*
* LANG=HTML
* <div id="some_id">...</div>
*
* If the specified node does not exist, @{JX.$()} will throw an exception.
*
* For other ways to select nodes from the document, see @{JX.DOM.scry()} and
* @{JX.DOM.find()}.
*
* @param string "id" attribute to select from the document.
* @return Node Node with the specified "id" attribute.
*
* @group dom
*/
JX.$ = function(id) {
if (__DEV__) {
if (!id) {
JX.$E('Empty ID passed to JX.$()!');
}
}
var node = document.getElementById(id);
if (!node || (node.id != id)) {
if (__DEV__) {
if (node && (node.id != id)) {
JX.$E(
'JX.$("'+id+'"): '+
'document.getElementById() returned an element without the '+
'correct ID. This usually means that the element you are trying '+
'to select is being masked by a form with the same value in its '+
'"name" attribute.');
}
}
JX.$E("JX.$('" + id + "') call matched no nodes.");
}
return node;
};
/**
* Upcast a string into an HTML object so it is treated as markup instead of
* plain text. See @{JX.$N} for discussion of Javelin's security model. Every
* time you call this function you potentially open up a security hole. Avoid
* its use wherever possible.
*
* This class intentionally supports only a subset of HTML because many browsers
* named "Internet Explorer" have awkward restrictions around what they'll
* accept for conversion to document fragments. Alter your datasource to emit
* valid HTML within this subset if you run into an unsupported edge case. All
* the edge cases are crazy and you should always be reasonably able to emit
* a cohesive tag instead of an unappendable fragment.
*
* You may use @{JX.$H} as a shortcut for creating new JX.HTML instances:
*
* JX.$N('div', {}, some_html_blob); // Treat as string (safe)
* JX.$N('div', {}, JX.$H(some_html_blob)); // Treat as HTML (unsafe!)
*
* @task build String into HTML
* @task nodes HTML into Nodes
*
* @group dom
*/
JX.install('HTML', {
construct : function(str) {
if (str instanceof JX.HTML) {
this._content = str._content;
return;
}
if (__DEV__) {
if ((typeof str !== 'string') && (!str || !str.match)) {
JX.$E(
'new JX.HTML(<empty?>): ' +
'call initializes an HTML object with an empty value.');
}
var tags = ['legend', 'thead', 'tbody', 'tfoot', 'column', 'colgroup',
'caption', 'tr', 'th', 'td', 'option'];
var evil_stuff = new RegExp('^\\s*<(' + tags.join('|') + ')\\b', 'i');
var match = str.match(evil_stuff);
if (match) {
JX.$E(
'new JX.HTML("<' + match[1] + '>..."): ' +
'call initializes an HTML object with an invalid partial fragment ' +
'and can not be converted into DOM nodes. The enclosing tag of an ' +
'HTML content string must be appendable to a document fragment. ' +
'For example, <table> is allowed but <tr> or <tfoot> are not.');
}
var really_evil = /<script\b/;
if (str.match(really_evil)) {
JX.$E(
'new JX.HTML("...<script>..."): ' +
'call initializes an HTML object with an embedded script tag! ' +
'Are you crazy?! Do NOT do this!!!');
}
var wont_work = /<object\b/;
if (str.match(wont_work)) {
JX.$E(
'new JX.HTML("...<object>..."): ' +
'call initializes an HTML object with an embedded <object> tag. IE ' +
'will not do the right thing with this.');
}
// TODO(epriestley): May need to deny <option> more broadly, see
// http://support.microsoft.com/kb/829907 and the whole mess in the
// heavy stack. But I seem to have gotten away without cloning into the
// documentFragment below, so this may be a nonissue.
}
this._content = str;
},
members : {
_content : null,
/**
* Convert the raw HTML string into a DOM node tree.
*
* @task nodes
* @return DocumentFragment A document fragment which contains the nodes
* corresponding to the HTML string you provided.
*/
getFragment : function() {
var wrapper = JX.$N('div');
wrapper.innerHTML = this._content;
var fragment = document.createDocumentFragment();
while (wrapper.firstChild) {
// TODO(epriestley): Do we need to do a bunch of cloning junk here?
// See heavy stack. I'm disconnecting the nodes instead; this seems
// to work but maybe my test case just isn't extensive enough.
fragment.appendChild(wrapper.removeChild(wrapper.firstChild));
}
return fragment;
},
/**
* Convert the raw HTML string into a single DOM node. This only works
* if the element has a single top-level element. Otherwise, use
* @{method:getFragment} to get a document fragment instead.
*
* @return Node Single node represented by the object.
* @task nodes
*/
getNode : function() {
var fragment = this.getFragment();
if (__DEV__) {
if (fragment.childNodes.length < 1) {
JX.$E('JX.HTML.getNode(): Markup has no root node!');
}
if (fragment.childNodes.length > 1) {
JX.$E('JX.HTML.getNode(): Markup has more than one root node!');
}
}
return fragment.firstChild;
}
}
});
/**
* Build a new HTML object from a trustworthy string. JX.$H is a shortcut for
* creating new JX.HTML instances.
*
* @task build
* @param string A string which you want to be treated as HTML, because you
* know it is from a trusted source and any data in it has been
* properly escaped.
* @return JX.HTML HTML object, suitable for use with @{JX.$N}.
*
* @group dom
*/
JX.$H = function(str) {
return new JX.HTML(str);
};
/**
* Create a new DOM node with attributes and content.
*
* var link = JX.$N('a');
*
* This creates a new, empty anchor tag without any attributes. The equivalent
* markup would be:
*
* LANG=HTML
* <a />
*
* You can also specify attributes by passing a dictionary:
*
* JX.$N('a', {name: 'anchor'});
*
* This is equivalent to:
*
* LANG=HTML
* <a name="anchor" />
*
* Additionally, you can specify content:
*
* JX.$N(
* 'a',
* {href: 'http://www.javelinjs.com'},
* 'Visit the Javelin Homepage');
*
* This is equivalent to:
*
* LANG=HTML
* <a href="http://www.javelinjs.com">Visit the Javelin Homepage</a>
*
* If you only want to specify content, you can omit the attribute parameter.
* That is, these calls are equivalent:
*
* JX.$N('div', {}, 'Lorem ipsum...'); // No attributes.
* JX.$N('div', 'Lorem ipsum...') // Same as above.
*
* Both are equivalent to:
*
* LANG=HTML
* <div>Lorem ipsum...</div>
*
* Note that the content is treated as plain text, not HTML. This means it is
* safe to use untrusted strings:
*
* JX.$N('div', '<script src="evil.com" />');
*
* This is equivalent to:
*
* LANG=HTML
* <div>&lt;script src="evil.com" /&gt;</div>
*
* That is, the content will be properly escaped and will not create a
* vulnerability. If you want to set HTML content, you can use @{JX.HTML}:
*
* JX.$N('div', JX.$H(some_html));
*
* **This is potentially unsafe**, so make sure you understand what you're
* doing. You should usually avoid passing HTML around in string form. See
* @{JX.HTML} for discussion.
*
* You can create new nodes with a Javelin sigil (and, optionally, metadata) by
* providing "sigil" and "meta" keys in the attribute dictionary.
*
* @param string Tag name, like 'a' or 'div'.
* @param dict|string|@{JX.HTML}? Property dictionary, or content if you don't
* want to specify any properties.
* @param string|@{JX.HTML}? Content string (interpreted as plain text)
* or @{JX.HTML} object (interpreted as HTML,
* which may be dangerous).
* @return Node New node with whatever attributes and
* content were specified.
*
* @group dom
*/
JX.$N = function(tag, attr, content) {
if (typeof content == 'undefined' &&
(typeof attr != 'object' || attr instanceof JX.HTML)) {
content = attr;
attr = {};
}
if (__DEV__) {
if (tag.toLowerCase() != tag) {
JX.$E(
'$N("'+tag+'", ...): '+
'tag name must be in lower case; '+
'use "'+tag.toLowerCase()+'", not "'+tag+'".');
}
}
var node = document.createElement(tag);
if (attr.style) {
JX.copy(node.style, attr.style);
delete attr.style;
}
if (attr.sigil) {
JX.Stratcom.addSigil(node, attr.sigil);
delete attr.sigil;
}
if (attr.meta) {
JX.Stratcom.addData(node, attr.meta);
delete attr.meta;
}
if (__DEV__) {
if (('metadata' in attr) || ('data' in attr)) {
JX.$E(
'$N(' + tag + ', ...): ' +
'use the key "meta" to specify metadata, not "data" or "metadata".');
}
}
JX.copy(node, attr);
if (content) {
JX.DOM.setContent(node, content);
}
return node;
};
/**
* Query and update the DOM. Everything here is static, this is essentially
* a collection of common utility functions.
*
* @task stratcom Attaching Event Listeners
* @task content Changing DOM Content
* @task nodes Updating Nodes
* @task serialize Serializing Forms
* @task test Testing DOM Properties
* @task convenience Convenience Methods
* @task query Finding Nodes in the DOM
* @task view Changing View State
*
* @group dom
*/
JX.install('DOM', {
statics : {
_autoid : 0,
_uniqid : 0,
_metrics : {},
/* -( Changing DOM Content )----------------------------------------------- */
/**
* Set the content of some node. This uses the same content semantics as
* other Javelin content methods, see @{function:JX.$N} for a detailed
* explanation. Previous content will be replaced: you can also
* @{method:prependContent} or @{method:appendContent}.
*
* @param Node Node to set content of.
* @param mixed Content to set.
* @return void
* @task content
*/
setContent : function(node, content) {
if (__DEV__) {
if (!JX.DOM.isNode(node)) {
JX.$E(
'JX.DOM.setContent(<yuck>, ...): '+
'first argument must be a DOM node.');
}
}
while (node.firstChild) {
JX.DOM.remove(node.firstChild);
}
JX.DOM.appendContent(node, content);
},
/**
* Prepend content to some node. This method uses the same content semantics
* as other Javelin methods, see @{function:JX.$N} for an explanation. You
* can also @{method:setContent} or @{method:appendContent}.
*
* @param Node Node to prepend content to.
* @param mixed Content to prepend.
* @return void
* @task content
*/
prependContent : function(node, content) {
if (__DEV__) {
if (!JX.DOM.isNode(node)) {
JX.$E(
'JX.DOM.prependContent(<junk>, ...): '+
'first argument must be a DOM node.');
}
}
this._insertContent(node, content, this._mechanismPrepend, true);
},
/**
* Append content to some node. This method uses the same content semantics
* as other Javelin methods, see @{function:JX.$N} for an explanation. You
* can also @{method:setContent} or @{method:prependContent}.
*
* @param Node Node to append the content of.
* @param mixed Content to append.
* @return void
* @task content
*/
appendContent : function(node, content) {
if (__DEV__) {
if (!JX.DOM.isNode(node)) {
JX.$E(
'JX.DOM.appendContent(<bleh>, ...): '+
'first argument must be a DOM node.');
}
}
this._insertContent(node, content, this._mechanismAppend);
},
/**
* Internal, add content to a node by prepending.
*
* @param Node Node to prepend content to.
* @param Node Node to prepend.
* @return void
* @task content
*/
_mechanismPrepend : function(node, content) {
node.insertBefore(content, node.firstChild);
},
/**
* Internal, add content to a node by appending.
*
* @param Node Node to append content to.
* @param Node Node to append.
* @task content
*/
_mechanismAppend : function(node, content) {
node.appendChild(content);
},
/**
* Internal, add content to a node using some specified mechanism.
*
* @param Node Node to add content to.
* @param mixed Content to add.
* @param function Callback for actually adding the nodes.
* @param bool True if array elements should be passed to the mechanism
* in reverse order, i.e. the mechanism prepends nodes.
* @return void
* @task content
*/
_insertContent : function(parent, content, mechanism, reverse) {
if (JX.isArray(content)) {
if (reverse) {
content = [].concat(content).reverse();
}
for (var ii = 0; ii < content.length; ii++) {
JX.DOM._insertContent(parent, content[ii], mechanism, reverse);
}
} else {
var type = typeof content;
if (content instanceof JX.HTML) {
content = content.getFragment();
} else if (type == 'string' || type == 'number') {
content = document.createTextNode(content);
}
if (__DEV__) {
if (content && !content.nodeType) {
JX.$E(
'JX.DOM._insertContent(<node>, ...): '+
'second argument must be a string, a number, ' +
'a DOM node or a JX.HTML instance');
}
}
content && mechanism(parent, content);
}
},
/* -( Updating Nodes )----------------------------------------------------- */
/**
* Remove a node from its parent, so it is no longer a child of any other
* node.
*
* @param Node Node to remove.
* @return Node The node.
* @task nodes
*/
remove : function(node) {
node.parentNode && JX.DOM.replace(node, null);
return node;
},
/**
* Replace a node with some other piece of content. This method obeys
* Javelin content semantics, see @{function:JX.$N} for an explanation.
* You can also @{method:setContent}, @{method:prependContent}, or
* @{method:appendContent}.
*
* @param Node Node to replace.
* @param mixed Content to replace it with.
* @return Node the original node.
* @task nodes
*/
replace : function(node, replacement) {
if (__DEV__) {
if (!node.parentNode) {
JX.$E(
'JX.DOM.replace(<node>, ...): '+
'node has no parent node, so it can not be replaced.');
}
}
var mechanism;
if (node.nextSibling) {
mechanism = JX.bind(node.nextSibling, function(parent, content) {
parent.insertBefore(content, this);
});
} else {
mechanism = this._mechanismAppend;
}
var parent = node.parentNode;
parent.removeChild(node);
this._insertContent(parent, replacement, mechanism);
return node;
},
/* -( Serializing Forms )-------------------------------------------------- */
/**
* Converts a form into a list of <name, value> pairs.
*
* Note: This function explicity does not match for submit inputs as there
* could be multiple in a form. It's the caller's obligation to add the
* submit input value if desired.
*
* @param Node The form element to convert into a list of pairs.
* @return List A list of <name, value> pairs.
* @task serialize
*/
convertFormToListOfPairs : function(form) {
var elements = form.getElementsByTagName('*');
var data = [];
for (var ii = 0; ii < elements.length; ++ii) {
if (!elements[ii].name) {
continue;
}
if (elements[ii].disabled) {
continue;
}
var type = elements[ii].type;
var tag = elements[ii].tagName;
if ((type in {radio: 1, checkbox: 1} && elements[ii].checked) ||
type in {text: 1, hidden: 1, password: 1, email: 1, tel: 1,
number: 1} ||
tag in {TEXTAREA: 1, SELECT: 1}) {
data.push([elements[ii].name, elements[ii].value]);
}
}
return data;
},
/**
* Converts a form into a dictionary mapping input names to values. This
* will overwrite duplicate inputs in an undefined way.
*
* @param Node The form element to convert into a dictionary.
* @return Dict A dictionary of form values.
* @task serialize
*/
convertFormToDictionary : function(form) {
var data = {};
var pairs = JX.DOM.convertFormToListOfPairs(form);
for (var ii = 0; ii < pairs.length; ii++) {
data[pairs[ii][0]] = pairs[ii][1];
}
return data;
},
/* -( Testing DOM Properties )--------------------------------------------- */
/**
* Test if an object is a valid Node.
*
* @param wild Something which might be a Node.
* @return bool True if the parameter is a DOM node.
* @task test
*/
isNode : function(node) {
return !!(node && node.nodeName && (node !== window));
},
/**
* Test if an object is a node of some specific (or one of several) types.
* For example, this tests if the node is an ##<input />##, ##<select />##,
* or ##<textarea />##.
*
* JX.DOM.isType(node, ['input', 'select', 'textarea']);
*
* @param wild Something which might be a Node.
* @param string|list One or more tags which you want to test for.
* @return bool True if the object is a node, and it's a node of one
* of the provided types.
* @task test
*/
isType : function(node, of_type) {
node = ('' + (node.nodeName || '')).toUpperCase();
of_type = JX.$AX(of_type);
for (var ii = 0; ii < of_type.length; ++ii) {
if (of_type[ii].toUpperCase() == node) {
return true;
}
}
return false;
},
/**
* Listen for events occuring beneath a specific node in the DOM. This is
* similar to @{JX.Stratcom.listen()}, but allows you to specify some node
* which serves as a scope instead of the default scope (the whole document)
* which you get if you install using @{JX.Stratcom.listen()} directly. For
* example, to listen for clicks on nodes with the sigil 'menu-item' below
* the root menu node:
*
* var the_menu = getReferenceToTheMenuNodeSomehow();
* JX.DOM.listen(the_menu, 'click', 'menu-item', function(e) { ... });
*
* @task stratcom
* @param Node The node to listen for events underneath.
* @param string|list One or more event types to listen for.
* @param list? A path to listen on, or a list of paths.
* @param function Callback to invoke when a matching event occurs.
* @return object A reference to the installed listener. You can later
* remove the listener by calling this object's remove()
* method.
*/
listen : function(node, type, path, callback) {
var auto_id = ['autoid:' + JX.DOM._getAutoID(node)];
path = JX.$AX(path || []);
if (!path.length) {
path = auto_id;
} else {
for (var ii = 0; ii < path.length; ii++) {
path[ii] = auto_id.concat(JX.$AX(path[ii]));
}
}
return JX.Stratcom.listen(type, path, callback);
},
/**
* Invoke a custom event on a node. This method is a companion to
* @{method:JX.DOM.listen} and parallels @{method:JX.Stratcom.invoke} in
* the same way that method parallels @{method:JX.Stratcom.listen}.
*
* This method can not be used to invoke native events (like 'click').
*
* @param Node The node to invoke an event on.
* @param string Custom event type.
* @param dict Event data.
* @return JX.Event The event object which was dispatched to listeners.
* The main use of this is to test whether any
* listeners prevented the event.
*/
invoke : function(node, type, data) {
if (__DEV__) {
if (type in JX.__allowedEvents) {
throw new Error(
'JX.DOM.invoke(..., "' + type + '", ...): ' +
'you cannot invoke with the same type as a native event.');
}
}
return JX.Stratcom.dispatch({
target: node,
type: type,
customData: data
});
},
uniqID : function(node) {
if (!node.getAttribute('id')) {
node.setAttribute('id', 'uniqid_'+(++JX.DOM._uniqid));
}
return node.getAttribute('id');
},
alterClass : function(node, className, add) {
if (__DEV__) {
if (add !== false && add !== true) {
JX.$E(
'JX.DOM.alterClass(...): ' +
'expects the third parameter to be Boolean: ' +
add + ' was provided');
}
}
var has = ((' '+node.className+' ').indexOf(' '+className+' ') > -1);
if (add && !has) {
node.className += ' '+className;
} else if (has && !add) {
node.className = node.className.replace(
new RegExp('(^|\\s)' + className + '(?:\\s|$)', 'g'), ' ');
}
},
htmlize : function(str) {
return (''+str)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
},
/**
* Show one or more elements, by removing their "display" style. This
* assumes you have hidden them with @{method:hide}, or explicitly set
* the style to `display: none;`.
*
* @task convenience
* @param ... One or more nodes to remove "display" styles from.
* @return void
*/
show : function() {
var ii;
if (__DEV__) {
for (ii = 0; ii < arguments.length; ++ii) {
if (!arguments[ii]) {
JX.$E(
'JX.DOM.show(...): ' +
'one or more arguments were null or empty.');
}
}
}
for (ii = 0; ii < arguments.length; ++ii) {
arguments[ii].style.display = '';
}
},
/**
* Hide one or more elements, by setting `display: none;` on them. This is
* a convenience method. See also @{method:show}.
*
* @task convenience
* @param ... One or more nodes to set "display: none" on.
* @return void
*/
hide : function() {
var ii;
if (__DEV__) {
for (ii = 0; ii < arguments.length; ++ii) {
if (!arguments[ii]) {
JX.$E(
'JX.DOM.hide(...): ' +
'one or more arguments were null or empty.');
}
}
}
for (ii = 0; ii < arguments.length; ++ii) {
arguments[ii].style.display = 'none';
}
},
textMetrics : function(node, pseudoclass, x) {
if (!this._metrics[pseudoclass]) {
var n = JX.$N(
'var',
{className: pseudoclass});
this._metrics[pseudoclass] = n;
}
var proxy = this._metrics[pseudoclass];
document.body.appendChild(proxy);
proxy.style.width = x ? (x+'px') : '';
JX.DOM.setContent(
proxy,
JX.$H(JX.DOM.htmlize(node.value).replace(/\n/g, '<br />')));
var metrics = JX.Vector.getDim(proxy);
document.body.removeChild(proxy);
return metrics;
},
/**
* Search the document for DOM nodes by providing a root node to look
* beneath, a tag name, and (optionally) a sigil. Nodes which match all
* specified conditions are returned.
*
* @task query
*
* @param Node Root node to search beneath.
* @param string Tag name, like 'a' or 'textarea'.
* @param string Optionally, a sigil which nodes are required to have.
*
* @return list List of matching nodes, which may be empty.
*/
scry : function(root, tagname, sigil) {
if (__DEV__) {
if (!JX.DOM.isNode(root)) {
JX.$E(
'JX.DOM.scry(<yuck>, ...): '+
'first argument must be a DOM node.');
}
}
var nodes = root.getElementsByTagName(tagname);
if (!sigil) {
return JX.$A(nodes);
}
var result = [];
for (var ii = 0; ii < nodes.length; ii++) {
if (JX.Stratcom.hasSigil(nodes[ii], sigil)) {
result.push(nodes[ii]);
}
}
return result;
},
/**
* Select a node uniquely identified by a root, tagname and sigil. This
* is similar to JX.DOM.scry() but expects exactly one result.
*
* @task query
*
* @param Node Root node to search beneath.
* @param string Tag name, like 'a' or 'textarea'.
* @param string Optionally, sigil which selected node must have.
*
* @return Node Node uniquely identified by the criteria.
*/
find : function(root, tagname, sigil) {
if (__DEV__) {
if (!JX.DOM.isNode(root)) {
JX.$E(
'JX.DOM.find(<glop>, "'+tagname+'", "'+sigil+'"): '+
'first argument must be a DOM node.');
}
}
var result = JX.DOM.scry(root, tagname, sigil);
if (__DEV__) {
if (result.length > 1) {
JX.$E(
'JX.DOM.find(<node>, "'+tagname+'", "'+sigil+'"): '+
'matched more than one node.');
}
}
if (!result.length) {
JX.$E(
'JX.DOM.find(<node>, "' + tagname + '", "' + sigil + '"): ' +
'matched no nodes.');
}
return result[0];
},
/**
* Select a node uniquely identified by an anchor, tagname, and sigil. This
* is similar to JX.DOM.find() but walks up the DOM tree instead of down
* it.
*
* @param Node Node to look above.
* @param string Tag name, like 'a' or 'textarea'.
* @param string Optionally, sigil which selected node must have.
* @return Node Matching node.
*
* @task query
*/
findAbove : function(anchor, tagname, sigil) {
if (__DEV__) {
if (!JX.DOM.isNode(anchor)) {
JX.$E(
'JX.DOM.findAbove(<glop>, "' + tagname + '", "' + sigil + '"): ' +
'first argument must be a DOM node.');
}
}
var result = anchor.parentNode;
while (true) {
if (!result) {
break;
}
if (JX.DOM.isType(result, tagname)) {
if (!sigil || JX.Stratcom.hasSigil(result, sigil)) {
break;
}
}
result = result.parentNode;
}
if (!result) {
JX.$E(
'JX.DOM.findAbove(<node>, "' + tagname + '", "' + sigil + '"): ' +
'no matching node.');
}
return result;
},
/**
* Focus a node safely. This is just a convenience wrapper that allows you
* to avoid IE's habit of throwing when nearly any focus operation is
* invoked.
*
* @task convenience
* @param Node Node to move cursor focus to, if possible.
* @return void
*/
focus : function(node) {
try { node.focus(); } catch (lol_ie) {}
},
+
/**
* Scroll to the position of an element in the document.
* @task view
* @param Node Node to move document scroll position to, if possible.
* @return void
*/
scrollTo : function(node) {
window.scrollTo(0, JX.$V(node).y);
},
_getAutoID : function(node) {
if (!node.getAttribute('data-autoid')) {
node.setAttribute('data-autoid', 'autoid_'+(++JX.DOM._autoid));
}
return node.getAttribute('data-autoid');
}
}
});
diff --git a/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js b/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js
index 8fbf856e79..d8fe6ee0bb 100644
--- a/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js
+++ b/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js
@@ -1,150 +1,167 @@
/**
* @provides javelin-behavior-differential-dropdown-menus
* @requires javelin-behavior
* javelin-dom
* javelin-util
* javelin-stratcom
- * phabricator-dropdown-menu
- * phabricator-menu-item
+ * phuix-dropdown-menu
+ * phuix-action-list-view
+ * phuix-action-view
* phabricator-phtize
*/
JX.behavior('differential-dropdown-menus', function(config) {
-
var pht = JX.phtize(config.pht);
function show_more(container) {
var nodes = JX.DOM.scry(container, 'tr', 'context-target');
for (var ii = 0; ii < nodes.length; ii++) {
var show = JX.DOM.scry(nodes[ii], 'a', 'show-more');
for (var jj = 0; jj < show.length; jj++) {
if (JX.Stratcom.getData(show[jj]).type != 'all') {
continue;
}
var event_data = {
context : nodes[ii],
show : show[jj]
};
JX.Stratcom.invoke('differential-reveal-context', null, event_data);
}
}
}
- function build_menu(button, data) {
-
- function link_to(name, uri) {
- var item = new JX.PhabricatorMenuItem(
- name,
- JX.bind(null, window.open, uri),
- uri);
- item.setDisabled(!uri);
- return item;
- }
-
- var reveal_item = new JX.PhabricatorMenuItem('', function () {
- show_more(JX.$(data.containerID));
- });
-
- var diffusion_item;
- if (data.diffusionURI) {
- // Show this only if we have a link, since when this appears in Diffusion
- // it is otherwise potentially confusing.
- diffusion_item = link_to(pht('Browse in Diffusion'), data.diffusionURI);
- }
-
- var menu = new JX.PhabricatorDropdownMenu(buttons[ii])
- .addItem(reveal_item);
-
- var visible_item = new JX.PhabricatorMenuItem('', function () {
- JX.Stratcom.invoke('differential-toggle-file', null, {
- diff: JX.DOM.scry(JX.$(data.containerID), 'table', 'differential-diff')
- });
+ JX.Stratcom.listen(
+ 'click',
+ 'differential-reveal-all',
+ function(e) {
+ var containers = JX.DOM.scry(
+ JX.$('differential-review-stage'),
+ 'div',
+ 'differential-changeset');
+ for (var i=0; i < containers.length; i++) {
+ show_more(containers[i]);
+ }
+ e.kill();
});
- menu.addItem(visible_item);
-
- if (diffusion_item) {
- menu.addItem(diffusion_item);
- }
- menu.addItem(link_to(pht('View Standalone'), data.standaloneURI));
+ var buildmenu = function(e) {
+ var button = e.getNode('differential-view-options');
+ var data = JX.Stratcom.getData(button);
- if (data.leftURI) {
- menu.addItem(link_to(pht('Show Raw File (Left)'), data.leftURI));
+ if (data.menu) {
+ return;
}
- if (data.rightURI) {
- menu.addItem(link_to(pht('Show Raw File (Right)'), data.rightURI));
- }
+ e.prevent();
- if (data.editor) {
- menu.addItem(new JX.PhabricatorMenuItem(
- pht('Open in Editor'),
- // Open in the same window.
- JX.bind(location, location.assign, data.editor),
- data.editor));
- }
+ var menu = new JX.PHUIXDropdownMenu(button);
+ var list = new JX.PHUIXActionListView();
- if (data.editorConfigure) {
- menu.addItem(link_to(pht('Configure Editor'), data.editorConfigure));
- }
-
- menu.listen(
- 'open',
- function() {
-
- // When the user opens the menu, check if there are any "Show More"
- // links in the changeset body. If there aren't, disable the "Show
- // Entire File" menu item since it won't change anything.
-
- var nodes = JX.DOM.scry(JX.$(data.containerID), 'a', 'show-more');
- if (nodes.length) {
- reveal_item.setDisabled(false);
- reveal_item.setName(pht('Show Entire File'));
- } else {
- reveal_item.setDisabled(true);
- reveal_item.setName(pht('Entire File Shown'));
- }
+ var add_link = function(icon, name, href, local) {
+ if (!href) {
+ return;
+ }
- visible_item.setDisabled(true);
- visible_item.setName(pht("Can't Toggle Unloaded File"));
- var diffs = JX.DOM.scry(JX.$(data.containerID),
- 'table', 'differential-diff');
- if (diffs.length > 1) {
- JX.$E(
- 'More than one node with sigil "differential-diff" was found in "'+
- data.containerID+'."');
- } else if (diffs.length == 1) {
- diff = diffs[0];
- visible_item.setDisabled(false);
- if (JX.Stratcom.getData(diff).hidden) {
- visible_item.setName(pht('Expand File'));
+ var link = new JX.PHUIXActionView()
+ .setIcon(icon)
+ .setName(name)
+ .setHref(href)
+ .setHandler(function(e) {
+ if (local) {
+ window.location.assign(href);
} else {
- visible_item.setName(pht('Collapse File'));
+ window.open(href);
}
- } else {
- // Do nothing when there is no diff shown in the table. For example,
- // the file is binary.
- }
+ menu.close();
+ e.prevent();
+ });
+
+ list.addItem(link);
+ return link;
+ };
+
+ var reveal_item = new JX.PHUIXActionView()
+ .setIcon('preview');
+ list.addItem(reveal_item);
+
+ var visible_item = new JX.PHUIXActionView()
+ .setHandler(function(e) {
+ var diff = JX.DOM.scry(
+ JX.$(data.containerID),
+ 'table',
+ 'differential-diff');
+
+ JX.Stratcom.invoke('differential-toggle-file', null, {diff: diff});
+ e.prevent();
+ menu.close();
});
- }
-
- var buttons = JX.DOM.scry(window.document, 'a', 'differential-view-options');
- for (var ii = 0; ii < buttons.length; ii++) {
- build_menu(buttons[ii], JX.Stratcom.getData(buttons[ii]));
- }
+ list.addItem(visible_item);
+
+ add_link('file', pht('Browse in Diffusion'), data.diffusionURI);
+ add_link('transcript', pht('View Standalone'), data.standaloneURI);
+ add_link('arrow_left', pht('Show Raw File (Left)'), data.leftURI);
+ add_link('arrow_right', pht('Show Raw File (Right)'), data.rightURI);
+ add_link('edit', pht('Open in Editor'), data.editor, true);
+ add_link('wrench', pht('Configure Editor'), data.editorConfigure);
+
+
+ menu.setContent(list.getNode());
+
+ menu.listen('open', function() {
+
+ // When the user opens the menu, check if there are any "Show More"
+ // links in the changeset body. If there aren't, disable the "Show
+ // Entire File" menu item since it won't change anything.
+
+ var nodes = JX.DOM.scry(JX.$(data.containerID), 'a', 'show-more');
+ if (nodes.length) {
+ reveal_item
+ .setDisabled(false)
+ .setName(pht('Show Entire File'))
+ .setHandler(function(e) {
+ show_more(JX.$(data.containerID));
+ e.prevent();
+ menu.close();
+ });
+ } else {
+ reveal_item
+ .setDisabled(true)
+ .setName(pht('Entire File Shown'))
+ .setHandler(function(e) { e.prevent(); });
+ }
- JX.Stratcom.listen(
- 'click',
- 'differential-reveal-all',
- function(e) {
- var containers = JX.DOM.scry(
- JX.$('differential-review-stage'),
- 'div',
- 'differential-changeset');
- for (var i=0; i < containers.length; i++) {
- show_more(containers[i]);
+ visible_item.setDisabled(true);
+ visible_item.setName(pht("Can't Toggle Unloaded File"));
+ var diffs = JX.DOM.scry(
+ JX.$(data.containerID),
+ 'table',
+ 'differential-diff');
+
+ if (diffs.length > 1) {
+ JX.$E(
+ 'More than one node with sigil "differential-diff" was found in "'+
+ data.containerID+'."');
+ } else if (diffs.length == 1) {
+ diff = diffs[0];
+ visible_item.setDisabled(false);
+ if (JX.Stratcom.getData(diff).hidden) {
+ visible_item
+ .setName(pht('Expand File'))
+ .setIcon('unmerge');
+ } else {
+ visible_item
+ .setName(pht('Collapse File'))
+ .setIcon('merge');
+ }
+ } else {
+ // Do nothing when there is no diff shown in the table. For example,
+ // the file is binary.
}
- e.kill();
+
});
+ data.menu = menu;
+ menu.open();
+ };
+ JX.Stratcom.listen('click', 'differential-view-options', buildmenu);
});
diff --git a/webroot/rsrc/js/phuix/PHUIXActionListView.js b/webroot/rsrc/js/phuix/PHUIXActionListView.js
new file mode 100644
index 0000000000..1eef60176b
--- /dev/null
+++ b/webroot/rsrc/js/phuix/PHUIXActionListView.js
@@ -0,0 +1,36 @@
+/**
+ * @provides phuix-action-list-view
+ * @requires javelin-install
+ * javelin-dom
+ */
+
+JX.install('PHUIXActionListView', {
+
+ construct: function() {
+ this._items = [];
+ },
+
+ members: {
+ _items: null,
+ _node: null,
+
+ addItem: function(item) {
+ this._items.push(item);
+ this.getNode().appendChild(item.getNode());
+ return this;
+ },
+
+ getNode: function() {
+ if (!this._node) {
+ var attrs = {
+ className: 'phabricator-action-list-view'
+ };
+
+ this._node = JX.$N('ul', attrs);
+ }
+
+ return this._node;
+ }
+ }
+
+});
diff --git a/webroot/rsrc/js/phuix/PHUIXActionView.js b/webroot/rsrc/js/phuix/PHUIXActionView.js
new file mode 100644
index 0000000000..09b0edc467
--- /dev/null
+++ b/webroot/rsrc/js/phuix/PHUIXActionView.js
@@ -0,0 +1,138 @@
+/**
+ * @provides phuix-action-view
+ * @requires javelin-install
+ * javelin-dom
+ * javelin-util
+ * @javelin
+ */
+
+JX.install('PHUIXActionView', {
+
+ members: {
+ _node: null,
+ _name: null,
+ _icon: 'none',
+ _disabled: false,
+ _handler: null,
+
+ _iconNode: null,
+ _nameNode: null,
+
+ setDisabled: function(disabled) {
+ this._disabled = disabled;
+ JX.DOM.alterClass(
+ this.getNode(),
+ 'phabricator-action-view-disabled',
+ disabled);
+
+ this._buildIconNode(true);
+
+ return this;
+ },
+
+ getDisabled: function() {
+ return this._disabled;
+ },
+
+ setName: function(name) {
+ this._name = name;
+ this._buildNameNode(true);
+ return this;
+ },
+
+ setHandler: function(handler) {
+ this._handler = handler;
+ this._buildNameNode(true);
+ return this;
+ },
+
+ setIcon: function(icon) {
+ this._icon = icon;
+ this._buildIconNode(true);
+ return this;
+ },
+
+ setHref: function(href) {
+ this._href = href;
+ this._buildNameNode(true);
+ return this;
+ },
+
+ getNode: function() {
+ if (!this._node) {
+ var attr = {
+ className: 'phabricator-action-view'
+ };
+
+ var content = [
+ this._buildIconNode(),
+ this._buildNameNode()
+ ];
+
+ this._node = JX.$N('li', attr, content);
+ }
+
+ return this._node;
+ },
+
+ _buildIconNode: function(dirty) {
+ if (!this._iconNode || dirty) {
+ var attr = {
+ className: 'phui-icon-view sprite-icons phabricator-action-view-icon'
+ };
+ var node = JX.$N('span', attr);
+
+ var icon_class = 'icons-' + this._icon;
+ if (this._disabled) {
+ icon_class = icon_class + '-grey';
+ }
+
+ JX.DOM.alterClass(node, icon_class, true);
+
+ if (this._iconNode && this._iconNode.parentNode) {
+ JX.DOM.replace(this._iconNode, node);
+ }
+ this._iconNode = node;
+ }
+
+ return this._iconNode;
+ },
+
+ _buildNameNode: function(dirty) {
+ if (!this._nameNode || dirty) {
+ var attr = {
+ className: 'phabricator-action-view-item'
+ };
+
+ var href = this._href;
+ if (!href && this._handler) {
+ href = '#';
+ }
+ if (href) {
+ attr.href = href;
+
+ }
+
+ var tag = href ? 'a' : 'span';
+
+ var node = JX.$N(tag, attr, this._name);
+ JX.DOM.listen(node, 'click', null, JX.bind(this, this._onclick));
+
+ if (this._nameNode && this._nameNode.parentNode) {
+ JX.DOM.replace(this._nameNode, node);
+ }
+ this._nameNode = node;
+ }
+
+ return this._nameNode;
+ },
+
+ _onclick: function(e) {
+ if (this._handler) {
+ this._handler(e);
+ }
+ }
+
+ }
+
+});
diff --git a/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js b/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js
new file mode 100644
index 0000000000..ed47a8a792
--- /dev/null
+++ b/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js
@@ -0,0 +1,177 @@
+/**
+ * @provides phuix-dropdown-menu
+ * @requires javelin-install
+ * javelin-util
+ * javelin-dom
+ * javelin-vector
+ * javelin-stratcom
+ * @javelin
+ */
+
+
+/**
+ * Basic interaction for a dropdown menu.
+ *
+ * The menu is unaware of the content inside it, so it can not close itself
+ * when an item is selected. Callers must make a call to @{method:close} after
+ * an item is chosen in order to close the menu.
+ */
+JX.install('PHUIXDropdownMenu', {
+
+ construct : function(node) {
+ this._node = node;
+
+ JX.DOM.listen(
+ this._node,
+ 'click',
+ null,
+ JX.bind(this, this._onclick));
+
+ JX.Stratcom.listen(
+ 'mousedown',
+ null,
+ JX.bind(this, this._onanyclick));
+
+ JX.Stratcom.listen(
+ 'resize',
+ null,
+ JX.bind(this, this._adjustposition));
+
+ JX.Stratcom.listen('phuix.dropdown.open', null, JX.bind(this, this.close));
+ },
+
+ events: ['open'],
+
+ properties: {
+ width: null,
+ align: 'right'
+ },
+
+ members: {
+ _node: null,
+ _menu: null,
+ _open: false,
+ _content: null,
+
+ setContent: function(content) {
+ JX.DOM.setContent(this._getMenuNode(), content);
+ return this;
+ },
+
+ open: function() {
+ if (this._open) {
+ return;
+ }
+
+ this.invoke('open');
+ JX.Stratcom.invoke('phuix.dropdown.open');
+
+ this._open = true;
+ this._show();
+
+ return this;
+ },
+
+ close: function() {
+ if (!this._open) {
+ return;
+ }
+ this._open = false;
+ this._hide();
+
+ return this;
+ },
+
+ _getMenuNode: function() {
+ if (!this._menu) {
+ var attrs = {
+ className: 'phuix-dropdown-menu',
+ role: 'button'
+ };
+
+ var menu = JX.$N('div', attrs);
+
+ this._node.setAttribute('aria-haspopup', 'true');
+ this._node.setAttribute('aria-expanded', 'false');
+
+ this._menu = menu;
+ }
+
+ return this._menu;
+ },
+
+ _onclick : function(e) {
+ if (this._open) {
+ this.close();
+ } else {
+ this.open();
+ }
+ e.prevent();
+ },
+
+ _onanyclick : function(e) {
+ if (!this._open) {
+ return;
+ }
+
+ if (JX.Stratcom.pass(e)) {
+ return;
+ }
+
+ var t = e.getTarget();
+ while (t) {
+ if (t == this._menu || t == this._node) {
+ return;
+ }
+ t = t.parentNode;
+ }
+
+ this.close();
+ },
+
+ _show : function() {
+ document.body.appendChild(this._menu);
+
+ if (this.getWidth()) {
+ new JX.Vector(this.getWidth(), null).setDim(this._menu);
+ }
+
+ this._adjustposition();
+
+ JX.DOM.alterClass(this._node, 'phuix-dropdown-open', true);
+
+ this._node.setAttribute('aria-expanded', 'true');
+ },
+
+ _hide : function() {
+ JX.DOM.remove(this._menu);
+
+ JX.DOM.alterClass(this._node, 'phuix-dropdown-open', false);
+
+ this._node.setAttribute('aria-expanded', 'false');
+ },
+
+ _adjustposition : function() {
+ if (!this._open) {
+ return;
+ }
+
+ var m = JX.Vector.getDim(this._menu);
+
+ var v = JX.$V(this._node);
+ var d = JX.Vector.getDim(this._node);
+
+ switch (this.getAlign()) {
+ case 'right':
+ v = v.add(d)
+ .add(JX.$V(-m.x, 0));
+ break;
+ default:
+ v = v.add(0, d.y);
+ break;
+ }
+
+ v.setPos(this._menu);
+ }
+ }
+});

File Metadata

Mime Type
text/x-diff
Expires
Fri, Nov 14, 4:37 PM (1 d, 34 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
337215
Default Alt Text
(55 KB)

Event Timeline