Skip to content

Commit

Permalink
Fix suggestions in Timelion Visualization. (elastic#11638)
Browse files Browse the repository at this point in the history
* Refactor timelionExpressionInput to use ng-transclude to accept any type of form control. Fix suggestions in Timelion Visualization.
* Fix bug so that users can click on suggestions to select them.
* Fix broken reference to input element.
  • Loading branch information
cjcenizal authored and snide committed May 30, 2017
1 parent 1fece4b commit b4c3b25
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 52 deletions.
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
<div class="timelionSearchInputContainer">
<input
id="timelionSearchInput"
input-focus
ng-model="sheet"
timelion-expression="{{sheet}}"
placeholder="Expression..."
aria-label="Expression input"
type="text"
class="kuiLocalSearchInput"
ng-keyup="keyUpHandler($event)"
ng-keydown="keyDownHandler($event)"
ng-mouseup="mouseUpHandler()"
ng-blur="blurHandler()"
>

<div
class="timelionSearchInputContainer"
ng-keyup="keyUpHandler($event)"
ng-keydown="keyDownHandler($event)"
ng-mouseup="mouseUpHandler()"
ng-blur="blurHandler()"
>
<timelion-expression-suggestions
ng-show="functionSuggestions.isVisible"
suggestions="functionSuggestions.list"
selected-index="functionSuggestions.index"
on-click-suggestion="suggestionClickHandler"
on-click-suggestion="onClickSuggestion(suggestionIndex)"
></timelion-expression-suggestions>
</div>
73 changes: 43 additions & 30 deletions src/core_plugins/timelion/public/directives/expression_directive.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ app.directive('timelionExpressionInput', function ($compile, $http, $timeout) {
sheet: '=',
},
replace: true,
transclude: true,
template: timelionExpressionInputTemplate,
link: function ($scope, $elem) {
link: function (scope, elem, attrs, ctrl, transclude) {
const navigationalKeys = {
ESC: 27,
UP: 38,
Expand All @@ -57,11 +58,18 @@ app.directive('timelionExpressionInput', function ($compile, $http, $timeout) {
ENTER: 13
};

// Add the transcluded content. Assuming it's an input control of some sort, it will need
// access to the parent scope so it can use ng-model correctly.
let input;
transclude(scope.$parent, clone => {
elem.prepend(clone);
input = elem.find('[data-timelion-expression-input]');
});

const functionReference = {};
const input = $elem.find('#timelionSearchInput');
const caretLocation = {};

$scope.functionSuggestions = new FunctionSuggestions();
scope.functionSuggestions = new FunctionSuggestions();

function init() {
$http.get('../api/timelion/functions').then(function (resp) {
Expand All @@ -72,22 +80,25 @@ app.directive('timelionExpressionInput', function ($compile, $http, $timeout) {
});
}

function setExpression(expression, caretPosition) {
input.val(expression);
input[0].selectionStart = input[0].selectionEnd = caretPosition;
}

function completeExpression(suggestionIndex) {
if ($scope.functionSuggestions.isEmpty()) {
if (scope.functionSuggestions.isEmpty()) {
return;
}

const functionName = `${$scope.functionSuggestions.list[suggestionIndex].name}()`;
const expression = $scope.sheet;
const functionName = `${scope.functionSuggestions.list[suggestionIndex].name}()`;
const expression = scope.sheet;
const { min, max } = caretLocation;

const newExpression = insertAtLocation(functionName, expression, min, max);
input.val(newExpression);

const newCaretPosition = min + functionName.length - 1;
input[0].selectionStart = input[0].selectionEnd = newCaretPosition;
setExpression(newExpression, newCaretPosition);

$scope.functionSuggestions.reset();
scope.functionSuggestions.reset();
}

function scrollTo(selected) {
Expand All @@ -105,21 +116,21 @@ app.directive('timelionExpressionInput', function ($compile, $http, $timeout) {
const caretPosition = input[0].selectionStart;

suggest(
$scope.sheet,
scope.sheet,
caretPosition,
functionReference.list,
Parser
).then(({ list, location }) => {
// We're using ES6 Promises, not $q, so we have to wrap this in $apply.
$scope.$apply(() => {
$scope.functionSuggestions.setList(list);
$scope.functionSuggestions.show();
scope.$apply(() => {
scope.functionSuggestions.setList(list);
scope.functionSuggestions.show();
Object.assign(caretLocation, location);
});
}, ({ location } = {}) => {
$scope.$apply(() => {
scope.$apply(() => {
Object.assign(caretLocation, location);
$scope.functionSuggestions.reset();
scope.functionSuggestions.reset();
});
});
}
Expand All @@ -129,17 +140,17 @@ app.directive('timelionExpressionInput', function ($compile, $http, $timeout) {
return keyCodes.includes(keyCode);
}

$scope.mouseUpHandler = () => {
scope.mouseUpHandler = () => {
getSuggestions();
};

$scope.blurHandler = () => {
scope.blurHandler = () => {
$timeout(() => {
$scope.functionSuggestions.hide();
scope.functionSuggestions.hide();
}, 100);
};

$scope.keyDownHandler = e => {
scope.keyDownHandler = e => {
// If we've pressed any non-navigational keys, then the user has typed something and we
// can exit early without doing any navigation.
if (!isNavigationalKey(e.keyCode)) {
Expand All @@ -150,51 +161,53 @@ app.directive('timelionExpressionInput', function ($compile, $http, $timeout) {
case navigationalKeys.UP:
// Up and down keys navigate through suggestions.
e.preventDefault();
$scope.functionSuggestions.stepForward();
scrollTo($scope.functionSuggestions.index);
scope.functionSuggestions.stepForward();
scrollTo(scope.functionSuggestions.index);
break;

case navigationalKeys.DOWN:
// Up and down keys navigate through suggestions.
e.preventDefault();
$scope.functionSuggestions.stepBackward();
scrollTo($scope.functionSuggestions.index);
scope.functionSuggestions.stepBackward();
scrollTo(scope.functionSuggestions.index);
break;

case navigationalKeys.TAB:
// If there are no suggestions, the user tabs to the next input.
if ($scope.functionSuggestions.isEmpty()) {
if (scope.functionSuggestions.isEmpty()) {
return;
}

// If we have suggestions, complete the selected one.
e.preventDefault();
completeExpression($scope.functionSuggestions.index);
completeExpression(scope.functionSuggestions.index);
break;

case navigationalKeys.ENTER:
// If the suggestions are open, complete the expression with the suggestion.
// Otherwise, the default action of submitting the input value will occur.
if (!$scope.functionSuggestions.isEmpty()) {
if (!scope.functionSuggestions.isEmpty()) {
e.preventDefault();
completeExpression($scope.functionSuggestions.index);
completeExpression(scope.functionSuggestions.index);
}
break;

case navigationalKeys.ESC:
e.preventDefault();
$scope.functionSuggestions.hide();
scope.functionSuggestions.hide();
break;
}
};

$scope.keyUpHandler = e => {
scope.keyUpHandler = e => {
// If the user isn't navigating, then we should update the suggestions based on their input.
if (!isNavigationalKey(e.keyCode)) {
getSuggestions();
}
};

scope.onClickSuggestion = completeExpression;

init();
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<div
class="suggestion"
data-suggestion-list-item
ng-click="completeExpression($index)"
ng-click="onClickSuggestion({ suggestionIndex: $index })"
ng-class="{active: $index === selectedIndex}"
ng-repeat="suggestion in suggestions track by $index | orderBy:'name'"
>
Expand Down
12 changes: 11 additions & 1 deletion src/core_plugins/timelion/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,17 @@
<div class="kuiLocalSearch timelionLocalSearch">
<timelion-expression-input
sheet="state.sheet[state.selected]"
></timelion-expression-input>
>
<input
data-timelion-expression-input
input-focus
ng-model="state.sheet[state.selected]"
placeholder="Expression..."
aria-label="Expression input"
type="text"
class="kuiLocalSearchInput"
>
</timelion-expression-input>

<timelion-interval
model="state.interval"
Expand Down
15 changes: 12 additions & 3 deletions src/core_plugins/timelion/public/vis/timelion_vis_params.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,18 @@
<div>
<label>Timelion Expression</label>
</div>
<textarea
ng-model="vis.params.expression" class="form-control" timelion-expression="{{vis.params.expression}}"
rows="5"></textarea>

<timelion-expression-input
sheet="vis.params.expression"
>
<textarea
data-timelion-expression-input
ng-model="vis.params.expression"
class="form-control"
timelion-expression=""
rows="5"
></textarea>
</timelion-expression-input>
</div>
</div>

Expand Down

0 comments on commit b4c3b25

Please sign in to comment.