462 lines
12 KiB
JavaScript
462 lines
12 KiB
JavaScript
|
|
(function(){"use strict";angular
|
||
|
|
.module('material.components.expansionPanels')
|
||
|
|
.directive('mdExpansionPanel', expansionPanelDirective);
|
||
|
|
|
||
|
|
|
||
|
|
var ANIMATION_TIME = 180; //ms
|
||
|
|
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @ngdoc directive
|
||
|
|
* @name mdExpansionPanel
|
||
|
|
* @module material.components.expansionPanels
|
||
|
|
*
|
||
|
|
* @restrict E
|
||
|
|
*
|
||
|
|
* @description
|
||
|
|
* `mdExpansionPanel` is the main container for panels
|
||
|
|
*
|
||
|
|
* @param {string=} md-component-id - add an id if you want to acces the panel via the `$mdExpansionPanel` service
|
||
|
|
**/
|
||
|
|
function expansionPanelDirective() {
|
||
|
|
var directive = {
|
||
|
|
restrict: 'E',
|
||
|
|
require: ['mdExpansionPanel', '?^^mdExpansionPanelGroup'],
|
||
|
|
scope: true,
|
||
|
|
compile: compile,
|
||
|
|
controller: ['$scope', '$element', '$attrs', '$window', '$$rAF', '$mdConstant', '$mdUtil', '$mdComponentRegistry', '$timeout', '$q', '$animate', '$parse', controller]
|
||
|
|
};
|
||
|
|
return directive;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
function compile(tElement, tAttrs) {
|
||
|
|
var INVALID_PREFIX = 'Invalid HTML for md-expansion-panel: ';
|
||
|
|
|
||
|
|
tElement.attr('tabindex', tAttrs.tabindex || '0');
|
||
|
|
|
||
|
|
if (tElement[0].querySelector('md-expansion-panel-collapsed') === null) {
|
||
|
|
throw Error(INVALID_PREFIX + 'Expected a child element of `md-epxansion-panel-collapsed`');
|
||
|
|
}
|
||
|
|
if (tElement[0].querySelector('md-expansion-panel-expanded') === null) {
|
||
|
|
throw Error(INVALID_PREFIX + 'Expected a child element of `md-epxansion-panel-expanded`');
|
||
|
|
}
|
||
|
|
|
||
|
|
return function postLink(scope, element, attrs, ctrls) {
|
||
|
|
var epxansionPanelCtrl = ctrls[0];
|
||
|
|
var epxansionPanelGroupCtrl = ctrls[1];
|
||
|
|
|
||
|
|
epxansionPanelCtrl.epxansionPanelGroupCtrl = epxansionPanelGroupCtrl || undefined;
|
||
|
|
epxansionPanelCtrl.init();
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
function controller($scope, $element, $attrs, $window, $$rAF, $mdConstant, $mdUtil, $mdComponentRegistry, $timeout, $q, $animate, $parse) {
|
||
|
|
/* jshint validthis: true */
|
||
|
|
var vm = this;
|
||
|
|
|
||
|
|
var collapsedCtrl;
|
||
|
|
var expandedCtrl;
|
||
|
|
var headerCtrl;
|
||
|
|
var footerCtrl;
|
||
|
|
var deregister;
|
||
|
|
var scrollContainer;
|
||
|
|
var stickyContainer;
|
||
|
|
var topKiller;
|
||
|
|
var resizeKiller;
|
||
|
|
var onRemoveCallback;
|
||
|
|
var transformParent;
|
||
|
|
var backdrop;
|
||
|
|
var inited = false;
|
||
|
|
var registerOnInit = false;
|
||
|
|
var _isOpen = false;
|
||
|
|
var isDisabled = false;
|
||
|
|
var debouncedUpdateScroll = $$rAF.throttle(updateScroll);
|
||
|
|
var debouncedUpdateResize = $$rAF.throttle(updateResize);
|
||
|
|
|
||
|
|
vm.registerCollapsed = function (ctrl) { collapsedCtrl = ctrl; };
|
||
|
|
vm.registerExpanded = function (ctrl) { expandedCtrl = ctrl; };
|
||
|
|
vm.registerHeader = function (ctrl) { headerCtrl = ctrl; };
|
||
|
|
vm.registerFooter = function (ctrl) { footerCtrl = ctrl; };
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
if ($attrs.mdComponentId === undefined) {
|
||
|
|
$attrs.$set('mdComponentId', '_expansion_panel_id_' + $mdUtil.nextUid());
|
||
|
|
registerPanel();
|
||
|
|
} else {
|
||
|
|
$attrs.$observe('mdComponentId', function() {
|
||
|
|
registerPanel();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
vm.$element = $element;
|
||
|
|
vm.expand = expand;
|
||
|
|
vm.collapse = collapse;
|
||
|
|
vm.remove = remove;
|
||
|
|
vm.destroy = destroy;
|
||
|
|
vm.onRemove = onRemove;
|
||
|
|
vm.init = init;
|
||
|
|
|
||
|
|
if ($attrs.ngDisabled !== undefined) {
|
||
|
|
$scope.$watch($attrs.ngDisabled, function(value) {
|
||
|
|
isDisabled = value;
|
||
|
|
$element.attr('tabindex', isDisabled ? -1 : 0);
|
||
|
|
});
|
||
|
|
} else if ($attrs.disabled !== undefined) {
|
||
|
|
isDisabled = ($attrs.disabled !== undefined && $attrs.disabled !== 'false' && $attrs.disabled !== false);
|
||
|
|
$element.attr('tabindex', isDisabled ? -1 : 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
$element
|
||
|
|
.on('focus', function (ev) {
|
||
|
|
$element.on('keydown', handleKeypress);
|
||
|
|
})
|
||
|
|
.on('blur', function (ev) {
|
||
|
|
$element.off('keydown', handleKeypress);
|
||
|
|
});
|
||
|
|
|
||
|
|
function handleKeypress(ev) {
|
||
|
|
var keyCodes = $mdConstant.KEY_CODE;
|
||
|
|
switch (ev.keyCode) {
|
||
|
|
case keyCodes.ENTER:
|
||
|
|
expand();
|
||
|
|
break;
|
||
|
|
case keyCodes.ESCAPE:
|
||
|
|
collapse();
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
$scope.$panel = {
|
||
|
|
collapse: collapse,
|
||
|
|
expand: expand,
|
||
|
|
remove: remove,
|
||
|
|
isOpen: isOpen
|
||
|
|
};
|
||
|
|
|
||
|
|
$scope.$on('$destroy', function () {
|
||
|
|
removeClickCatcher();
|
||
|
|
|
||
|
|
// remove component from registry
|
||
|
|
if (typeof deregister === 'function') {
|
||
|
|
deregister();
|
||
|
|
deregister = undefined;
|
||
|
|
}
|
||
|
|
killEvents();
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
function init() {
|
||
|
|
inited = true;
|
||
|
|
if (registerOnInit === true) {
|
||
|
|
registerPanel();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
function registerPanel() {
|
||
|
|
if (inited === false) {
|
||
|
|
registerOnInit = true;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// deregister if component was already registered
|
||
|
|
if (typeof deregister === 'function') {
|
||
|
|
deregister();
|
||
|
|
deregister = undefined;
|
||
|
|
}
|
||
|
|
// remove component from group ctrl if component was already added
|
||
|
|
if (vm.componentId && vm.epxansionPanelGroupCtrl) {
|
||
|
|
vm.epxansionPanelGroupCtrl.removePanel(vm.componentId);
|
||
|
|
}
|
||
|
|
|
||
|
|
// if componentId was removed then set one
|
||
|
|
if ($attrs.mdComponentId === undefined) {
|
||
|
|
$attrs.$set('mdComponentId', '_expansion_panel_id_' + $mdUtil.nextUid());
|
||
|
|
}
|
||
|
|
|
||
|
|
vm.componentId = $attrs.mdComponentId;
|
||
|
|
deregister = $mdComponentRegistry.register({
|
||
|
|
expand: expand,
|
||
|
|
collapse: collapse,
|
||
|
|
remove: remove,
|
||
|
|
onRemove: onRemove,
|
||
|
|
isOpen: isOpen,
|
||
|
|
addClickCatcher: addClickCatcher,
|
||
|
|
removeClickCatcher: removeClickCatcher,
|
||
|
|
componentId: $attrs.mdComponentId
|
||
|
|
}, $attrs.mdComponentId);
|
||
|
|
|
||
|
|
if (vm.epxansionPanelGroupCtrl) {
|
||
|
|
vm.epxansionPanelGroupCtrl.addPanel(vm.componentId, {
|
||
|
|
expand: expand,
|
||
|
|
collapse: collapse,
|
||
|
|
remove: remove,
|
||
|
|
onRemove: onRemove,
|
||
|
|
destroy: destroy,
|
||
|
|
isOpen: isOpen
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
function isOpen() {
|
||
|
|
return _isOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
function expand(options) {
|
||
|
|
if (_isOpen === true || isDisabled === true) { return; }
|
||
|
|
_isOpen = true;
|
||
|
|
options = options || {};
|
||
|
|
|
||
|
|
var deferred = $q.defer();
|
||
|
|
|
||
|
|
if (vm.epxansionPanelGroupCtrl) {
|
||
|
|
vm.epxansionPanelGroupCtrl.expandPanel(vm.componentId);
|
||
|
|
}
|
||
|
|
|
||
|
|
$element.removeClass('md-close');
|
||
|
|
$element.addClass('md-open');
|
||
|
|
if (options.animation === false) {
|
||
|
|
$element.addClass('md-no-animation');
|
||
|
|
} else {
|
||
|
|
$element.removeClass('md-no-animation');
|
||
|
|
}
|
||
|
|
|
||
|
|
initEvents();
|
||
|
|
collapsedCtrl.hide(options);
|
||
|
|
expandedCtrl.show(options);
|
||
|
|
|
||
|
|
if (headerCtrl) { headerCtrl.show(options); }
|
||
|
|
if (footerCtrl) { footerCtrl.show(options); }
|
||
|
|
|
||
|
|
$timeout(function () {
|
||
|
|
deferred.resolve();
|
||
|
|
}, options.animation === false ? 0 : ANIMATION_TIME);
|
||
|
|
return deferred.promise;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
function collapse(options) {
|
||
|
|
if (_isOpen === false) { return; }
|
||
|
|
_isOpen = false;
|
||
|
|
options = options || {};
|
||
|
|
|
||
|
|
var deferred = $q.defer();
|
||
|
|
|
||
|
|
$element.addClass('md-close');
|
||
|
|
$element.removeClass('md-open');
|
||
|
|
if (options.animation === false) {
|
||
|
|
$element.addClass('md-no-animation');
|
||
|
|
} else {
|
||
|
|
$element.removeClass('md-no-animation');
|
||
|
|
}
|
||
|
|
|
||
|
|
killEvents();
|
||
|
|
collapsedCtrl.show(options);
|
||
|
|
expandedCtrl.hide(options);
|
||
|
|
|
||
|
|
if (headerCtrl) { headerCtrl.hide(options); }
|
||
|
|
if (footerCtrl) { footerCtrl.hide(options); }
|
||
|
|
|
||
|
|
$timeout(function () {
|
||
|
|
deferred.resolve();
|
||
|
|
}, options.animation === false ? 0 : ANIMATION_TIME);
|
||
|
|
return deferred.promise;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
function remove(options) {
|
||
|
|
options = options || {};
|
||
|
|
var deferred = $q.defer();
|
||
|
|
|
||
|
|
if (vm.epxansionPanelGroupCtrl) {
|
||
|
|
vm.epxansionPanelGroupCtrl.removePanel(vm.componentId);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof deregister === 'function') {
|
||
|
|
deregister();
|
||
|
|
deregister = undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.animation === false || _isOpen === false) {
|
||
|
|
$scope.$destroy();
|
||
|
|
$element.remove();
|
||
|
|
deferred.resolve();
|
||
|
|
callbackRemove();
|
||
|
|
} else {
|
||
|
|
collapse();
|
||
|
|
$timeout(function () {
|
||
|
|
$scope.$destroy();
|
||
|
|
$element.remove();
|
||
|
|
deferred.resolve();
|
||
|
|
callbackRemove();
|
||
|
|
}, ANIMATION_TIME);
|
||
|
|
}
|
||
|
|
|
||
|
|
return deferred.promise;
|
||
|
|
}
|
||
|
|
|
||
|
|
function onRemove(callback) {
|
||
|
|
onRemoveCallback = callback;
|
||
|
|
}
|
||
|
|
|
||
|
|
function callbackRemove() {
|
||
|
|
if (typeof onRemoveCallback === 'function') {
|
||
|
|
onRemoveCallback();
|
||
|
|
onRemoveCallback = undefined;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function destroy() {
|
||
|
|
$scope.$destroy();
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
function initEvents() {
|
||
|
|
if ((!footerCtrl || footerCtrl.noSticky === true) && (!headerCtrl || headerCtrl.noSticky === true)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// watch for panel position changes
|
||
|
|
topKiller = $scope.$watch(function () { return $element[0].offsetTop; }, debouncedUpdateScroll, true);
|
||
|
|
|
||
|
|
// watch for panel position changes
|
||
|
|
resizeKiller = $scope.$watch(function () { return $element[0].offsetWidth; }, debouncedUpdateResize, true);
|
||
|
|
|
||
|
|
// listen to md-content scroll events id we are nested in one
|
||
|
|
scrollContainer = $mdUtil.getNearestContentElement($element);
|
||
|
|
if (scrollContainer.nodeName === 'MD-CONTENT') {
|
||
|
|
transformParent = getTransformParent(scrollContainer);
|
||
|
|
angular.element(scrollContainer).on('scroll', debouncedUpdateScroll);
|
||
|
|
} else {
|
||
|
|
transformParent = undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
// listen to expanded content scroll if height is set
|
||
|
|
if (expandedCtrl.setHeight === true) {
|
||
|
|
expandedCtrl.$element.on('scroll', debouncedUpdateScroll);
|
||
|
|
}
|
||
|
|
|
||
|
|
// listen to window scroll events
|
||
|
|
angular.element($window)
|
||
|
|
.on('scroll', debouncedUpdateScroll)
|
||
|
|
.on('resize', debouncedUpdateScroll)
|
||
|
|
.on('resize', debouncedUpdateResize);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
function killEvents() {
|
||
|
|
if (typeof topKiller === 'function') {
|
||
|
|
topKiller();
|
||
|
|
topKiller = undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof resizeKiller === 'function') {
|
||
|
|
resizeKiller();
|
||
|
|
resizeKiller = undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (scrollContainer && scrollContainer.nodeName === 'MD-CONTENT') {
|
||
|
|
angular.element(scrollContainer).off('scroll', debouncedUpdateScroll);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (expandedCtrl.setHeight === true) {
|
||
|
|
expandedCtrl.$element.off('scroll', debouncedUpdateScroll);
|
||
|
|
}
|
||
|
|
|
||
|
|
angular.element($window)
|
||
|
|
.off('scroll', debouncedUpdateScroll)
|
||
|
|
.off('resize', debouncedUpdateScroll)
|
||
|
|
.off('resize', debouncedUpdateResize);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
function getTransformParent(el) {
|
||
|
|
var parent = el.parentNode;
|
||
|
|
|
||
|
|
while (parent && parent !== document) {
|
||
|
|
if (hasComputedStyle(parent, 'transform')) {
|
||
|
|
return parent;
|
||
|
|
}
|
||
|
|
parent = parent.parentNode;
|
||
|
|
}
|
||
|
|
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
function hasComputedStyle(target, key) {
|
||
|
|
var hasValue = false;
|
||
|
|
|
||
|
|
if (target) {
|
||
|
|
var computedStyles = $window.getComputedStyle(target);
|
||
|
|
hasValue = computedStyles[key] !== undefined && computedStyles[key] !== 'none';
|
||
|
|
}
|
||
|
|
|
||
|
|
return hasValue;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
function updateScroll(e) {
|
||
|
|
var top;
|
||
|
|
var bottom;
|
||
|
|
var bounds;
|
||
|
|
if (expandedCtrl.setHeight === true) {
|
||
|
|
bounds = expandedCtrl.$element[0].getBoundingClientRect();
|
||
|
|
} else {
|
||
|
|
bounds = scrollContainer.getBoundingClientRect();
|
||
|
|
}
|
||
|
|
var transformTop = transformParent ? transformParent.getBoundingClientRect().top : 0;
|
||
|
|
|
||
|
|
// we never want the header going post the top of the page. to prevent this don't allow top to go below 0
|
||
|
|
top = Math.max(bounds.top, 0);
|
||
|
|
bottom = top + bounds.height;
|
||
|
|
|
||
|
|
if (footerCtrl && footerCtrl.noSticky === false) { footerCtrl.onScroll(top, bottom, transformTop); }
|
||
|
|
if (headerCtrl && headerCtrl.noSticky === false) { headerCtrl.onScroll(top, bottom, transformTop); }
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
function updateResize() {
|
||
|
|
var value = $element[0].offsetWidth;
|
||
|
|
if (footerCtrl && footerCtrl.noSticky === false) { footerCtrl.onResize(value); }
|
||
|
|
if (headerCtrl && headerCtrl.noSticky === false) { headerCtrl.onResize(value); }
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
function addClickCatcher(clickCallback) {
|
||
|
|
backdrop = $mdUtil.createBackdrop($scope);
|
||
|
|
backdrop[0].tabIndex = -1;
|
||
|
|
|
||
|
|
if (typeof clickCallback === 'function') {
|
||
|
|
backdrop.on('click', clickCallback);
|
||
|
|
}
|
||
|
|
|
||
|
|
$animate.enter(backdrop, $element.parent(), null, {duration: 0});
|
||
|
|
$element.css('z-index', 60);
|
||
|
|
}
|
||
|
|
|
||
|
|
function removeClickCatcher() {
|
||
|
|
if (backdrop) {
|
||
|
|
backdrop.remove();
|
||
|
|
backdrop.off('click');
|
||
|
|
backdrop = undefined;
|
||
|
|
$element.css('z-index', '');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}());
|