Commit 67469995 authored by Randall Leeds's avatar Randall Leeds

Refactor page search

Refactor the page search rendering algorithm for fewer scope
variables, simpler template expressions, camel case, support
for holes in threads, less looping, conciseness and readability.
parent 5736d80b
...@@ -342,7 +342,7 @@ class AnnotationViewer ...@@ -342,7 +342,7 @@ class AnnotationViewer
# to annotations loaded into the stream. # to annotations loaded into the stream.
$scope.activate = angular.noop $scope.activate = angular.noop
$scope.shouldShowAnnotation = (id) -> true $scope.shouldShowThread = -> true
$scope.$watch 'updater', (updater) -> $scope.$watch 'updater', (updater) ->
if updater? if updater?
...@@ -359,18 +359,16 @@ class AnnotationViewer ...@@ -359,18 +359,16 @@ class AnnotationViewer
class Viewer class Viewer
this.$inject = [ this.$inject = [
'$filter', '$routeParams', '$sce', '$scope', '$filter', '$routeParams', '$sce', '$scope',
'annotator', 'viewFilter' 'annotator', 'searchfilter', 'viewFilter'
] ]
constructor: ( constructor: (
$filter, $routeParams, $sce, $scope, $filter, $routeParams, $sce, $scope,
annotator, viewFilter annotator, searchfilter, viewFilter
) -> ) ->
# Tells the view that these annotations are embedded into the owner doc # Tells the view that these annotations are embedded into the owner doc
$scope.isEmbedded = true $scope.isEmbedded = true
$scope.isStream = true $scope.isStream = true
{providers} = annotator
$scope.activate = (annotation) -> $scope.activate = (annotation) ->
if angular.isObject annotation if angular.isObject annotation
highlights = [annotation.$$tag] highlights = [annotation.$$tag]
...@@ -378,235 +376,109 @@ class Viewer ...@@ -378,235 +376,109 @@ class Viewer
highlights = [$scope.ongoingEdit.message.$$tag] highlights = [$scope.ongoingEdit.message.$$tag]
else else
highlights = [] highlights = []
for p in providers for p in annotator.providers
p.channel.notify p.channel.notify
method: 'setActiveHighlights' method: 'setActiveHighlights'
params: highlights params: highlights
$scope.shouldShowAnnotation = (id) -> $scope.shouldShowThread = (container) ->
if $routeParams.q
shownAnnotations = $scope.ann_info.shown
(shownAnnotations[id] or $scope.render_order[id])
else
selectedAnnotations = $scope.selectedAnnotations
(!selectedAnnotations or selectedAnnotations?[id])
$scope.highlighter = '<span class="search-hl-active">$&</span>'
$scope.filter_orderBy = $filter('orderBy')
$scope.render_order = {}
$scope.render_pos = {}
$scope.ann_info =
shown : {}
show_quote: {}
more_top : {}
more_bottom : {}
more_top_num : {}
more_bottom_num: {}
buildRenderOrder = (threadid, threads) =>
unless threads?.length
return
sorted = $scope.filter_orderBy threads, $scope.sortThread, true
for thread in sorted
$scope.render_pos[thread.message.id] = $scope.render_order[threadid].length
$scope.render_order[threadid].push thread.message.id
buildRenderOrder(threadid, thread.children)
setMoreTop = (threadid, annotation) =>
unless annotation.id in $scope.matches
return false
result = false
pos = $scope.render_pos[annotation.id]
if pos > 0
prev = $scope.render_order[threadid][pos-1]
unless prev is threadid or prev in $scope.matches
result = true
result
setMoreBottom = (threadid, annotation) =>
unless annotation.id in $scope.matches
return false
result = false
pos = $scope.render_pos[annotation.id]
if pos < $scope.render_order[threadid].length-1
next = $scope.render_order[threadid][pos+1]
unless next in $scope.matches
result = true
result
refresh = =>
if $routeParams.q if $routeParams.q
delete $scope.selectedAnnotations container.shown
else if $scope.selectedAnnotations?
$scope.selectedAnnotations[container.message?.id]
else else
delete $scope.matches true
return
$scope.showMoreTop = (container) ->
rendered = container.renderOrder
pos = rendered.indexOf(container)
container.moreTop = 0
while --pos >= 0
prev = rendered[pos]
prev.moreBottom = 0
break if prev.shown
prev.moreTop = 0
prev.shown = true
$scope.showMoreBottom = (container) ->
$event?.stopPropagation()
rendered = container.renderOrder
pos = rendered.indexOf(container)
container.moreBottom = 0
while ++pos < rendered.length
next = rendered[pos]
next.moreTop = 0
break if next.shown
next.moreBottom = 0
next.shown = true
matches = null
orderBy = $filter('orderBy')
render = (thread, last, current) ->
current.renderOrder = thread.renderOrder
current.shown = matches[current.message?.id]?
order = thread.renderOrder.length
thread.renderOrder.push current
if current.shown
current.renderOrder[last].moreBottom = 0
current.moreTop = order - last - 1
current.moreBottom = 0
last = order
else if last != order
current.moreTop = 0
current.moreBottom = 0
current.renderOrder[last].moreBottom++
sorted = orderBy (current.children or []), 'message.updated'
sorted.reduce(angular.bind(null, render, thread), last)
refresh = ->
annotations = $scope.threading.root.flattenChildren() or []
matches = {}
rendered = {}
query = $routeParams.q query = $routeParams.q
annotations = $scope.threading.root.flattenChildren()
[$scope.matches, $scope.filters] = viewFilter.filter annotations, query if query
# Create the regexps for highlighting the matches inside the annotations' bodies # Get the set of matches
$scope.text_tokens = $scope.filters.text.terms.slice() filters = searchfilter.generateFacetedFilter query
$scope.text_regexp = [] for match in viewFilter.filter annotations, filters
$scope.quote_tokens = $scope.filters.quote.terms.slice() matches[match] = true
$scope.quote_regexp = []
# Find the top posts and render them
# Highligh any matches for annotation in annotations
for term in $scope.filters.any.terms {id, references} = annotation
$scope.text_tokens.push term if typeof(references) == 'string' then references = [references]
$scope.quote_tokens.push term if matches[id]?
top = references?[0] or id
# Saving the regexps and higlighter to the annotator for highlighttext regeneration continue if rendered[top]
for token in $scope.text_tokens container = $scope.threading.getContainer(top)
regexp = new RegExp(token,"ig") container.renderOrder = []
$scope.text_regexp.push regexp render container, 0, container
container.shown = true
for token in $scope.quote_tokens rendered[id] = true
regexp = new RegExp(token,"ig")
$scope.quote_regexp.push regexp
annotator.text_regexp = $scope.text_regexp
annotator.quote_regexp = $scope.quote_regexp
annotator.highlighter = $scope.highlighter
threads = []
roots = {}
$scope.render_order = {}
# Choose the root annotations to work with
for id, thread of $scope.threading.idTable when thread.message?
annotation = thread.message
annotation_root = if annotation.references? then annotation.references[0] else annotation.id
# Already handled thread
if roots[annotation_root]? then continue
root_annotation = ($scope.threading.getContainer annotation_root).message
unless root_annotation in annotations then continue
if annotation.id in $scope.matches
# We have a winner, let's put its root annotation into our list and build the rendering
root_thread = $scope.threading.getContainer annotation_root
threads.push root_thread
$scope.render_order[annotation_root] = []
buildRenderOrder(annotation_root, [root_thread])
roots[annotation_root] = true
# Re-construct exact order the annotation threads will be shown
# Fill search related data before display
# - add highlights
# - populate the top/bottom show more links
# - decide that by default the annotation is shown or hidden
# - Open detail mode for quote hits
for thread in threads
thread.message.highlightText = thread.message.text
if thread.message.id in $scope.matches
$scope.ann_info.shown[thread.message.id] = true
if thread.message.text?
for regexp in $scope.text_regexp
thread.message.highlightText = thread.message.highlightText.replace regexp, $scope.highlighter
else
$scope.ann_info.shown[thread.message.id] = false
$scope.ann_info.more_top[thread.message.id] = setMoreTop(thread.message.id, thread.message)
$scope.ann_info.more_bottom[thread.message.id] = setMoreBottom(thread.message.id, thread.message)
if $scope.quote_tokens?.length > 0
$scope.ann_info.show_quote[thread.message.id] = true
for target in thread.message.target
target.highlightQuote = target.quote
for regexp in $scope.quote_regexp
target.highlightQuote = target.highlightQuote.replace regexp, $scope.highlighter
target.highlightQuote = $sce.trustAsHtml target.highlightQuote
if target.diffHTML?
target.trustedDiffHTML = $sce.trustAsHtml target.diffHTML
target.showDiff = not target.diffCaseOnly
else
delete target.trustedDiffHTML
target.showDiff = false
else
for target in thread.message.target
target.highlightQuote = target.quote
$scope.ann_info.show_quote[thread.message.id] = false
children = thread.flattenChildren()
if children?
for child in children
child.highlightText = child.text
if child.id in $scope.matches
$scope.ann_info.shown[child.id] = true
for regexp in $scope.text_regexp
child.highlightText = child.highlightText.replace regexp, $scope.highlighter
else
$scope.ann_info.shown[child.id] = false
$scope.ann_info.more_top[child.id] = setMoreTop(thread.message.id, child)
$scope.ann_info.more_bottom[child.id] = setMoreBottom(thread.message.id, child)
$scope.ann_info.show_quote[child.id] = false
# Calculate the number of hidden annotations for <x> more labels
for threadid, order of $scope.render_order
hidden = 0
last_shown = null
for id in order
if id in $scope.matches
if last_shown? then $scope.ann_info.more_bottom_num[last_shown] = hidden
$scope.ann_info.more_top_num[id] = hidden
last_shown = id
hidden = 0
else else
hidden += 1 container = $scope.threading.getContainer(annotation.id)
if last_shown? then $scope.ann_info.more_bottom_num[last_shown] = hidden container.shown = false
else
for annotation in annotations
container = $scope.threading.getContainer(annotation.id)
angular.extend container,
moreTop: 0
moreBottom: 0
renderOrder: null
shown: true
$scope.$on '$routeUpdate', refresh $scope.matches = Object.keys(matches).length
delete $scope.selectedAnnotations
$scope.getThreadId = (id) ->
thread = $scope.threading.getContainer id
threadid = id
if thread.message.references?
threadid = thread.message.references[0]
threadid
$scope.clickMoreTop = (id, $event) -> $scope.$on '$routeChangeSuccess', refresh
$event?.stopPropagation() $scope.$on '$routeUpdate', refresh
threadid = $scope.getThreadId id
pos = $scope.render_pos[id]
rendered = $scope.render_order[threadid]
$scope.ann_info.more_top[id] = false
pos -= 1
while pos >= 0
prev_id = rendered[pos]
if $scope.ann_info.shown[prev_id]
$scope.ann_info.more_bottom[prev_id] = false
break
$scope.ann_info.more_bottom[prev_id] = false
$scope.ann_info.more_top[prev_id] = false
$scope.ann_info.shown[prev_id] = true
pos -= 1
$scope.clickMoreBottom = (id, $event) ->
$event?.stopPropagation()
threadid = $scope.getThreadId id
pos = $scope.render_pos[id]
rendered = $scope.render_order[threadid]
$scope.ann_info.more_bottom[id] = false
pos += 1
while pos < rendered.length
next_id = rendered[pos]
if $scope.ann_info.shown[next_id]
$scope.ann_info.more_top[next_id] = false
break
$scope.ann_info.more_bottom[next_id] = false
$scope.ann_info.more_top[next_id] = false
$scope.ann_info.shown[next_id] = true
pos += 1
refresh()
angular.module('h.controllers', imports) angular.module('h.controllers', imports)
......
...@@ -73,7 +73,7 @@ class SearchFilter ...@@ -73,7 +73,7 @@ class SearchFilter
filter = term.slice 0, term.indexOf ":" filter = term.slice 0, term.indexOf ":"
unless filter? then filter = "" unless filter? then filter = ""
switch filter switch filter
when 'quote' then quote.push term[6..] when 'quote' then quote.push term[6..].toLowerCase()
when 'result' then result.push term[7..] when 'result' then result.push term[7..]
when 'since' when 'since'
# We'll turn this into seconds # We'll turn this into seconds
...@@ -109,44 +109,36 @@ class SearchFilter ...@@ -109,44 +109,36 @@ class SearchFilter
# Time given in year # Time given in year
t = /^(\d+)year$/.exec(time)[1] t = /^(\d+)year$/.exec(time)[1]
since.push t * 60 * 60 * 24 * 365 since.push t * 60 * 60 * 24 * 365
when 'tag' then tag.push term[4..] when 'tag' then tag.push term[4..].toLowerCase()
when 'text' then text.push term[5..] when 'text' then text.push term[5..].toLowerCase()
when 'uri' then uri.push term[4..] when 'uri' then uri.push term[4..].toLowerCase()
when 'user' then user.push term[5..] when 'user' then user.push term[5..].toLowerCase()
else any.push term else any.push term.toLowerCase()
any: any:
terms: any terms: any
operator: 'and' operator: 'and'
lowercase: true
quote: quote:
terms: quote terms: quote
operator: 'and' operator: 'and'
lowercase: true
result: result:
terms: result terms: result
operator: 'min' operator: 'min'
lowercase: false
since: since:
terms: since terms: since
operator: 'and' operator: 'and'
lowercase: false
tag: tag:
terms: tag terms: tag
operator: 'and' operator: 'and'
lowercase: true
text: text:
terms: text terms: text
operator: 'and' operator: 'and'
lowercase: true
uri: uri:
terms: uri terms: uri
operator: 'or' operator: 'or'
lowercase: true
user: user:
terms: user terms: user
operator: 'or' operator: 'or'
lowercase: true
# This class will process the results of search and generate the correct filter # This class will process the results of search and generate the correct filter
...@@ -235,19 +227,6 @@ class QueryParser ...@@ -235,19 +227,6 @@ class QueryParser
and_or: 'and' and_or: 'and'
fields: ['quote', 'tag', 'text', 'uri', 'user'] fields: ['quote', 'tag', 'text', 'uri', 'user']
parseModels: (models) ->
# Cluster facets together
categories = {}
for searchItem in models
category = searchItem.attributes.category
value = searchItem.attributes.value
if category of categories
categories[category].push value
else
categories[category] = [value]
categories
populateFilter: (filter, query) => populateFilter: (filter, query) =>
# Populate a filter with a query object # Populate a filter with a query object
for category, value of query for category, value of query
......
imports = [
'h.filters',
'h.searchfilters'
]
# The render function accepts a scope and a data object and schedule the scope # The render function accepts a scope and a data object and schedule the scope
# to be updated with the provided data and digested before the next repaint # to be updated with the provided data and digested before the next repaint
# using window.requestAnimationFrame() (or a fallback). If the resulting digest # using window.requestAnimationFrame() (or a fallback). If the resulting digest
...@@ -501,12 +495,6 @@ class ViewFilter ...@@ -501,12 +495,6 @@ class ViewFilter
any: any:
fields: ['quote', 'text', 'tag', 'user'] fields: ['quote', 'text', 'tag', 'user']
this.$inject = ['searchfilter']
constructor: (searchfilter) ->
@searchfilter = searchfilter
_matches: (filter, value, match) -> _matches: (filter, value, match) ->
matches = true matches = true
...@@ -549,17 +537,17 @@ class ViewFilter ...@@ -549,17 +537,17 @@ class ViewFilter
value = checker.value annotation value = checker.value annotation
if angular.isArray value if angular.isArray value
if filter.lowercase then value = value.map (e) -> e.toLowerCase() if typeof(value[0]) == 'string'
value = value.map (v) -> v.toLowerCase()
return @_arrayMatches filter, value, checker.match return @_arrayMatches filter, value, checker.match
else else
value = value.toLowerCase() if filter.lowercase value = value.toLowerCase() if typeof(value) == 'string'
return @_matches filter, value, checker.match return @_matches filter, value, checker.match
# Filters a set of annotations, according to a given query. # Filters a set of annotations, according to a given query.
# Inputs: # Inputs:
# annotations is the input list of annotations (array) # annotations is the input list of annotations (array)
# query is the query string. It will be converted to faceted filter by the SearchFilter # filters is the query is a faceted filter generated by SearchFilter
# #
# It'll handle the annotation matching by the returned facet configuration (operator, lowercase, etc.) # It'll handle the annotation matching by the returned facet configuration (operator, lowercase, etc.)
# and the here configured @checkers. This @checkers object contains instructions how to verify the match. # and the here configured @checkers. This @checkers object contains instructions how to verify the match.
...@@ -577,84 +565,34 @@ class ViewFilter ...@@ -577,84 +565,34 @@ class ViewFilter
# matched annotation IDs list, # matched annotation IDs list,
# the faceted filters # the faceted filters
# ] # ]
filter: (annotations, query) => filter: (annotations, filters) ->
filters = @searchfilter.generateFacetedFilter query limit = Math.min((filters.result?.terms or [])...)
results = [] count = 0
# Check for given limit results = for annotation in annotations
# Find the minimal break if count >= limit
limit = 0
if filters.result.terms.length
limit = filter.result.terms[0]
for term in filter.result.terms
if limit > term then limit = term
# Convert terms to lowercase if needed
for _, filter of filters
if filter.lowercase then filter.terms.map (e) -> e.toLowerCase()
# Now that this filter is called with the top level annotations, we have to add the children too
annotationsWithChildren = []
for annotation in annotations
annotationsWithChildren.push annotation
children = annotation.thread?.flattenChildren()
if children?.length > 0
for child in children
annotationsWithChildren.push child
for annotation in annotationsWithChildren
matches = true
#ToDo: What about given zero limit?
# Limit reached
if limit and results.length >= limit then break
match = true
for category, filter of filters for category, filter of filters
break unless matches break unless match
terms = filter.terms continue unless filter.terms.length
# No condition for this category
continue unless terms.length
switch category switch category
when 'result'
# Handled above
continue
when 'any' when 'any'
# Special case categoryMatch = false
matchterms = []
matchterms.push false for term in terms
for field in @checkers.any.fields for field in @checkers.any.fields
conf = @checkers[field] if @_checkMatch(filter, annotation, @checkers[field])
categoryMatch = true
continue if conf.autofalse? and conf.autofalse annotation break
value = conf.value annotation match = categoryMatch
if angular.isArray value
if filter.lowercase
value = value.map (e) -> e.toLowerCase()
else
value = value.toLowerCase() if filter.lowercase
matchresult = @_anyMatches filter, value, conf.match
matchterms = matchterms.map (t, i) -> t or matchresult[i]
# Now let's see what we got.
matched = 0
for _, value of matchterms
matched++ if value
if (filter.operator is 'or' and matched > 0) or (filter.operator is 'and' and matched is terms.length)
matches = true
else
matches = false
else else
# For all other categories match = @_checkMatch filter, annotation, @checkers[category]
matches = @_checkMatch filter, annotation, @checkers[category]
if matches
results.push annotation.id
[results, filters]
continue unless match
count++
annotation.id
angular.module('h.services', imports) angular.module('h.services', [])
.factory('render', renderFactory) .factory('render', renderFactory)
.provider('drafts', DraftProvider) .provider('drafts', DraftProvider)
.service('annotator', Hypothesis) .service('annotator', Hypothesis)
......
...@@ -36,7 +36,7 @@ class StreamSearch ...@@ -36,7 +36,7 @@ class StreamSearch
$scope.sort.name = 'Newest' $scope.sort.name = 'Newest'
$scope.shouldShowAnnotation = (id) -> true $scope.shouldShowThread = (container) -> true
$scope.$watch 'updater', (updater) -> $scope.$watch 'updater', (updater) ->
updater?.then (sock) -> updater?.then (sock) ->
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment