diff --git a/Packages/ohif-core/client/components/bootstrap/index.js b/Packages/ohif-core/client/components/bootstrap/index.js
index 1e4fc3db5..cf5e5ed2f 100644
--- a/Packages/ohif-core/client/components/bootstrap/index.js
+++ b/Packages/ohif-core/client/components/bootstrap/index.js
@@ -32,4 +32,6 @@ import './input/range.html';
import './input/select.html';
import './input/text.html';
+import './notification';
+
import './popover/form.html';
diff --git a/Packages/ohif-core/client/components/bootstrap/notification/index.js b/Packages/ohif-core/client/components/bootstrap/notification/index.js
new file mode 100644
index 000000000..0ae286d8e
--- /dev/null
+++ b/Packages/ohif-core/client/components/bootstrap/notification/index.js
@@ -0,0 +1,4 @@
+import './notificationArea.html';
+import './notificationArea.styl';
+import './notificationNote.html';
+import './notificationNote.js';
diff --git a/Packages/ohif-core/client/components/bootstrap/notification/notificationArea.html b/Packages/ohif-core/client/components/bootstrap/notification/notificationArea.html
new file mode 100644
index 000000000..4648c287d
--- /dev/null
+++ b/Packages/ohif-core/client/components/bootstrap/notification/notificationArea.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/Packages/ohif-core/client/components/bootstrap/notification/notificationArea.styl b/Packages/ohif-core/client/components/bootstrap/notification/notificationArea.styl
new file mode 100644
index 000000000..c7b3b8790
--- /dev/null
+++ b/Packages/ohif-core/client/components/bootstrap/notification/notificationArea.styl
@@ -0,0 +1,62 @@
+@require '{ohif:design}/app'
+
+.notification-area
+ position: fixed
+ right: 0
+ top: 50px
+ max-width: 320px
+ z-index: 10000
+
+.notification-note
+ opacity: 0
+ position: relative
+ transition(opacity 0.5s linear\, max-height 0.5s linear)
+ width: 300px
+ z-index: 2
+
+ .note-container
+ padding: 10px
+ transform(translateY(0))
+ transition(transform 0.5s linear)
+
+ .note-body
+ margin: 0
+ padding: 10px
+ position: relative
+
+ &:not(.hide-dismiss)
+ padding-right: 40px
+
+ .note-dismiss
+ display: block
+
+ .note-dismiss
+ bottom: 0
+ cursor: pointer
+ display: none
+ position: absolute
+ right: 0
+ top: 0
+ width: 40px
+
+ i
+ display: block
+ font-size: 20px
+ left: 0
+ position: absolute
+ text-align: center
+ top: 50%
+ transform(translateY(-50%))
+ width: 100%
+
+ &.in
+ max-height: auto
+ opacity: 1
+
+ &.out
+ max-height: 0 !important
+ opacity: 0
+ z-index: 1
+
+ .note-container
+ transform(translateY(-100%))
diff --git a/Packages/ohif-core/client/components/bootstrap/notification/notificationNote.html b/Packages/ohif-core/client/components/bootstrap/notification/notificationNote.html
new file mode 100644
index 000000000..72c244bf8
--- /dev/null
+++ b/Packages/ohif-core/client/components/bootstrap/notification/notificationNote.html
@@ -0,0 +1,12 @@
+
+
+
diff --git a/Packages/ohif-core/client/components/bootstrap/notification/notificationNote.js b/Packages/ohif-core/client/components/bootstrap/notification/notificationNote.js
new file mode 100644
index 000000000..c0581c38b
--- /dev/null
+++ b/Packages/ohif-core/client/components/bootstrap/notification/notificationNote.js
@@ -0,0 +1,21 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { OHIF } from 'meteor/ohif:core';
+
+Template.notificationNote.onRendered(() => {
+ const instance = Template.instance();
+ Meteor.setTimeout(() => {
+ const $note = instance.$('.notification-note');
+ $note.css('max-height', $note.outerHeight()).addClass('in');
+ }, 100);
+});
+
+Template.notificationNote.events({
+ 'click .note-dismiss'(event, instance) {
+ if (instance.data.promiseResolve) {
+ instance.data.promiseResolve();
+ } else {
+ OHIF.ui.notifications.dismiss(instance.data.id);
+ }
+ }
+});
diff --git a/Packages/ohif-core/client/ui/index.js b/Packages/ohif-core/client/ui/index.js
index 30d66293d..76df02b1c 100644
--- a/Packages/ohif-core/client/ui/index.js
+++ b/Packages/ohif-core/client/ui/index.js
@@ -6,6 +6,7 @@ import './dialog/unsavedChangesDialog.js';
import './draggable/draggable.js';
import './dropdown/class.js';
import './dropdown/display.js';
+import './notifications/notifications.js';
import './popover/display.js';
import './resizable/resizable.js';
import './unsavedChanges/unsavedChanges.js';
diff --git a/Packages/ohif-core/client/ui/notifications/notifications.js b/Packages/ohif-core/client/ui/notifications/notifications.js
new file mode 100644
index 000000000..57fb9e205
--- /dev/null
+++ b/Packages/ohif-core/client/ui/notifications/notifications.js
@@ -0,0 +1,105 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { ReactiveDict } from 'meteor/reactive-dict';
+import { Tracker } from 'meteor/tracker';
+import { $ } from 'meteor/jquery';
+import { OHIF } from 'meteor/ohif:core';
+
+const Notifications = {
+ currentId: 0,
+ views: new Map()
+};
+
+// Remove the view object from DOM and from views Map
+const removeView = (id, view) => {
+ Notifications.views.delete(id);
+ Blaze.remove(view);
+};
+
+// Dismiss a single notification note by its id
+Notifications.dismiss = id => {
+ const view = Notifications.views.get(id);
+ if (!view || view.isDestroyed) {
+ return Notifications.views.delete(id);
+ }
+
+ const node = view.firstNode();
+ const $note = node && $(node);
+
+ if ($note.length) {
+ $note.addClass('out').one('transitionend', () => removeView(id, view));
+ } else {
+ removeView(id, view)
+ }
+};
+
+// Dismiss all notification notes
+Notifications.clear = () => Array.from(Notifications.views.keys()).forEach(Notifications.dismiss);
+
+// Display a notification note
+Notifications.show = ({ template, promise, text, style, timeout=5000 }) => {
+ // Check if the given template exists
+ const templateObject = Template[template];
+ if (template && !templateObject) {
+ throw new Meteor.Error('TEMPLATE_NOT_FOUND', `Template ${template} not found.`);
+ }
+
+ // Check if there is a notification area container
+ const $area = $('#notificationArea');
+ if (!$area.length) {
+ throw new Meteor.Error('NOTIFICATION_AREA_NOT_FOUND', `Notification area not found.`);
+ }
+
+ let notificationPromise;
+ let templateData = {
+ text,
+ style,
+ timeout,
+ id: Notifications.currentId++
+ };
+
+ if (promise instanceof Promise) {
+ // Use the given promise to control the notification
+ notificationPromise = templateData.promise = promise;
+ } else {
+ // Create a new promise to control the modal and store its resolve and reject callbacks
+ let promiseResolve;
+ let promiseReject;
+ notificationPromise = new Promise((resolve, reject) => {
+ promiseResolve = resolve;
+ promiseReject = reject;
+ });
+
+ // Render the notification passing the promise object and callbacks
+ _.extend({}, templateData, {
+ promise: notificationPromise,
+ promiseResolve,
+ promiseReject
+ });
+ }
+
+ const view = Blaze.renderWithData(Template.notificationNote, templateData, $area[0]);
+ const dismissNotification = () => Notifications.dismiss(templateData.id);
+
+ // Add the current view to the list of views to allow clearing all notifications
+ Notifications.views.set(templateData.id, view);
+
+ // Destroy the created notification view when the promise is either resolved or rejected
+ notificationPromise.then(dismissNotification).catch(dismissNotification);
+
+ // Destroy the created notification view if the given timeout time has passed
+ if (timeout > 0) {
+ Meteor.setTimeout(() => {
+ if (templateData.promiseResolve) {
+ templateData.promiseResolve();
+ } else {
+ dismissNotification();
+ }
+ }, timeout);
+ }
+
+ // Return the promise to allow callbacks stacking from outside
+ return notificationPromise;
+};
+
+OHIF.ui.notifications = Notifications;
diff --git a/Packages/ohif-hotkeys/client/components/form.html b/Packages/ohif-hotkeys/client/components/form.html
index 185b8b1f1..6fd147fd5 100644
--- a/Packages/ohif-hotkeys/client/components/form.html
+++ b/Packages/ohif-hotkeys/client/components/form.html
@@ -15,7 +15,7 @@
{{#button class='btn btn-danger pull-left' action='resetDefaults'}}Reset to Defaults{{/button}}
{{#button class='btn btn-primary pull-right' action='save'}}Save{{/button}}
- {{#button class='btn btn-default pull-right m-r-1' tagAttributes=(extend '' data-dismiss='modal')}}Cancel{{/button}}
+ {{#button class='btn btn-default pull-right m-r-1' tagAttributes=(extend '' data-dismiss='modal')}}Close{{/button}}
{{/form}}
diff --git a/Packages/ohif-hotkeys/client/components/form.js b/Packages/ohif-hotkeys/client/components/form.js
index 1bae95c4a..47a9ef790 100644
--- a/Packages/ohif-hotkeys/client/components/form.js
+++ b/Packages/ohif-hotkeys/client/components/form.js
@@ -11,7 +11,12 @@ Template.hotkeysForm.onCreated(() => {
const { contextName } = instance.data;
const form = instance.$('form').first().data('component');
const definitions = form.value();
- return OHIF.hotkeys.store(contextName, definitions);
+ const promise = OHIF.hotkeys.store(contextName, definitions);
+ promise.then(() => OHIF.ui.notifications.show({
+ text: 'The keyboard shortcut preferences were successfully saved.',
+ style: 'success'
+ }));
+ return promise;
},
resetDefaults() {