Angular cleanup: Move directives (#35330)

* angular2react: Remove json_editor_ctrl

* angular2react: Move directives

* Add to angular dir
This commit is contained in:
Tobias Skarhed
2021-06-08 08:28:56 +02:00
committed by GitHub
parent 8a0cef9954
commit 5e7df4cf6a
14 changed files with 29 additions and 42 deletions

View File

@@ -0,0 +1,29 @@
import { isArray } from 'lodash';
import coreModule from '../core/core_module';
export function arrayJoin() {
'use strict';
return {
restrict: 'A',
require: 'ngModel',
link: (scope: any, element: any, attr: any, ngModel: any) => {
function split_array(text: string) {
return (text || '').split(',');
}
function join_array(text: string) {
if (isArray(text)) {
return ((text || '') as any).join(',');
} else {
return text;
}
}
ngModel.$parsers.push(split_array);
ngModel.$formatters.push(join_array);
},
};
}
coreModule.directive('arrayJoin', arrayJoin);

View File

@@ -0,0 +1,35 @@
import coreModule from '../core/core_module';
/** @ngInject */
export function autofillEventFix($compile: any) {
return {
link: ($scope: any, elem: any) => {
const input = elem[0];
const dispatchChangeEvent = () => {
const event = new Event('change');
return input.dispatchEvent(event);
};
const onAnimationStart = ({ animationName }: AnimationEvent) => {
switch (animationName) {
case 'onAutoFillStart':
return dispatchChangeEvent();
case 'onAutoFillCancel':
return dispatchChangeEvent();
}
return null;
};
// const onChange = (evt: Event) => console.log(evt);
input.addEventListener('animationstart', onAnimationStart);
// input.addEventListener('change', onChange);
$scope.$on('$destroy', () => {
input.removeEventListener('animationstart', onAnimationStart);
// input.removeEventListener('change', onChange);
});
},
};
}
coreModule.directive('autofillEventFix', autofillEventFix);

View File

@@ -0,0 +1,72 @@
import angular from 'angular';
import coreModule from '../core/core_module';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
export class DeltaCtrl {
observer: any;
/** @ngInject */
constructor() {
const waitForCompile = () => {};
this.observer = new MutationObserver(waitForCompile);
const observerConfig = {
attributes: true,
attributeFilter: ['class'],
characterData: false,
childList: true,
subtree: false,
};
this.observer.observe(angular.element('.delta-html')[0], observerConfig);
}
$onDestroy() {
this.observer.disconnect();
}
}
export function delta() {
return {
controller: DeltaCtrl,
replace: false,
restrict: 'A',
};
}
coreModule.directive('diffDelta', delta);
// Link to JSON line number
export class LinkJSONCtrl {
/** @ngInject */
constructor(private $scope: any, private $rootScope: GrafanaRootScope, private $anchorScroll: any) {}
goToLine(line: number) {
let unbind: () => void;
const scroll = () => {
this.$anchorScroll(`l${line}`);
unbind();
};
this.$scope.switchView().then(() => {
unbind = this.$rootScope.$on('json-diff-ready', scroll.bind(this));
});
}
}
export function linkJson() {
return {
controller: LinkJSONCtrl,
controllerAs: 'ctrl',
replace: true,
restrict: 'E',
scope: {
line: '@lineDisplay',
link: '@lineLink',
switchView: '&',
},
template: `<a class="diff-linenum btn btn-inverse btn-small" ng-click="ctrl.goToLine(link)">Line {{ line }}</a>`,
};
}
coreModule.directive('diffLinkJson', linkJson);

View File

@@ -0,0 +1,274 @@
import { each, reduce } from 'lodash';
import $ from 'jquery';
import coreModule from '../core/core_module';
/** @ngInject */
export function dropdownTypeahead($compile: any) {
const inputTemplate =
'<input type="text"' +
' class="gf-form-input input-medium tight-form-input"' +
' spellcheck="false" style="display:none"></input>';
const buttonTemplate =
'<a class="gf-form-label tight-form-func dropdown-toggle"' +
' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
' ><i class="fa fa-plus"></i></a>';
return {
scope: {
menuItems: '=dropdownTypeahead',
dropdownTypeaheadOnSelect: '&dropdownTypeaheadOnSelect',
model: '=ngModel',
},
link: ($scope: any, elem: any, attrs: any) => {
const $input = $(inputTemplate);
const $button = $(buttonTemplate);
$input.appendTo(elem);
$button.appendTo(elem);
if (attrs.linkText) {
$button.html(attrs.linkText);
}
if (attrs.ngModel) {
$scope.$watch('model', (newValue: any) => {
each($scope.menuItems, (item) => {
each(item.submenu, (subItem) => {
if (subItem.value === newValue) {
$button.html(subItem.text);
}
});
});
});
}
const typeaheadValues = reduce(
$scope.menuItems,
(memo: any[], value, index) => {
if (!value.submenu) {
value.click = 'menuItemSelected(' + index + ')';
memo.push(value.text);
} else {
each(value.submenu, (item, subIndex) => {
item.click = 'menuItemSelected(' + index + ',' + subIndex + ')';
memo.push(value.text + ' ' + item.text);
});
}
return memo;
},
[]
);
const closeDropdownMenu = () => {
$input.hide();
$input.val('');
$button.show();
$button.focus();
elem.removeClass('open');
};
$scope.menuItemSelected = (index: number, subIndex: number) => {
const menuItem = $scope.menuItems[index];
const payload: any = { $item: menuItem };
if (menuItem.submenu && subIndex !== void 0) {
payload.$subItem = menuItem.submenu[subIndex];
}
$scope.dropdownTypeaheadOnSelect(payload);
closeDropdownMenu();
};
$input.attr('data-provide', 'typeahead');
$input.typeahead({
source: typeaheadValues,
minLength: 1,
items: 10,
updater: (value: string) => {
const result: any = {};
each($scope.menuItems, (menuItem) => {
each(menuItem.submenu, (submenuItem) => {
if (value === menuItem.text + ' ' + submenuItem.text) {
result.$subItem = submenuItem;
result.$item = menuItem;
}
});
});
if (result.$item) {
$scope.$apply(() => {
$scope.dropdownTypeaheadOnSelect(result);
});
}
$input.trigger('blur');
return '';
},
});
$button.click(() => {
$button.hide();
$input.show();
$input.focus();
});
$input.keyup(() => {
elem.toggleClass('open', $input.val() === '');
});
elem.mousedown((evt: Event) => {
evt.preventDefault();
});
$input.blur(() => {
$input.hide();
$input.val('');
$button.show();
$button.focus();
// clicking the function dropdown menu won't
// work if you remove class at once
setTimeout(() => {
elem.removeClass('open');
}, 200);
});
$compile(elem.contents())($scope);
},
};
}
/** @ngInject */
export function dropdownTypeahead2($compile: any) {
const inputTemplate =
'<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
const buttonTemplate =
'<a class="{{buttonTemplateClass}} dropdown-toggle"' +
' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
' ><i class="fa fa-plus"></i></a>';
return {
scope: {
menuItems: '=dropdownTypeahead2',
dropdownTypeaheadOnSelect: '&dropdownTypeaheadOnSelect',
model: '=ngModel',
buttonTemplateClass: '@',
},
link: ($scope: any, elem: any, attrs: any) => {
const $input = $(inputTemplate);
if (!$scope.buttonTemplateClass) {
$scope.buttonTemplateClass = 'gf-form-input';
}
const $button = $(buttonTemplate);
const timeoutId = {
blur: null as any,
};
$input.appendTo(elem);
$button.appendTo(elem);
if (attrs.linkText) {
$button.html(attrs.linkText);
}
if (attrs.ngModel) {
$scope.$watch('model', (newValue: any) => {
each($scope.menuItems, (item) => {
each(item.submenu, (subItem) => {
if (subItem.value === newValue) {
$button.html(subItem.text);
}
});
});
});
}
const typeaheadValues = reduce(
$scope.menuItems,
(memo: any[], value, index) => {
if (!value.submenu) {
value.click = 'menuItemSelected(' + index + ')';
memo.push(value.text);
} else {
each(value.submenu, (item, subIndex) => {
item.click = 'menuItemSelected(' + index + ',' + subIndex + ')';
memo.push(value.text + ' ' + item.text);
});
}
return memo;
},
[]
);
const closeDropdownMenu = () => {
$input.hide();
$input.val('');
$button.show();
$button.focus();
elem.removeClass('open');
};
$scope.menuItemSelected = (index: number, subIndex: number) => {
const menuItem = $scope.menuItems[index];
const payload: any = { $item: menuItem };
if (menuItem.submenu && subIndex !== void 0) {
payload.$subItem = menuItem.submenu[subIndex];
}
$scope.dropdownTypeaheadOnSelect(payload);
closeDropdownMenu();
};
$input.attr('data-provide', 'typeahead');
$input.typeahead({
source: typeaheadValues,
minLength: 1,
items: 10,
updater: (value: string) => {
const result: any = {};
each($scope.menuItems, (menuItem) => {
each(menuItem.submenu, (submenuItem) => {
if (value === menuItem.text + ' ' + submenuItem.text) {
result.$subItem = submenuItem;
result.$item = menuItem;
}
});
});
if (result.$item) {
$scope.$apply(() => {
$scope.dropdownTypeaheadOnSelect(result);
});
}
$input.trigger('blur');
return '';
},
});
$button.click(() => {
$button.hide();
$input.show();
$input.focus();
});
$input.keyup(() => {
elem.toggleClass('open', $input.val() === '');
});
elem.mousedown((evt: Event) => {
evt.preventDefault();
timeoutId.blur = null;
});
$input.blur(() => {
timeoutId.blur = setTimeout(() => {
closeDropdownMenu();
}, 1);
});
$compile(elem.contents())($scope);
},
};
}
coreModule.directive('dropdownTypeahead', dropdownTypeahead);
coreModule.directive('dropdownTypeahead2', dropdownTypeahead2);

View File

@@ -0,0 +1,29 @@
import coreModule from '../core/core_module';
coreModule.directive('giveFocus', () => {
return (scope: any, element: any, attrs: any) => {
element.click((e: any) => {
e.stopPropagation();
});
scope.$watch(
attrs.giveFocus,
(newValue: any) => {
if (!newValue) {
return;
}
setTimeout(() => {
element.focus();
const domEl: any = element[0];
if (domEl.setSelectionRange) {
const pos = element.val().length * 2;
domEl.setSelectionRange(pos, pos);
}
}, 200);
},
true
);
};
});
export default {};

View File

@@ -0,0 +1,266 @@
import { debounce, find, indexOf, map, escape, unescape } from 'lodash';
import $ from 'jquery';
import coreModule from '../core/core_module';
import { TemplateSrv } from 'app/features/templating/template_srv';
/** @ngInject */
export function metricSegment($compile: any, $sce: any, templateSrv: TemplateSrv) {
const inputTemplate =
'<input type="text" data-provide="typeahead" ' +
' class="gf-form-input input-medium"' +
' spellcheck="false" style="display:none"></input>';
const linkTemplate =
'<a class="gf-form-label" ng-class="segment.cssClass" ' +
'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
const selectTemplate =
'<a class="gf-form-input gf-form-input--dropdown" ng-class="segment.cssClass" ' +
'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
return {
scope: {
segment: '=',
getOptions: '&',
onChange: '&',
debounce: '@',
},
link: ($scope: any, elem: any) => {
const $input = $(inputTemplate);
const segment = $scope.segment;
const $button = $(segment.selectMode ? selectTemplate : linkTemplate);
let options = null;
let cancelBlur: any = null;
let linkMode = true;
const debounceLookup = $scope.debounce;
$input.appendTo(elem);
$button.appendTo(elem);
$scope.updateVariableValue = (value: string) => {
if (value === '' || segment.value === value) {
return;
}
$scope.$apply(() => {
const selected: any = find($scope.altSegments, { value: value });
if (selected) {
segment.value = selected.value;
segment.html = selected.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(selected.value));
segment.fake = false;
segment.expandable = selected.expandable;
if (selected.type) {
segment.type = selected.type;
}
} else if (segment.custom !== 'false') {
segment.value = value;
segment.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(value));
segment.expandable = true;
segment.fake = false;
}
$scope.onChange();
});
};
$scope.switchToLink = (fromClick: boolean) => {
if (linkMode && !fromClick) {
return;
}
clearTimeout(cancelBlur);
cancelBlur = null;
linkMode = true;
$input.hide();
$button.show();
$scope.updateVariableValue($input.val());
};
$scope.inputBlur = () => {
// happens long before the click event on the typeahead options
// need to have long delay because the blur
cancelBlur = setTimeout($scope.switchToLink, 200);
};
$scope.source = (query: string, callback: any) => {
$scope.$apply(() => {
$scope.getOptions({ $query: query }).then((altSegments: any) => {
$scope.altSegments = altSegments;
options = map($scope.altSegments, (alt) => {
return escape(alt.value);
});
// add custom values
if (segment.custom !== 'false') {
if (!segment.fake && indexOf(options, segment.value) === -1) {
options.unshift(escape(segment.value));
}
}
callback(options);
});
});
};
$scope.updater = (value: string) => {
value = unescape(value);
if (value === segment.value) {
clearTimeout(cancelBlur);
$input.focus();
return value;
}
$input.val(value);
$scope.switchToLink(true);
return value;
};
$scope.matcher = function (item: string) {
if (linkMode) {
return false;
}
let str = this.query;
if (str[0] === '/') {
str = str.substring(1);
}
if (str[str.length - 1] === '/') {
str = str.substring(0, str.length - 1);
}
try {
return item.toLowerCase().match(str.toLowerCase());
} catch (e) {
return false;
}
};
$input.attr('data-provide', 'typeahead');
$input.typeahead({
source: $scope.source,
minLength: 0,
items: 10000,
updater: $scope.updater,
matcher: $scope.matcher,
});
const typeahead = $input.data('typeahead');
typeahead.lookup = function () {
this.query = this.$element.val() || '';
const items = this.source(this.query, $.proxy(this.process, this));
return items ? this.process(items) : items;
};
if (debounceLookup) {
typeahead.lookup = debounce(typeahead.lookup, 500, { leading: true });
}
$button.keydown((evt) => {
// trigger typeahead on down arrow or enter key
if (evt.keyCode === 40 || evt.keyCode === 13) {
$button.click();
}
});
$button.click(() => {
options = null;
$input.css('width', Math.max($button.width()!, 80) + 16 + 'px');
$button.hide();
$input.show();
$input.focus();
linkMode = false;
const typeahead = $input.data('typeahead');
if (typeahead) {
$input.val('');
typeahead.lookup();
}
});
$input.blur($scope.inputBlur);
$compile(elem.contents())($scope);
},
};
}
/** @ngInject */
export function metricSegmentModel(uiSegmentSrv: any) {
return {
template:
'<metric-segment segment="segment" get-options="getOptionsInternal()" on-change="onSegmentChange()"></metric-segment>',
restrict: 'E',
scope: {
property: '=',
options: '=',
getOptions: '&',
onChange: '&',
},
link: {
pre: function postLink($scope: any, elem: any, attrs: any) {
let cachedOptions: any;
$scope.valueToSegment = (value: any) => {
const option: any = find($scope.options, { value: value });
const segment = {
cssClass: attrs.cssClass,
custom: attrs.custom,
value: option ? option.text : value,
selectMode: attrs.selectMode,
};
return uiSegmentSrv.newSegment(segment);
};
$scope.getOptionsInternal = () => {
if ($scope.options) {
cachedOptions = $scope.options;
return Promise.resolve(
map($scope.options, (option) => {
return { value: option.text };
})
);
} else {
return $scope.getOptions().then((options: any) => {
cachedOptions = options;
return map(options, (option) => {
if (option.html) {
return option;
}
return { value: option.text };
});
});
}
};
$scope.onSegmentChange = () => {
if (cachedOptions) {
const option: any = find(cachedOptions, { text: $scope.segment.value });
if (option && option.value !== $scope.property) {
$scope.property = option.value;
} else if (attrs.custom !== 'false') {
$scope.property = $scope.segment.value;
}
} else {
$scope.property = $scope.segment.value;
}
// needs to call this after digest so
// property is synced with outerscope
$scope.$$postDigest(() => {
$scope.$apply(() => {
$scope.onChange();
});
});
};
$scope.segment = $scope.valueToSegment($scope.property);
},
},
};
}
coreModule.directive('metricSegment', metricSegment);
coreModule.directive('metricSegmentModel', metricSegmentModel);

