mirror of https://github.com/MISP/MISP
new: [eventReport] Added context replacements and suggestions
parent
7ad3a2d901
commit
76c4869514
|
@ -224,12 +224,14 @@ class EventReportsController extends AppController
|
|||
throw new MethodNotAllowedException(__('This function can only be reached via AJAX.'));
|
||||
} else {
|
||||
$report = $this->EventReport->fetchIfAuthorized($this->Auth->user(), $reportId, 'view', $throwErrors=true, $full=false);
|
||||
$results = $this->EventReport->getComplexTypeToolResultWithReplacementsFromReport($this->Auth->user(), $report);
|
||||
$dataResults = $this->EventReport->getComplexTypeToolResultWithReplacementsFromReport($this->Auth->user(), $report);
|
||||
$contextResults = $this->EventReport->extractWithReplacementsFromReport($this->Auth->user(), $report);
|
||||
$typeToCategoryMapping = $this->EventReport->Event->Attribute->typeToCategoryMapping();
|
||||
$data = [
|
||||
'complexTypeToolResult' => $results['complexTypeToolResult'],
|
||||
'complexTypeToolResult' => $dataResults['complexTypeToolResult'],
|
||||
'typeToCategoryMapping' => $typeToCategoryMapping,
|
||||
'replacementValues' => $results['replacementResult']['replacedValues']
|
||||
'replacementValues' => $dataResults['replacementResult']['replacedValues'],
|
||||
'replacementContext' => $contextResults['replacedContext']
|
||||
];
|
||||
return $this->RestResponse->viewData($data, $this->response->type());
|
||||
}
|
||||
|
|
|
@ -674,4 +674,161 @@ class EventReport extends AppModel
|
|||
'replacementResult' => $replacementResult,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* extractWithReplacementsFromReport Extract context information from report with special care for ATT&CK
|
||||
*
|
||||
* @param array $user
|
||||
* @param array $report
|
||||
* @return array
|
||||
*/
|
||||
public function extractWithReplacementsFromReport(array $user, array $report, $options = [])
|
||||
{
|
||||
$baseOptions = [
|
||||
'replace' => false,
|
||||
'tags' => true,
|
||||
'synonyms' => true,
|
||||
'synonyms_min_characters' => 4,
|
||||
'prune_deprecated' => true,
|
||||
'attack' => true,
|
||||
];
|
||||
$options = array_merge($baseOptions, $options);
|
||||
$originalContent = $report['EventReport']['content'];
|
||||
$replacedContext = [];
|
||||
$content = $originalContent;
|
||||
$this->GalaxyCluster = ClassRegistry::init('GalaxyCluster');
|
||||
$mitreAttackGalaxyId = $this->GalaxyCluster->Galaxy->getMitreAttackGalaxyId();
|
||||
$clusterContain = ['Tag'];
|
||||
|
||||
if ($options['prune_deprecated']) {
|
||||
$clusterContain['Galaxy'] = ['conditions' => ['Galaxy.namespace !=' => 'deprecated']];
|
||||
}
|
||||
if ($options['synonyms']) {
|
||||
$clusterContain['GalaxyElement'] = ['conditions' => ['GalaxyElement.key' => 'synonyms']];
|
||||
}
|
||||
$clusterConditions = [];
|
||||
if ($options['attack']) {
|
||||
$clusterConditions = ['GalaxyCluster.galaxy_id !=' => $mitreAttackGalaxyId];
|
||||
}
|
||||
$clusters = $this->GalaxyCluster->find('all', [
|
||||
'conditions' => $clusterConditions,
|
||||
'contain' => $clusterContain
|
||||
]);
|
||||
|
||||
if ($options['tags']) {
|
||||
$this->Tag = ClassRegistry::init('Tag');
|
||||
$tags = $this->Tag->fetchUsableTags($user);
|
||||
foreach ($tags as $i => $tag) {
|
||||
$tagName = $tag['Tag']['name'];
|
||||
$textToInject = sprintf('@[tag](%s)', $tagName);
|
||||
$count = 0;
|
||||
if ($options['replace']) {
|
||||
$count = 0;
|
||||
$content = str_replace($tagName, $textToInject, $content, $count);
|
||||
} else {
|
||||
$count = strpos($originalContent, $tagName) === false ? -1 : 1;
|
||||
}
|
||||
if ($count > 0) {
|
||||
$replacedContext[$tagName][$tagName] = $tag['Tag'];
|
||||
} else {
|
||||
$tagNameUpper = strtoupper($tagName);
|
||||
if ($options['replace']) {
|
||||
$count = 0;
|
||||
$content = str_replace($tagNameUpper, $textToInject, $content, $count);
|
||||
} else {
|
||||
$count = strpos($originalContent, $tagNameUpper) === false ? -1 : 1;
|
||||
}
|
||||
if ($count > 0) {
|
||||
$replacedContext[$tagNameUpper][$tagName] = $tag['Tag'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($clusters as $i => $cluster) {
|
||||
$cluster['GalaxyCluster']['colour'] = '#0088cc';
|
||||
$tagName = $cluster['GalaxyCluster']['tag_name'];
|
||||
$textToInject = sprintf('@[tag](%s)', $tagName);
|
||||
if ($options['replace']) {
|
||||
$count = 0;
|
||||
$content = str_replace($tagName, $textToInject, $content, $count);
|
||||
} else {
|
||||
$count = strpos($originalContent, $tagName) === false ? -1 : 1;
|
||||
}
|
||||
if ($count > 0) {
|
||||
$replacedContext[$tagName][$tagName] = $cluster['GalaxyCluster'];
|
||||
}
|
||||
if ($options['replace']) {
|
||||
$count = 0;
|
||||
$content = str_replace($cluster['GalaxyCluster']['value'], $textToInject, $content, $count);
|
||||
} else {
|
||||
$count = strpos($originalContent, $cluster['GalaxyCluster']['value']) === false ? -1 : 1;
|
||||
}
|
||||
if ($count > 0) {
|
||||
$replacedContext[$cluster['GalaxyCluster']['value']][$tagName] = $cluster['GalaxyCluster'];
|
||||
}
|
||||
if ($options['synonyms']) {
|
||||
foreach ($cluster['GalaxyElement'] as $j => $element) {
|
||||
if (strlen($element['value']) >= $options['synonyms_min_characters']) {
|
||||
if ($options['replace']) {
|
||||
$count = 0;
|
||||
$content = str_replace($element['value'], $textToInject, $content, $count);
|
||||
} else {
|
||||
$count = strpos($originalContent, $element['value']) === false ? -1 : 1;
|
||||
}
|
||||
if ($count > 0) {
|
||||
$replacedContext[$element['value']][$tagName] = $cluster['GalaxyCluster'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($options['attack']) {
|
||||
unset($clusterContain['Galaxy']);
|
||||
$attackClusters = $this->GalaxyCluster->find('all', [
|
||||
'conditions' => ['GalaxyCluster.galaxy_id' => $mitreAttackGalaxyId],
|
||||
'contain' => $clusterContain
|
||||
]);
|
||||
foreach ($attackClusters as $i => $cluster) {
|
||||
$cluster['GalaxyCluster']['colour'] = '#0088cc';
|
||||
$tagName = $cluster['GalaxyCluster']['tag_name'];
|
||||
$textToInject = sprintf('@[tag](%s)', $tagName);
|
||||
if ($options['replace']) {
|
||||
$count = 0;
|
||||
$content = str_replace($cluster['GalaxyCluster']['value'], $textToInject, $content, $count);
|
||||
} else {
|
||||
$count = strpos($originalContent, $cluster['GalaxyCluster']['value']) === false ? -1 : 1;
|
||||
}
|
||||
if ($count > 0) {
|
||||
$replacedContext[$cluster['GalaxyCluster']['value']][$tagName] = $cluster['GalaxyCluster'];
|
||||
} else {
|
||||
$clusterParts = explode(' - ', $cluster['GalaxyCluster']['value']);
|
||||
if ($options['replace']) {
|
||||
$count = 0;
|
||||
$content = str_replace($clusterParts[0], $textToInject, $content, $count);
|
||||
} else {
|
||||
$count = strpos($originalContent, $clusterParts[0]) === false ? -1 : 1;
|
||||
}
|
||||
if ($count > 0) {
|
||||
$replacedContext[$clusterParts[0]][$tagName] = $cluster['GalaxyCluster'];
|
||||
} else {
|
||||
if ($options['replace']) {
|
||||
$count = 0;
|
||||
$content = str_replace($clusterParts[1], $textToInject, $content, $count);
|
||||
} else {
|
||||
$count = strpos($originalContent, $clusterParts[1]) === false ? -1 : 1;
|
||||
}
|
||||
if ($count > 0) {
|
||||
$replacedContext[$clusterParts[1]][$tagName] = $cluster['GalaxyCluster'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
'replacedContext' => $replacedContext,
|
||||
'contentWithReplacements' => $content
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,9 +48,12 @@ var contentBeforeSuggestions
|
|||
var typeToCategoryMapping
|
||||
var entitiesFromComplexTool
|
||||
var $suggestionContainer
|
||||
var unreferenceValues;
|
||||
var unreferencedElements = {
|
||||
values: null,
|
||||
context: null
|
||||
};
|
||||
var suggestions = {}
|
||||
var pickedSuggestion = { tableID: null, tr: null, entity: null, index: null }
|
||||
var pickedSuggestion = { tableID: null, tr: null, entity: null, index: null, isContext: null }
|
||||
|
||||
/**
|
||||
_____ _ __ __ _
|
||||
|
@ -676,7 +679,9 @@ function attachRemoteMISPElements() {
|
|||
clearTimeout(tagTimers[cacheKey]);
|
||||
if (cache_tag[cacheKey] === undefined) {
|
||||
tagTimers[cacheKey] = setTimeout(function() {
|
||||
attachTagInfo($div, eventID, elementID)
|
||||
fetchTagInfo(eventID, elementID, function() {
|
||||
attachTagInfo($div, eventID, elementID, true)
|
||||
})
|
||||
}, slowDebounceDelay);
|
||||
} else {
|
||||
$div.html(cache_tag[cacheKey])
|
||||
|
@ -728,10 +733,20 @@ function attachGalaxyMatrix($elem, eventid, elementID) {
|
|||
})
|
||||
}
|
||||
|
||||
function attachTagInfo($elem, eventid, elementID) {
|
||||
function attachTagInfo($elem, eventid, elementID, all) {
|
||||
var cacheKey = eventid + '-' + elementID
|
||||
$elem.html(cache_tag[cacheKey])
|
||||
if (all === true) {
|
||||
$('.embeddedTag[data-scope="tag"]').filter(function() {
|
||||
return $(this).data('eventid') == eventid && $(this).data('elementid') == elementID
|
||||
}).html(cache_tag[cacheKey])
|
||||
}
|
||||
}
|
||||
|
||||
function fetchTagInfo(eventid, tagName, callback) {
|
||||
$.ajax({
|
||||
data: {
|
||||
"tag": elementID,
|
||||
"tag": tagName,
|
||||
},
|
||||
success:function(data, textStatus) {
|
||||
var $tag
|
||||
|
@ -739,31 +754,33 @@ function attachTagInfo($elem, eventid, elementID) {
|
|||
var tagData;
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var tag = data[i];
|
||||
if (tag.Tag.name == elementID) {
|
||||
if (tag.Tag.name == tagName) {
|
||||
tagData = data[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (tagData === undefined) {
|
||||
tagData = {}
|
||||
$tag = constructTagHtml(elementID, '#ffffff', {'border': '1px solid #000'})
|
||||
$tag = constructTagHtml(tagName, '#ffffff', {'border': '1px solid #000'})
|
||||
} else {
|
||||
$tag = getTagReprensentation(tagData)
|
||||
proxyMISPElements['tag'][elementID] = tagData
|
||||
proxyMISPElements['tag'][tagName] = tagData
|
||||
}
|
||||
$elem.empty().append($tag)
|
||||
var cacheKey = eventid + '-' + elementID
|
||||
var cacheKey = eventid + '-' + tagName
|
||||
cache_tag[cacheKey] = $tag[0].outerHTML;
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
var templateVariables = sanitizeObject({
|
||||
scope: 'Error while fetching tag',
|
||||
id: elementID
|
||||
id: tagName
|
||||
})
|
||||
var placeholder = dotTemplateInvalid(templateVariables)
|
||||
$elem.empty()
|
||||
.css({'text-align': 'center'})
|
||||
.append($(placeholder))
|
||||
cache_tag[cacheKey] = placeholder;
|
||||
},
|
||||
complete: function() {
|
||||
if (callback !== undefined) {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
type:"post",
|
||||
url: baseurl + "/tags/search/0/1/0"
|
||||
|
@ -937,18 +954,19 @@ function automaticEntitiesExtraction() {
|
|||
|
||||
function manualEntitiesExtraction() {
|
||||
contentBeforeSuggestions = getEditorData()
|
||||
pickedSuggestion = { tableID: null, tr: null, entity: null, index: null }
|
||||
pickedSuggestion = { tableID: null, tr: null, entity: null, index: null, isContext: null }
|
||||
extractFromReport(function(data) {
|
||||
typeToCategoryMapping = data.typeToCategoryMapping
|
||||
prepareSuggestionInterface(data.complexTypeToolResult, data.replacementValues)
|
||||
prepareSuggestionInterface(data.complexTypeToolResult, data.replacementValues, data.replacementContext)
|
||||
toggleSuggestionInterface(true)
|
||||
})
|
||||
}
|
||||
|
||||
function prepareSuggestionInterface(complexTypeToolResult, replacementValues) {
|
||||
function prepareSuggestionInterface(complexTypeToolResult, replacementValues, replacementContext) {
|
||||
toggleMarkdownEditorLoading(true, 'Processing document')
|
||||
entitiesFromComplexTool = complexTypeToolResult
|
||||
searchForUnreferenceValues(replacementValues)
|
||||
searchForUnreferencedValues(replacementValues)
|
||||
searchForUnreferencedContext(replacementContext)
|
||||
entitiesFromComplexTool = injectNumberOfOccurencesInReport(entitiesFromComplexTool)
|
||||
setupSuggestionMarkdownListeners()
|
||||
constructSuggestionTables(entitiesFromComplexTool)
|
||||
|
@ -1050,8 +1068,8 @@ function toggleSuggestionInterface(enabled) {
|
|||
}
|
||||
}
|
||||
|
||||
function searchForUnreferenceValues(replacementValues) {
|
||||
unreferenceValues = {}
|
||||
function searchForUnreferencedValues(replacementValues) {
|
||||
unreferencedElements.values = {}
|
||||
var content = getEditorData()
|
||||
Object.keys(replacementValues).forEach(function(attributeValue) {
|
||||
var replacementValue = replacementValues[attributeValue]
|
||||
|
@ -1063,17 +1081,29 @@ function searchForUnreferenceValues(replacementValues) {
|
|||
attributes.push(proxyMISPElements['attribute'][uuid])
|
||||
}
|
||||
});
|
||||
unreferenceValues[replacementValue.valueInReport] = {
|
||||
unreferencedElements.values[replacementValue.valueInReport] = {
|
||||
attributes: attributes,
|
||||
indices: indices
|
||||
}
|
||||
if (attributeValue != replacementValue.valueInReport) {
|
||||
unreferenceValues[replacementValue.valueInReport].importRegexMatch = attributeValue
|
||||
unreferencedElements.values[replacementValue.valueInReport].importRegexMatch = attributeValue
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function searchForUnreferencedContext(replacementContext) {
|
||||
unreferencedElements.context = {}
|
||||
var content = getEditorData()
|
||||
Object.keys(replacementContext).forEach(function(rawText) {
|
||||
var indices = getAllIndicesOf(content, rawText, true, true)
|
||||
if (indices.length > 0) {
|
||||
replacementContext[rawText].indices = indices
|
||||
}
|
||||
})
|
||||
unreferencedElements.context = replacementContext;
|
||||
}
|
||||
|
||||
function pickSuggestionColumn(index, tableID, force) {
|
||||
tableID = tableID === undefined ? 'replacementTable' : tableID
|
||||
force = force === undefined ? false : force;
|
||||
|
@ -1101,6 +1131,14 @@ function pickSuggestionColumn(index, tableID, force) {
|
|||
pickedSuggestion['entity']['importRegexMatch'] = proxyMISPElements['attribute'][uuid].importRegexValue
|
||||
}
|
||||
highlightPickedReplacementInReport()
|
||||
} else if (tableID == 'contextReplacementTable') {
|
||||
pickedSuggestion['entity'] = {
|
||||
value: $tr.data('contextValue'),
|
||||
picked_type: 'tag',
|
||||
replacement: $tr.find('select.context-replacement').val()
|
||||
}
|
||||
pickedSuggestion['isContext'] = true
|
||||
highlightPickedReplacementInReport()
|
||||
} else {
|
||||
pickedSuggestion['entity'] = $tr.data('entity')
|
||||
pickedSuggestion['entity']['picked_type'] = $tr.find('select.type').val()
|
||||
|
@ -1122,7 +1160,11 @@ function getContentWithCheckedElements(isReplacement) {
|
|||
nextIndex = suggestion.startIndex.index
|
||||
if (suggestion.checked) {
|
||||
if (isReplacement) {
|
||||
contentWithPickedSuggestions += '@[attribute](' + suggestion.complexTypeToolResult.replacement + ')'
|
||||
if (suggestion.isContext === true) {
|
||||
contentWithPickedSuggestions += '@[attribute](' + suggestion.complexTypeToolResult.replacement + ')'
|
||||
} else {
|
||||
contentWithPickedSuggestions += '@[tag](' + suggestion.complexTypeToolResult.replacement + ')'
|
||||
}
|
||||
} else {
|
||||
contentWithPickedSuggestions += content.substr(nextIndex, suggestionLength)
|
||||
}
|
||||
|
@ -1190,7 +1232,7 @@ function submitExtractionSuggestion() {
|
|||
fetchProxyMISPElements(function() {
|
||||
setEditorData(originalRaw)
|
||||
contentBeforeSuggestions = originalRaw
|
||||
pickedSuggestion = { tableID: null, tr: null, entity: null, index: null }
|
||||
pickedSuggestion = { tableID: null, tr: null, entity: null, index: null, isContext: null }
|
||||
pickSuggestionColumn(-1)
|
||||
prepareSuggestionInterface(complexTypeToolResult, replacementValues)
|
||||
})
|
||||
|
@ -1558,24 +1600,34 @@ function buildBodyForMISPElement(data) {
|
|||
|
||||
function constructSuggestionTables(entities) {
|
||||
var $extractionTable = constructExtractionTable(entities)
|
||||
var $replacementTable = constructReplacementTable(unreferenceValues)
|
||||
var $replacementTable = constructReplacementTable(unreferencedElements.values)
|
||||
var $contextReplacementTable = constructContextReplacementTable(unreferencedElements.context)
|
||||
var $collapsibleControl = $('<ul class="nav nav-tabs" id="suggestionTableTabs" />').append(
|
||||
$('<li/>').append(
|
||||
$('<a/>').attr('href', '#replacement-table').append(
|
||||
$('<span/>').text('Replacement table'),
|
||||
$('<span class="badge badge-important"/>').css({'padding': '2px 6px', 'margin-left': '3px'}).text(Object.keys(unreferenceValues).length)
|
||||
).attr('title', 'Replace raw text into attribute reference')
|
||||
$('<i/>').addClass('fas fa-cube'),
|
||||
$('<span/>').text(' Data Replacement'),
|
||||
$('<span class="badge badge-important"/>').css({'padding': '2px 6px', 'margin-left': '3px'}).text(Object.keys(unreferencedElements.values).length)
|
||||
).attr('title', 'Replace raw text into attribute reference').css('padding', '8px 8px')
|
||||
),
|
||||
$('<li/>').append(
|
||||
$('<a/>').attr('href', '#replacement-context-table').append(
|
||||
$('<i/>').addClass('fas fa-atlas'),
|
||||
$('<span/>').text(' Context replacement'),
|
||||
$('<span class="badge badge-important"/>').css({'padding': '2px 6px', 'margin-left': '3px'}).text(Object.keys(unreferencedElements.context).length)
|
||||
).attr('title', 'Replace raw text into context reference').css('padding', '8px 8px')
|
||||
),
|
||||
$('<li/>').append(
|
||||
$('<a/>').attr('href', '#extraction-table').append(
|
||||
$('<span/>').text('Extraction table'),
|
||||
$('<span/>').text('Data extraction'),
|
||||
$('<span class="badge badge-warning"/>').css({'padding': '2px 6px', 'margin-left': '3px'}).text(entities.length)
|
||||
).attr('title', 'Convert raw text into attribute and reference it')
|
||||
)
|
||||
)
|
||||
var $collapsibleContent = $('<div class="tab-content"/>').append(
|
||||
$('<div class="tab-pane" id="extraction-table" />').append($extractionTable),
|
||||
$('<div class="tab-pane" id="replacement-table" />').append($replacementTable),
|
||||
$('<div class="tab-pane" id="replacement-context-table" />').append($contextReplacementTable),
|
||||
$('<div class="tab-pane" id="extraction-table" />').append($extractionTable),
|
||||
$('<div class="tab-pane active" />').text('Pick a table to view available actions').css({
|
||||
'text-align': 'center',
|
||||
'opacity': '80%'
|
||||
|
@ -1588,7 +1640,7 @@ function constructSuggestionTables(entities) {
|
|||
'margin-right': '3px'
|
||||
}).append(
|
||||
$('<i/>').addClass('fas fa-expand-arrows-alt').css('margin-right', '5px'),
|
||||
$('<span/>').text('Toggle fullscreen')
|
||||
$('<span/>').text('Fullscreen')
|
||||
).click(toggleFullscreenMode),
|
||||
$collapsibleControl
|
||||
)
|
||||
|
@ -1679,7 +1731,7 @@ function constructExtractionTable(entities) {
|
|||
return $table
|
||||
}
|
||||
|
||||
function constructReplacementTable(unreferenceValues) {
|
||||
function constructReplacementTable(unreferencedValues) {
|
||||
var $table = $('<table/>').attr('id', 'replacementTable').addClass('table table-striped table-condensed').css('flex-grow', '1')
|
||||
var $thead = $('<thead/>').append($('<tr/>').append(
|
||||
$('<th/>').text('Value').css('min-width', '10rem'),
|
||||
|
@ -1688,9 +1740,9 @@ function constructReplacementTable(unreferenceValues) {
|
|||
$('<th/>').text('Action')
|
||||
))
|
||||
var $tbody = $('<tbody/>')
|
||||
Object.keys(unreferenceValues).forEach(function(value, index) {
|
||||
Object.keys(unreferencedValues).forEach(function(value, index) {
|
||||
var $selectContainer, $select, $option
|
||||
var unreferenceValue = unreferenceValues[value]
|
||||
var unreferenceValue = unreferencedValues[value]
|
||||
if(unreferenceValue.attributes.length > 1) {
|
||||
$select = $('<select/>').addClass('attribute-replacement').css({
|
||||
'width': 'auto',
|
||||
|
@ -1802,6 +1854,96 @@ function constructReplacementTable(unreferenceValues) {
|
|||
return $table
|
||||
}
|
||||
|
||||
function constructContextReplacementTable(unreferencedContext) {
|
||||
var $table = $('<table/>').attr('id', 'contextReplacementTable').addClass('table table-striped table-condensed').css('flex-grow', '1')
|
||||
var $thead = $('<thead/>').append($('<tr/>').append(
|
||||
$('<th/>').text('Value').css('min-width', '10rem'),
|
||||
$('<th/>').text('Existing context'),
|
||||
$('<th/>').text('Occurrences'),
|
||||
$('<th/>').text('Action')
|
||||
))
|
||||
var $tbody = $('<tbody/>')
|
||||
Object.keys(unreferencedContext).forEach(function(rawText, index) {
|
||||
var contexts = unreferencedContext[rawText]
|
||||
var $selectContainer, $select, $option
|
||||
if(Object.keys(contexts).length > 2) {
|
||||
$select = $('<select/>').addClass('context-replacement').css({
|
||||
'width': 'auto',
|
||||
'max-width': '300px'
|
||||
}).change(function() {
|
||||
if ($('#viewer-container .popover.in').length > 0) {
|
||||
$(this).parent().find('.helpicon').popover('show')
|
||||
}
|
||||
pickSuggestionColumn(index, 'contextReplacementTable', true)
|
||||
})
|
||||
Object.keys(contexts).forEach(function(tagName, index) {
|
||||
if (tagName == 'indices') {
|
||||
return
|
||||
}
|
||||
var context = contexts[tagName]
|
||||
var contextToRender = jQuery.extend(true, { }, context)
|
||||
contextToRender.value = tagName
|
||||
contextToRender.name = tagName
|
||||
$option = $('<option/>').val(tagName).append(renderHintElement('tag', contextToRender))
|
||||
$option = $('<option/>').val(tagName).text(tagName)
|
||||
$select.append($option)
|
||||
})
|
||||
$selectContainer = $('<span/>').css({
|
||||
'white-space': 'nowrap'
|
||||
}).append($select)
|
||||
} else {
|
||||
var context = jQuery.extend(true, { }, contexts)
|
||||
delete context.indices
|
||||
var tagName = Object.keys(context)[0]
|
||||
context = context[tagName]
|
||||
var contextToRender = jQuery.extend(true, { }, context)
|
||||
contextToRender.value = tagName
|
||||
contextToRender.name = tagName
|
||||
$selectContainer = $('<span/>')
|
||||
.append(renderHintElement('tag', contextToRender))
|
||||
.append($('<select/>').addClass('context-replacement hidden').append($('<option/>').text(tagName).val(tagName)))
|
||||
}
|
||||
|
||||
var $tr = $('<tr/>').attr('data-entityindex', index)
|
||||
.data('contextValue', rawText)
|
||||
.addClass('useCursorPointer')
|
||||
.append(
|
||||
$('<td/>').addClass('bold blue').text(rawText).css('word-wrap', 'anywhere'),
|
||||
$('<td/>').append($selectContainer),
|
||||
$('<td/>').append($('<span/>').addClass('input-prepend input-append').append(
|
||||
$('<button type="button"/>').attr('title', 'Jump to previous occurrence').addClass('add-on btn btn-mini').css('height', 'auto').append(
|
||||
$('<a/>').addClass('fas fa-caret-left')
|
||||
).click(function(e) {
|
||||
e.stopPropagation()
|
||||
jumpToPreviousOccurrence()
|
||||
}),
|
||||
$('<input type="text" disabled />').css('max-width', '2em').val(contexts.indices.length),
|
||||
$('<button type="button"/>').attr('title', 'Jump to next occurrence').addClass('add-on btn btn-mini').css('height', 'auto').append(
|
||||
$('<a/>').addClass('fas fa-caret-right')
|
||||
).click(function(e) {
|
||||
e.stopPropagation()
|
||||
jumpToNextOccurrence()
|
||||
}),
|
||||
)),
|
||||
$('<td/>').append(
|
||||
$('<span/>').css('white-space', 'nowrap').append(
|
||||
$('<button type="button"/>').addClass('btn')
|
||||
.prop('disabled', true)
|
||||
.text('Replace & Save')
|
||||
.click(submitReplacement)
|
||||
)
|
||||
)
|
||||
)
|
||||
$tr.click(function() {
|
||||
var index = $(this).data('entityindex')
|
||||
pickSuggestionColumn(index, 'contextReplacementTable')
|
||||
})
|
||||
$tbody.append($tr)
|
||||
})
|
||||
$table.append($thead, $tbody)
|
||||
return $table
|
||||
}
|
||||
|
||||
function addCloseSuggestionButtonToToolbar() {
|
||||
var $toolbarMode = $mardownViewerToolbar.find('.btn-group:first')
|
||||
if ($toolbarMode.find('#suggestionCloseButton').length == 0) {
|
||||
|
|
Loading…
Reference in New Issue