angular-stackables
Advanced tools
Comparing version 0.0.1 to 0.0.5
{ | ||
"name": "angular-stackables", | ||
"version": "0.0.1", | ||
"version": "0.0.5", | ||
"description": "AngularJS stackable widgets built on HTML5 dialog.", | ||
@@ -13,3 +13,7 @@ "authors": [ | ||
"dialog-polyfill": "0.2.0" | ||
} | ||
}, | ||
"ignore": [ | ||
"node_modules", | ||
"bower_components" | ||
] | ||
} |
{ | ||
"name": "angular-stackables", | ||
"version": "0.0.1", | ||
"version": "0.0.5", | ||
"dependencies": {} | ||
} |
@@ -5,1 +5,96 @@ angular-stackables | ||
AngularJS stackable widgets (modals, popovers, menus) that use HTML5 dialog | ||
# Examples | ||
## A simple modal | ||
```html | ||
<div ng-controller="TestController as test"> | ||
<div stackable="test.isOpen" | ||
stackable-modal="true" | ||
stackable-disable-escape="false" | ||
stackable-closing="test.modalClosing(err, result)" | ||
stackable-closed="test.modalClosed(err, result)"> | ||
<div class="stackable-dialog"> | ||
<div inner-directive> | ||
<p>Test Dialog</p> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
``` | ||
```js | ||
function TestController() { | ||
this.isOpen = false; | ||
this.modalClosing = function(err, result) { | ||
/* return false or a Promise that resolves to false to prevent close */ | ||
return true; | ||
}; | ||
this.modalClosed = function(err, result) { | ||
console.log('modal closed', err, result); | ||
}; | ||
} | ||
module.directive({ | ||
innerDirective: function() { | ||
return { | ||
restrict: 'A', | ||
require: '^stackable', | ||
replace: true, | ||
transclude: true, | ||
template: '<div ng-transclude></div>', | ||
link: function(scope, element, attrs, ctrl) { | ||
// use stackable controller API to close stackable programmatically | ||
ctrl.close(null, 'closed from inner directive'); | ||
} | ||
}; | ||
} | ||
}); | ||
``` | ||
## A simple popover | ||
```html | ||
<div ng-controller="TestController as test"> | ||
<a class="btn" stackable-trigger="test.popoverState"> | ||
<i class="caret"></i> | ||
</a> | ||
<div stackable-popover="test.popoverState" | ||
stackable-placement="bottom" | ||
stackable-alignment="center" | ||
stackable-enable-escape="true"> | ||
<h3 class="stackable-popover-title">Title</h3> | ||
<div class="stackable-popover-body"> | ||
<p>Hello World</p> | ||
</div> | ||
</div> | ||
</div> | ||
``` | ||
## A simple menu | ||
```html | ||
<div ng-controller="TestController as test"> | ||
<a class="btn" stackable-trigger="test.menuState"> | ||
<i class="caret"></i> | ||
</a> | ||
<div stackable-popover="test.menuState" | ||
stackable-hide-arrow="true" | ||
stackable-placement="bottom" | ||
stackable-alignment="right" | ||
stackable-enable-escape="true"> | ||
<div class="stackable-popover-body"> | ||
<ul class="unstyled"> | ||
<li> | ||
<a href="#">Menu Item</a> | ||
</li> | ||
</ul> | ||
</div> | ||
</div> | ||
</div> | ||
``` |
@@ -14,5 +14,8 @@ /*! | ||
module.directive({'stackable': factory}); | ||
module.directive({stackable: stackableDirective}); | ||
module.directive({stackableCancel: stackableCancelDirective}); | ||
module.directive({stackablePopover: stackablePopoverDirective}); | ||
module.directive({stackableTrigger: ['$parse', stackableTriggerDirective]}); | ||
function factory() { | ||
function stackableDirective() { | ||
return { | ||
@@ -30,5 +33,6 @@ scope: { | ||
template: ' \ | ||
<dialog class="stackable"> \ | ||
<div data-ng-if="show" class="stackable-content"> \ | ||
<div data-ng-transclude></div> \ | ||
<dialog class="stackable" ng-class="{\'stackable-modal\': modal}" \ | ||
ng-show="show"> \ | ||
<div ng-if="show" class="stackable-content"> \ | ||
<div ng-transclude></div> \ | ||
</div> \ | ||
@@ -80,3 +84,4 @@ </dialog>', | ||
dialog.addEventListener('close', function() { | ||
dialog.addEventListener('close', function(e) { | ||
e.stopPropagation(); | ||
scope.show = open = false; | ||
@@ -123,4 +128,333 @@ var count = body.data('stackables') - 1; | ||
return {stackable: factory}; | ||
function stackableDirective() { | ||
return { | ||
scope: { | ||
show: '=stackable', | ||
modal: '=?stackableModal', | ||
disableEscape: '=?stackableDisableEscape', | ||
closing: '&?stackableClosing', | ||
closed: '&?stackableClosed' | ||
}, | ||
restrict: 'A', | ||
replace: true, | ||
transclude: true, | ||
template: ' \ | ||
<dialog class="stackable" ng-class="{\'stackable-modal\': modal}" \ | ||
ng-show="show"> \ | ||
<div ng-if="show" class="stackable-content"> \ | ||
<div ng-transclude></div> \ | ||
</div> \ | ||
</dialog>', | ||
controller: ['$scope', Controller], | ||
link: Link | ||
}; | ||
function Controller($scope) { | ||
var self = this; | ||
var stackable = $scope.stackable = self; | ||
// close the stackable unless 'closing' callback aborts | ||
self.close = function(err, result) { | ||
var closing = $scope.closing || angular.noop; | ||
var shouldClose = closing.call($scope.$parent, { | ||
err: err, | ||
result: result | ||
}); | ||
Promise.resolve(shouldClose).then(function() { | ||
if(shouldClose !== false) { | ||
stackable.error = err; | ||
stackable.result = result; | ||
$scope.show = false; | ||
$scope.$apply(); | ||
} | ||
}); | ||
}; | ||
} | ||
function Link(scope, element) { | ||
var open = false; | ||
var body = angular.element('body'); | ||
var dialog = element[0]; | ||
// use polyfill if necessary | ||
if(!dialog.showModal && typeof dialogPolyfill !== 'undefined') { | ||
dialogPolyfill.registerDialog(dialog); | ||
} | ||
dialog.addEventListener('cancel', function(e) { | ||
if(!!scope.disableEscape) { | ||
e.preventDefault(); | ||
} else { | ||
scope.stackable.error = 'canceled'; | ||
scope.stackable.result = null; | ||
} | ||
}); | ||
dialog.addEventListener('close', function(e) { | ||
e.stopPropagation(); | ||
scope.show = open = false; | ||
var count = body.data('stackables') - 1; | ||
body.data('stackables', count); | ||
if(count === 0) { | ||
body.removeClass('stackable-modal-open'); | ||
} | ||
scope.$apply(); | ||
if(scope.closed) { | ||
scope.closed.call(scope.$parent, { | ||
err: scope.stackable.error, | ||
result: scope.stackable.result | ||
}); | ||
} | ||
}); | ||
scope.$watch('show', function(value) { | ||
if(value) { | ||
if(!open) { | ||
if(!!scope.modal) { | ||
dialog.showModal(); | ||
body.addClass('stackable-modal-open'); | ||
} else { | ||
dialog.show(); | ||
} | ||
open = true; | ||
scope.stackable.error = scope.stackable.result = undefined; | ||
var count = body.data('stackables') || 0; | ||
body.data('stackables', count + 1); | ||
} | ||
} else if(open) { | ||
// schedule dialog close to avoid $digest already in progress | ||
// as 'close' event handler may be called from here or externally | ||
setTimeout(function() { | ||
dialog.close(); | ||
}); | ||
open = false; | ||
} | ||
}); | ||
} | ||
} | ||
function stackableCancelDirective() { | ||
return { | ||
restrict: 'AC', | ||
require: '^stackable', | ||
link: function(scope, element, attrs, ctrl) { | ||
element.on('click', function() { | ||
ctrl.close('canceled', null); | ||
}); | ||
} | ||
}; | ||
} | ||
function stackablePopoverDirective() { | ||
return { | ||
scope: { | ||
state: '=stackablePopover', | ||
hideArrow: '=?stackableHideArrow', | ||
alignment: '@?stackableAlignment', | ||
placement: '@?stackablePlacement', | ||
enableEscape: '=?stackableEnableEscape', | ||
disableBlurClose: '=?stackableDisableBlurClose' | ||
}, | ||
restrict: 'A', | ||
replace: true, | ||
transclude: true, | ||
template: ' \ | ||
<div class="stackable-popover" ng-class="{ \ | ||
\'stackable-place-top\': !placement || placement == \'top\', \ | ||
\'stackable-place-right\': placement == \'right\', \ | ||
\'stackable-place-bottom\': placement == \'bottom\', \ | ||
\'stackable-place-left\': placement == \'left\', \ | ||
\'stackable-align-center\': !alignment || alignment == \'center\', \ | ||
\'stackable-align-top\': alignment == \'top\', \ | ||
\'stackable-align-right\': alignment == \'right\', \ | ||
\'stackable-align-bottom\': alignment == \'bottom\', \ | ||
\'stackable-align-left\': alignment == \'left\', \ | ||
\'stackable-no-arrow\': hideArrow}"> \ | ||
<div stackable="state.show"> \ | ||
<div class="stackable-popover-content"> \ | ||
<div ng-if="!hideArrow" class="stackable-arrow"></div> \ | ||
<div ng-transclude></div> \ | ||
</div> \ | ||
</div> \ | ||
</div>', | ||
compile: Compile | ||
}; | ||
function Compile(tElement, tAttrs, transcludeFn) { | ||
var extents = {}; | ||
return function(scope, element) { | ||
// measure popover content | ||
transcludeFn(scope.$parent, function(clone) { | ||
var content = angular.element(' \ | ||
<div class="stackable-popover-content" style="width:auto"> \ | ||
<div ng-if="!hideArrow" class="stackable-arrow"></div> \ | ||
</div>'); | ||
content.append(clone); | ||
content.css({display: 'none'}); | ||
angular.element('body').append(content); | ||
extents.height = content.outerHeight(true); | ||
extents.width = content.outerWidth(true); | ||
// setTimeout hack to ensure content size has settled on chrome | ||
setTimeout(function() { | ||
extents.height = content.outerHeight(true); | ||
extents.width = content.outerWidth(true); | ||
content.remove(); | ||
}); | ||
}); | ||
// whenever state changes, reposition popover | ||
scope.$watch('state', function() { | ||
setTimeout(reposition); | ||
}, true); | ||
// close when pressing escape anywhere or clicking away | ||
angular.element(document) | ||
.on('keyup', closeOnEscape) | ||
.on('click', closeOnClick); | ||
scope.$on('$destroy', function() { | ||
angular.element(document) | ||
.off('keyup', closeOnEscape) | ||
.off('click', closeOnClick); | ||
}); | ||
function closeOnClick(e) { | ||
// close if target is not the trigger and is not in the popover | ||
var target = angular.element(e.target); | ||
var trigger = target.data('stackable-state'); | ||
if(scope.state !== trigger && target.closest(element).length === 0) { | ||
scope.state.show = false; | ||
scope.$apply(); | ||
} | ||
} | ||
function closeOnEscape(e) { | ||
if(scope.enableEscape && e.keyCode === 27) { | ||
e.stopPropagation(); | ||
scope.state.show = false; | ||
scope.$apply(); | ||
} | ||
} | ||
function reposition() { | ||
// resize popover content | ||
var content = element.find('.stackable-popover-content'); | ||
if(!content.length) { | ||
return; | ||
} | ||
content.css(extents); | ||
var height = content.outerHeight(true); | ||
var width = content.outerWidth(true); | ||
// position popover | ||
var position = {top: 0, left: 0}; | ||
var alignment = scope.alignment || 'center'; | ||
var placement = scope.placement || 'top'; | ||
if(placement === 'top' || placement === 'bottom') { | ||
// treat invalid 'top' or 'bottom' as 'center' | ||
if(['center', 'top', 'bottom'].indexOf(alignment) !== -1) { | ||
var triggerCenterX = (scope.state.position.left + | ||
scope.state.position.width / 2); | ||
position.left = triggerCenterX - width / 2; | ||
} else if(alignment === 'left') { | ||
position.left = scope.state.position.left; | ||
} else { | ||
// alignment 'right' | ||
position.left = (scope.state.position.left + | ||
scope.state.position.width - width); | ||
} | ||
position.top = scope.state.position.top; | ||
if(placement === 'top') { | ||
position.top -= height; | ||
} else { | ||
position.top += scope.state.position.height; | ||
} | ||
} else { | ||
// else placement is 'left' or 'right' | ||
// treat invalid 'left' or 'right' as 'center' | ||
if(['center', 'left', 'right'].indexOf(alignment) !== -1) { | ||
var triggerCenterY = (scope.state.position.top + | ||
scope.state.position.height / 2); | ||
position.top = triggerCenterY - height / 2; | ||
} else if(alignment === 'top') { | ||
position.top = scope.state.position.top; | ||
} else { | ||
// alignment 'bottom' | ||
position.top = (scope.state.position.top + | ||
scope.state.position.height - height); | ||
} | ||
position.left = scope.state.position.left; | ||
if(placement === 'left') { | ||
position.left -= width; | ||
} else { | ||
position.left += scope.state.position.width; | ||
} | ||
} | ||
position.top += 'px'; | ||
position.left += 'px'; | ||
element.css(position); | ||
} | ||
}; | ||
} | ||
} | ||
function stackableTriggerDirective($parse) { | ||
return { | ||
restrict: 'A', | ||
link: Link | ||
}; | ||
function Link(scope, element, attrs) { | ||
// track stackable state | ||
var state; | ||
initState(attrs.stackableTrigger); | ||
attrs.$observe('stackableTrigger', function(value) { | ||
initState(value); | ||
}); | ||
// update element position when window resized | ||
angular.element(window).resize(resized); | ||
scope.$on('$destroy', function() { | ||
angular.element(window).off('resize', resized); | ||
}); | ||
// toggle show on click | ||
element.on('click', function() { | ||
scope.$apply(function() { | ||
state.show = !state.show; | ||
updateState(state); | ||
}); | ||
}); | ||
function resized() { | ||
updateState(state); | ||
scope.$apply(); | ||
} | ||
function initState(expr) { | ||
var get = $parse(expr); | ||
var set = get.assign || angular.noop; | ||
state = get(scope) || {}; | ||
if(!('show' in state)) { | ||
state.show = false; | ||
} | ||
updateState(state); | ||
set(scope, state); | ||
element.data('stackable-state', state); | ||
} | ||
function updateState(state) { | ||
var offset = element.offset(); | ||
state.position = { | ||
top: offset.top, | ||
left: offset.left, | ||
height: element.outerHeight(false), | ||
width: element.outerWidth(false), | ||
heightWithMargin: element.outerHeight(true), | ||
widthWithMargin: element.outerWidth(true) | ||
}; | ||
return state; | ||
} | ||
} | ||
} | ||
})(); |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
24301
701
100
1