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
# to annotations loaded into the stream.
$scope.activate = angular.noop
$scope.shouldShowAnnotation = (id) -> true
$scope.shouldShowThread = -> true
$scope.$watch 'updater', (updater) ->
if updater?
......@@ -359,18 +359,16 @@ class AnnotationViewer
class Viewer
this.$inject = [
'$filter', '$routeParams', '$sce', '$scope',
'annotator', 'viewFilter'
'annotator', 'searchfilter', 'viewFilter'
]
constructor: (
$filter, $routeParams, $sce, $scope,
annotator, viewFilter
annotator, searchfilter, viewFilter
) ->
# Tells the view that these annotations are embedded into the owner doc
$scope.isEmbedded = true
$scope.isStream = true
{providers} = annotator
$scope.activate = (annotation) ->
if angular.isObject annotation
highlights = [annotation.$$tag]
......@@ -378,235 +376,109 @@ class Viewer
highlights = [$scope.ongoingEdit.message.$$tag]
else
highlights = []
for p in providers
for p in annotator.providers
p.channel.notify
method: 'setActiveHighlights'
params: highlights
$scope.shouldShowAnnotation = (id) ->
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 = =>
$scope.shouldShowThread = (container) ->
if $routeParams.q
delete $scope.selectedAnnotations
container.shown
else if $scope.selectedAnnotations?
$scope.selectedAnnotations[container.message?.id]
else
delete $scope.matches
return
true
$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
annotations = $scope.threading.root.flattenChildren()
[$scope.matches, $scope.filters] = viewFilter.filter annotations, query
# Create the regexps for highlighting the matches inside the annotations' bodies
$scope.text_tokens = $scope.filters.text.terms.slice()
$scope.text_regexp = []
$scope.quote_tokens = $scope.filters.quote.terms.slice()
$scope.quote_regexp = []
# Highligh any matches
for term in $scope.filters.any.terms
$scope.text_tokens.push term
$scope.quote_tokens.push term
# Saving the regexps and higlighter to the annotator for highlighttext regeneration
for token in $scope.text_tokens
regexp = new RegExp(token,"ig")
$scope.text_regexp.push regexp
for token in $scope.quote_tokens
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
if query
# Get the set of matches
filters = searchfilter.generateFacetedFilter query
for match in viewFilter.filter annotations, filters
matches[match] = true
# Find the top posts and render them
for annotation in annotations
{id, references} = annotation
if typeof(references) == 'string' then references = [references]
if matches[id]?
top = references?[0] or id
continue if rendered[top]
container = $scope.threading.getContainer(top)
container.renderOrder = []
render container, 0, container
container.shown = true
rendered[id] = true
else
hidden += 1
if last_shown? then $scope.ann_info.more_bottom_num[last_shown] = hidden
container = $scope.threading.getContainer(annotation.id)
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) ->
$event?.stopPropagation()
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()
$scope.$on '$routeChangeSuccess', refresh
$scope.$on '$routeUpdate', refresh
angular.module('h.controllers', imports)
......
......@@ -73,7 +73,7 @@ class SearchFilter
filter = term.slice 0, term.indexOf ":"
unless filter? then 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 'since'
# We'll turn this into seconds
......@@ -109,44 +109,36 @@ class SearchFilter
# Time given in year
t = /^(\d+)year$/.exec(time)[1]
since.push t * 60 * 60 * 24 * 365
when 'tag' then tag.push term[4..]
when 'text' then text.push term[5..]
when 'uri' then uri.push term[4..]
when 'user' then user.push term[5..]
else any.push term
when 'tag' then tag.push term[4..].toLowerCase()
when 'text' then text.push term[5..].toLowerCase()
when 'uri' then uri.push term[4..].toLowerCase()
when 'user' then user.push term[5..].toLowerCase()
else any.push term.toLowerCase()
any:
terms: any
operator: 'and'
lowercase: true
quote:
terms: quote
operator: 'and'
lowercase: true
result:
terms: result
operator: 'min'
lowercase: false
since:
terms: since
operator: 'and'
lowercase: false
tag:
terms: tag
operator: 'and'
lowercase: true
text:
terms: text
operator: 'and'
lowercase: true
uri:
terms: uri
operator: 'or'
lowercase: true
user:
terms: user
operator: 'or'
lowercase: true
# This class will process the results of search and generate the correct filter
......@@ -235,19 +227,6 @@ class QueryParser
and_or: 'and'
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) =>
# Populate a filter with a query object
for category, value of query
......
imports = [
'h.filters',
'h.searchfilters'
]
# 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
# using window.requestAnimationFrame() (or a fallback). If the resulting digest
......@@ -501,12 +495,6 @@ class ViewFilter
any:
fields: ['quote', 'text', 'tag', 'user']
this.$inject = ['searchfilter']
constructor: (searchfilter) ->
@searchfilter = searchfilter
_matches: (filter, value, match) ->
matches = true
......@@ -549,17 +537,17 @@ class ViewFilter
value = checker.value annotation
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
else
value = value.toLowerCase() if filter.lowercase
value = value.toLowerCase() if typeof(value) == 'string'
return @_matches filter, value, checker.match
# Filters a set of annotations, according to a given query.
# Inputs:
# 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.)
# and the here configured @checkers. This @checkers object contains instructions how to verify the match.
......@@ -577,84 +565,34 @@ class ViewFilter
# matched annotation IDs list,
# the faceted filters
# ]
filter: (annotations, query) =>
filters = @searchfilter.generateFacetedFilter query
results = []
# Check for given limit
# Find the minimal
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
filter: (annotations, filters) ->
limit = Math.min((filters.result?.terms or [])...)
count = 0
results = for annotation in annotations
break if count >= limit
match = true
for category, filter of filters
break unless matches
terms = filter.terms
# No condition for this category
continue unless terms.length
break unless match
continue unless filter.terms.length
switch category
when 'result'
# Handled above
continue
when 'any'
# Special case
matchterms = []
matchterms.push false for term in terms
categoryMatch = false
for field in @checkers.any.fields
conf = @checkers[field]
continue if conf.autofalse? and conf.autofalse annotation
value = conf.value annotation
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
if @_checkMatch(filter, annotation, @checkers[field])
categoryMatch = true
break
match = categoryMatch
else
# For all other categories
matches = @_checkMatch filter, annotation, @checkers[category]
if matches
results.push annotation.id
[results, filters]
match = @_checkMatch filter, annotation, @checkers[category]
continue unless match
count++
annotation.id
angular.module('h.services', imports)
angular.module('h.services', [])
.factory('render', renderFactory)
.provider('drafts', DraftProvider)
.service('annotator', Hypothesis)
......
......@@ -36,7 +36,7 @@ class StreamSearch
$scope.sort.name = 'Newest'
$scope.shouldShowAnnotation = (id) -> true
$scope.shouldShowThread = (container) -> true
$scope.$watch 'updater', (updater) ->
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