216
public/app/angular/misc.ts Normal file
View File

@@ -0,0 +1,216 @@
import angular from 'angular';
import Clipboard from 'clipboard';
import coreModule from '../core/core_module';
import kbn from 'app/core/utils/kbn';
import { appEvents } from 'app/core/core';
import { AppEvents } from '@grafana/data';
/** @ngInject */
function tip($compile: any) {
return {
restrict: 'E',
link: (scope: any, elem: any, attrs: any) => {
let _t =
'<i class="grafana-tip fa fa-' +
(attrs.icon || 'question-circle') +
'" bs-tooltip="\'' +
kbn.addSlashes(elem.text()) +
'\'"></i>';
_t = _t.replace(/{/g, '\\{').replace(/}/g, '\\}');
elem.replaceWith($compile(angular.element(_t))(scope));
},
};
}
function clipboardButton() {
return {
scope: {
getText: '&clipboardButton',
},
link: (scope: any, elem: any) => {
scope.clipboard = new Clipboard(elem[0], {
text: () => {
return scope.getText();
},
});
scope.clipboard.on('success', () => {
appEvents.emit(AppEvents.alertSuccess, ['Content copied to clipboard']);
});
scope.$on('$destroy', () => {
if (scope.clipboard) {
scope.clipboard.destroy();
}
});
},
};
}
/** @ngInject */
function compile($compile: any) {
return {
restrict: 'A',
link: (scope: any, element: any, attrs: any) => {
scope.$watch(
(scope: any) => {
return scope.$eval(attrs.compile);
},
(value: any) => {
element.html(value);
$compile(element.contents())(scope);
}
);
},
};
}
function watchChange() {
return {
scope: { onchange: '&watchChange' },
link: (scope: any, element: any) => {
element.on('input', () => {
scope.$apply(() => {
scope.onchange({ inputValue: element.val() });
});
});
},
};
}
/** @ngInject */
function editorOptBool($compile: any) {
return {
restrict: 'E',
link: (scope: any, elem: any, attrs: any) => {
const ngchange = attrs.change ? ' ng-change="' + attrs.change + '"' : '';
const tip = attrs.tip ? ' <tip>' + attrs.tip + '</tip>' : '';
const showIf = attrs.showIf ? ' ng-show="' + attrs.showIf + '" ' : '';
const template =
'<div class="editor-option gf-form-checkbox text-center"' +
showIf +
'>' +
' <label for="' +
attrs.model +
'" class="small">' +
attrs.text +
tip +
'</label>' +
'<input class="cr1" id="' +
attrs.model +
'" type="checkbox" ' +
' ng-model="' +
attrs.model +
'"' +
ngchange +
' ng-checked="' +
attrs.model +
'"></input>' +
' <label for="' +
attrs.model +
'" class="cr1"></label>';
elem.replaceWith($compile(angular.element(template))(scope));
},
};
}
/** @ngInject */
function editorCheckbox($compile: any, $interpolate: any) {
return {
restrict: 'E',
link: (scope: any, elem: any, attrs: any) => {
const text = $interpolate(attrs.text)(scope);
const model = $interpolate(attrs.model)(scope);
const ngchange = attrs.change ? ' ng-change="' + attrs.change + '"' : '';
const tip = attrs.tip ? ' <tip>' + attrs.tip + '</tip>' : '';
const label = '<label for="' + scope.$id + model + '" class="checkbox-label">' + text + tip + '</label>';
let template =
'<input class="cr1" id="' +
scope.$id +
model +
'" type="checkbox" ' +
' ng-model="' +
model +
'"' +
ngchange +
' ng-checked="' +
model +
'"></input>' +
' <label for="' +
scope.$id +
model +
'" class="cr1"></label>';
template = template + label;
elem.addClass('gf-form-checkbox');
elem.html($compile(angular.element(template))(scope));
},
};
}
/** @ngInject */
function gfDropdown($parse: any, $compile: any, $timeout: any) {
function buildTemplate(items: any, placement?: any) {
const upclass = placement === 'top' ? 'dropup' : '';
const ul = ['<ul class="dropdown-menu ' + upclass + '" role="menu" aria-labelledby="drop1">', '</ul>'];
for (let index = 0; index < items.length; index++) {
const item = items[index];
if (item.divider) {
ul.splice(index + 1, 0, '<li class="divider"></li>');
continue;
}
let li =
'<li' +
(item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') +
'>' +
'<a tabindex="-1" ng-href="' +
(item.href || '') +
'"' +
(item.click ? ' ng-click="' + item.click + '"' : '') +
(item.target ? ' target="' + item.target + '"' : '') +
(item.method ? ' data-method="' + item.method + '"' : '') +
'>' +
(item.text || '') +
'</a>';
if (item.submenu && item.submenu.length) {
li += buildTemplate(item.submenu).join('\n');
}
li += '</li>';
ul.splice(index + 1, 0, li);
}
return ul;
}
return {
restrict: 'EA',
scope: true,
link: function postLink(scope: any, iElement: any, iAttrs: any) {
const getter = $parse(iAttrs.gfDropdown),
items = getter(scope);
$timeout(() => {
const placement = iElement.data('placement');
const dropdown = angular.element(buildTemplate(items, placement).join(''));
dropdown.insertAfter(iElement);
$compile(iElement.next('ul.dropdown-menu'))(scope);
});
iElement.addClass('dropdown-toggle').attr('data-toggle', 'dropdown');
},
};
}
coreModule.directive('tip', tip);
coreModule.directive('clipboardButton', clipboardButton);
coreModule.directive('compile', compile);
coreModule.directive('watchChange', watchChange);
coreModule.directive('editorOptBool', editorOptBool);
coreModule.directive('editorCheckbox', editorCheckbox);
coreModule.directive('gfDropdown', gfDropdown);

View File

@@ -0,0 +1,59 @@
import coreModule from '../core/core_module';
import { rangeUtil } from '@grafana/data';
function ngModelOnBlur() {
return {
restrict: 'A',
priority: 1,
require: 'ngModel',
link: (scope: any, elm: any, attr: any, ngModelCtrl: any) => {
if (attr.type === 'radio' || attr.type === 'checkbox') {
return;
}
elm.off('input keydown change');
elm.bind('blur', () => {
scope.$apply(() => {
ngModelCtrl.$setViewValue(elm.val());
});
});
},
};
}
function emptyToNull() {
return {
restrict: 'A',
require: 'ngModel',
link: (scope: any, elm: any, attrs: any, ctrl: any) => {
ctrl.$parsers.push((viewValue: any) => {
if (viewValue === '') {
return null;
}
return viewValue;
});
},
};
}
function validTimeSpan() {
return {
require: 'ngModel',
link: (scope: any, elm: any, attrs: any, ctrl: any) => {
ctrl.$validators.integer = (modelValue: any, viewValue: any) => {
if (ctrl.$isEmpty(modelValue)) {
return true;
}
if (viewValue.indexOf('$') === 0 || viewValue.indexOf('+$') === 0) {
return true; // allow template variable
}
const info = rangeUtil.describeTextRange(viewValue);
return info.invalid !== true;
};
},
};
}
coreModule.directive('ngModelOnblur', ngModelOnBlur);
coreModule.directive('emptyToNull', emptyToNull);
coreModule.directive('validTimeSpan', validTimeSpan);

View File

@@ -0,0 +1,73 @@
import $ from 'jquery';
import coreModule from '../core/core_module';
function getBlockNodes(nodes: any[]) {
let node = nodes[0];
const endNode = nodes[nodes.length - 1];
let blockNodes: any[] | undefined;
node = node.nextSibling;
for (let i = 1; node !== endNode && node; i++) {
if (blockNodes || nodes[i] !== node) {
if (!blockNodes) {
blockNodes = $([].slice.call(nodes, 0, i)) as any;
}
blockNodes!.push(node);
}
node = node.nextSibling;
}
return blockNodes || nodes;
}
/** @ngInject */
function rebuildOnChange($animate: any) {
return {
multiElement: true,
terminal: true,
transclude: true,
priority: 600,
restrict: 'E',
link: (scope: any, elem: any, attrs: any, ctrl: any, transclude: any) => {
let block: any, childScope: any, previousElements: any;
function cleanUp() {
if (previousElements) {
previousElements.remove();
previousElements = null;
}
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
previousElements = getBlockNodes(block.clone);
$animate.leave(previousElements).then(() => {
previousElements = null;
});
block = null;
}
}
scope.$watch(attrs.property, function rebuildOnChangeAction(value: any, oldValue: any) {
if (childScope && value !== oldValue) {
cleanUp();
}
if (!childScope && (value || attrs.showNull)) {
transclude((clone: any, newScope: any) => {
childScope = newScope;
clone[clone.length++] = document.createComment(' end rebuild on change ');
block = { clone: clone };
$animate.enter(clone, elem.parent(), elem);
});
} else {
cleanUp();
}
});
},
};
}
coreModule.directive('rebuildOnChange', rebuildOnChange);

