@@ 4486-5026 (lines=541) @@ | ||
4483 | * The $tooltip service creates tooltip- and popover-like directives as well as |
|
4484 | * houses global options for them. |
|
4485 | */ |
|
4486 | .provider('$uibTooltip', function() { |
|
4487 | // The default options tooltip and popover. |
|
4488 | var defaultOptions = { |
|
4489 | placement: 'top', |
|
4490 | placementClassPrefix: '', |
|
4491 | animation: true, |
|
4492 | popupDelay: 0, |
|
4493 | popupCloseDelay: 0, |
|
4494 | useContentExp: false |
|
4495 | }; |
|
4496 | ||
4497 | // Default hide triggers for each show trigger |
|
4498 | var triggerMap = { |
|
4499 | 'mouseenter': 'mouseleave', |
|
4500 | 'click': 'click', |
|
4501 | 'outsideClick': 'outsideClick', |
|
4502 | 'focus': 'blur', |
|
4503 | 'none': '' |
|
4504 | }; |
|
4505 | ||
4506 | // The options specified to the provider globally. |
|
4507 | var globalOptions = {}; |
|
4508 | ||
4509 | /** |
|
4510 | * `options({})` allows global configuration of all tooltips in the |
|
4511 | * application. |
|
4512 | * |
|
4513 | * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { |
|
4514 | * // place tooltips left instead of top by default |
|
4515 | * $tooltipProvider.options( { placement: 'left' } ); |
|
4516 | * }); |
|
4517 | */ |
|
4518 | this.options = function(value) { |
|
4519 | angular.extend(globalOptions, value); |
|
4520 | }; |
|
4521 | ||
4522 | /** |
|
4523 | * This allows you to extend the set of trigger mappings available. E.g.: |
|
4524 | * |
|
4525 | * $tooltipProvider.setTriggers( { 'openTrigger': 'closeTrigger' } ); |
|
4526 | */ |
|
4527 | this.setTriggers = function setTriggers(triggers) { |
|
4528 | angular.extend(triggerMap, triggers); |
|
4529 | }; |
|
4530 | ||
4531 | /** |
|
4532 | * This is a helper function for translating camel-case to snake_case. |
|
4533 | */ |
|
4534 | function snake_case(name) { |
|
4535 | var regexp = /[A-Z]/g; |
|
4536 | var separator = '-'; |
|
4537 | return name.replace(regexp, function(letter, pos) { |
|
4538 | return (pos ? separator : '') + letter.toLowerCase(); |
|
4539 | }); |
|
4540 | } |
|
4541 | ||
4542 | /** |
|
4543 | * Returns the actual instance of the $tooltip service. |
|
4544 | * TODO support multiple triggers |
|
4545 | */ |
|
4546 | this.$get = ['$window', '$compile', '$timeout', '$document', '$uibPosition', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) { |
|
4547 | var openedTooltips = $$stackedMap.createNew(); |
|
4548 | $document.on('keypress', keypressListener); |
|
4549 | ||
4550 | $rootScope.$on('$destroy', function() { |
|
4551 | $document.off('keypress', keypressListener); |
|
4552 | }); |
|
4553 | ||
4554 | function keypressListener(e) { |
|
4555 | if (e.which === 27) { |
|
4556 | var last = openedTooltips.top(); |
|
4557 | if (last) { |
|
4558 | last.value.close(); |
|
4559 | openedTooltips.removeTop(); |
|
4560 | last = null; |
|
4561 | } |
|
4562 | } |
|
4563 | } |
|
4564 | ||
4565 | return function $tooltip(ttType, prefix, defaultTriggerShow, options) { |
|
4566 | options = angular.extend({}, defaultOptions, globalOptions, options); |
|
4567 | ||
4568 | /** |
|
4569 | * Returns an object of show and hide triggers. |
|
4570 | * |
|
4571 | * If a trigger is supplied, |
|
4572 | * it is used to show the tooltip; otherwise, it will use the `trigger` |
|
4573 | * option passed to the `$tooltipProvider.options` method; else it will |
|
4574 | * default to the trigger supplied to this directive factory. |
|
4575 | * |
|
4576 | * The hide trigger is based on the show trigger. If the `trigger` option |
|
4577 | * was passed to the `$tooltipProvider.options` method, it will use the |
|
4578 | * mapped trigger from `triggerMap` or the passed trigger if the map is |
|
4579 | * undefined; otherwise, it uses the `triggerMap` value of the show |
|
4580 | * trigger; else it will just use the show trigger. |
|
4581 | */ |
|
4582 | function getTriggers(trigger) { |
|
4583 | var show = (trigger || options.trigger || defaultTriggerShow).split(' '); |
|
4584 | var hide = show.map(function(trigger) { |
|
4585 | return triggerMap[trigger] || trigger; |
|
4586 | }); |
|
4587 | return { |
|
4588 | show: show, |
|
4589 | hide: hide |
|
4590 | }; |
|
4591 | } |
|
4592 | ||
4593 | var directiveName = snake_case(ttType); |
|
4594 | ||
4595 | var startSym = $interpolate.startSymbol(); |
|
4596 | var endSym = $interpolate.endSymbol(); |
|
4597 | var template = |
|
4598 | '<div '+ directiveName + '-popup ' + |
|
4599 | 'uib-title="' + startSym + 'title' + endSym + '" ' + |
|
4600 | (options.useContentExp ? |
|
4601 | 'content-exp="contentExp()" ' : |
|
4602 | 'content="' + startSym + 'content' + endSym + '" ') + |
|
4603 | 'placement="' + startSym + 'placement' + endSym + '" ' + |
|
4604 | 'popup-class="' + startSym + 'popupClass' + endSym + '" ' + |
|
4605 | 'animation="animation" ' + |
|
4606 | 'is-open="isOpen" ' + |
|
4607 | 'origin-scope="origScope" ' + |
|
4608 | 'class="uib-position-measure"' + |
|
4609 | '>' + |
|
4610 | '</div>'; |
|
4611 | ||
4612 | return { |
|
4613 | compile: function(tElem, tAttrs) { |
|
4614 | var tooltipLinker = $compile(template); |
|
4615 | ||
4616 | return function link(scope, element, attrs, tooltipCtrl) { |
|
4617 | var tooltip; |
|
4618 | var tooltipLinkedScope; |
|
4619 | var transitionTimeout; |
|
4620 | var showTimeout; |
|
4621 | var hideTimeout; |
|
4622 | var positionTimeout; |
|
4623 | var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false; |
|
4624 | var triggers = getTriggers(undefined); |
|
4625 | var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']); |
|
4626 | var ttScope = scope.$new(true); |
|
4627 | var repositionScheduled = false; |
|
4628 | var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false; |
|
4629 | var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false; |
|
4630 | var observers = []; |
|
4631 | var lastPlacement; |
|
4632 | ||
4633 | var positionTooltip = function() { |
|
4634 | // check if tooltip exists and is not empty |
|
4635 | if (!tooltip || !tooltip.html()) { return; } |
|
4636 | ||
4637 | if (!positionTimeout) { |
|
4638 | positionTimeout = $timeout(function() { |
|
4639 | var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); |
|
4640 | tooltip.css({ top: ttPosition.top + 'px', left: ttPosition.left + 'px' }); |
|
4641 | ||
4642 | if (!tooltip.hasClass(ttPosition.placement.split('-')[0])) { |
|
4643 | tooltip.removeClass(lastPlacement.split('-')[0]); |
|
4644 | tooltip.addClass(ttPosition.placement.split('-')[0]); |
|
4645 | } |
|
4646 | ||
4647 | if (!tooltip.hasClass(options.placementClassPrefix + ttPosition.placement)) { |
|
4648 | tooltip.removeClass(options.placementClassPrefix + lastPlacement); |
|
4649 | tooltip.addClass(options.placementClassPrefix + ttPosition.placement); |
|
4650 | } |
|
4651 | ||
4652 | // first time through tt element will have the |
|
4653 | // uib-position-measure class or if the placement |
|
4654 | // has changed we need to position the arrow. |
|
4655 | if (tooltip.hasClass('uib-position-measure')) { |
|
4656 | $position.positionArrow(tooltip, ttPosition.placement); |
|
4657 | tooltip.removeClass('uib-position-measure'); |
|
4658 | } else if (lastPlacement !== ttPosition.placement) { |
|
4659 | $position.positionArrow(tooltip, ttPosition.placement); |
|
4660 | } |
|
4661 | lastPlacement = ttPosition.placement; |
|
4662 | ||
4663 | positionTimeout = null; |
|
4664 | }, 0, false); |
|
4665 | } |
|
4666 | }; |
|
4667 | ||
4668 | // Set up the correct scope to allow transclusion later |
|
4669 | ttScope.origScope = scope; |
|
4670 | ||
4671 | // By default, the tooltip is not open. |
|
4672 | // TODO add ability to start tooltip opened |
|
4673 | ttScope.isOpen = false; |
|
4674 | openedTooltips.add(ttScope, { |
|
4675 | close: hide |
|
4676 | }); |
|
4677 | ||
4678 | function toggleTooltipBind() { |
|
4679 | if (!ttScope.isOpen) { |
|
4680 | showTooltipBind(); |
|
4681 | } else { |
|
4682 | hideTooltipBind(); |
|
4683 | } |
|
4684 | } |
|
4685 | ||
4686 | // Show the tooltip with delay if specified, otherwise show it immediately |
|
4687 | function showTooltipBind() { |
|
4688 | if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) { |
|
4689 | return; |
|
4690 | } |
|
4691 | ||
4692 | cancelHide(); |
|
4693 | prepareTooltip(); |
|
4694 | ||
4695 | if (ttScope.popupDelay) { |
|
4696 | // Do nothing if the tooltip was already scheduled to pop-up. |
|
4697 | // This happens if show is triggered multiple times before any hide is triggered. |
|
4698 | if (!showTimeout) { |
|
4699 | showTimeout = $timeout(show, ttScope.popupDelay, false); |
|
4700 | } |
|
4701 | } else { |
|
4702 | show(); |
|
4703 | } |
|
4704 | } |
|
4705 | ||
4706 | function hideTooltipBind() { |
|
4707 | cancelShow(); |
|
4708 | ||
4709 | if (ttScope.popupCloseDelay) { |
|
4710 | if (!hideTimeout) { |
|
4711 | hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false); |
|
4712 | } |
|
4713 | } else { |
|
4714 | hide(); |
|
4715 | } |
|
4716 | } |
|
4717 | ||
4718 | // Show the tooltip popup element. |
|
4719 | function show() { |
|
4720 | cancelShow(); |
|
4721 | cancelHide(); |
|
4722 | ||
4723 | // Don't show empty tooltips. |
|
4724 | if (!ttScope.content) { |
|
4725 | return angular.noop; |
|
4726 | } |
|
4727 | ||
4728 | createTooltip(); |
|
4729 | ||
4730 | // And show the tooltip. |
|
4731 | ttScope.$evalAsync(function() { |
|
4732 | ttScope.isOpen = true; |
|
4733 | assignIsOpen(true); |
|
4734 | positionTooltip(); |
|
4735 | }); |
|
4736 | } |
|
4737 | ||
4738 | function cancelShow() { |
|
4739 | if (showTimeout) { |
|
4740 | $timeout.cancel(showTimeout); |
|
4741 | showTimeout = null; |
|
4742 | } |
|
4743 | ||
4744 | if (positionTimeout) { |
|
4745 | $timeout.cancel(positionTimeout); |
|
4746 | positionTimeout = null; |
|
4747 | } |
|
4748 | } |
|
4749 | ||
4750 | // Hide the tooltip popup element. |
|
4751 | function hide() { |
|
4752 | if (!ttScope) { |
|
4753 | return; |
|
4754 | } |
|
4755 | ||
4756 | // First things first: we don't show it anymore. |
|
4757 | ttScope.$evalAsync(function() { |
|
4758 | if (ttScope) { |
|
4759 | ttScope.isOpen = false; |
|
4760 | assignIsOpen(false); |
|
4761 | // And now we remove it from the DOM. However, if we have animation, we |
|
4762 | // need to wait for it to expire beforehand. |
|
4763 | // FIXME: this is a placeholder for a port of the transitions library. |
|
4764 | // The fade transition in TWBS is 150ms. |
|
4765 | if (ttScope.animation) { |
|
4766 | if (!transitionTimeout) { |
|
4767 | transitionTimeout = $timeout(removeTooltip, 150, false); |
|
4768 | } |
|
4769 | } else { |
|
4770 | removeTooltip(); |
|
4771 | } |
|
4772 | } |
|
4773 | }); |
|
4774 | } |
|
4775 | ||
4776 | function cancelHide() { |
|
4777 | if (hideTimeout) { |
|
4778 | $timeout.cancel(hideTimeout); |
|
4779 | hideTimeout = null; |
|
4780 | } |
|
4781 | ||
4782 | if (transitionTimeout) { |
|
4783 | $timeout.cancel(transitionTimeout); |
|
4784 | transitionTimeout = null; |
|
4785 | } |
|
4786 | } |
|
4787 | ||
4788 | function createTooltip() { |
|
4789 | // There can only be one tooltip element per directive shown at once. |
|
4790 | if (tooltip) { |
|
4791 | return; |
|
4792 | } |
|
4793 | ||
4794 | tooltipLinkedScope = ttScope.$new(); |
|
4795 | tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) { |
|
4796 | if (appendToBody) { |
|
4797 | $document.find('body').append(tooltip); |
|
4798 | } else { |
|
4799 | element.after(tooltip); |
|
4800 | } |
|
4801 | }); |
|
4802 | ||
4803 | prepObservers(); |
|
4804 | } |
|
4805 | ||
4806 | function removeTooltip() { |
|
4807 | cancelShow(); |
|
4808 | cancelHide(); |
|
4809 | unregisterObservers(); |
|
4810 | ||
4811 | if (tooltip) { |
|
4812 | tooltip.remove(); |
|
4813 | tooltip = null; |
|
4814 | } |
|
4815 | if (tooltipLinkedScope) { |
|
4816 | tooltipLinkedScope.$destroy(); |
|
4817 | tooltipLinkedScope = null; |
|
4818 | } |
|
4819 | } |
|
4820 | ||
4821 | /** |
|
4822 | * Set the initial scope values. Once |
|
4823 | * the tooltip is created, the observers |
|
4824 | * will be added to keep things in sync. |
|
4825 | */ |
|
4826 | function prepareTooltip() { |
|
4827 | ttScope.title = attrs[prefix + 'Title']; |
|
4828 | if (contentParse) { |
|
4829 | ttScope.content = contentParse(scope); |
|
4830 | } else { |
|
4831 | ttScope.content = attrs[ttType]; |
|
4832 | } |
|
4833 | ||
4834 | ttScope.popupClass = attrs[prefix + 'Class']; |
|
4835 | ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement; |
|
4836 | var placement = $position.parsePlacement(ttScope.placement); |
|
4837 | lastPlacement = placement[1] ? placement[0] + '-' + placement[1] : placement[0]; |
|
4838 | ||
4839 | var delay = parseInt(attrs[prefix + 'PopupDelay'], 10); |
|
4840 | var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10); |
|
4841 | ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay; |
|
4842 | ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay; |
|
4843 | } |
|
4844 | ||
4845 | function assignIsOpen(isOpen) { |
|
4846 | if (isOpenParse && angular.isFunction(isOpenParse.assign)) { |
|
4847 | isOpenParse.assign(scope, isOpen); |
|
4848 | } |
|
4849 | } |
|
4850 | ||
4851 | ttScope.contentExp = function() { |
|
4852 | return ttScope.content; |
|
4853 | }; |
|
4854 | ||
4855 | /** |
|
4856 | * Observe the relevant attributes. |
|
4857 | */ |
|
4858 | attrs.$observe('disabled', function(val) { |
|
4859 | if (val) { |
|
4860 | cancelShow(); |
|
4861 | } |
|
4862 | ||
4863 | if (val && ttScope.isOpen) { |
|
4864 | hide(); |
|
4865 | } |
|
4866 | }); |
|
4867 | ||
4868 | if (isOpenParse) { |
|
4869 | scope.$watch(isOpenParse, function(val) { |
|
4870 | if (ttScope && !val === ttScope.isOpen) { |
|
4871 | toggleTooltipBind(); |
|
4872 | } |
|
4873 | }); |
|
4874 | } |
|
4875 | ||
4876 | function prepObservers() { |
|
4877 | observers.length = 0; |
|
4878 | ||
4879 | if (contentParse) { |
|
4880 | observers.push( |
|
4881 | scope.$watch(contentParse, function(val) { |
|
4882 | ttScope.content = val; |
|
4883 | if (!val && ttScope.isOpen) { |
|
4884 | hide(); |
|
4885 | } |
|
4886 | }) |
|
4887 | ); |
|
4888 | ||
4889 | observers.push( |
|
4890 | tooltipLinkedScope.$watch(function() { |
|
4891 | if (!repositionScheduled) { |
|
4892 | repositionScheduled = true; |
|
4893 | tooltipLinkedScope.$$postDigest(function() { |
|
4894 | repositionScheduled = false; |
|
4895 | if (ttScope && ttScope.isOpen) { |
|
4896 | positionTooltip(); |
|
4897 | } |
|
4898 | }); |
|
4899 | } |
|
4900 | }) |
|
4901 | ); |
|
4902 | } else { |
|
4903 | observers.push( |
|
4904 | attrs.$observe(ttType, function(val) { |
|
4905 | ttScope.content = val; |
|
4906 | if (!val && ttScope.isOpen) { |
|
4907 | hide(); |
|
4908 | } else { |
|
4909 | positionTooltip(); |
|
4910 | } |
|
4911 | }) |
|
4912 | ); |
|
4913 | } |
|
4914 | ||
4915 | observers.push( |
|
4916 | attrs.$observe(prefix + 'Title', function(val) { |
|
4917 | ttScope.title = val; |
|
4918 | if (ttScope.isOpen) { |
|
4919 | positionTooltip(); |
|
4920 | } |
|
4921 | }) |
|
4922 | ); |
|
4923 | ||
4924 | observers.push( |
|
4925 | attrs.$observe(prefix + 'Placement', function(val) { |
|
4926 | ttScope.placement = val ? val : options.placement; |
|
4927 | if (ttScope.isOpen) { |
|
4928 | positionTooltip(); |
|
4929 | } |
|
4930 | }) |
|
4931 | ); |
|
4932 | } |
|
4933 | ||
4934 | function unregisterObservers() { |
|
4935 | if (observers.length) { |
|
4936 | angular.forEach(observers, function(observer) { |
|
4937 | observer(); |
|
4938 | }); |
|
4939 | observers.length = 0; |
|
4940 | } |
|
4941 | } |
|
4942 | ||
4943 | // hide tooltips/popovers for outsideClick trigger |
|
4944 | function bodyHideTooltipBind(e) { |
|
4945 | if (!ttScope || !ttScope.isOpen || !tooltip) { |
|
4946 | return; |
|
4947 | } |
|
4948 | // make sure the tooltip/popover link or tool tooltip/popover itself were not clicked |
|
4949 | if (!element[0].contains(e.target) && !tooltip[0].contains(e.target)) { |
|
4950 | hideTooltipBind(); |
|
4951 | } |
|
4952 | } |
|
4953 | ||
4954 | var unregisterTriggers = function() { |
|
4955 | triggers.show.forEach(function(trigger) { |
|
4956 | if (trigger === 'outsideClick') { |
|
4957 | element.off('click', toggleTooltipBind); |
|
4958 | } else { |
|
4959 | element.off(trigger, showTooltipBind); |
|
4960 | element.off(trigger, toggleTooltipBind); |
|
4961 | } |
|
4962 | }); |
|
4963 | triggers.hide.forEach(function(trigger) { |
|
4964 | if (trigger === 'outsideClick') { |
|
4965 | $document.off('click', bodyHideTooltipBind); |
|
4966 | } else { |
|
4967 | element.off(trigger, hideTooltipBind); |
|
4968 | } |
|
4969 | }); |
|
4970 | }; |
|
4971 | ||
4972 | function prepTriggers() { |
|
4973 | var val = attrs[prefix + 'Trigger']; |
|
4974 | unregisterTriggers(); |
|
4975 | ||
4976 | triggers = getTriggers(val); |
|
4977 | ||
4978 | if (triggers.show !== 'none') { |
|
4979 | triggers.show.forEach(function(trigger, idx) { |
|
4980 | if (trigger === 'outsideClick') { |
|
4981 | element.on('click', toggleTooltipBind); |
|
4982 | $document.on('click', bodyHideTooltipBind); |
|
4983 | } else if (trigger === triggers.hide[idx]) { |
|
4984 | element.on(trigger, toggleTooltipBind); |
|
4985 | } else if (trigger) { |
|
4986 | element.on(trigger, showTooltipBind); |
|
4987 | element.on(triggers.hide[idx], hideTooltipBind); |
|
4988 | } |
|
4989 | ||
4990 | element.on('keypress', function(e) { |
|
4991 | if (e.which === 27) { |
|
4992 | hideTooltipBind(); |
|
4993 | } |
|
4994 | }); |
|
4995 | }); |
|
4996 | } |
|
4997 | } |
|
4998 | ||
4999 | prepTriggers(); |
|
5000 | ||
5001 | var animation = scope.$eval(attrs[prefix + 'Animation']); |
|
5002 | ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation; |
|
5003 | ||
5004 | var appendToBodyVal; |
|
5005 | var appendKey = prefix + 'AppendToBody'; |
|
5006 | if (appendKey in attrs && attrs[appendKey] === undefined) { |
|
5007 | appendToBodyVal = true; |
|
5008 | } else { |
|
5009 | appendToBodyVal = scope.$eval(attrs[appendKey]); |
|
5010 | } |
|
5011 | ||
5012 | appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; |
|
5013 | ||
5014 | // Make sure tooltip is destroyed and removed. |
|
5015 | scope.$on('$destroy', function onDestroyTooltip() { |
|
5016 | unregisterTriggers(); |
|
5017 | removeTooltip(); |
|
5018 | openedTooltips.remove(ttScope); |
|
5019 | ttScope = null; |
|
5020 | }); |
|
5021 | }; |
|
5022 | } |
|
5023 | }; |
|
5024 | }; |
|
5025 | }]; |
|
5026 | }) |
|
5027 | ||
5028 | // This is mostly ngInclude code but with a custom scope |
|
5029 | .directive('uibTooltipTemplateTransclude', [ |
@@ 4485-5025 (lines=541) @@ | ||
4482 | * The $tooltip service creates tooltip- and popover-like directives as well as |
|
4483 | * houses global options for them. |
|
4484 | */ |
|
4485 | .provider('$uibTooltip', function() { |
|
4486 | // The default options tooltip and popover. |
|
4487 | var defaultOptions = { |
|
4488 | placement: 'top', |
|
4489 | placementClassPrefix: '', |
|
4490 | animation: true, |
|
4491 | popupDelay: 0, |
|
4492 | popupCloseDelay: 0, |
|
4493 | useContentExp: false |
|
4494 | }; |
|
4495 | ||
4496 | // Default hide triggers for each show trigger |
|
4497 | var triggerMap = { |
|
4498 | 'mouseenter': 'mouseleave', |
|
4499 | 'click': 'click', |
|
4500 | 'outsideClick': 'outsideClick', |
|
4501 | 'focus': 'blur', |
|
4502 | 'none': '' |
|
4503 | }; |
|
4504 | ||
4505 | // The options specified to the provider globally. |
|
4506 | var globalOptions = {}; |
|
4507 | ||
4508 | /** |
|
4509 | * `options({})` allows global configuration of all tooltips in the |
|
4510 | * application. |
|
4511 | * |
|
4512 | * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { |
|
4513 | * // place tooltips left instead of top by default |
|
4514 | * $tooltipProvider.options( { placement: 'left' } ); |
|
4515 | * }); |
|
4516 | */ |
|
4517 | this.options = function(value) { |
|
4518 | angular.extend(globalOptions, value); |
|
4519 | }; |
|
4520 | ||
4521 | /** |
|
4522 | * This allows you to extend the set of trigger mappings available. E.g.: |
|
4523 | * |
|
4524 | * $tooltipProvider.setTriggers( { 'openTrigger': 'closeTrigger' } ); |
|
4525 | */ |
|
4526 | this.setTriggers = function setTriggers(triggers) { |
|
4527 | angular.extend(triggerMap, triggers); |
|
4528 | }; |
|
4529 | ||
4530 | /** |
|
4531 | * This is a helper function for translating camel-case to snake_case. |
|
4532 | */ |
|
4533 | function snake_case(name) { |
|
4534 | var regexp = /[A-Z]/g; |
|
4535 | var separator = '-'; |
|
4536 | return name.replace(regexp, function(letter, pos) { |
|
4537 | return (pos ? separator : '') + letter.toLowerCase(); |
|
4538 | }); |
|
4539 | } |
|
4540 | ||
4541 | /** |
|
4542 | * Returns the actual instance of the $tooltip service. |
|
4543 | * TODO support multiple triggers |
|
4544 | */ |
|
4545 | this.$get = ['$window', '$compile', '$timeout', '$document', '$uibPosition', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) { |
|
4546 | var openedTooltips = $$stackedMap.createNew(); |
|
4547 | $document.on('keypress', keypressListener); |
|
4548 | ||
4549 | $rootScope.$on('$destroy', function() { |
|
4550 | $document.off('keypress', keypressListener); |
|
4551 | }); |
|
4552 | ||
4553 | function keypressListener(e) { |
|
4554 | if (e.which === 27) { |
|
4555 | var last = openedTooltips.top(); |
|
4556 | if (last) { |
|
4557 | last.value.close(); |
|
4558 | openedTooltips.removeTop(); |
|
4559 | last = null; |
|
4560 | } |
|
4561 | } |
|
4562 | } |
|
4563 | ||
4564 | return function $tooltip(ttType, prefix, defaultTriggerShow, options) { |
|
4565 | options = angular.extend({}, defaultOptions, globalOptions, options); |
|
4566 | ||
4567 | /** |
|
4568 | * Returns an object of show and hide triggers. |
|
4569 | * |
|
4570 | * If a trigger is supplied, |
|
4571 | * it is used to show the tooltip; otherwise, it will use the `trigger` |
|
4572 | * option passed to the `$tooltipProvider.options` method; else it will |
|
4573 | * default to the trigger supplied to this directive factory. |
|
4574 | * |
|
4575 | * The hide trigger is based on the show trigger. If the `trigger` option |
|
4576 | * was passed to the `$tooltipProvider.options` method, it will use the |
|
4577 | * mapped trigger from `triggerMap` or the passed trigger if the map is |
|
4578 | * undefined; otherwise, it uses the `triggerMap` value of the show |
|
4579 | * trigger; else it will just use the show trigger. |
|
4580 | */ |
|
4581 | function getTriggers(trigger) { |
|
4582 | var show = (trigger || options.trigger || defaultTriggerShow).split(' '); |
|
4583 | var hide = show.map(function(trigger) { |
|
4584 | return triggerMap[trigger] || trigger; |
|
4585 | }); |
|
4586 | return { |
|
4587 | show: show, |
|
4588 | hide: hide |
|
4589 | }; |
|
4590 | } |
|
4591 | ||
4592 | var directiveName = snake_case(ttType); |
|
4593 | ||
4594 | var startSym = $interpolate.startSymbol(); |
|
4595 | var endSym = $interpolate.endSymbol(); |
|
4596 | var template = |
|
4597 | '<div '+ directiveName + '-popup ' + |
|
4598 | 'uib-title="' + startSym + 'title' + endSym + '" ' + |
|
4599 | (options.useContentExp ? |
|
4600 | 'content-exp="contentExp()" ' : |
|
4601 | 'content="' + startSym + 'content' + endSym + '" ') + |
|
4602 | 'placement="' + startSym + 'placement' + endSym + '" ' + |
|
4603 | 'popup-class="' + startSym + 'popupClass' + endSym + '" ' + |
|
4604 | 'animation="animation" ' + |
|
4605 | 'is-open="isOpen" ' + |
|
4606 | 'origin-scope="origScope" ' + |
|
4607 | 'class="uib-position-measure"' + |
|
4608 | '>' + |
|
4609 | '</div>'; |
|
4610 | ||
4611 | return { |
|
4612 | compile: function(tElem, tAttrs) { |
|
4613 | var tooltipLinker = $compile(template); |
|
4614 | ||
4615 | return function link(scope, element, attrs, tooltipCtrl) { |
|
4616 | var tooltip; |
|
4617 | var tooltipLinkedScope; |
|
4618 | var transitionTimeout; |
|
4619 | var showTimeout; |
|
4620 | var hideTimeout; |
|
4621 | var positionTimeout; |
|
4622 | var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false; |
|
4623 | var triggers = getTriggers(undefined); |
|
4624 | var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']); |
|
4625 | var ttScope = scope.$new(true); |
|
4626 | var repositionScheduled = false; |
|
4627 | var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false; |
|
4628 | var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false; |
|
4629 | var observers = []; |
|
4630 | var lastPlacement; |
|
4631 | ||
4632 | var positionTooltip = function() { |
|
4633 | // check if tooltip exists and is not empty |
|
4634 | if (!tooltip || !tooltip.html()) { return; } |
|
4635 | ||
4636 | if (!positionTimeout) { |
|
4637 | positionTimeout = $timeout(function() { |
|
4638 | var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); |
|
4639 | tooltip.css({ top: ttPosition.top + 'px', left: ttPosition.left + 'px' }); |
|
4640 | ||
4641 | if (!tooltip.hasClass(ttPosition.placement.split('-')[0])) { |
|
4642 | tooltip.removeClass(lastPlacement.split('-')[0]); |
|
4643 | tooltip.addClass(ttPosition.placement.split('-')[0]); |
|
4644 | } |
|
4645 | ||
4646 | if (!tooltip.hasClass(options.placementClassPrefix + ttPosition.placement)) { |
|
4647 | tooltip.removeClass(options.placementClassPrefix + lastPlacement); |
|
4648 | tooltip.addClass(options.placementClassPrefix + ttPosition.placement); |
|
4649 | } |
|
4650 | ||
4651 | // first time through tt element will have the |
|
4652 | // uib-position-measure class or if the placement |
|
4653 | // has changed we need to position the arrow. |
|
4654 | if (tooltip.hasClass('uib-position-measure')) { |
|
4655 | $position.positionArrow(tooltip, ttPosition.placement); |
|
4656 | tooltip.removeClass('uib-position-measure'); |
|
4657 | } else if (lastPlacement !== ttPosition.placement) { |
|
4658 | $position.positionArrow(tooltip, ttPosition.placement); |
|
4659 | } |
|
4660 | lastPlacement = ttPosition.placement; |
|
4661 | ||
4662 | positionTimeout = null; |
|
4663 | }, 0, false); |
|
4664 | } |
|
4665 | }; |
|
4666 | ||
4667 | // Set up the correct scope to allow transclusion later |
|
4668 | ttScope.origScope = scope; |
|
4669 | ||
4670 | // By default, the tooltip is not open. |
|
4671 | // TODO add ability to start tooltip opened |
|
4672 | ttScope.isOpen = false; |
|
4673 | openedTooltips.add(ttScope, { |
|
4674 | close: hide |
|
4675 | }); |
|
4676 | ||
4677 | function toggleTooltipBind() { |
|
4678 | if (!ttScope.isOpen) { |
|
4679 | showTooltipBind(); |
|
4680 | } else { |
|
4681 | hideTooltipBind(); |
|
4682 | } |
|
4683 | } |
|
4684 | ||
4685 | // Show the tooltip with delay if specified, otherwise show it immediately |
|
4686 | function showTooltipBind() { |
|
4687 | if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) { |
|
4688 | return; |
|
4689 | } |
|
4690 | ||
4691 | cancelHide(); |
|
4692 | prepareTooltip(); |
|
4693 | ||
4694 | if (ttScope.popupDelay) { |
|
4695 | // Do nothing if the tooltip was already scheduled to pop-up. |
|
4696 | // This happens if show is triggered multiple times before any hide is triggered. |
|
4697 | if (!showTimeout) { |
|
4698 | showTimeout = $timeout(show, ttScope.popupDelay, false); |
|
4699 | } |
|
4700 | } else { |
|
4701 | show(); |
|
4702 | } |
|
4703 | } |
|
4704 | ||
4705 | function hideTooltipBind() { |
|
4706 | cancelShow(); |
|
4707 | ||
4708 | if (ttScope.popupCloseDelay) { |
|
4709 | if (!hideTimeout) { |
|
4710 | hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false); |
|
4711 | } |
|
4712 | } else { |
|
4713 | hide(); |
|
4714 | } |
|
4715 | } |
|
4716 | ||
4717 | // Show the tooltip popup element. |
|
4718 | function show() { |
|
4719 | cancelShow(); |
|
4720 | cancelHide(); |
|
4721 | ||
4722 | // Don't show empty tooltips. |
|
4723 | if (!ttScope.content) { |
|
4724 | return angular.noop; |
|
4725 | } |
|
4726 | ||
4727 | createTooltip(); |
|
4728 | ||
4729 | // And show the tooltip. |
|
4730 | ttScope.$evalAsync(function() { |
|
4731 | ttScope.isOpen = true; |
|
4732 | assignIsOpen(true); |
|
4733 | positionTooltip(); |
|
4734 | }); |
|
4735 | } |
|
4736 | ||
4737 | function cancelShow() { |
|
4738 | if (showTimeout) { |
|
4739 | $timeout.cancel(showTimeout); |
|
4740 | showTimeout = null; |
|
4741 | } |
|
4742 | ||
4743 | if (positionTimeout) { |
|
4744 | $timeout.cancel(positionTimeout); |
|
4745 | positionTimeout = null; |
|
4746 | } |
|
4747 | } |
|
4748 | ||
4749 | // Hide the tooltip popup element. |
|
4750 | function hide() { |
|
4751 | if (!ttScope) { |
|
4752 | return; |
|
4753 | } |
|
4754 | ||
4755 | // First things first: we don't show it anymore. |
|
4756 | ttScope.$evalAsync(function() { |
|
4757 | if (ttScope) { |
|
4758 | ttScope.isOpen = false; |
|
4759 | assignIsOpen(false); |
|
4760 | // And now we remove it from the DOM. However, if we have animation, we |
|
4761 | // need to wait for it to expire beforehand. |
|
4762 | // FIXME: this is a placeholder for a port of the transitions library. |
|
4763 | // The fade transition in TWBS is 150ms. |
|
4764 | if (ttScope.animation) { |
|
4765 | if (!transitionTimeout) { |
|
4766 | transitionTimeout = $timeout(removeTooltip, 150, false); |
|
4767 | } |
|
4768 | } else { |
|
4769 | removeTooltip(); |
|
4770 | } |
|
4771 | } |
|
4772 | }); |
|
4773 | } |
|
4774 | ||
4775 | function cancelHide() { |
|
4776 | if (hideTimeout) { |
|
4777 | $timeout.cancel(hideTimeout); |
|
4778 | hideTimeout = null; |
|
4779 | } |
|
4780 | ||
4781 | if (transitionTimeout) { |
|
4782 | $timeout.cancel(transitionTimeout); |
|
4783 | transitionTimeout = null; |
|
4784 | } |
|
4785 | } |
|
4786 | ||
4787 | function createTooltip() { |
|
4788 | // There can only be one tooltip element per directive shown at once. |
|
4789 | if (tooltip) { |
|
4790 | return; |
|
4791 | } |
|
4792 | ||
4793 | tooltipLinkedScope = ttScope.$new(); |
|
4794 | tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) { |
|
4795 | if (appendToBody) { |
|
4796 | $document.find('body').append(tooltip); |
|
4797 | } else { |
|
4798 | element.after(tooltip); |
|
4799 | } |
|
4800 | }); |
|
4801 | ||
4802 | prepObservers(); |
|
4803 | } |
|
4804 | ||
4805 | function removeTooltip() { |
|
4806 | cancelShow(); |
|
4807 | cancelHide(); |
|
4808 | unregisterObservers(); |
|
4809 | ||
4810 | if (tooltip) { |
|
4811 | tooltip.remove(); |
|
4812 | tooltip = null; |
|
4813 | } |
|
4814 | if (tooltipLinkedScope) { |
|
4815 | tooltipLinkedScope.$destroy(); |
|
4816 | tooltipLinkedScope = null; |
|
4817 | } |
|
4818 | } |
|
4819 | ||
4820 | /** |
|
4821 | * Set the initial scope values. Once |
|
4822 | * the tooltip is created, the observers |
|
4823 | * will be added to keep things in sync. |
|
4824 | */ |
|
4825 | function prepareTooltip() { |
|
4826 | ttScope.title = attrs[prefix + 'Title']; |
|
4827 | if (contentParse) { |
|
4828 | ttScope.content = contentParse(scope); |
|
4829 | } else { |
|
4830 | ttScope.content = attrs[ttType]; |
|
4831 | } |
|
4832 | ||
4833 | ttScope.popupClass = attrs[prefix + 'Class']; |
|
4834 | ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement; |
|
4835 | var placement = $position.parsePlacement(ttScope.placement); |
|
4836 | lastPlacement = placement[1] ? placement[0] + '-' + placement[1] : placement[0]; |
|
4837 | ||
4838 | var delay = parseInt(attrs[prefix + 'PopupDelay'], 10); |
|
4839 | var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10); |
|
4840 | ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay; |
|
4841 | ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay; |
|
4842 | } |
|
4843 | ||
4844 | function assignIsOpen(isOpen) { |
|
4845 | if (isOpenParse && angular.isFunction(isOpenParse.assign)) { |
|
4846 | isOpenParse.assign(scope, isOpen); |
|
4847 | } |
|
4848 | } |
|
4849 | ||
4850 | ttScope.contentExp = function() { |
|
4851 | return ttScope.content; |
|
4852 | }; |
|
4853 | ||
4854 | /** |
|
4855 | * Observe the relevant attributes. |
|
4856 | */ |
|
4857 | attrs.$observe('disabled', function(val) { |
|
4858 | if (val) { |
|
4859 | cancelShow(); |
|
4860 | } |
|
4861 | ||
4862 | if (val && ttScope.isOpen) { |
|
4863 | hide(); |
|
4864 | } |
|
4865 | }); |
|
4866 | ||
4867 | if (isOpenParse) { |
|
4868 | scope.$watch(isOpenParse, function(val) { |
|
4869 | if (ttScope && !val === ttScope.isOpen) { |
|
4870 | toggleTooltipBind(); |
|
4871 | } |
|
4872 | }); |
|
4873 | } |
|
4874 | ||
4875 | function prepObservers() { |
|
4876 | observers.length = 0; |
|
4877 | ||
4878 | if (contentParse) { |
|
4879 | observers.push( |
|
4880 | scope.$watch(contentParse, function(val) { |
|
4881 | ttScope.content = val; |
|
4882 | if (!val && ttScope.isOpen) { |
|
4883 | hide(); |
|
4884 | } |
|
4885 | }) |
|
4886 | ); |
|
4887 | ||
4888 | observers.push( |
|
4889 | tooltipLinkedScope.$watch(function() { |
|
4890 | if (!repositionScheduled) { |
|
4891 | repositionScheduled = true; |
|
4892 | tooltipLinkedScope.$$postDigest(function() { |
|
4893 | repositionScheduled = false; |
|
4894 | if (ttScope && ttScope.isOpen) { |
|
4895 | positionTooltip(); |
|
4896 | } |
|
4897 | }); |
|
4898 | } |
|
4899 | }) |
|
4900 | ); |
|
4901 | } else { |
|
4902 | observers.push( |
|
4903 | attrs.$observe(ttType, function(val) { |
|
4904 | ttScope.content = val; |
|
4905 | if (!val && ttScope.isOpen) { |
|
4906 | hide(); |
|
4907 | } else { |
|
4908 | positionTooltip(); |
|
4909 | } |
|
4910 | }) |
|
4911 | ); |
|
4912 | } |
|
4913 | ||
4914 | observers.push( |
|
4915 | attrs.$observe(prefix + 'Title', function(val) { |
|
4916 | ttScope.title = val; |
|
4917 | if (ttScope.isOpen) { |
|
4918 | positionTooltip(); |
|
4919 | } |
|
4920 | }) |
|
4921 | ); |
|
4922 | ||
4923 | observers.push( |
|
4924 | attrs.$observe(prefix + 'Placement', function(val) { |
|
4925 | ttScope.placement = val ? val : options.placement; |
|
4926 | if (ttScope.isOpen) { |
|
4927 | positionTooltip(); |
|
4928 | } |
|
4929 | }) |
|
4930 | ); |
|
4931 | } |
|
4932 | ||
4933 | function unregisterObservers() { |
|
4934 | if (observers.length) { |
|
4935 | angular.forEach(observers, function(observer) { |
|
4936 | observer(); |
|
4937 | }); |
|
4938 | observers.length = 0; |
|
4939 | } |
|
4940 | } |
|
4941 | ||
4942 | // hide tooltips/popovers for outsideClick trigger |
|
4943 | function bodyHideTooltipBind(e) { |
|
4944 | if (!ttScope || !ttScope.isOpen || !tooltip) { |
|
4945 | return; |
|
4946 | } |
|
4947 | // make sure the tooltip/popover link or tool tooltip/popover itself were not clicked |
|
4948 | if (!element[0].contains(e.target) && !tooltip[0].contains(e.target)) { |
|
4949 | hideTooltipBind(); |
|
4950 | } |
|
4951 | } |
|
4952 | ||
4953 | var unregisterTriggers = function() { |
|
4954 | triggers.show.forEach(function(trigger) { |
|
4955 | if (trigger === 'outsideClick') { |
|
4956 | element.off('click', toggleTooltipBind); |
|
4957 | } else { |
|
4958 | element.off(trigger, showTooltipBind); |
|
4959 | element.off(trigger, toggleTooltipBind); |
|
4960 | } |
|
4961 | }); |
|
4962 | triggers.hide.forEach(function(trigger) { |
|
4963 | if (trigger === 'outsideClick') { |
|
4964 | $document.off('click', bodyHideTooltipBind); |
|
4965 | } else { |
|
4966 | element.off(trigger, hideTooltipBind); |
|
4967 | } |
|
4968 | }); |
|
4969 | }; |
|
4970 | ||
4971 | function prepTriggers() { |
|
4972 | var val = attrs[prefix + 'Trigger']; |
|
4973 | unregisterTriggers(); |
|
4974 | ||
4975 | triggers = getTriggers(val); |
|
4976 | ||
4977 | if (triggers.show !== 'none') { |
|
4978 | triggers.show.forEach(function(trigger, idx) { |
|
4979 | if (trigger === 'outsideClick') { |
|
4980 | element.on('click', toggleTooltipBind); |
|
4981 | $document.on('click', bodyHideTooltipBind); |
|
4982 | } else if (trigger === triggers.hide[idx]) { |
|
4983 | element.on(trigger, toggleTooltipBind); |
|
4984 | } else if (trigger) { |
|
4985 | element.on(trigger, showTooltipBind); |
|
4986 | element.on(triggers.hide[idx], hideTooltipBind); |
|
4987 | } |
|
4988 | ||
4989 | element.on('keypress', function(e) { |
|
4990 | if (e.which === 27) { |
|
4991 | hideTooltipBind(); |
|
4992 | } |
|
4993 | }); |
|
4994 | }); |
|
4995 | } |
|
4996 | } |
|
4997 | ||
4998 | prepTriggers(); |
|
4999 | ||
5000 | var animation = scope.$eval(attrs[prefix + 'Animation']); |
|
5001 | ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation; |
|
5002 | ||
5003 | var appendToBodyVal; |
|
5004 | var appendKey = prefix + 'AppendToBody'; |
|
5005 | if (appendKey in attrs && attrs[appendKey] === undefined) { |
|
5006 | appendToBodyVal = true; |
|
5007 | } else { |
|
5008 | appendToBodyVal = scope.$eval(attrs[appendKey]); |
|
5009 | } |
|
5010 | ||
5011 | appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; |
|
5012 | ||
5013 | // Make sure tooltip is destroyed and removed. |
|
5014 | scope.$on('$destroy', function onDestroyTooltip() { |
|
5015 | unregisterTriggers(); |
|
5016 | removeTooltip(); |
|
5017 | openedTooltips.remove(ttScope); |
|
5018 | ttScope = null; |
|
5019 | }); |
|
5020 | }; |
|
5021 | } |
|
5022 | }; |
|
5023 | }; |
|
5024 | }]; |
|
5025 | }) |
|
5026 | ||
5027 | // This is mostly ngInclude code but with a custom scope |
|
5028 | .directive('uibTooltipTemplateTransclude', [ |