Conversation
…ferences. Used on iOS when a reference is tapped to show a panel which lets user drag side to side to see any info for references which were adjacent to the tapped reference. ie: if a lone reference is tapped "[4]" we show a panel with its reference text, but if that reference has adjacent references ie "[3][4][5]" the panel we show can be dragged side to side to see the references text for "[3]" and "[5]". To use: Pass `isCitation` the href string of the tapped reference anchor, if it returns true call `collectNearbyReferences` with the document and the tapped anchor element to get extracted references info including a `referencesGroup` array and the index of the selected element `selectedIndex` from that group. The each item in `referencesGroup` will contain: `id` - id of the reference `rect` - ClientRect of the reference (so you can do a native overlay dimming everthing but the tapped reference link ie "[4]") `text` - the text of the tapped reference link ie "[4]" `html` - the html/text of the reference from the actual reference at the bottom of the page
Thankyou @montehurd :) yay! i'm looking forward to use this! |
@sharvaniharan You're welcome! I just edited the description with a few more details of how we use this on iOS. Let me know if you have any questions or issues :) |
This doesn't include the pagelib version bump because the PR moving those bits to the page lib isn't merged at the moment. As soon as wikimedia/wikimedia-page-library#140 is merged we can bump the pagelib version (as a new commit on this PR) and it should work.
src/transform/ReferenceCollection.js
Outdated
|
||
/** | ||
* Checks if element has a child anchor with a citation link. | ||
* @param {!HTMLElement} element |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra space between type and name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
src/transform/ReferenceCollection.js
Outdated
* @return {!boolean} | ||
*/ | ||
const isWhitespaceTextNode = node => | ||
!(!node || node.nodeType !== Node.TEXT_NODE || !node.textContent.match(/^\s+$/)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After discussion, this may run faster but it would read more easily if the negation was pushed inward:
node && node.nodeType === Node.TEXT_NODE && node.textContent.match(/^\s+$/))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
src/transform/ReferenceCollection.js
Outdated
const hasCitationLink = element => { | ||
try { | ||
return isCitation(getFirstChildAnchor(element).getAttribute('href')) | ||
} catch (e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this was wrapped previously to prevent catch on empty links. I feel that needless try / catch blocks obscure how easy it is to reason about the code. What do you think of:
const anchor = element.querySelector('a')
return anchor && anchor.getAttribute('href')
Instead of
try {
return isCitation(getFirstChildAnchor(element).getAttribute('href'))
} catch (e) {
return false
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I don't recall the exact reason for the try/catch
- been a while. I played with your alternative for a bit (adding call to isCitation
where needed) but didn't get it to work (tests failed). Would be ok to leave this as-is for now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh nevermind I think I fixed it...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect! I'm glad we have a test case that fails! Now we can easily make the improvement right now and be confident in our code instead of punting it down the line and be confused later.
I looked into this myself. This try / catch block was obscuring the real underlying issue which was that adjacentNonWhitespaceNode() can return null and getFirstChildAnchor() expects nonnull.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heya just a heads-up I reverted your change - see my comment here #140 (comment) for details.
src/transform/ReferenceCollection.js
Outdated
* @param {!string} href | ||
* @return {!boolean} | ||
*/ | ||
const isCitation = href => href.indexOf('#cite_note') > -1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be more more precise to say anchor.hash.startsWith('#cite_note')
. You may consider also moving the selector to the top of the file, as CITE_SELECTOR_PREFIX, as you have done with REFERENCE_SELECTOR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I prefer to keep the method narrowly scoped vs passing it the entire anchor element. Open to refactoring this later if you feel strongly about it though :)
I added and used a constant per your suggestion.
src/transform/ReferenceCollection.js
Outdated
|
||
/** | ||
* Determines if node is a text node containing only whitespace. | ||
* @param {!Node} node |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra space between type and name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
src/transform/ReferenceCollection.js
Outdated
const getRefTextContainer = (document, sourceNode) => { | ||
const refTextContainerID = getFirstChildAnchor(sourceNode).getAttribute('href').slice(1) | ||
const refTextContainer = document.getElementById(refTextContainerID) | ||
|| document.getElementById(decodeURIComponent(refTextContainerID)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need to check both decoded and undecoded?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps no longer, but I do recall some of these had funky encoding at one time...
/** | ||
* Reference item model. | ||
*/ | ||
class ReferenceItem { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about using a JSDoc for typing instead of a class? Then creating a ReferenceItem is just matching the shape which requires explicit labeling of properties. For example, before:
class ReferenceItem {
constructor(id, rect, text, html) {
this.id = id
this.rect = rect
this.text = text
this.html = html
}
}
const reference = new ReferenceItem('a', {width, height}, 'foo', '<b>bar</b>')
With labeled properties:
/**
* @typedef ReferenceItem
* @prop {string} id
* @prop {DOMRect} rect
* @prop {?string} text
* @prop {?string} html
*/
const reference = {id: 'a', rect: {width, height}, text: 'foo', html: '<b>bar</b>'}
Same thing for NearbyReferences.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I prefer using explicit language constructs over JSDoc shape conformance, though I'd be interested in chatting about this in the future. I think there are some benefits you get with classes, but there may be details I'm missing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usage of the class keyword exposes us to misusage of extends and we're not using any extra functionality here. It's just plain data. I think it's easier to reason about a simple JSON object than a class. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usage of the class keyword exposes us to misusage of extends
Using classes to model data is pretty common. How is classes being extendable practically risky here?
beforeEach(() => { | ||
document.documentElement.innerHTML = ` | ||
<stuff> | ||
\t<b id=one>one</b> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry but why an escaped tab here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test is for adjacentNonWhitespaceNode()
, so iirc I separated the elements with newlines and leading space "whitespace", so I thought it would be good to throw tabs in there too.
src/transform/ReferenceCollection.js
Outdated
currentNode = adjacentNonWhitespaceNode(currentNode, siblingGetter) | ||
if (hasCitationLink(currentNode)) { | ||
nodeCollector(currentNode) | ||
continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you invert this condition, you can replace the continue here with the break below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doh! Good catch! Fixed
const collectAdjacentReferenceNodes = (node, siblingGetter, nodeCollector) => { | ||
let currentNode = node | ||
while (true) { | ||
currentNode = adjacentNonWhitespaceNode(currentNode, siblingGetter) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You may wish to explicate in the JSDocs that the starting node is never collected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah good point. Added
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And that when there is only one node, the passed in collector is never called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't it go without saying that if there no adjacent reference nodes, none will be collected?
@montehurd works on Android now! @niedzielski will let you merge once the changes you have requested are in! |
Re-review pending follow-ups from @montehurd. |
@niedzielski I think I replied to and/or pushed commits to all your comments. Should be good for a re-review when you have time. Thanks again for the great feedback! |
@niedzielski @montehurd can we merge this? |
Many JavaScript developers agree to favor strict equality (===) to loose equality (==). This refactor makes the same argument for tests that strict is generally safer.
/** | ||
* Reference item model. | ||
*/ | ||
class ReferenceItem { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usage of the class keyword exposes us to misusage of extends and we're not using any extra functionality here. It's just plain data. I think it's easier to reason about a simple JSON object than a class. What do you think?
const ReferenceCollection = pagelib.ReferenceCollection | ||
|
||
const referenceGroupHTML = ` | ||
<sup id="cite_ref-a" class="reference"><a id='a1' href="#cite_note-a">[4]</a></sup> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's mixed single and double quotes in this string and others. Can we use single?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/transform/ReferenceCollection.js
Outdated
* @param {!HTMLElement} element | ||
* @return {?HTMLAnchorElement} | ||
*/ | ||
const getFirstChildAnchor = element => element.querySelector('A') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heya!
I wanted to share my perspective on replacing comments with functions as you've done here.
In general, I think it's great practice to use good naming on our symbols and, of course, abstraction for the win. However, I probably wouldn't write a comment for querying the first anchor of a DOM subtree since it's so commonplace as to be un-noteworthy and the abstraction here does nothing. Function calls may now read more like English but they also obscure the underlying simplicity of what's actually happening here with a new custom DSL and greatly inflate the verbosity of the code which is more of a decrease to readability than an increase.
Consider what happens also when the second child anchor is needed in some new patch. Do we make a new function that sits alongside getFirstChildAnchor() called getSecondChildAnchor()? That seems bad enough but when the third anchor is needed, suddenly we might wonder if getNChildAnchor() should be added which takes an argument, n. Now we have three functions that all boil down to querySelector / querySelectorAll and we still need to pass an argument.
I think we both feel very comfortable working with querySelector / querySelectorAll so my opinion is that we should favor their direct usage unless we're doing something extra that we want to capture in a function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya this was overly abstracted. I removed it. Good catch :)
}) | ||
it('correctly affirms child anchor does not have citation link', () => { | ||
const element = document.querySelector('#cite_ref-b') | ||
assert.equal(hasCitationLink(element), false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I replaced all these calls to assert.equal with assert.strictEqual since for the same reason we favor ===
to ==
. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coolio! Thanks!
src/transform/ReferenceCollection.js
Outdated
const hasCitationLink = element => { | ||
try { | ||
return isCitation(getFirstChildAnchor(element).getAttribute('href')) | ||
} catch (e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect! I'm glad we have a test case that fails! Now we can easily make the improvement right now and be confident in our code instead of punting it down the line and be confused later.
I looked into this myself. This try / catch block was obscuring the real underlying issue which was that adjacentNonWhitespaceNode() can return null and getFirstChildAnchor() expects nonnull.
const collectAdjacentReferenceNodes = (node, siblingGetter, nodeCollector) => { | ||
let currentNode = node | ||
while (true) { | ||
currentNode = adjacentNonWhitespaceNode(currentNode, siblingGetter) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And that when there is only one node, the passed in collector is never called.
@niedzielski Heya! Just a head's-up I reverted 22bfca3 as it didn't work. I'll take a peek at the cause and undo the revert if I can suss it out :) Edit: So it looks like it's not working because you're trying to call |
This reverts commit 22bfca3.
7fec1a6
to
e1e06a8
Compare
Great! Thanks for diagnosing. I've added a test case for the scenario you've described and as I understand the desired behavior, and made the fix. Please be sure to check it out! |
@niedzielski Your 5ae2c77 commit worked! Thanks! If you have a sec there are a couple questions above: |
@niedzielski btw thanks again for all the great comments and CR!!! |
@niedzielski @montehurd merging this! thanks for all the review work you guys did!! 💯 |
- Added a ViewPager to view grouped citations - integrated with the PR for grouped citations : wikimedia/wikimedia-page-library#140 Bug: T172283 Change-Id: Id1302b4a4ee0ce5553d397301ce17c528c08d07d
- Added a ViewPager to view grouped citations - integrated with the PR for grouped citations : wikimedia/wikimedia-page-library#140 Bug: T172283 Change-Id: Id1302b4a4ee0ce5553d397301ce17c528c08d07d
I mentioned this PR on... |
Used to make it easier to show native adjacent reference panel carousel:
Usage details:
Used on iOS when a reference is tapped to show a native panel which lets user drag side to side to see any info for references which were adjacent to the tapped reference. ie: if a lone reference is tapped ie
[4]
we show a panel with its reference text, but if that reference has adjacent references ie[3][4][5]
the panel we show can be dragged side to side to see the references text for[3]
and[5]
.To use:
Pass
isCitation
the href string of the tapped reference anchor, if it returns true (on iOS we use it to determine if the type of the clicked item isreference
) callcollectNearbyReferences
with the document and the tapped anchor element to extract relevant references info (iOS example) including areferencesGroup
array and the index of the selected elementselectedIndex
from that group.The each item in
referencesGroup
will contain:id
- id of the referencerect
- ClientRect of the reference (so you can do a native overlay dimming everthing but the tapped reference link ie[4]
)text
- the text of the tapped reference link ie[4]
html
- the html/text of the reference from the actual reference at the bottom of the page (in the animation above this is everything shown below the horizontal line in the panels)iOS native interface details:
[11]
.Notes:
NodeUtilities.js
- apologies that this wasn't as separate commit.