View File

@@ -0,0 +1,9 @@
import './alert_srv';
import './util_srv';
import './context_srv';
import './timer';
import './analytics';
import './popover_srv';
import './segment_srv';
import './backend_srv';
import './dynamic_directive_srv';

118
public/app/angular/tags.ts Normal file
View File

@@ -0,0 +1,118 @@
import angular from 'angular';
import { getTagColorsFromName } from '@grafana/ui';
import $ from 'jquery';
import coreModule from '../core/core_module';
import 'vendor/tagsinput/bootstrap-tagsinput.js';
function setColor(name: string, element: JQuery) {
const { color, borderColor } = getTagColorsFromName(name);
element.css('background-color', color);
element.css('border-color', borderColor);
}
function tagColorFromName() {
return {
scope: { tagColorFromName: '=' },
link: (scope: any, element: any) => {
setColor(scope.tagColorFromName, element);
},
};
}
function bootstrapTagsinput() {
function getItemProperty(scope: any, property: any) {
if (!property) {
return undefined;
}
if (angular.isFunction(scope.$parent[property])) {
return scope.$parent[property];
}
return (item: any) => {
return item[property];
};
}
return {
restrict: 'EA',
scope: {
model: '=ngModel',
onTagsUpdated: '&',
},
template: '<select multiple></select>',
replace: false,
link: function (scope: any, element: any, attrs: any) {
if (!angular.isArray(scope.model)) {
scope.model = [];
}
const select = $('select', element);
if (attrs.placeholder) {
select.attr('placeholder', attrs.placeholder);
}
select.tagsinput({
typeahead: {
source: angular.isFunction(scope.$parent[attrs.typeaheadSource])
? scope.$parent[attrs.typeaheadSource]
: null,
},
widthClass: attrs.widthClass,
itemValue: getItemProperty(scope, attrs.itemvalue),
itemText: getItemProperty(scope, attrs.itemtext),
tagClass: angular.isFunction(scope.$parent[attrs.tagclass])
? scope.$parent[attrs.tagclass]
: () => {
return attrs.tagclass;
},
});
select.on('itemAdded', (event: any) => {
if (scope.model.indexOf(event.item) === -1) {
scope.model.push(event.item);
if (scope.onTagsUpdated) {
scope.onTagsUpdated();
}
}
const tagElement = select
.next()
.children('span')
.filter(() => {
return $(this).text() === event.item;
});
setColor(event.item, tagElement);
});
select.on('itemRemoved', (event: any) => {
const idx = scope.model.indexOf(event.item);
if (idx !== -1) {
scope.model.splice(idx, 1);
if (scope.onTagsUpdated) {
scope.onTagsUpdated();
}
}
});
scope.$watch(
'model',
() => {
if (!angular.isArray(scope.model)) {
scope.model = [];
}
select.tagsinput('removeAll');
for (let i = 0; i < scope.model.length; i++) {
select.tagsinput('add', scope.model[i]);
}
},
true
);
},
};
}
coreModule.directive('tagColorFromName', tagColorFromName);
coreModule.directive('bootstrapTagsinput', bootstrapTagsinput);