[fieldFormat] implement highlighting

Asking for a formatted field of contentType 'html' will now highlight the formatted html
when appropriate. In order to do this the format converter function must be passed
the field and hit that corelate to the value. This makes the code read a bit like this:

var convert = field.format.getConverterFor('html');
var formatted = convert(value, field, hit);

The field and hit arguments are not required if highlighting is not desired (like when
formatting results that are not search hits).
This commit is contained in:
Spencer Alger 2015-04-30 07:45:27 -07:00
parent e19024e22e
commit fa41afceb2
11 changed files with 90 additions and 117 deletions

View file

@ -22,7 +22,7 @@ define(function (require) {
* <tr ng-repeat="row in rows" kbn-table-row="row"></tr>
* ```
*/
module.directive('kbnTableRow', function ($compile, highlightFilter) {
module.directive('kbnTableRow', function ($compile) {
var openRowHtml = require('text!components/doc_table/components/table_row/open.html');
var detailsHtml = require('text!components/doc_table/components/table_row/details.html');
var cellTemplate = _.template(require('text!components/doc_table/components/table_row/cell.html'));
@ -150,18 +150,7 @@ define(function (require) {
*/
function _displayField(row, fieldName, breakWords) {
var indexPattern = $scope.indexPattern;
if (fieldName === '_source') {
if (!row.$$_formattedSource) {
var field = indexPattern.fields.byName._source;
var converter = field.format.getConverterFor('html');
row.$$_formattedSource = converter(row._source, indexPattern, row);
}
return row.$$_formattedSource;
}
var text = indexPattern.formatField(row, fieldName);
text = highlightFilter(text, row.highlight && row.highlight[fieldName]);
if (breakWords) {
text = addWordBreaks(text, MIN_LINE_LENGTH);

View file

@ -1,13 +0,0 @@
<dl class="source truncate-by-height">
<% _.each(highlight, function (value, field) { /* show fields that match the query first */ %>
<dt><%- shortDotsFilter(field) %>:</dt>
<dd><%= source[field] %></dd>
<%= ' ' %>
<% }); %>
<% _.each(source, function (value, field) { %>
<% if (_.has(highlight, field)) return; %>
<dt><%- shortDotsFilter(field) %>:</dt>
<dd><%= value %></dd>
<%= ' ' %>
<% }); %>
</dl>

View file

@ -49,7 +49,7 @@
tooltip-placement="top"
tooltip="Objects in arrays are not well supported."
class="fa fa-warning text-color-warning ng-scope doc-viewer-object-array"></i>
<div class="doc-viewer-value" ng-bind-html="(typeof(formatted[field]) === 'undefined' ? hit[field] : formatted[field]) | highlight : hit.highlight[field] | trustAsHtml"></div>
<div class="doc-viewer-value" ng-bind-html="typeof(formatted[field]) === 'undefined' ? hit[field] : formatted[field] | trustAsHtml"></div>
</td>
</tr>
</tbody>

View file

@ -36,12 +36,11 @@ define(function (require) {
/**
* Get a convert function that is bound to a specific contentType
* @param {string} [contentType=html]
* @return {function} - a bound converter function, which accepts a single "value"
* argument of any type
* @param {string} [contentType=text]
* @return {function} - a bound converter function
*/
FieldFormat.prototype.getConverterFor = function (contentType) {
return this._convert[contentType] || this._convert.text;
return this._convert[contentType || 'text'];
};
/**

View file

@ -1,11 +1,11 @@
define(function (require) {
return function contentTypesProvider() {
return function contentTypesProvider(highlightFilter) {
var _ = require('lodash');
var angular = require('angular');
var types = {
html: function (format, convert) {
return function recurse(value) {
return function recurse(value, field, hit) {
var type = typeof value;
if (type === 'object' && typeof value.map === 'function') {
@ -19,14 +19,14 @@ define(function (require) {
return value.$$_formattedField = subVals.join(',' + (useMultiLine ? '\n' : ' '));
}
return convert.call(format, value);
return convert.call(format, value, field, hit);
};
},
text: function (format, convert) {
return function recurse(value) {
if (value && typeof value.map === 'function') {
return angular.toJson(value.map(recurse));
return angular.toJson(value.map(recurse), true);
}
return _.escape(convert.call(format, value));
@ -34,21 +34,26 @@ define(function (require) {
}
};
function fallbackText(value) {
return _.escape(_.asPrettyString(value));
}
function fallbackHtml(value, field, hit) {
var formatted = this.convert(value, 'text');
if (!hit || !hit.highlight || !hit.highlight[field.name]) {
return formatted;
} else {
return highlightFilter(formatted, hit.highlight[field.name]);
}
}
function setup(format) {
var src = format._convert || {};
var converters = format._convert = {};
if (src.text) {
converters.text = types.text(format, src.text);
} else {
converters.text = types.text(format, _.escape);
}
if (src.html) {
converters.html = types.html(format, src.html);
} else {
converters.html = types.html(format, converters.text);
}
converters.text = types.text(format, src.text || fallbackText);
converters.html = types.html(format, src.html || fallbackHtml);
return format._convert;
}

View file

@ -5,36 +5,41 @@ define(function (require) {
return function (indexPattern, defaultFormat) {
function transformField(memo, val, name) {
var field = indexPattern.fields.byName[name];
return memo[name] = field ? field.format.convert(val, 'html') : defaultFormat.convert(val, 'html');
function convert(hit, val, fieldName) {
var field = indexPattern.fields.byName[fieldName];
if (!field) return defaultFormat.convert(val, 'html');
return field.format.getConverterFor('html')(val, field, hit);
}
function formatHit(hit) {
if (hit.$$_formatted) return hit.$$_formatted;
var cache = hit.$$_partialFormatted = hit.$$_formatted = {};
// use and update the partial cache, but don't rewrite it. _source is stored in partials
// but not $$_formatted
var partials = hit.$$_partialFormatted || (hit.$$_partialFormatted = {});
var cache = hit.$$_formatted = {};
_.forOwn(indexPattern.flattenHit(hit), function (val, fieldName) {
transformField(cache, val, fieldName);
// sync the formatted and partial cache
var formatted = partials[fieldName] == null ? convert(hit, val, fieldName) : partials[fieldName];
cache[fieldName] = partials[fieldName] = formatted;
});
return hit.$$_formatted;
return cache;
}
formatHit.formatField = function (hit, fieldName) {
// formatHit was previously called
if (hit.$$_formatted) return hit.$$_formatted[fieldName];
var partial = hit.$$_partialFormatted;
if (partial && _.has(partial, fieldName)) {
return partial[fieldName];
var partials = hit.$$_partialFormatted;
if (partials && partials[fieldName] != null) {
return partials[fieldName];
}
if (!partial) {
partial = hit.$$_partialFormatted = {};
if (!partials) {
partials = hit.$$_partialFormatted = {};
}
return transformField(partial, indexPattern.flattenHit(hit)[fieldName], fieldName);
var val = fieldName === '_source' ? hit._source : indexPattern.flattenHit(hit)[fieldName];
return partials[fieldName] = convert(hit, val, fieldName);
};
return formatHit;

View file

@ -1,59 +1,41 @@
define(function (require) {
return function _StringProvider(Private, shortDotsFilter, highlightFilter) {
return function _SourceProvider(Private, shortDotsFilter) {
var _ = require('lodash');
var FieldFormat = Private(require('components/index_patterns/_field_format/FieldFormat'));
var noWhiteSpace = require('utils/no_white_space');
var template = _.template(noWhiteSpace(require('text!components/stringify/types/_source.html')));
var angular = require('angular');
_(Source).inherits(FieldFormat);
function Source(params) {
var self = this;
// _source converters are weird and override the _convert object
// that is setup by the FieldFormat constructor
Source.prototype._convert = {};
Source.Super.call(self, params);
function sourceToText(source) {
return _.escape(JSON.stringify(source));
}
function sourceToHtml(source, indexPattern, hit) {
if (!indexPattern) return sourceToText(source);
var highlights = (hit && hit.highlight) || {};
var formatted = indexPattern.formatHit(hit);
var highlightPairs = [];
var sourcePairs = [];
_.keys(formatted).forEach(function (key) {
var pairs = sourcePairs;
var field = shortDotsFilter(key);
var val = formatted[key];
if (highlights[key]) {
pairs = highlightPairs;
val = highlightFilter(val, highlights[key]);
}
pairs.push([field, val]);
}, []);
return template({ defPairs: highlightPairs.concat(sourcePairs) });
}
self._convert = {
text: sourceToText,
html: sourceToHtml
};
Source.Super.call(this, params);
}
Source.id = '_source';
Source.title = '_source';
Source.fieldType = '_source';
Source.prototype._convert = {
text: angular.toJson,
html: function sourceToHtml(source, field, hit) {
if (!field) return this.getConverter('text')(source, field, hit);
var highlights = (hit && hit.highlight) || {};
var formatted = field.indexPattern.formatHit(hit);
var highlightPairs = [];
var sourcePairs = [];
_.keys(formatted).forEach(function (key) {
var pairs = highlights[key] ? highlightPairs : sourcePairs;
var field = shortDotsFilter(key);
var val = formatted[key];
pairs.push([field, val]);
}, []);
return template({ defPairs: highlightPairs.concat(sourcePairs) });
}
};
return Source;
};
});

View file

@ -1,5 +1,5 @@
define(function (require) {
return function UrlFormatProvider(Private) {
return function UrlFormatProvider(Private, highlightFilter) {
var _ = require('lodash');
var FieldFormat = Private(require('components/index_patterns/_field_format/FieldFormat'));
@ -44,20 +44,25 @@ define(function (require) {
];
Url.prototype._convert = {
html: function (rawValue) {
text: function (value) {
var template = this.param('template');
return !template ? value : this._compileTemplate(template)(value);
},
html: function (rawValue, field, hit) {
var url = this.convert(rawValue, 'text');
var value = _.escape(rawValue);
switch (this.param('type')) {
case 'img': return '<img src="' + url + '" alt="' + value + '">';
case 'img': return '<img src="' + url + '" alt="' + value + '" title="' + value + '">';
default:
return '<a href="' + url + '" target="_blank">' + url + '</a>';
}
},
var urlDisplay = url;
if (hit && hit.highlight && hit.highlight[field.name]) {
urlDisplay = highlightFilter(url, hit.highlight[field.name]);
}
text: function (value) {
var template = this.param('template');
return !template ? value : this._compileTemplate(template)(value);
return '<a href="' + url + '" target="_blank">' + urlDisplay + '</a>';
}
}
};

View file

@ -1,4 +1,4 @@
define(function (require) {
define(function () {
function AggConfigResult(aggConfig, parent, value, key) {
this.$parent = parent;
this.key = key;
@ -32,8 +32,8 @@ define(function (require) {
return this.aggConfig.createFilter(this.key);
};
AggConfigResult.prototype.toString = function () {
return this.aggConfig.fieldFormatter()(this.value);
AggConfigResult.prototype.toString = function (contentType) {
return this.aggConfig.fieldFormatter(contentType)(this.value);
};
AggConfigResult.prototype.valueOf = function () {

View file

@ -23,7 +23,7 @@ define(function (require) {
$cell.scope = $scope.$new();
$cell.addClass('cell-hover');
$cell.attr('ng-click', 'clickHandler()');
$cell.scope.clickHandler = function (negate) {
$cell.scope.clickHandler = function () {
clickHandler({ point: { aggConfigResult: aggConfigResult } });
};
return $compile($cell)($cell.scope);
@ -33,7 +33,7 @@ define(function (require) {
if (contents.type === 'bucket' && contents.aggConfig.field() && contents.aggConfig.field().filterable) {
$cell = createAggConfigResultCell(contents);
}
contents = contents.toString();
contents = contents.toString('html');
}
if (_.isObject(contents)) {

View file

@ -15,7 +15,8 @@ define(function (require) {
'number',
'percent',
'string',
'url'
'url',
'_source'
];