-
Notifications
You must be signed in to change notification settings - Fork 539
[WIP] Simplify UI.Highlighter API #452
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
Conversation
Marked as WIP because the highlighter plugin should also be updated. |
31d7909
to
28844a0
Compare
I just added a commit which removes the Highlighter plugin. The code for this is all within the DefaultUI plugin. We should have a conversation about this. I'm very tempted by the following after discussing with @azaroth42 today and thinking a bit more about the future:
So the proposal is that it's much better for most non-trivial uses of Annotator if we don't have plugins that wire themselves up on I would suggest that our DefaultUI instead be a legacy Annotator that defines the As a user of Annotator, I would much rather just write my application and use the tools Annotator gives me to construct it. function MyApp (element) {
var annotation, interactionPoint;
this.storage = OaHttpStorage('https://annotateit.org/oa');
this.adder = new UI.Adder({
onClick: function () {
storage.post(annotation).then(function () {
var highlights = this.highlighter.draw(annotation._local.ranges);
annotation._local.highlights = highlights;
});
}
});
this.highlighter = new UI.TextHighlighter(element);
this.selector = new UI.TextSelector(element, {
onSelection: function (ranges, event) {
annotation = {
_local: {ranges: ranges},
hasTarget: {
hasSource: document.location.href,
hasSelector: {
exact: ranges.map(function (r) { return r.toString() }).join('')
}
}
};
interactionPoint = Util.mousePosition(event);
adder.show(interactionPoint);
});
};
} |
So, anyway, toward that direction, this PR simplifies the API of I don't really think there's any need at this point to bless a particular internal model. We have a bunch of useful components now. Let's just use them for a bit an see what happens. And without a plugin API in core we're free to make a legacy Annotator application that implements it in a backward-compatible way, being an event emitter and firing the old events. |
Rather than working with annotation objects, the `UI.Highlighter` component receives ranges directly and returns highlights. It is now the job of the caller to determine what to do with those highlights and when to undraw/redraw them.
28844a0
to
b142562
Compare
|
||
loader(); | ||
}); | ||
}, |
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.
This block belongs back in a highlighter.drawMany() method. This glue code shouldn't be concerned with the performance of rendering many highlights. By putting it back into the highlighter module anyone wishing to render multiple annotations gets the benefits of this code without having to re-implement 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.
I'm not so sure. The draw function already takes an Array of Ranges and returns an Array of Highlights. For a drawAll
to be useful to the place it's been used, loading many annotations, it would be expecting an Array of Arrays of Ranges and would have to return an Array of Arrays of Elements. That may be getting a bit busy unnecessarily. @nickstenning clearly felt some desire to give the developer more control over how to optimize this, because he had options on the UI.Highlighter
to control the chunk size and interval. I might rather let the developer do that if it fits their needs. For instance, other approaches might be to use requestAnimationFrame
, some other async flow control library of choice, etc.
Do you think it's too onerous to push this up to the caller?
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 it should definitely be using requestAnimationFrame
. I'm pretty sure that method wasn't very widely supported (if it even existed) when the code was originally written.
Do you think it's too onerous to push this up to the caller?
I look at this file and what it's doing and this particular block of code looks completely out of place to me, which is a general indication that it should be moved either down into the module or into some kind of helper 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.
Definitely agree with the last. Let me see about a prettier helper function 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.
See what you think now.
I've left some comments on the implementation here, I think it's a good start but moves too much UI code out of the UI module. I need a little more time to think about the larger direction changes to how plugins work, in general though the plugin API is in flux, and suggestions like this are a good thing. |
Now with rAF sauce. Love me some browserify. |
So, I like some of this, but like @aron I think some of the highlighter's concerns have been pulled out too far. If I can try to summarise the main concern that has led to this rejig: I agree completely with this assessment. However, by moving as much as you have into Perhaps a better compromise would be for I also think |
More generally, I'm not sure about the above. Certainly, we should make it possible for people to wire up their own Annotator applications as they wish, and they can already do this by reusing the components from But there's also a class of users that falls somewhere in between the extremes of "plug and play" and "hypothesis" -- the kind of users that might want to be able to add specific lifecycle plugins with default behaviours. For example, maybe I have a static set of annotations on a document, but I don't want to allow people to create or edit annotations. I'd like to be able to do something like: annotator
.addPlugin(Annotator.Plugin.Highlighter(element))
.addPlugin(Annotator.Plugin.Viewer(element));
annotator.registry.annotations.load(query); This is more-or-less possible at the moment, but not if you remove the |
+1 I think we've seen this many times on the mailing list over the years. And I think having to wire up the UI elements to do this creates a high barrier to entry. |
To put in Stanford's support for the change, we want to reuse the framework with modular components and fine-grained control of where and when annotators can be instantiated. It seems that by decomposing the code it makes this much easier than the current plugin architecture and defaultUI. .addPlugin() could be a function of LegacyAnnotator rather than the baseline. The functions on LegacyAnnotator would then implement the current model of calling the hooks on each of the plugins in the order added. This gives the opportunity for other extensions of the base Annotator to be smarter about which order the components are used in, getting rid of the potentially infinite beforeBeforeBefore...AnnotationCreated hooks. |
Before this PR, the
That's fine, but such a plugin would be dependent then on finding What we end up with when plugins try to respond to generic annotation hooks is a situation where sets of plugins are all sharing implicit assumptions about the data they receive. We should, at the very least, do this:
I hate the hooks as much as I hated the events. Now we call them hooks because they weren't being used like events but they're still a crap way to orchestrate components. A hooks system is fine, but a hooks system to allows multiple subscription and does it well and allows for flexibility without a proliferation of |
Here's my high level summary of the situation. There are three kinds of things we can build:
I think we used to have only (3). Now we have (1) and (3), but it needs some work and this PR is an example. We probably, eventually want (2), but we don't know the shape of that yet. I don't think we will know until we build several different applications with (1). I know it shouldn't be a generic front-end JS framework. I think it should have some configuration-driven setup, involve things like what @csillag has for an anchoring manager, and reduce boilerplate for applications. A good registry is a key component of this, but the current registry is next to useless (if the plugins receive a registry then the registry is where the plugin API belongs, so plugins can load other plugins and plugins can share settings, at which point it becomes possible to shift the knowledge about shape-of-things and property names, options, et al to one place). |
function Registry() {
this.components = {};
// This is important.
// It's how plugins share config, so they don't have to make assumptions.
// It increases their re-usability.
// It also means they have a place to get their setting so that they don't take another argument.
// That allows us to call `.include()` and pass the constructor, rather than instantiating an Object
// that then gets passed to `addPlugin`, important so that `addPlugin` can be idempotent, which
// allows plugins to include their dependencies (if they depend on other plugins).
this.settings = {};
}
Registry.prototype.include = function(componentFactory) {
// Go through this.components and if componentFactory has not been included yet, invoke it, passing the registry, and include it.
}; This is the direction I was going at one point. @nickstenning are you sure you don't want to go this way? |
The registry is also the place for a hooks API, so plugins can fire hooks and subscribe to hooks. But we need to make a decision. Hooks should either a) have only one subscriber or b) have no defined return value. Having both is a recipe for awful plugin ordering issues and whatnot. |
Pyramid and Angular are two of the best examples I've experienced of frameworks that successfully allow for patterns of composable / extensible applications. Admittedly, my direct experience with other frameworks is somewhat limited, though I did extensive research and this belief was the primary decision driver for using both in Hypothes.is, where I expected to need to white label and remix applications for different deployments. Pyramid (Zope Component Registry) and Angular (Dependency Injection) both provide two things I think are crucial for this kind of modularity:
This is why I believe that we need:
|
Without these things we have an Annotator application which isn't really very modular nor easily extensible (very limited ability to mix and match plugins). If we want the plugin pattern to be useful, we should abandon the way hooks are done, we should pass settings on the registry, and plugins should be able to require other plugins. Otherwise, we should support it as a LegacyAnnotator implementation for old Plugins in the old style for backward compatibility and say nothing of plugins in the core. At one point I had been trying to do two things at the same time, and I think we still could:
I did also have the registry holding properties it then transferred to the Annotator, which I realize now was stupid. However, I think the general direction was right. |
Going to close this for now. |
Rather than working with annotation objects, the
UI.Highlighter
component receives ranges directly and returns highlights. It is
now the job of the caller to determine what to do with those
highlights and when to undraw/redraw them.