Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
c3a2ebaf
Commit
c3a2ebaf
authored
Nov 21, 2022
by
Lyza Danger Gardner
Committed by
Lyza Gardner
Nov 22, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update highlight data, ordering to support styling SVG nested highlights
parent
5c94d10f
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
286 additions
and
3 deletions
+286
-3
guest.ts
src/annotator/guest.ts
+1
-0
highlight-clusters.tsx
src/annotator/highlight-clusters.tsx
+21
-0
highlighter.ts
src/annotator/highlighter.ts
+151
-0
highlight-clusters-test.js
src/annotator/test/highlight-clusters-test.js
+39
-0
highlighter-test.js
src/annotator/test/highlighter-test.js
+74
-3
No files found.
src/annotator/guest.ts
View file @
c3a2ebaf
...
@@ -613,6 +613,7 @@ export class Guest implements Annotator, Destroyable {
...
@@ -613,6 +613,7 @@ export class Guest implements Annotator, Destroyable {
_updateAnchors
(
anchors
:
Anchor
[],
notify
:
boolean
)
{
_updateAnchors
(
anchors
:
Anchor
[],
notify
:
boolean
)
{
this
.
anchors
=
anchors
;
this
.
anchors
=
anchors
;
this
.
_clusterToolbar
?.
scheduleClusterUpdates
();
if
(
notify
)
{
if
(
notify
)
{
this
.
_bucketBarClient
.
update
(
this
.
anchors
);
this
.
_bucketBarClient
.
update
(
this
.
anchors
);
}
}
...
...
src/annotator/highlight-clusters.tsx
View file @
c3a2ebaf
...
@@ -9,6 +9,8 @@ import type { HighlightCluster } from '../types/shared';
...
@@ -9,6 +9,8 @@ import type { HighlightCluster } from '../types/shared';
import
ClusterToolbar
from
'./components/ClusterToolbar'
;
import
ClusterToolbar
from
'./components/ClusterToolbar'
;
import
{
createShadowRoot
}
from
'./util/shadow-root'
;
import
{
createShadowRoot
}
from
'./util/shadow-root'
;
import
{
updateClusters
}
from
'./highlighter'
;
export
type
HighlightStyle
=
{
export
type
HighlightStyle
=
{
color
:
string
;
color
:
string
;
secondColor
:
string
;
secondColor
:
string
;
...
@@ -71,6 +73,7 @@ export class HighlightClusterController implements Destroyable {
...
@@ -71,6 +73,7 @@ export class HighlightClusterController implements Destroyable {
private
_features
:
IFeatureFlags
;
private
_features
:
IFeatureFlags
;
private
_outerContainer
:
HTMLElement
;
private
_outerContainer
:
HTMLElement
;
private
_shadowRoot
:
ShadowRoot
;
private
_shadowRoot
:
ShadowRoot
;
private
_updateTimeout
?:
number
;
constructor
(
element
:
HTMLElement
,
options
:
{
features
:
IFeatureFlags
})
{
constructor
(
element
:
HTMLElement
,
options
:
{
features
:
IFeatureFlags
})
{
this
.
_element
=
element
;
this
.
_element
=
element
;
...
@@ -101,11 +104,21 @@ export class HighlightClusterController implements Destroyable {
...
@@ -101,11 +104,21 @@ export class HighlightClusterController implements Destroyable {
}
}
destroy
()
{
destroy
()
{
clearTimeout
(
this
.
_updateTimeout
);
render
(
null
,
this
.
_shadowRoot
);
// unload the Preact component
render
(
null
,
this
.
_shadowRoot
);
// unload the Preact component
this
.
_activate
(
false
);
// De-activate cluster styling
this
.
_activate
(
false
);
// De-activate cluster styling
this
.
_outerContainer
.
remove
();
this
.
_outerContainer
.
remove
();
}
}
/**
* Indicate that the set of highlights in the document has been dirtied and we
* should schedule an update to highlight data attributes and stacking order.
*/
scheduleClusterUpdates
()
{
clearTimeout
(
this
.
_updateTimeout
);
this
.
_updateTimeout
=
setTimeout
(()
=>
this
.
_updateClusters
(),
100
);
}
/**
/**
* Set initial values for :root CSS custom properties (variables) based on the
* Set initial values for :root CSS custom properties (variables) based on the
* applied styles for each cluster. This has no effect if this feature
* applied styles for each cluster. This has no effect if this feature
...
@@ -121,6 +134,14 @@ export class HighlightClusterController implements Destroyable {
...
@@ -121,6 +134,14 @@ export class HighlightClusterController implements Destroyable {
this
.
_activate
(
this
.
_isActive
());
this
.
_activate
(
this
.
_isActive
());
}
}
_updateClusters
()
{
if
(
!
this
.
_isActive
())
{
/* istanbul ignore next */
return
;
}
updateClusters
(
this
.
_element
);
}
_isActive
()
{
_isActive
()
{
return
this
.
_features
.
flagEnabled
(
'styled_highlight_clusters'
);
return
this
.
_features
.
flagEnabled
(
'styled_highlight_clusters'
);
}
}
...
...
src/annotator/highlighter.ts
View file @
c3a2ebaf
import
classnames
from
'classnames'
;
import
classnames
from
'classnames'
;
import
type
{
HighlightCluster
}
from
'../types/shared'
;
import
{
isInPlaceholder
}
from
'./anchoring/placeholder'
;
import
{
isInPlaceholder
}
from
'./anchoring/placeholder'
;
import
{
isNodeInRange
}
from
'./range-util'
;
import
{
isNodeInRange
}
from
'./range-util'
;
...
@@ -12,6 +14,12 @@ type HighlightProps = {
...
@@ -12,6 +14,12 @@ type HighlightProps = {
export
type
HighlightElement
=
HTMLElement
&
HighlightProps
;
export
type
HighlightElement
=
HTMLElement
&
HighlightProps
;
export
const
clusterValues
:
HighlightCluster
[]
=
[
'user-annotations'
,
'user-highlights'
,
'other-content'
,
];
/**
/**
* Return the canvas element underneath a highlight element in a PDF page's
* Return the canvas element underneath a highlight element in a PDF page's
* text layer.
* text layer.
...
@@ -385,3 +393,146 @@ export function getBoundingClientRect(collection: HTMLElement[]): Rect {
...
@@ -385,3 +393,146 @@ export function getBoundingClientRect(collection: HTMLElement[]): Rect {
right
:
Math
.
max
(
acc
.
right
,
r
.
right
),
right
:
Math
.
max
(
acc
.
right
,
r
.
right
),
}));
}));
}
}
/**
* Add metadata and manipulate ordering of all highlights in `element` to
* allow styling of nested, clustered highlights.
*/
export
function
updateClusters
(
element
:
Element
)
{
setNestingData
(
getHighlights
(
element
));
updateSVGHighlightOrdering
(
element
);
}
/**
* Is `el` a highlight element? Work around inconsistency between HTML documents
* (`tagName` is upper-case) and XHTML documents (`tagName` is lower-case)
*/
const
isHighlightElement
=
(
el
:
Element
):
boolean
=>
el
.
tagName
.
toLowerCase
()
===
'hypothesis-highlight'
;
/**
* Return the closest generation of HighlightElements to `element`.
*
* If `element` is itself a HighlightElement, return immediate children that
* are also HighlightElements.
*
* Otherwise, return all HighlightElements that have no parent HighlightElement,
* i.e. the outermost highlights within `element`.
*/
function
getHighlights
(
element
:
Element
)
{
let
highlights
;
if
(
isHighlightElement
(
element
))
{
highlights
=
Array
.
from
(
element
.
children
).
filter
(
isHighlightElement
);
}
else
{
highlights
=
Array
.
from
(
element
.
getElementsByTagName
(
'hypothesis-highlight'
)
).
filter
(
highlight
=>
!
highlight
.
parentElement
||
!
isHighlightElement
(
highlight
.
parentElement
)
);
}
return
highlights
as
HighlightElement
[];
}
/**
* Get all of the SVG highlights within `root`, grouped by layer. A PDF
* document may have multiple layers of SVG highlights, typically one per page.
*
* @return a Map of layer Elements to all SVG highlights within that
* layer Element
*/
function
getSVGHighlights
(
root
?:
Element
):
Map
<
Element
,
HighlightElement
[]
>
{
const
svgHighlights
:
Map
<
Element
,
HighlightElement
[]
>
=
new
Map
();
for
(
const
layer
of
(
root
??
document
).
getElementsByClassName
(
'hypothesis-highlight-layer'
))
{
svgHighlights
.
set
(
layer
,
Array
.
from
(
layer
.
querySelectorAll
(
'.hypothesis-svg-highlight'
)
)
as
HighlightElement
[]
);
}
return
svgHighlights
;
}
/**
* Walk a tree of <hypothesis-highlight> elements, adding `data-nesting-level`
* and `data-cluster-level` data attributes to <hypothesis-highlight>s and
* their associated SVG highlight (<rect>) elements.
*
* - `data-nesting-level` - generational depth of the applicable
* `<hypothesis-highlight>` relative to outermost `<hypothesis-highlight>`.
* - `data-cluster-level` - number of `<hypothesis-highlight>` generations
* since the cluster value changed.
*
* @param highlightEls - A collection of sibling <hypothesis-highlight>
* elements
* @param parentCluster - The cluster value of the parent highlight to
* `highlightEls`, if any
* @param nestingLevel - The nesting "level", relative to the outermost
* <hypothesis-highlight> element (0-based)
* @param parentClusterLevel - The parent's nesting depth, per its cluster
* value (`parentCluster`). i.e. How many levels since the cluster value
* changed? This allows for nested styling of highlights of the same cluster
* value.
*/
function
setNestingData
(
highlightEls
:
HighlightElement
[],
parentCluster
=
''
,
nestingLevel
=
0
,
parentClusterLevel
=
0
)
{
for
(
const
hEl
of
highlightEls
)
{
const
elCluster
=
clusterValues
.
find
(
cv
=>
hEl
.
classList
.
contains
(
cv
))
??
'other-content'
;
const
elClusterLevel
=
parentCluster
&&
elCluster
===
parentCluster
?
parentClusterLevel
+
1
:
0
;
hEl
.
setAttribute
(
'data-nesting-level'
,
`
${
nestingLevel
}
`
);
hEl
.
setAttribute
(
'data-cluster-level'
,
`
${
elClusterLevel
}
`
);
if
(
hEl
.
svgHighlight
)
{
hEl
.
svgHighlight
.
setAttribute
(
'data-nesting-level'
,
`
${
nestingLevel
}
`
);
hEl
.
svgHighlight
.
setAttribute
(
'data-cluster-level'
,
`
${
elClusterLevel
}
`
);
}
setNestingData
(
getHighlights
(
hEl
),
elCluster
/* parentCluster */
,
nestingLevel
+
1
/* nestingLevel */
,
elClusterLevel
/* parentClusterLevel */
);
}
}
/**
* Ensure that SVG <rect> elements are ordered correctly: inner (nested)
* highlights should be visible on top of outer highlights.
*
* All SVG <rect>s drawn for a PDF page are siblings. To ensure that the
* <rect>s associated with outer highlights don't show up on top of (and thus
* obscure) nested highlights, order the <rects> by their `data-nesting-level`
* value if they are not already.
*/
function
updateSVGHighlightOrdering
(
element
:
Element
)
{
const
nestingLevel
=
(
el
:
Element
)
=>
parseInt
(
el
.
getAttribute
(
'data-nesting-level'
)
??
'0'
,
10
);
for
(
const
[
layer
,
layerHighlights
]
of
getSVGHighlights
(
element
))
{
const
correctlyOrdered
=
layerHighlights
.
every
((
svgEl
,
idx
,
allEls
)
=>
{
if
(
idx
===
0
)
{
return
true
;
}
return
nestingLevel
(
svgEl
)
>=
nestingLevel
(
allEls
[
idx
-
1
]);
});
if
(
!
correctlyOrdered
)
{
layerHighlights
.
sort
((
a
,
b
)
=>
nestingLevel
(
a
)
-
nestingLevel
(
b
));
layer
.
replaceChildren
(...
layerHighlights
);
}
}
}
src/annotator/test/highlight-clusters-test.js
View file @
c3a2ebaf
...
@@ -7,6 +7,8 @@ import { FeatureFlags } from '../features';
...
@@ -7,6 +7,8 @@ import { FeatureFlags } from '../features';
describe
(
'HighlightClusterController'
,
()
=>
{
describe
(
'HighlightClusterController'
,
()
=>
{
let
fakeFeatures
;
let
fakeFeatures
;
let
fakeSetProperty
;
let
fakeSetProperty
;
let
fakeUpdateClusters
;
let
toolbarProps
;
let
toolbarProps
;
let
container
;
let
container
;
let
controllers
;
let
controllers
;
...
@@ -22,8 +24,11 @@ describe('HighlightClusterController', () => {
...
@@ -22,8 +24,11 @@ describe('HighlightClusterController', () => {
beforeEach
(()
=>
{
beforeEach
(()
=>
{
controllers
=
[];
controllers
=
[];
fakeFeatures
=
new
FeatureFlags
();
fakeFeatures
=
new
FeatureFlags
();
fakeSetProperty
=
sinon
.
stub
(
document
.
documentElement
.
style
,
'setProperty'
);
fakeSetProperty
=
sinon
.
stub
(
document
.
documentElement
.
style
,
'setProperty'
);
fakeUpdateClusters
=
sinon
.
stub
();
container
=
document
.
createElement
(
'div'
);
container
=
document
.
createElement
(
'div'
);
toolbarProps
=
{};
toolbarProps
=
{};
...
@@ -34,6 +39,9 @@ describe('HighlightClusterController', () => {
...
@@ -34,6 +39,9 @@ describe('HighlightClusterController', () => {
$imports
.
$mock
({
$imports
.
$mock
({
'./components/ClusterToolbar'
:
FakeToolbar
,
'./components/ClusterToolbar'
:
FakeToolbar
,
'./highlighter'
:
{
updateClusters
:
fakeUpdateClusters
,
},
});
});
});
});
...
@@ -110,4 +118,35 @@ describe('HighlightClusterController', () => {
...
@@ -110,4 +118,35 @@ describe('HighlightClusterController', () => {
assert
.
equal
(
fakeSetProperty
.
callCount
,
3
);
assert
.
equal
(
fakeSetProperty
.
callCount
,
3
);
});
});
describe
(
'updating highlight element data and ordering'
,
()
=>
{
let
clock
;
beforeEach
(()
=>
{
clock
=
sinon
.
useFakeTimers
();
fakeFeatures
.
update
({
styled_highlight_clusters
:
true
});
});
afterEach
(()
=>
{
clock
.
restore
();
});
it
(
'schedules a debounced task to update highlights'
,
()
=>
{
const
controller
=
createToolbar
();
controller
.
scheduleClusterUpdates
();
assert
.
notCalled
(
fakeUpdateClusters
);
clock
.
tick
(
1
);
assert
.
notCalled
(
fakeUpdateClusters
);
controller
.
scheduleClusterUpdates
();
controller
.
scheduleClusterUpdates
();
clock
.
tick
(
150
);
assert
.
calledOnce
(
fakeUpdateClusters
);
});
});
});
});
src/annotator/test/highlighter-test.js
View file @
c3a2ebaf
...
@@ -8,6 +8,7 @@ import {
...
@@ -8,6 +8,7 @@ import {
removeAllHighlights
,
removeAllHighlights
,
setHighlightsFocused
,
setHighlightsFocused
,
setHighlightsVisible
,
setHighlightsVisible
,
updateClusters
,
}
from
'../highlighter'
;
}
from
'../highlighter'
;
/**
/**
...
@@ -49,7 +50,7 @@ function PDFPage({ showPlaceholder = false }) {
...
@@ -49,7 +50,7 @@ function PDFPage({ showPlaceholder = false }) {
* component has been rendered
* component has been rendered
* @param {string} [cssClass] additional CSS class(es) to apply to the highlight
* @param {string} [cssClass] additional CSS class(es) to apply to the highlight
* and SVG rect elements
* and SVG rect elements
* @return {H
TMLElement
} - `<hypothesis-highlight>` element
* @return {H
ighlightElement[]
} - `<hypothesis-highlight>` element
*/
*/
function
highlightPDFRange
(
pageContainer
,
cssClass
=
''
)
{
function
highlightPDFRange
(
pageContainer
,
cssClass
=
''
)
{
const
textSpan
=
pageContainer
.
querySelector
(
'.testText'
);
const
textSpan
=
pageContainer
.
querySelector
(
'.testText'
);
...
@@ -375,7 +376,7 @@ describe('annotator/highlighter', () => {
...
@@ -375,7 +376,7 @@ describe('annotator/highlighter', () => {
*
*
* Returns all the highlight elements.
* Returns all the highlight elements.
*/
*/
function
createHighlights
(
root
)
{
function
createHighlights
(
root
,
cssClass
=
''
)
{
let
highlights
=
[];
let
highlights
=
[];
for
(
let
i
=
0
;
i
<
3
;
i
++
)
{
for
(
let
i
=
0
;
i
<
3
;
i
++
)
{
...
@@ -385,12 +386,82 @@ describe('annotator/highlighter', () => {
...
@@ -385,12 +386,82 @@ describe('annotator/highlighter', () => {
range
.
setStartBefore
(
span
.
childNodes
[
0
]);
range
.
setStartBefore
(
span
.
childNodes
[
0
]);
range
.
setEndAfter
(
span
.
childNodes
[
0
]);
range
.
setEndAfter
(
span
.
childNodes
[
0
]);
root
.
appendChild
(
span
);
root
.
appendChild
(
span
);
highlights
.
push
(...
highlightRange
(
range
));
highlights
.
push
(...
highlightRange
(
range
,
cssClass
));
}
}
return
highlights
;
return
highlights
;
}
}
describe
(
'updateClusters'
,
()
=>
{
const
nestingLevel
=
el
=>
parseInt
(
el
.
getAttribute
(
'data-nesting-level'
),
10
);
const
clusterLevel
=
el
=>
parseInt
(
el
.
getAttribute
(
'data-cluster-level'
),
10
);
it
(
'sets nesting data on highlight elements'
,
()
=>
{
const
container
=
document
.
createElement
(
'div'
);
render
(
<
PDFPage
/>
,
container
);
const
highlights
=
[
...
highlightPDFRange
(
container
,
'user-annotations'
),
...
highlightPDFRange
(
container
,
'user-annotations'
),
...
highlightPDFRange
(
container
,
'user-annotations'
),
...
highlightPDFRange
(
container
,
'other-content'
),
];
updateClusters
(
container
);
assert
.
equal
(
nestingLevel
(
highlights
[
0
]),
0
);
assert
.
equal
(
nestingLevel
(
highlights
[
1
]),
1
);
assert
.
equal
(
nestingLevel
(
highlights
[
2
]),
2
);
assert
.
equal
(
nestingLevel
(
highlights
[
3
]),
3
);
assert
.
equal
(
clusterLevel
(
highlights
[
0
]),
0
);
assert
.
equal
(
clusterLevel
(
highlights
[
1
]),
1
);
assert
.
equal
(
clusterLevel
(
highlights
[
2
]),
2
);
assert
.
equal
(
clusterLevel
(
highlights
[
3
]),
0
);
});
it
(
'sets nesting data on SVG highlights'
,
()
=>
{
const
container
=
document
.
createElement
(
'div'
);
render
(
<
PDFPage
/>
,
container
);
const
highlights
=
[
...
highlightPDFRange
(
container
,
'user-annotations'
),
...
highlightPDFRange
(
container
,
'user-annotations'
),
];
updateClusters
(
container
);
assert
.
equal
(
nestingLevel
(
highlights
[
0
].
svgHighlight
),
0
);
assert
.
equal
(
nestingLevel
(
highlights
[
1
].
svgHighlight
),
1
);
assert
.
equal
(
clusterLevel
(
highlights
[
0
].
svgHighlight
),
0
);
assert
.
equal
(
clusterLevel
(
highlights
[
1
].
svgHighlight
),
1
);
});
it
(
'reorders SVG highlights based on nesting level'
,
()
=>
{
const
container
=
document
.
createElement
(
'div'
);
render
(
<
PDFPage
/>
,
container
);
// SVG highlights for these highlights will be added in order.
// These first three highlights will nest.
highlightPDFRange
(
container
,
'user-annotations'
);
highlightPDFRange
(
container
,
'user-annotations'
);
highlightPDFRange
(
container
,
'other-content'
);
// these second three highlights are outer highlights
createHighlights
(
container
.
querySelector
(
'.textLayer'
));
updateClusters
(
container
);
const
orderedNestingLevels
=
Array
.
from
(
container
.
querySelectorAll
(
'rect'
)
).
map
(
el
=>
nestingLevel
(
el
));
assert
.
deepEqual
(
orderedNestingLevels
,
[
0
,
0
,
0
,
0
,
1
,
2
]);
});
});
describe
(
'removeAllHighlights'
,
()
=>
{
describe
(
'removeAllHighlights'
,
()
=>
{
it
(
'removes all highlight elements under the root element'
,
()
=>
{
it
(
'removes all highlight elements under the root element'
,
()
=>
{
const
root
=
document
.
createElement
(
'div'
);
const
root
=
document
.
createElement
(
'div'
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment