-
Notifications
You must be signed in to change notification settings - Fork 112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement Pop-Up Noterefs #139
Comments
The popup footnotes feature was indeed implemented first in the Kotlin toolkit, by @aferditamuriqi. The original idea was to start the development of a JS module common to the Swift and Kotlin toolkits, a lib we called "Glue JS" at the time (as it was acting as a glue between native code and browsing actions). This Glue JS lib was never finalized, but the repo is present here. Touch handling would have moved to this repo if it had been made common btw the different toolkits. I don't know if this overarching goal can still be reached without large refactoring.
Few reading systems implement EPUB 3 popup footnotes; iBooks is and the authoring part is correctly described by Apple here. It is said there that if the footnote is an Logically, the target footnote text is in the same resource as the hyperlink. But this is not explicitly stated in any guideline I know of. |
I don't think I will attempt this. Do you want a PR for this functionality, and then it can be refactored and centralized later, with everything taken into consideration? It might not be a short task given potential browser variations.
Okay great, so
Sure, you would think so logically, but as you say not specified anywhere. I have crafted books where they were in separate files. The Android implementation allows for them being in separate files so I will do the same for Swift. |
Just to give some more information here, the entire purpose on the footnote when I implemented this in kotlin, was to do most if not all the rendering in kotlin vs in js. the javascript code simply passes the html through the click handler. and the rest is done in kotlin, this allows for native views to be shown, and dom manipulation avoided. Also, all html sanitizing is not in kotlin, which was an important part of footnotes. At the time this was implemented there was no click handler whatsoever neither in swift or kotlin. Since then the click handlers in Swift have evolved, but not including any footnote handling. |
Thanks for jumping in @aferditamuriqi
Great, that makes sense and matches what I see in the code. I will do it the same in Swift, basically just try to port the Kotlin. (Figure I will use the SwiftSoup library, so I get a mostly direct translation from Jsoup.)
Indeed. The tap handler in iOS is engineered in a specific fashion and I don't think it makes sense to include |
RFC: Rather than taking the content of the
If you display the bare HTML, you're going to lose any styles that aren't inlined. By using the original document as a framework, you will keep any styles in the (Of course, depending on the exact implementation of step 3, selectors targeting |
@tooolbox Thanks for this great analysis and particularly for pursuing feedback before sending a PR 👍 The strategy implemented in Kotlin by Aferdita sounds good to me, from the native side. And the hiding approach described by Apple for iBooks should probably be followed as well.
That's an interesting point. I tend to think that extracted notes live outside the rendering context of a publication, since they are presented in a native modal, and therefore should not be styled. But I would love to hear more opinions from other contributors. On a side note, we're experimenting right now with a proposal format for changes adding non trivial features and that should be aligned across platforms. Would you consider writing one for handling EPUB noterefs? This doesn't prevent you from implementing a PR which could be used as a proof-of-concept companion to the proposal. You can find a ready-to-use template for proposals here: readium/architecture#129 And a few examples of proposals in these PRs: https://github.com/readium/architecture/pulls |
By the way, we don't want to create the modal directly in /// Called when the user activated a note reference.
/// You should present its content in a modal dialog.
/// Use `link.type` to know the format of the given `content`, e.g. `text/html`.
func navigator(_ navigator: Navigator, presentNote content: String, at link: Link) And then display the pop-up in With this, a reading app may decide to not show a pop-up, but instead to go to the location in the navigator. Assuming that we add a way to disable footnotes' hiding. |
👍
I know that iBooks (when I checked years ago) displays pop-ups without links to external CSS styles. However, I don't see why we shouldn't enable this. Some years ago I had an author who wanted the footnote styles to be preserved, so I had to go through pain to inline styles into the HTML as part of the processing I did to the book. This problem can be avoided by using the original document as a context. Do we gain something by displaying the
The existing pop-up functionality in Android is implemented in the Navigator. That doesn't seem particularly incorrect, because the Navigator is responsible for navigation and this is an alternate method of navigation. The Navigator is perceiving the hyperlink-being-navigated-to and deciding to do something besides shifting over the main viewing window. Now, if we want the pop-up behavior to be customizable on a per-app basis, then to keep Navigator as a clean library, we should have a delegate method. But maybe there should be a default.
Hm... Can't say that I'm overjoyed at the thought 😅 But sure, I'll put something together this afternoon. 👍 |
It all depends how the notes will be presented, and this should be decided by reading apps. For example, a bottom sheet without any UX, dismissed when taping outside its content, would look great with the book's styling. But with other more native components, it would look weird in my opinion.
This strategy sounds great to keep the styles, but it would be doing too much work and be too opinionated in the navigator IMHO. With the
We're moving away from having an opinionated UI in the navigator to have more flexibility in reading apps. That being said, having a default implementation with the most idiomatic native component for that would be perfectly fine. The navigator is still responsible of intercepting the navigation to the footnote link, but the presentation is decoupled and handled by the reading app.
That's really not necessary, I understand that integrating the feature would be more exciting to you ;) |
Great, your logic makes sense to me; I'll add a In terms of adding
That was something I did to hack around limitations in a reading app, it wasn't meant as a suggestion, more like a reason not to limit this app :) |
I'm not sure, maybe @JayPanoz has an opinion on this? In any case, a reading app should be able to disable hiding the footnotes (we don't need to expose the API to disable it right now). So either the CSS is injected manually in the navigator, or we have a ReadiumCSS variable to enable this feature. |
That’s an interesting question as this will probably be the option chosen by a lot of implementations, but I’d be considering it as something different than the existing flags we have in ReadiumCSS as it’s relatively stable – except if it could change on user’s demand, for instance in advanced settings. On first glance, seems to me this is something you likely don’t want to change, and it would be much better as something “static” in your own version of ReadiumCSS than something you have to enable on each load. But again, a CSS variable would help make it a setting if you wanted to let users decide what they want to make of these asides. I prefer to mention that because it seems iBooks’ implementation has suffered from various accessibility issues over time – e.g. I can recall bug reports that Voice Over couldn’t even reach the popup footnote on one platform, so a publisher decided to move away from More globally, I’d lean towards putting this question into context: we’ve regularly seen that CSS might be an issue for some implementers, and they are more likely to try doing things in the app than forking/cloning the repo, customising the modules, and building their own version. So I’m wondering whether we’re not at this point we should try to accommodate popular options such as this one, so that they can just set a CSS variable instead of customising ReadiumCSS. |
I've been thinking that maybe navigating to the footnote in the main WebView should actually be the default behavior of the navigator. Since this is the least opinionated, and relies on standard HTML. The delegate function would look like this instead: func navigator(_ navigator: Navigator, shouldNavigateToNoteAt link: Link, content: String) -> Bool` And a reading app wanting to show the footnote as pop-ups would just return |
Also we'll need to add extensible CSS injection capabilities, which could also address this issue without modifying ReadiumCSS. |
Seems like you could give your
I'm honestly not familiar with CSS variables. Do you have an example of how this would go or what I would need to do to enable this?
Interesting. Maybe if VoiceOver is enabled, the app should inject an override (or alter a variable) which reveals the Also, I'm not very familiar with Voice Over, so forgive me: could it not read the contents of our pop-up window? I.e. is there a reason our implementation would suffer from the same issue as iBooks, or is this a matter of compatibility with books that have been developed a certain way because of iBooks limitations? Also, what's the UX of a person using Voice Over clicking a noteref link; is it reasonable to expect them to be able to do that?
Seems like:
I don't think this is good UX. You get dropped at the end of a chapter--maybe, if the note is in the same file, which they typically are for sure--and there may be 1 or 10 footnotes, and then you have to see which one is being referred to, read it, and then navigate back. The reason why iBooks and some Kindles have pop-up footnotes is because is creates a better UX. I don't think we gain anything by settling for less or crippling the default, as long as we have Voice Over sorted out, right? Maybe my perspective is off, but it seems like if the author puts |
I was actually considering using this variable as a "constant", not necessarily exposed in the user settings. For example, an app could have its own CSS file injected after ReadiumCSS, that would override some of the default CSS variables. Some kind of ReadiumCSS configuration. So not as dynamic as the user settings.
I would think that should work fine, but maybe there's an issue with breaking the flow in the WebView, we'll need to experiment I guess. As an alternative, we could revert to a normal WebView jump when clicking on a noteref, with the
Definitely, I wouldn't expect an app not showing the footnotes in some kind of pop-up. I'm just thinking that this might not be the responsibility of the navigator, which should be as simple as possible – which is already pretty complex – but with a lot of extension points. So far we've used the testapp for "best practices" on the presentation side. I'm just thinking out loud for the sake of brainstorming though, and I'm not sure what's the right solution yet. Considering the 2 main responsibilities of the navigator:
We could have something a bit more generic to intercept a navigation event with added context, and let the reading app decide what to do. Something like that: enum NavigationLocation {
case locator(Locator)
case link(Link)
case resource(Link)
case internal(href: String, title: String)
case external(url: String, title: String)
case note(href: String, type: String, content: String)
}
func navigator(_ navigator: Navigator, shouldNavigateTo location: NavigationLocation) -> Bool |
This is not specific to footnotes and this is something that needs to be addressed for any link. If a user jumps ahead or backward in a publication while following a link, we need to:
IMO saying that a popup is better than jumping to a locator is quite subjective, which is why we should not take this decision on behalf of app developers.
There are many use cases for these two features and this goes beyond footnotes. |
I imagine that jumping somewhere else when clicking on a noteref would break the flow too, right? I'll note that I don't think that the testapp has a
It hurts a little bit, but I can accept it. I do think a footnote popup is very germane to navigation and display of content (the two responsibilities of the navigator, as you say). But I have less experience with these different libraries and components so if you want it in the app, I can respect that.
(This is worth discussing, but in my case I would like to keep my PR focused so I am making a minimum of changes while adding this functionality to match Android. Obviously I don't want to inhibit what you guys are planning but I think this is out of scope for me.) |
As an app developer, I don't think it's very subjective. If the markup is It's even discussed in the DAISY docs: http://kb.daisy.org/publishing/docs/html/notes.html#desc
Seems pretty straightforward.
I'm sure you're right, but I'm curious, what are those use cases? I think listing them out would help give perspective. |
I expect that the WebView will handle basic hyperlink features properly with VoiceOver.
Yes, that's what @HadrienGardeur was talking about, this will be something needed soon. Although an app could already implement it by building a history of the changed locations. But it would be better if we have a way to differentiate if we jumped to a location, or simply turned a page.
We're getting into a broader discussion, and I'm afraid it will be a bit too much off-topic. But here we go. We probably all have slightly different opinions about what should be in Readium and what should live in the apps, but we already defined pretty clearly the scope of the navigators. Now Readium is an ever-evolving project, and we're not yet aligned with this strategy on all platforms. What we don't want to do is limit reading apps, to avoid having implementors forking Readium like with R1. Now I hear the need for basic features such as footnote popups, of which a default, sound implementation could be provided by Readium. And two complementary ways of achieving that have been on my mind lately:
Yes, and thank you for the brainstorming opportunity that your post created 👍 I think for this PR we can stick with: func navigator(_ navigator: Navigator, shouldNavigateToNoteAt link: Link, content: String) -> Bool And have the pop-up implemented in the test app. This way we can improve this later without breaking reading apps, by pulling back the pop-up code in a plugin or something else. |
Sorry but I strongly disagree. There's not much in terms of guidance in the EPUB spec, app developers have a lot of room to decide what's best.
"It allows them" being the most important of the sentence. You're certainly not forced to do it, and you're also free to do it the way you want (pop-up is just used as an example). Don't get me wrong, I have nothing against pop-up footnotes, I just don't think that this is as universal as you think. We're quite opiniated in this project, but when it comes to such UX, it's subjective and should be entirely in the hands of developers. |
There is IMO a balance to find between the flexibility the toolkit must offer (and for that I'm 100% with Mickaël and Hadrien) and the fact that many developers wish to integrate a toolkit which provides most of what is needed by default, i.e. off the shelf features (and there I tend to follow Matt). Could we find this balance by having a default behavior in the navigator (popup with raw text) that can be triggered by the app (which therefore keeps the control with few lines of code) or be unused if the app takes full control of the footnote layout? At the VisualNavigator interface level, it could mean adding a method "displayPopup" with the id of the footnote as param (aïe, issue if the note is in a different resource). |
I disagree. It's not the responsibility of the navigator to handle such UI. From one app to another this could be extremely different: for example there's a move towards using bottom sheets recently on Android, which are modal but not really what one would call a "pop-up".
Add on top of that all the custom components that app developers are free to build... Each app developer will be opiniated not only about how this is displayed (modal dialog, jump to reference, side panel display) but also how each of these UI elements are handled. We can illustrate how APIs can be leveraged to handle this in the test app, but we need to draw a clear line between the navigator concerns and the app concerns. |
It seems to me that the plugins and |
@tooolbox just a comment on:
From what I read in the Apple guidelines and in the Daisy guidelines, the author does not hide the aside element. iBooks hides it automatically when it recognizes a footnote in an aside. It's not a mandatory app behavior however, by EPUB 3 (absence of) rules. Helicon guidelines add "It is advised to put the aside tag at the bottom of the HTML file." which solves a visual issue if the app does not treat the footnote is a smart manner. |
I think we are debating ourselves into a corner, within a vacuum. 😄 If you think regular hyperlinks will be handled correctly by VoiceOver, I'm not sure how pop-ups would be different, but I think I just need stop talking and look at it.
Okay, I read the blurb you linked to and that's pretty clear. I will note that the NavigatorDelegate protocol has a default implementation for handling external links.
Sure :) Sounds like quite a bit of architectural work, but great.
Great, I am more-or-less down with that, I will take up some technical points on that in another comment. |
I have been a little inconsistent throughout this rather long thread, so it's possible you missed it, but I'm not talking about a generic
Maybe you missed it, but one of the last things I said on this was:
So, I'm not trying to limit customization, I'm supporting a sensible default. It would be great if you could give examples of other UX besides a popup (or similar UI) showing the footnote. You seem very much in support of customization and you seem to have specific scenarios in mind but I don't know what they are. (Note that I am specifically talking about the above markup.)
Yes, this is what I was getting at. The prior art is that the NavigatorDelegate protocol has a default implementation for handling external links.
Sure, so not the Navigator, but how about the NavigatorDelegate protocol? That way an App Developer who's using R2 and implementing a NavigatorDelegate doesn't have to implement this UI (or copy-paste it from your implementation) if he doesn't need/want to. You say you disagree, but nothing in your post actually shows why "a fully customizable UI/UX with a sensible default" is undesirable. Hm, if the goal is to keep all UI code out of the
Sure, IMO this is superior to a default method on the NavigatorDelegate protocol, for one reason: to me, the whole package consists of (1) the popup handler and (2) CSS to hide the While I do agree with you, "plugins" is a vague concept in my mind and implementing a default method for a protocol is a few keystrokes away. :) |
You're absolutely right, the hiding is done by the app, not the author. And yeah, good point that it's not mandatory.
Yeah, that makes sense. If I'm authoring a general ePub and write that specialized markup, I am hoping for popups, but I'm definitely putting the |
Okay, so I've opened a formative PR which is what I had written last night, before all of this discussion :) Aside from anything else, it does work. @mickael-menu shortly I will give some more thoughts on that method signature we've been discussing. |
Okay, I wanted to give some notes on the PR:
JS Click HandlerAs was mentioned in this thread, (1, 2) it may be desirable moving forward to generically allow the App to intercept & control what occurs with tapping hyperlinks, and yet I didn't go this route; I wanted to explain my logic a bit more. As I said above, the JS bridge doesn't normally allow Swift to return a value to JS, so you can't do this: function onClick(event) {
let eventWillBeHandledBySwift = webkit.messageHandlers.tap.postMessage(event);
if (eventWillBeHandledBySwift) {
event.preventDefault();
event.stopPropagation();
return;
}
// ...
} Any other options? There's this method I found: // js
function callNativeCode() {
webkit.messageHandlers.callbackHandler.postMessage("Hello from JavaScript");
}
// this function will be later called by native code
function callNativeCodeReturn(returnedValue) {
// do your stuff with returned value here
} // swift
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if(message.name == "callbackHandler") {
print("JavaScript is sending a message \(message.body)")
webView.evaluateJavaScript("callNativeCodeReturn(0)")
}
} ...Gross, and probably not terribly performant. Even if you use promises layered on this, which some people suggested. You can also override the On the other end of this, we have the actual link navigation which we can intercept with You could go the route of what was done in Android, whereby the Swift click handler sets a little semaphore variable that the navigation-policy delegate method uses to know if it should proceed...but that also feels icky (no offense). The above is why I opted to purely detect this specific scenario in the JS: it's not generic, but it seemed the least hacky and most performant. However, it doesn't allow the signature suggested by @mickael-menu func navigator(_ navigator: Navigator, shouldNavigateToNoteAt link: Link, content: String) -> Bool Having that NavigatorDelegateAs I said, I feel like making the I could make it more specific by adding I could also attempt to call Maybe this other thing I mentioned of somehow subclassing the EDIT: Having looked at this, it doesn't really make any sense. The SummaryAs I said, I had done this work, and it's functional, so I wanted to make it visible as a starting point. |
@tooolbox Thanks for the detailed explanation of your PR, that's really useful. For information, we discussed about this issue in our weekly team call yesterday. You can find a summary here. But basically, we agreed that such features should not be in R2Navigator directly, but the navigator should provide building blocks for reading apps to implement them. Now to facilitate usage of default implementations of these features by app developers, a plugin architecture was deemed too complex at the moment. Instead, we considered splitting the R2TestApp project in 2 (but in the same repository):
Yes I think so too, maybe I was ambiguous.
That's a bit different, there's nothing we can do in the navigator with external links, because opening them in the navigator's web view would break everything. You'll notice that the default implementation (opening the system default browser) is different from the recommended one we have in the test app (presenting a In the case of footnotes, we already have a default behavior provided by the WebView: following the link, like any other internal link.
@HadrienGardeur gave a few here (albeit for Kotlin): #139 But to add different kind of examples, you could have different strategies on how to display the HTML: in a
Like you noticed, it gets complicated to present stuff from the context of the Navigator (which should not inherit from About avoiding copy-pasting, I think that the UX library mentioned above should answer this? About the PR:
Do we really need SwiftSoup or could Fuzi do the job as well? If we can avoid another dependency that would be best. If we can't, that's fine, especially since SwiftSoup might be useful to implement CSS/JS injection as well later on.
Yes we shouldn't do that. I'm working right now on an implementation of an
I agree.
Swift String API is garbage... So complicated.
It seems like if we can't solve this problem, that's going to be complicated to not have the pop-up directly in the navigator. Let's focus on that before discussing the rest.
Maybe we could also send information about the current link in |
Why? Web Publications are XHTML5 pages glued together with a JSON manifest. Many Web Publications will be dynamically created out of an EPUB via a Publication Server (i.e. a Streamer). So they may have footnotes using the epub:type notation. |
You're right, many WebPubs will be repackaged EPUBs. However, the WebPub spec itself doesn't contain anything specific for footnotes, so it won't apply for WebPubs created outside an EPUB context, for example from webpages. |
@mickael-menu Thanks for the review & feedback!
Okay, tracking.
Sounds like a lot of work, but makes sense. I do think your team's goals with making this very customizable and helpful are great. I could probably give some feedback at some point or another on my experience with customizing R2, I had one point which I think would help DX but isn't germane to this thread. So it seems like at this point most of the pop-up stuff will be handled in my App, meaning injecting CSS to hide the
Okay, tracking.
I mentioned this over in readium/readium-css#83 but those examples aren't what I was looking for; they're all basically the same UX of displaying the note in native UI, and as long as the dev can customize what the UI is, I was having a hard time seeing why you wouldn't want to use that UI in every case where the markup indicated it. The question was "in what example scenario, given the aforementioned markup, do you not want to display the note in the native UI of your choice?" Regardless, @JayPanoz did provide an example of an interesting implementation where the use of a pop-up could vary based on the screen width. Actually, you would probably do a media query and set
Tracking, makes sense...
I don't follow you here. I understand that the Navigator isn't meant to be used only with EPUB, but if the markup is
👍
Huh! I will scope out Fuzi and see if it will work.
Interesting... I mean, it must display something, right? But I get your point, we won't go this route. I'll discuss the API/method signature next. |
Any feedback, especially concerning pain points integrating/extending Readium, is most welcome! We're trying to improve a lot in this area right now, so it's the right time to take current issues into account. We have Slack channels if you're interested in discussing more informally about that.
I see, then indeed I'm not sure if there are much more viable options besides normal webview jumps or footnote popups.
My bad, my point was unclear and ambiguous. It was one potential reason for a reading app to want to keep normal hyperlinking behavior: to have the same footnote behavior across all formats: EPUB, and the formats not having
Thanks, but don't worry too much if it doesn't, I considered integrating SwiftSoup at some point for the JS/CSS injection.
In the reading app, there's a |
👍
If it didn't have Otherwise, while this is a bright idea, and I do think you're right about As an idea, we could perhaps somehow inject relevant data into the URL. Add a query param, change the scheme... Actually, if the delegate method returned
Yeah, I'm not too keen on the whole semaphore thing between a click handler & |
Yes that's an interesting solution, definitely worth exploring.
Actually I wasn't talking about a semaphore, but simply sending a message through |
@tooolbox I just checked and it seems to be the case, so you could potentially send information about the upcoming navigation action right from the click handler. That might potentially break in a new version of the |
Sorry, that's kinda what I mean by semaphore, i.e. a variable or flag shared between two different execution contexts—normally concurrently, but not in this case. A similar thing is implemented in Kotlin whereby the click handler sets (Specifically, what I understand is that you're saying is to use
Hm! Okay...okay...I think I get your concept and I'm liking it more. I felt weird about a simple "allow/disallow" sempahore between the click handler and I'll take a shot at implementing it today and let you know! |
… navigator, made touch handling always send data across the bridge, noteref delegate method returns bool.
Okay @mickael-menu so I re-worked it, a few points:
I'm pretty happy with it at this point; if you're fine then we can move into a formal review from the code owner. |
@tooolbox That sounds great! I'll follow up in the PR itself on minutiae, once I review the code. I can see that being used by reading apps to intercept other kind of links 👍 Thanks a lot for this great contribution and interesting brainstorming. |
The r2-navigator-kotlin has a click handler that is bridged from JS that checks for
<a epub:type="noteref">
and displays the target content in a modal.Relevant code:=
and
The same could be implemented for iOS.
Current click handling:
In terms of implementation, I'll start with comparing to the Android version.
In reading through the behavior, it appears that all taps (on any element) call the Kotlin function before the event is propagated through the DOM. The Kotlin checks to see if the tapped element is
a[epub:type=noteref]
and if so, identifies the target material. It's important to do this in the host code, rather than JS, so that you can access whatever file is being targeted. If the Kotlin func successfully displays the modal, it does a little semaphore action (so to speak) to the WebView delegate to cancel the navigation. (That last bit is a little convoluted sinceshouldOverrideUrlLoading
appears to be employed in reverse, but that's what I gather.)In contrast, the iOS click handler explicitly avoids calling across the JS bridge to Swift for any interactive elements, including
<a>
tags. The recursiveisInteractiveElement
JS function references this https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling regarding don't-mess-up-the-author's-scripts.The test is here and looks like:
along with
I am tempted to check for
epub:type=noteref
in the JS and call a new, special Swift handler specifically for this purpose. I think that links of that type should be handled by the app and any user scripts are forfeit.The other question in my mind is what to do on the targeted content, because typically (in my understanding of things) it should be hidden. Not sure if the author should have to
display:none
it or if that should be part of the styles injected by the app...I lean towards the latter.I intend to do this work over the next day or two, but I wanted to ask for feedback before my PR leaps full-armed from the brow of Jove.
The text was updated successfully, but these errors were encountered: