diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php
index f3a0c56218..350005420a 100644
--- a/src/__celerity_resource_map__.php
+++ b/src/__celerity_resource_map__.php
@@ -1332,6 +1332,18 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/application/core/behavior-keyboard-shortcuts.js',
),
+ 'javelin-behavior-phabricator-notification-example' =>
+ array(
+ 'uri' => '/res/0b8fadf5/rsrc/js/application/uiexample/notification-example.js',
+ 'type' => 'js',
+ 'requires' =>
+ array(
+ 0 => 'phabricator-notification',
+ 1 => 'javelin-stratcom',
+ 2 => 'javelin-behavior',
+ ),
+ 'disk' => '/rsrc/js/application/uiexample/notification-example.js',
+ ),
'javelin-behavior-phabricator-object-selector' =>
array(
'uri' => '/res/0c4b0d82/rsrc/js/application/core/behavior-object-selector.js',
@@ -2146,6 +2158,27 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/application/core/DropdownMenuItem.js',
),
+ 'phabricator-notification' =>
+ array(
+ 'uri' => '/res/8497d4b2/rsrc/js/application/core/Notification.js',
+ 'type' => 'js',
+ 'requires' =>
+ array(
+ 0 => 'javelin-install',
+ 1 => 'javelin-dom',
+ 2 => 'javelin-stratcom',
+ ),
+ 'disk' => '/rsrc/js/application/core/Notification.js',
+ ),
+ 'phabricator-notification-css' =>
+ array(
+ 'uri' => '/res/423a14d1/rsrc/css/aphront/notification.css',
+ 'type' => 'css',
+ 'requires' =>
+ array(
+ ),
+ 'disk' => '/rsrc/css/aphront/notification.css',
+ ),
'phabricator-object-selector-css' =>
array(
'uri' => '/res/7eb4c705/rsrc/css/application/objectselector/object-selector.css',
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 1ef665b6eb..04db725959 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -966,6 +966,7 @@ phutil_register_library_map(array(
'PhabricatorUIExampleController' => 'applications/uiexample/controller/PhabricatorUIExampleController.php',
'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php',
'PhabricatorUIListFilterExample' => 'applications/uiexample/examples/PhabricatorUIListFilterExample.php',
+ 'PhabricatorUINotificationExample' => 'applications/uiexample/examples/PhabricatorUINotificationExample.php',
'PhabricatorUIPagerExample' => 'applications/uiexample/examples/PhabricatorUIPagerExample.php',
'PhabricatorUITooltipExample' => 'applications/uiexample/examples/PhabricatorUITooltipExample.php',
'PhabricatorUnitsTestCase' => 'view/__tests__/PhabricatorUnitsTestCase.php',
@@ -1900,6 +1901,7 @@ phutil_register_library_map(array(
'PhabricatorUIExampleController' => 'PhabricatorController',
'PhabricatorUIExampleRenderController' => 'PhabricatorUIExampleController',
'PhabricatorUIListFilterExample' => 'PhabricatorUIExample',
+ 'PhabricatorUINotificationExample' => 'PhabricatorUIExample',
'PhabricatorUIPagerExample' => 'PhabricatorUIExample',
'PhabricatorUITooltipExample' => 'PhabricatorUIExample',
'PhabricatorUnitsTestCase' => 'PhabricatorTestCase',
diff --git a/src/applications/uiexample/examples/PhabricatorUINotificationExample.php b/src/applications/uiexample/examples/PhabricatorUINotificationExample.php
new file mode 100644
index 0000000000..fd5082b62e
--- /dev/null
+++ b/src/applications/uiexample/examples/PhabricatorUINotificationExample.php
@@ -0,0 +1,46 @@
+JX.Notification to create notifications.';
+ }
+
+ public function renderExample() {
+
+ require_celerity_resource('phabricator-notification-css');
+ Javelin::initBehavior('phabricator-notification-example');
+
+ $content = javelin_render_tag(
+ 'a',
+ array(
+ 'sigil' => 'notification-example',
+ 'class' => 'button green',
+ ),
+ 'Show Notification');
+
+ $content = '
'.$content.'';
+
+ return $content;
+ }
+}
diff --git a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php
index 75a0588d26..1a3e536875 100644
--- a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php
+++ b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php
@@ -31,6 +31,9 @@ final class PhabricatorJavelinLinter extends ArcanistLinter {
public function willLintPaths(array $paths) {
+ $root = dirname(phutil_get_library_root('phabricator'));
+ require_once $root.'/scripts/__init_script__.php';
+
if ($this->haveSymbolsBinary === null) {
$binary = $this->getSymbolsBinaryPath();
$this->haveSymbolsBinary = Filesystem::pathExists($binary);
diff --git a/webroot/rsrc/css/aphront/notification.css b/webroot/rsrc/css/aphront/notification.css
new file mode 100644
index 0000000000..0ae4826a18
--- /dev/null
+++ b/webroot/rsrc/css/aphront/notification.css
@@ -0,0 +1,24 @@
+/**
+ * @provides phabricator-notification-css
+ */
+
+.jx-notification-container {
+ position: fixed;
+
+ bottom: 24px;
+ left: 24px;
+ width: 240px;
+ padding: 8px 16px;
+
+ font-size: 11px;
+ overflow: hidden;
+
+ background: #f3f6ff;
+ color: #444444;
+ border: 1px solid #afbfcf;
+
+ cursor: pointer;
+
+ border-radius: 3px;
+ box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.25);
+}
diff --git a/webroot/rsrc/js/application/core/Notification.js b/webroot/rsrc/js/application/core/Notification.js
new file mode 100644
index 0000000000..421df0520a
--- /dev/null
+++ b/webroot/rsrc/js/application/core/Notification.js
@@ -0,0 +1,106 @@
+/**
+ * @requires javelin-install
+ * javelin-dom
+ * javelin-stratcom
+ * @provides phabricator-notification
+ * @javelin
+ */
+
+/**
+ * Show a notification. Usage:
+ *
+ * var n = new JX.Notification()
+ * .setContent('click me!');
+ * n.listen('activate', function(e) { alert('you clicked!'); });
+ * n.show();
+ *
+ */
+JX.install('Notification', {
+
+ events : ['activate', 'close'],
+
+ members : {
+ show : function() {
+ var self = JX.Notification;
+
+ self.close();
+ self._installListener();
+ self._active = this;
+
+ var container = JX.$N(
+ 'div',
+ {
+ className: 'jx-notification-container',
+ sigil: 'jx-notification'
+ },
+ this.getContent());
+ document.body.appendChild(container);
+
+ self._container = container;
+
+ if (this.getDuration()) {
+ self._timeout = setTimeout(self.close, this.getDuration());
+ }
+ }
+ },
+
+ properties : {
+ content : null,
+
+ /**
+ * Duration before the notification fades away, in milliseconds. If set to
+ * 0, the notification persists until dismissed.
+ *
+ * @param int Notification duration, in milliseconds.
+ */
+ duration : 12000
+ },
+
+ statics : {
+ _container : null,
+ _listening : false,
+ _active : null,
+ _installListener : function() {
+ var self = JX.Notification;
+
+ if (self._listening) {
+ return;
+ } else {
+ self._listening = true;
+ }
+
+ JX.Stratcom.listen(
+ 'click',
+ 'jx-notification',
+ function(e) {
+ // NOTE: Don't kill the event since the user might have clicked a
+ // link, and we want to follow the link if they did. Istead, invoke
+ // the activate event for the active notification and dismiss it if it
+ // isn't handled.
+
+ var activation = self._active.invoke('activate');
+ if (activation.getPrevented()) {
+ return;
+ }
+
+ self.close();
+ });
+ },
+ close : function() {
+ var self = JX.Notification;
+
+ if (self._container) {
+ JX.DOM.remove(self._container);
+ self._container = null;
+
+ self._active.invoke('close');
+ self._active = null;
+ }
+
+ self._timeout && clearTimeout(self._timeout);
+ self._timeout = null;
+ }
+ }
+
+});
+
diff --git a/webroot/rsrc/js/application/uiexample/notification-example.js b/webroot/rsrc/js/application/uiexample/notification-example.js
new file mode 100644
index 0000000000..1563fa30ed
--- /dev/null
+++ b/webroot/rsrc/js/application/uiexample/notification-example.js
@@ -0,0 +1,29 @@
+/**
+ * @requires phabricator-notification
+ * javelin-stratcom
+ * javelin-behavior
+ * @provides javelin-behavior-phabricator-notification-example
+ */
+
+JX.behavior('phabricator-notification-example', function(config) {
+ JX.Stratcom.listen(
+ 'click',
+ 'notification-example',
+ function(e) {
+ e.kill();
+
+ var notification = new JX.Notification()
+ .setContent('It is ' + new Date().toString());
+
+ notification.listen(
+ 'activate',
+ function(e) {
+ if (!confirm("Close notification?")) {
+ e.kill();
+ }
+ });
+
+ notification.show()
+ });
+
+});