func_editor.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import _ from 'lodash';
  2. import $ from 'jquery';
  3. import coreModule from 'app/core/core_module';
  4. import { TemplateSrv } from 'app/features/templating/template_srv';
  5. /** @ngInject */
  6. export function graphiteFuncEditor($compile: any, templateSrv: TemplateSrv) {
  7. const funcSpanTemplate = `
  8. <function-editor
  9. func="func"
  10. onRemove="ctrl.handleRemoveFunction"
  11. onMoveLeft="ctrl.handleMoveLeft"
  12. onMoveRight="ctrl.handleMoveRight"
  13. /><span>(</span>
  14. `;
  15. const paramTemplate =
  16. '<input type="text" style="display:none"' + ' class="input-small tight-form-func-param"></input>';
  17. return {
  18. restrict: 'A',
  19. link: function postLink($scope: any, elem: JQuery) {
  20. const $funcLink = $(funcSpanTemplate);
  21. const ctrl = $scope.ctrl;
  22. const func = $scope.func;
  23. let scheduledRelink = false;
  24. let paramCountAtLink = 0;
  25. let cancelBlur: any = null;
  26. ctrl.handleRemoveFunction = (func: any) => {
  27. ctrl.removeFunction(func);
  28. };
  29. ctrl.handleMoveLeft = (func: any) => {
  30. ctrl.moveFunction(func, -1);
  31. };
  32. ctrl.handleMoveRight = (func: any) => {
  33. ctrl.moveFunction(func, 1);
  34. };
  35. function clickFuncParam(this: any, paramIndex: any) {
  36. /*jshint validthis:true */
  37. const $link = $(this);
  38. const $comma = $link.prev('.comma');
  39. const $input = $link.next();
  40. $input.val(func.params[paramIndex]);
  41. $comma.removeClass('query-part__last');
  42. $link.hide();
  43. $input.show();
  44. $input.focus();
  45. $input.select();
  46. const typeahead = $input.data('typeahead');
  47. if (typeahead) {
  48. $input.val('');
  49. typeahead.lookup();
  50. }
  51. }
  52. function scheduledRelinkIfNeeded() {
  53. if (paramCountAtLink === func.params.length) {
  54. return;
  55. }
  56. if (!scheduledRelink) {
  57. scheduledRelink = true;
  58. setTimeout(() => {
  59. relink();
  60. scheduledRelink = false;
  61. }, 200);
  62. }
  63. }
  64. function paramDef(index: number) {
  65. if (index < func.def.params.length) {
  66. return func.def.params[index];
  67. }
  68. if ((_.last(func.def.params) as any).multiple) {
  69. return _.assign({}, _.last(func.def.params), { optional: true });
  70. }
  71. return {};
  72. }
  73. function switchToLink(inputElem: HTMLElement, paramIndex: any) {
  74. /*jshint validthis:true */
  75. const $input = $(inputElem);
  76. clearTimeout(cancelBlur);
  77. cancelBlur = null;
  78. const $link = $input.prev();
  79. const $comma = $link.prev('.comma');
  80. const newValue = $input.val();
  81. // remove optional empty params
  82. if (newValue !== '' || paramDef(paramIndex).optional) {
  83. func.updateParam(newValue, paramIndex);
  84. $link.html(newValue ? templateSrv.highlightVariablesAsHtml(newValue) : '&nbsp;');
  85. }
  86. scheduledRelinkIfNeeded();
  87. $scope.$apply(() => {
  88. ctrl.targetChanged();
  89. });
  90. if ($link.hasClass('query-part__last') && newValue === '') {
  91. $comma.addClass('query-part__last');
  92. } else {
  93. $link.removeClass('query-part__last');
  94. }
  95. $input.hide();
  96. $link.show();
  97. }
  98. // this = input element
  99. function inputBlur(this: any, paramIndex: any) {
  100. /*jshint validthis:true */
  101. const inputElem = this;
  102. // happens long before the click event on the typeahead options
  103. // need to have long delay because the blur
  104. cancelBlur = setTimeout(() => {
  105. switchToLink(inputElem, paramIndex);
  106. }, 200);
  107. }
  108. function inputKeyPress(this: any, paramIndex: any, e: any) {
  109. /*jshint validthis:true */
  110. if (e.which === 13) {
  111. $(this).blur();
  112. }
  113. }
  114. function inputKeyDown(this: any) {
  115. /*jshint validthis:true */
  116. this.style.width = (3 + this.value.length) * 8 + 'px';
  117. }
  118. function addTypeahead($input: any, paramIndex: any) {
  119. $input.attr('data-provide', 'typeahead');
  120. let options = paramDef(paramIndex).options;
  121. if (paramDef(paramIndex).type === 'int') {
  122. options = _.map(options, val => {
  123. return val.toString();
  124. });
  125. }
  126. $input.typeahead({
  127. source: options,
  128. minLength: 0,
  129. items: 20,
  130. updater: (value: any) => {
  131. $input.val(value);
  132. switchToLink($input[0], paramIndex);
  133. return value;
  134. },
  135. });
  136. const typeahead = $input.data('typeahead');
  137. typeahead.lookup = function() {
  138. this.query = this.$element.val() || '';
  139. return this.process(this.source);
  140. };
  141. }
  142. function addElementsAndCompile() {
  143. $funcLink.appendTo(elem);
  144. const defParams: any = _.clone(func.def.params);
  145. const lastParam: any = _.last(func.def.params);
  146. while (func.params.length >= defParams.length && lastParam && lastParam.multiple) {
  147. defParams.push(_.assign({}, lastParam, { optional: true }));
  148. }
  149. _.each(defParams, (param: any, index: number) => {
  150. if (param.optional && func.params.length < index) {
  151. return false;
  152. }
  153. let paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
  154. const hasValue = paramValue !== null && paramValue !== undefined;
  155. const last = index >= func.params.length - 1 && param.optional && !hasValue;
  156. if (last && param.multiple) {
  157. paramValue = '+';
  158. }
  159. if (index > 0) {
  160. $('<span class="comma' + (last ? ' query-part__last' : '') + '">, </span>').appendTo(elem);
  161. }
  162. const $paramLink = $(
  163. '<a ng-click="" class="graphite-func-param-link' +
  164. (last ? ' query-part__last' : '') +
  165. '">' +
  166. (hasValue ? paramValue : '&nbsp;') +
  167. '</a>'
  168. );
  169. const $input = $(paramTemplate);
  170. $input.attr('placeholder', param.name);
  171. paramCountAtLink++;
  172. $paramLink.appendTo(elem);
  173. $input.appendTo(elem);
  174. $input.blur(_.partial(inputBlur, index));
  175. $input.keyup(inputKeyDown);
  176. $input.keypress(_.partial(inputKeyPress, index));
  177. $paramLink.click(_.partial(clickFuncParam, index));
  178. if (param.options) {
  179. addTypeahead($input, index);
  180. }
  181. return true;
  182. });
  183. $('<span>)</span>').appendTo(elem);
  184. $compile(elem.contents())($scope);
  185. }
  186. function ifJustAddedFocusFirstParam() {
  187. if ($scope.func.added) {
  188. $scope.func.added = false;
  189. setTimeout(() => {
  190. elem
  191. .find('.graphite-func-param-link')
  192. .first()
  193. .click();
  194. }, 10);
  195. }
  196. }
  197. function relink() {
  198. elem.children().remove();
  199. addElementsAndCompile();
  200. ifJustAddedFocusFirstParam();
  201. }
  202. relink();
  203. },
  204. };
  205. }
  206. coreModule.directive('graphiteFuncEditor', graphiteFuncEditor);