Skip to content
This repository has been archived by the owner on May 29, 2019. It is now read-only.

feat(dialog): rewrite $dialog #441

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
314 changes: 277 additions & 37 deletions src/modal/modal.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,287 @@
angular.module('ui.bootstrap.modal', ['ui.bootstrap.dialog'])
.directive('modal', ['$parse', '$dialog', function($parse, $dialog) {
return {
restrict: 'EA',
terminal: true,
link: function(scope, elm, attrs) {
var opts = angular.extend({}, scope.$eval(attrs.uiOptions || attrs.bsOptions || attrs.options));
var shownExpr = attrs.modal || attrs.show;
var setClosed;

// Create a dialog with the template as the contents of the directive
// Add the current scope as the resolve in order to make the directive scope as a dialog controller scope
opts = angular.extend(opts, {
template: elm.html(),
resolve: { $scope: function() { return scope; } }
});
var dialog = $dialog.dialog(opts);
angular.module('ui.bootstrap.modal', [])

elm.remove();
/**
* A helper, internal data structure that acts as a map but also allows getting / removing
* elements in the LIFO order
*/
.factory('$$stackedMap', function () {
return {
createNew: function () {
var stack = [];

if (attrs.close) {
setClosed = function() {
$parse(attrs.close)(scope);
return {
add: function (key, value) {
stack.push({
key: key,
value: value
});
},
get: function (key) {
for (var i = 0; i < stack.length; i++) {
if (key == stack[i].key) {
return stack[i];
}
}
},
top: function () {
return stack[stack.length - 1];
},
remove: function (key) {
var idx = -1;
for (var i = 0; i < stack.length; i++) {
if (key == stack[i].key) {
idx = i;
break;
}
}
return stack.splice(idx, 1)[0];
},
removeTop: function () {
return stack.splice(stack.length - 1, 1)[0];
},
length: function () {
return stack.length;
}
};
} else {
setClosed = function() {
if (angular.isFunction($parse(shownExpr).assign)) {
$parse(shownExpr).assign(scope, false);
}
};
})

/**
* A helper directive for the $modal service. It creates a backdrop element.
*/
.directive('modalBackdrop', ['$modalStack', '$timeout', function ($modalStack, $timeout) {
return {
restrict: 'EA',
scope: {},
replace: true,
templateUrl: 'template/modal/backdrop.html',
link: function (scope, element, attrs) {

//trigger CSS transitions
$timeout(function () {
scope.animate = true;
}, 0, false);

scope.close = function (evt) {
var modal = $modalStack.getTop();
//TODO: this logic is duplicated with the place where modal gets opened
if (modal && modal.window.backdrop && modal.window.backdrop != 'static') {
evt.preventDefault();
evt.stopPropagation();
$modalStack.dismiss(modal.instance, 'backdrop click');
}
};
}
};
}])

.directive('modalWindow', ['$timeout', function ($timeout) {
return {
restrict: 'EA',
scope: {},
replace: true,
transclude: true,
templateUrl: 'template/modal/window.html',
link: function (scope, element, attrs) {
//trigger CSS transitions
$timeout(function () {
scope.animate = true;
}, 0, false);
}
};
}])

.factory('$modalStack', ['$document', '$compile', '$rootScope', '$$stackedMap',
function ($document, $compile, $rootScope, $$stackedMap) {

var body = $document.find('body').eq(0);
var openedWindows = $$stackedMap.createNew();
var $modalStack = {};

function removeModalWindow(modalInstance) {

var modalWindow = openedWindows.get(modalInstance).value;

//clean up the stack
openedWindows.remove(modalInstance);

//remove DOM element
modalWindow.modalDomEl.remove();

//remove backdrop
if (modalWindow.backdropDomEl) {
modalWindow.backdropDomEl.remove();
}

//destroy scope
modalWindow.modalScope.$destroy();
}

scope.$watch(shownExpr, function(isShown, oldShown) {
if (isShown) {
dialog.open().then(function(){
setClosed();
});
} else {
//Make sure it is not opened
if (dialog.isOpen()){
dialog.close();
$document.bind('keydown', function (evt) {
var modal;

if (evt.which === 27) {
modal = openedWindows.top();
if (modal && modal.value.keyboard) {
$rootScope.$apply(function () {
$modalStack.dismiss(modal.key);
});
}
}
});
}
};
}]);

$modalStack.open = function (modalInstance, modal) {

var backdropDomEl;
var modalDomEl = $compile(angular.element('<modal-window>').html(modal.content))(modal.scope);
body.append(modalDomEl);

if (modal.backdrop) {
backdropDomEl = $compile(angular.element('<modal-backdrop>'))($rootScope);
body.append(backdropDomEl);
}

openedWindows.add(modalInstance, {
deferred: modal.deferred,
modalScope: modal.scope,
modalDomEl: modalDomEl,
backdrop: modal.backdrop,
backdropDomEl: backdropDomEl,
keyboard: modal.keyboard
});
};

$modalStack.close = function (modalInstance, result) {
var modal = openedWindows.get(modalInstance);
if (modal) {
modal.value.deferred.resolve(result);
removeModalWindow(modalInstance);
}
};

$modalStack.dismiss = function (modalInstance, reason) {
var modalWindow = openedWindows.get(modalInstance).value;
if (modalWindow) {
modalWindow.deferred.reject(reason);
removeModalWindow(modalInstance);
}
};

$modalStack.getTop = function () {
var top = openedWindows.top();
if (top) {
return {
instance: top.key,
window: top.value
};
}
};

return $modalStack;
}])

.provider('$modal', function () {

var defaultOptions = {
backdrop: true, //can be also false or 'static'
keyboard: true
};

return {
options: defaultOptions,
$get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack',
function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) {

var $modal = {};

function getTemplatePromise(options) {
return options.template ? $q.when(options.template) :
$http.get(options.templateUrl, {cache: $templateCache}).then(function (result) {
return result.data;
});
}

function getResolvePromises(resolves) {
var promisesArr = [];
angular.forEach(resolves, function (value, key) {
if (angular.isFunction(value) || angular.isArray(value)) {
promisesArr.push($q.when($injector.invoke(value)));
}
});
return promisesArr;
}

$modal.open = function (modalOptions) {

var modalResultDeferred = $q.defer();
var modalOpenedDeferred = $q.defer();

//prepare an instance of a modal to be injected into controllers and returned to a caller
var modalInstance = {
result: modalResultDeferred.promise,
opened: modalOpenedDeferred.promise,
close: function (result) {
$modalStack.close(this, result);
},
dismiss: function (reason) {
$modalStack.dismiss(this, reason);
}
};

//merge and clean up options
modalOptions = angular.extend(defaultOptions, modalOptions);
modalOptions.resolve = modalOptions.resolve || {};

//verify options
if (!modalOptions.template && !modalOptions.templateUrl) {
throw new Error('One of template or templateUrl options is required.');
}

var templateAndResolvePromise =
$q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));


templateAndResolvePromise.then(function resolveSuccess(tplAndVars) {

var modalScope = (modalOptions.scope || $rootScope).$new();

var ctrlInstance, ctrlLocals = {};
var resolveIter = 1;

//controllers
if (modalOptions.controller) {
ctrlLocals.$scope = modalScope;
ctrlLocals.$modalInstance = modalInstance;
angular.forEach(modalOptions.resolve, function (value, key) {
ctrlLocals[key] = tplAndVars[resolveIter++];
});

ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
}

$modalStack.open(modalInstance, {
scope: modalScope,
deferred: modalResultDeferred,
content: tplAndVars[0],
backdrop: modalOptions.backdrop,
keyboard: modalOptions.keyboard
});

}, function resolveError(reason) {
modalResultDeferred.reject(reason);
});

templateAndResolvePromise.then(function () {
modalOpenedDeferred.resolve(true);
}, function () {
modalOpenedDeferred.reject(false);
});

return modalInstance;
};

return $modal;
}]
};
});
Loading