WWDC2010 Session 506

Transcript

>> Good morning.
My name is Adam Roben.
I'm an engineer on the Safari and WebKit team and
I've been working on extensions in Safari for a while
and I'm really excited to talk to you about them.
So if you were at the Safari Internet and
Web State of the Union on Monday you heard
about how Safari 5 includes support for
building extensions for new developers;
and if you were in the last session just before this one you
heard a lot about what extensions were and what they can do
and how you can get started building them.
So let's start with a brief overview
of what we saw in the last session
and then we'll move into the topics for this one.
So over the last hour you heard about
how Safari is made up of multiple windows
and each window can have multiple tabs
in it and each tab has a web page.
And Safari's user interface also has bars and toolbar items
like the bookmarks bar and the back and forward buttons.
So extensions can add into this experience in Safari.
An extension can include in it other toolbar items that
can be added to Safari as well as context menu items
that Safari will show, and one or more bars that
will be included in Safari's user interface.
Also if you want to modify web pages with your extension
your extension can include content style sheets
and content scripts that will be run in
the web pages that the user browses to.
And finally your extension can include a global
page that's kind of the control unit that you use
to tie all these pieces of your extension together.
Now in the last session Tim also talked about how extensions
and Safari are both split into an application layer
and a web content layer; and how there's a
separation of communication between these two
that can be bridged using message passing.
So Tim gave you an overview of all of these things and he
also went into a lot of detail on the application layer.
Now we're going to mostly focus on the web content
layer and then how to tie the two together.
So specifically, we're going to be talking about three
things; how to modify web pages behavior and appearance;
how to add items to Safari's context menus and
how to publish new versions of your extensions
so that your users can get the latest versions.
So first we're going to talk about how to modify
web pages and maybe the best way to start by talking
about it is with a demo to show you what's possible.
So here we are at vimeo.com.
The folks at Vimeo have made a great HTML5 video
player that's all built using standards technology.
And if you were at the Delivering Audio and Video Using
Web Standards talk yesterday morning you learned a lot
about how you can do this on a website as well.
So I've made an extension that modifies
this website to add a new feature to it.
So I'll install it here; I'll just double-click on the
extension and Safari will ask if I want to install it
and now if I reload this page you'll see that we're
going to add a new button over here on the side.
We have a new lights button here.
And if I click on it, the rest of the page dims out
so that you can really focus on the video itself.
So this is just one simple example
of what you can do with an extension.
So let's talk about how this is possible.
Again, we're going to be focusing on the web content layer.
That's the layer that can actually interact with
web pages directly and so specifically we're talking
about content style sheets and content scripts.
Now as Tim told you in the last session content style
sheets are added to the web pages that the user browses to.
If you're familiar with Safari's user style sheet preference
in the Advanced preference pane you already
kind of have an idea of what these can do.
A style sheet can be applied to a page to modify
the size of the text, the colors on the page
or the images or to hide certain elements.
Now the style sheets are called user style sheets and
that has a very specific meaning in the CSS specification.
But all that you really need to know is if you want to
override rules that are already in the page with rules
in your style sheet, you just need to
mark them with the !important keyword
and that will overrule any rules that the page has.
So there are two important differences
from Safari's user style sheet preference.
You can have more than one content style sheet
where Safari can only have one user style sheet,
and you can apply your style sheets to specific pages
while Safari's user style sheet is applied to all pages.
And we'll be talking more about how to do that in a moment.
Now content scripts are very similar
to content style sheets.
They also run in the page.
Content scripts can run at two specific
points while the page is loading.
We call them "start" and "end".
The start time is roughly equivalent to
when the DOMContentLoaded event is fired.
This is right after the main document of your page
has finished loading and if DOM is available to you.
But the sub resources of that page have not finished
loading yet so images on the page and sub-frames
and scripts may not have finished at that point.
Now the end time is roughly equivalent
to when the load event fires and that is
after all those sub resources have finished.
These content scripts have access to special APIs that
are only available to extensions, and the APIs are similar
to the ones that are available in the application layer.
But the content layer APIs are a little more limited
and this is to enhance the security of your extension.
These content scripts are interacting directly with the
page and we want to provide a minimal interface so that
if your extension is compromised the web
page won't really be able to cause any harm.
Now just as in the application layer
all of these APIs are available
through the Safari namespace object
so it's very easy to find them.
And just as with content style sheets you can have more than
one content script and you can apply them to specific pages.
So let's talk about how you would modify only
specific web pages instead of all web pages.
Well they're really two settings that are important here--
one is the website access setting and one is the whitelist
and blacklist and you can configure
both of these in the Extension Builder.
Now both of these settings are based around
URLs so it might be a good idea to just talk
about what a URL is as far as extensions are concerned.
So here's an example URL; you've
all seen them before but as far
as extensions care there are really
only three important parts to a URL.
First on the left before the first colon is the
scheme in this case http; between the double slashes
and the first slash after that is the host often
also called the domain, so here it's www.apple.com;
and then from the first slash after the host to the end of
the URL is the path; so here it's /Safari/what-snew.html.
Now you may know that URLs can also have other parts
such as user names and passwords and ports and queries
and fragments, but as far as we are concerned in the
extension for setting what pages your extension is going
to modify these three parts are all you need to worry about.
So now that we've talked about what a
URL is let's talk about these settings.
Website access is the top level setting that
affects what web pages your extension can run on.
So it controls both where you content style sheets and
scripts are injected, and it also controls what websites
and servers you can send XMLHttpRequests to.
Tim mentioned in the last session how content scripts
are running in the security context of the webpage itself
and so are subject to all the same
restrictions of that page which means
that from a content script you can't
send an XMLHttpRequest to another server.
But in the application layer we do allow that and
this website access setting is how you specify
which servers you need to send those requests to.
Now the website access setting only cares
about the host or the domain part of the URL.
And we actually allow you to specify
patterns so let's take a look at what some
of the potential patterns are that you could list.
So this first one here is just the host or domain
that we had in that last URL www.apple.com;
and this will match just that single host.
Another one is apple.com and that will match
just apple.com; it won't match www.apple.com.
But if you wanted to have your extension to be
able to access both www.apple.com and apple.com
and maybe also trailers.apple.com and any other
subdomains that exist, you can write *.apple.com
and this will match all of those hosts so
you don't have to list them separately.
Now there's also one more part of the website access
setting which is the Include Secure Pages checkbox.
If you have that checked, then you could access for example
the https version of www.apple.com and the http version.
If you don't have it checked, you
won't have access to the https version.
So the whitelist and blacklist is the other
setting that I mentioned and this controls just
where your content scripts and style sheets are injected.
And the control is more fine grained
than the website access level.
You can actually target specific paths on a particular
domain instead of just targeting a whole domain.
And for that you have to use something that we
call URL patterns so let's take a look at some.
So here's a very simple URL pattern-- it's
just that same URL that we looked at before,
and so this will match that particular
page and no other ones.
But if you want to start matching more than a single
page you can start using stars to act as wild cards.
So in this case we've replaced the "www."
with "*." just like we did in the domain
pattern and so this will match this scheme
and path on any apple.com or apple.com subdomain.
So here are some examples-- it
will match it on www.apple.com;
or on just apple.com or on any other subdomain.
It can even be more than a single level of subdomains.
So here's another example where we've put the * in the path.
This will match sites that are only on www.apple.com
but anything could come before the
"what-s new.html" part of the path.
So this would match Safari's What's New page or iTunes'
What's New page or again you could even have multiple parts
of the path in there that the * will stand in for.
So a very common thing you might want to do is to match
any page on any subdomain of apple.com or any other server.
And so this is the form that that pattern would take.
We have *.apple.com as the host which will match
apple.com or any subdomain; and just /* as the path
which will match any path on any of those hosts.
And so all of the URL's that we've seen so far would match
this pattern and many others that I haven't gone over.
Now I should also note that in all of
these examples I've used http as the scheme
but you could also have https to match the secure versions.
So let's look at now we could actually put together this
Lights Out Demo now that you've kind of learned the basics.
So here we are back on Vimeo-- I'll turn
the lights back on and actually I'm going
to uninstall the extension so that
we can start from scratch.
So I'll go to Safari's preferences and
uninstall it and now we're back to a clean slate.
If we reload Vimeo you'll be able to see that our
button is gone because we've uninstalled that extension.
So before we actually build the extension I think it
might be good to look at the code so we get an idea
of what this extension is actually doing.
There are two main parts to it-- there's the button
that we add and then the overlay that dims out the page.
And so let's take a look at the code for that.
First we have a content style sheet
and this style is the overlay.
Now we actually don't need to style the button at all
because Vimeo has set up their style sheets in a way
that we could just take advantage of their built in
styles since they built their player using HTML and CSS.
But here we have a rule that is for our overlay div.
It's a fixed position div that covers
up the whole viewport and it starts
out as transparent so that then we could fade it in.
It has a black background color and we've
set a WebKit transition on it to give
that nice fading effect instead of
having it just pop in immediately.
Now we also have a z-index property here to put
it in front of all the elements on the page.
But of course we want the video to be in front of
the overlay and so we also have a rule that we apply
to the video that increases it's c-index so it isn't
in front of the overlay and doesn't get covered up.
So here's the content script that
we actually run in the page.
You'll notice that at the beginning of the
script we start running code immediately.
We don't add any event listeners of wait
for a load event or anything like that;
and that's because as I said you can
specify that your content script run
at either the start or the end part of loading.
And so this is an end script here and so we're running
when the load event has fired so we don't need to listen
for the event explicitly-- Safari
will just run us at the right time.
So as soon as the script is run we call the Add Lights
Button function and you can see that function here.
It just is using normal down calls to create a
button element, to add an image into that element
and then to add all of these into this side doc
element that's built into Vimeo's HTML5 player.
You'll notice here that we're using
the Safari.extension.base URI API
so that we can reference an image
that's actually inside our extension.
So once this is in the page we also
set up a click event listener for it
to call this toggle lights button
whenever the button is clicked.
And toggle lights you'll see right down here is very simple.
If the lights are on then we turn the lights
off; and if they are off then we turn them on.
So the way that we turn the lights on and off, here's the
turn lights off function and we just create that overlay
if we haven't created it before; we set
its opacity to be our maximum opacity value
so it will fade it from transparent to opaque.
And we also apply a class name to the video that's built
into the player so that we can add that c-index property
to it to pull it in front of the overlay.
You'll notice that the class attribute that
I've used here and the ID that we're using
for the overlay itself are all prefixed based on my
extensions bundle identifier and that's a good way
to isolate your rules from those in the page.
So finally when we turn the lights off we just update
the image that we're using for the light button
so I guess those nice rays coming out of the side;
and we set our global lights on variable to false.
So turning the lights back on is very similar, we find the
overlay, we make it transparent, we switch the button back
to the old image, and then we wait for the fading
animation to end by using the webkitTransitionEnd event
and when that happens we remove the overlay from the
page entirely and remove that class name that we added
to the video to put the page back in the
state that it was before we had done anything.
And again just to show you that there
isn't any special magic going on here,
here is that create overlay if needed function.
It's just using normal DOM calls to create a div and to give
it our overlays ID so that it will get the right styles,
and adding another click event listener so that the
lights will turn on when you click anywhere on it.
So now that we've seen these, let's see how to actually put
it together into an extension using the Extension Builder.
So I'll come up here to the Develop
Menu and go to the Extension Builder
and I'll click on the + button to make a new extension.
And we will call it Lights Out
and let's put it on the desktop.
So here it is as Tim showed you the Extension
Builder fills in some things for you automatically.
Now the first thing we have to do is set
that website access level that I mentioned.
So it starts out as none meaning that
you can't access any web pages at all
but we'll change it to some since we want to access Vimeo.
So I'll add a new domain pattern and I'll do *.vimeo.com.
So now we need to add some of these files to our extension.
So I'll open up the extension folder here that the
Builder created for us and I'll find those files
that I already prepared and they're right here.
We just have the style sheet and the script they already
saw and then the two images for the light button.
So let's add those to our extension.
And the Builder will automatically detect that these have
been added so we just have to come back to the Builder
and tell it about our end script which is right here
in the popup menu and the style sheet that we added.
Now down below here we can specify whitelist and blacklist.
But Safari actually makes this a little bit easier for you.
If you want to access every web page on all the
sub-domains of vimeo.com, Safari will actually add
that whitelist pattern for you-- the one
that I showed before that *.apple.com/*.
It's done the same thing here for
*.vimeo.com based on the website access level.
All right so let's install this and see what happens.
So I'll click Install and we'll reload Vimeo and you can see
that the lights button has been added and if we click it,
[Laughter] I guess it doesn't have
as much power as we thought.
It only turned off the lights on the
page but it does seem to be working
so you can see how the Extension Builder makes
it very easy to put these pieces together
and get your extension working right away.
So we've written this extension and it works pretty well
and we sent it out to our users and people are loving it.
Well one thing that we've heard from some users is they
really don't like that exact shade of gray that we chose
for that overlay and they'd really like to customize it.
So an obvious way to do this would be
to add some settings to our extension.
Tim showed how your extension can
have settings that are right
in Safari's preferences just alongside all
the other preferences that Safari contains.
So we'd like to do this for our extension.
But as Tim also mentioned in the last sessions settings
are something that are up in the application layer
and of course our content script
is down in the web content layer.
And so we're going to need some way to get the
information about the settings from the application
down into the content script and into the page.
So the way to do this is through messages.
Messages are implemented as events
in Safari and they're very simple.
They really just have two parts; there's a name and
there's some optional data that comes along with it.
So here is some examples.
We could have a message where the name is "get-setting"
and the name of the data of that message is "opacity"
to say what setting value we want to retrieve; or we
could have a message where the name is "show-bar".
You know if you only had one bar in your extension you
might not need to say anything other than show that one bar
that we have and so here we don't have any data.
Here's another message called "send-xhr" and you
can see that we're sending an object as the data
of the message telling it what server and
what method to use for this XMLHttpRequest.
And again this is useful if you need to be
sending an XMLHtdpRequest to another server.
The content script can't do that directly
but the application layer can do it for you
and so this message will let that happen.
So these messages are sent using two proxy objects.
There's one to go in each direction
from the web content layer
up to the application; and from the application back down.
So going from the content back to the application you
would do it like this with safari.self.tab.dispatchMessage.
Now this tab object while it looks like the
tabs that you see up in the application layer,
it's actually just a tab proxy and mostly the only thing
that it can do is send a message up into the application.
Going in the reverse is very similar; from the
application to content you'll find any tab object
so here we're getting the active tab in the front
most browser window and you use its page property.
Now this isn't the actual web page;
it's a page proxy and you can use it
to send a message using dispatchMessage
again down to the real web page.
So an illustration of what we're going to do
in our extension to implement these settings,
when the page loads we need to find out the
current setting value so as soon as the user clicks
on the light button the lights
will go out to the right level.
And so to do that when the content script runs
it will send a message up to the application
and in this case the global page is going to receive it.
Now you could also send a message and have a bar receive it.
So once the global page gets that message it's
going to retrieve the opacity value out of settings
and make a new message and send it back down
to the script where the script will act on it.
So let's see how to do that.
So the first thing that we need to do is in that
content script we need to send that message up as soon
as the page loads to find out the preference value.
So let's go back to our content script and if you'll scroll
down a little bit farther you'll see
that I've already written that code.
So as soon as the script runs we'll send a
message that we're calling get maximum opacity
and we also add an event listener for the message event
so we can receive messages back from the application.
When we receive a message event we first check its name,
and if that name is the set maximum opacity message
that we are expecting back, then we update
our maximum opacity global variable.
And we also update the overlay in case it's already showing.
These messages are sent asynchronously
and so it's possible although unlikely
that the user could have already interacted with the page.
So an update overlay was needed.
Here we just have normal DOM calls
to find that overlay element
and update its opacity value so it's all very simple.
Now I mention that we are going to be using a global
page to receive these messages so let's look at that.
This is our global page it's just a normal HTML page and
it adds an event listener for the message event to receive
that message from the content, and when we get that message
it's that get maximum opacity message that we are expecting,
then we send a message back down to that
content script and tell it the maximum opacity
by retrieving out of the settings object.
Now you'll notice that we're using event.target.page here
to send the message back down into the particular page
that sent us the message; event.target is the
tab that contains the page that sent the message.
So there's actually one other time that we would like to
be able to update the opacity of this overlay and that's
if the user changes their preferences
while the overlay is already visible.
And so there's actually a way to do that.
Every time a preference changes we send
a change event to the settings object
and so the global page listens for that event here.
And when we get that change event the event
will have a couple of different properties on it
that help us determine what has happened.
So the first one is the key property.
This tells you which setting actually changed so in this
case if it's our maximum opacity setting that we're going
to loop over all of the windows that are open and all
the tabs in all of those windows and send a message
down to each one of them telling
them about the new opacity value.
And we'll just use that same set maximum
opacity message that we used before.
And the way that we get that new value
is it's right here on the event--
event.newvalue gives you the new value of the preference.
So let's see how to put this together into our extension.
We'll come back to the Extension Builder and really the
first thing that we need to do is to get this global page
into our extension folder so the
Extension Builder can find it.
So let's open up our extension and we'll copy
that global page over that I've already written.
And so now that it's in our folder the
Builder will find it and if we open
up this menu it says global.html
that's the one that we want.
And now we also have to add the setting.
If we were to go into Preferences right now,
you could see that we don't have any UI here
for setting up what the opacity of that overlay is.
So I'd like to add a slider so that users
can really tweak it to their exact liking.
So we'll come down here to the bottom of the
Builder and click on the New Setting item button.
And now the first thing we have to choose is the type;
you can see there are a lot of different controls here
like text fields and checkboxes and popup
buttons but we're going to go with the slider.
We'll give it a title which is what is shown in the
UI to the user so we'll call it Overlay Opacity.
The key is the settings key that we use in the API to
retrieve the value so I'll call that Maximum Opacity;
the default value we'll set to 0.8 which is the
same one that we had in our content script before;
the minimum and maximum value, we'll have the
slider go from 0 to 1 because those are the values
that the CSS opacity property accepts;
and then we'll set the set value to .01
so users can really tweak to exactly the way they want.
So now if I click reload and we
go back to Safari's preferences
and click on our extension you can
see that we have a slider already.
I didn't have to write any code; the
Extension Builder did it all for me.
And if I reload Vimeo so that our content script will
be updated, if we turn off the lights and then come back
to Preferences here we can actually slide that slider around
and see the opacity changing live
because of that change event.
[ Applause ]
>> Adam Roben: So let's go over
what we've learned in this section.
We talked about how to use injected content meaning
style sheets and scripts to modify web pages;
how you can use URL patterns and domain patterns to specify
what web pages you modify; and how you can use messages
to tie together the web content layer and
the application layer of your extension.
So next up is adding context menu items and
again we're just going to start out with a demo.
So I'm going to uninstall our Lights Out extension here and
install that same Twitter extension that Tim was showing you
so I'll just double-click on it and choose to install it.
So again we have that Twitter bar here
that Tim already showed you but I'll hide
that for now because we don't actually need it.
So the basic idea of this context menu item is that I
am a new Twitter user and I don't have a lot of content
on my Twitter page yet and I would like to get some
so this extension has made it really easy for me.
If I'm browsing around the web and find something
interesting that I really think people should know
about like WebKit is an open source browser engine;
that sounds pretty important so I'll just right click
on that text and you can see in the Context
Menu we have this new item that's been added
by the extension that's called "Tweet This Text".
And so if I click on that the extension will send that text
off to Twitter using Twitter's API and post it to my page.
There it is and we even have a link back to the original
page so this makes it really easy to add new tweets.
So let's talk a little bit about how
you could do this in your extension.
So in order to add context menus in your extension
first you need to know how they work in Safari.
So the idea is very simple when the user right clicks
on a web page, WebKit itself makes a context menu;
but before that context menu is shown to the
user it's sends the context menu up to Safari
and gives Safari a chance to customize it.
And Safari in fact does this; it adds Safari-specific
context menu items such as open link in new tab.
Now extensions do something very similar; in
fact the beginning of the process is identical.
The user right clicks and WebKit creates a context menu,
it sends it off to Safari and Safari gets to customize it.
But then before the context menu is shown to the
user, Safari gives the context menu to each extension
that is installed and the extensions
can add their own items.
So the context menu items before
we go any further are very simple.
They're very similar to the toolbar items
that you saw in detail in Tim's talk.
They are made up of four parts.
There's an Identifier which is how you refer to the item in
your own code; there's a Title which is shown to the user
in the context menu; there's a Command string which is just
a string that you use internally to represent the action
that this context menu item performs;
and then they have a Disabled state.
Now we never show disabled items to the
user they would just clutter up the menu
and so if you have a disabled item it's
actually hidden from the menu entirely.
So the way that you get to add your items
to the context menu before it is shown is
by listening for the contextmenu event.
This is sent up in the application layer.
Specifically it's sent to the tab and so that's
the target of the event and then it bubbles
up to the window and then to the application object.
On this event is a context menu property and that
context menu is how you interact with the context menu
that will be shown and it has a
few methods that you can use.
There's appendContextMenuItem, insertContextMenuItem,
and there's a contextMenuItems array
that gives you access to the items that you've added so far.
Now this array will only include
the items that your extension adds.
You won't be able to see WebKits' items or Safari's items
or items from other extensions, and this prevents extensions
from interfering with each other or with
the default behavior of the browser.
Now you can also specify these items in the Builder.
If you find that you're adding the same item every time
it might be easier just to specify it once in the Builder
and Safari will add it to the context menu for you
without you having to listen to the contextmenu event.
Now the other events that come along with
context menu items are the "validate:"
and "command:" events that you also use for toolbar items.
Now the validate event is sent for
each item just before the menu is shown
and this is your chance to disable or hide those items.
This is particularly useful when you've
added those items in the Extension Builder.
Safari will put them in the context menu for
you automatically but maybe you don't want
to show them right now and so the validate event
is your chance to hide them before they're shown.
And then the command event is sent when a particular item
is clicked and that will tell you which action to perform.
So of course with context menu's the
real point is that they're contextual.
What is in the menu and what happens when you click on it
depends on what the user right clicked in the first place.
And so you need to find this out to
really provide a good contextual menu.
Now of course as I said the contextmenu
event and the validate
and command events are all sent up in the application.
But the user is right clicking down into the right content
layer and so you need to find out what was clicked on.
Now you could just do this using messages but
there's actually an easier way and a better way.
The way to do it is in the content meaning a content script,
you can listen for the contextmenu event in the web page.
This is something that's part of the
DOM, it's been in WebKit for a long time
and in the past really the only useful thing you could do
in this case was to prevent a context menu from showing
at all say if you had some custom
menus to show in your page.
But with extensions you can do something new.
We have an API on that tab proxy object,
safari.self.tab.setContextMenuEventUserInfo.
And this is your way to associate some bit of
information with this particular contextmenu event.
So after you've called this in the content script, then
up in the application when you receive that context menu
or validate or command event, this UserInfo will be
available to you as a property on the event itself;
and so you can use that context to determine whether to hide
or show a particular item or exactly what action to perform.
So let's look at how we do this or to tweet this text item.
So I'm going to go back to the Builder and I'm
actually going to add the Twitter extension
which we've already written so instead of
choosing New Extension I'll choose Add Extension
and I'll navigate to where we have it saved.
And so now here it is in the Builder
just like you saw it from Tim.
Now if we scroll down you'll see that under
Context Menu Items we've added an item here.
So the name of it is "Tweet This Text" and the
identifier and command are also both "Tweet This Text".
So Safari will add this item for us so now
let's look at the code that handles the item.
First we have a contents script that
listens for that contextmenu event
in that web page again this is the DOM contextmenu event;
not the one that we send up in the application layer.
When we get that event we call
that setContextMenuEventUserInfo API we pass the contentmenu
event and then we get the selected text on the page.
Now if there's no text selected this will just be an
empty string and we'll be using that information shortly.
So now up in the global page that's where we
listen for the validate and command events.
You've already seen some of this code in Tim's demo
so here's our command event listener
and here's our validate event listener.
So first let's look at validation.
Here we are in validate command now if the command that
we're validating is "Tweet This Text" command meaning
that this is for that context menu item,
we choose to disable the item in some cases
and remember disabling really means that it will be hidden.
So the case where we disable it is when the user
info which is the selected text in the page is empty.
When the length is zero we hide the item
because there's nothing to tweet in that case.
Now when we receive a command event that
code is right up here in Perform Command.
So we have another command that we use
for a toolbar item but here is the one
for our context menu item-- again
it's just "Tweet This Text".
So all that we do is take that same user info which is
the selected text, we surround it in some curly quotes
and then we append the URL of the front most tab
which is where this context menu is being shown.
And so that is the Tweet Text and all we do is sent that off
to Twitter using their normal API
and a cross-domain XMLHttpRequest.
And because in the Builder we've specified
that we need to be able to access--
well in this case we've actually changed the access level to
all pages, so that will let us both send the XMLHttpRequest
to Twitter and it will also let us show
the Context Menu on any page on any server.
So that's all that it takes to put this
together-- it's very little code and very easy.
So just a quick recap to add context menus into
Safari the best way to do it is to first listen
for the contextmenu event in the web page
and that's your chance to provide context
to your application so that it knows which items to add.
Then you can listen for the contextmenu event up in the
application layer and that's your chance to add items.
Now you can also add the items using the Extension Builder.
Then the validate event is sent before the menu is
shown and that's your chance to disable or hide items;
and the command event is when you should
perform the selected action that's sent
when the user clicks on one of your items.
So that's everything for context menus.
Now let's talk about publishing updates.
So say you've written this great extension and a lot
of people are using it and they all really like it
and now you've made a better version
that you want people to install.
So what are your options?
Well you could of course post it on your web page and hope
that people go to your web page frequently enough and decide
to install it; or if you are a really kind of naggy web
developer you could make a mailing list and send e-mails
to all of your users and hope that they don't
filed as SPAM and hope people actually read them
and don't hate you forever because of it;
or there's actually a better way and that's
to have your updates listed right in Safari's preferences.
Safari will automatically discover updates for the
extensions that are installed and present them to the user
in this UI and the user can install them individually
or install them all at once or even check a checkbox
to install all the updates automatically
as Safari discovers them.
So this is an opt-in feature that you have
to opt-in to each extension that you write.
And the way you do it is very simple it's
through a file that we call the update manifest.
This is just a simple XML file that you host on your
web server and Safari downloads it periodically.
And what the file contains is a list
of all the extensions that you develop.
Now for each extension you just need
to list a little bit of information.
First you need two different identifiers-- one of them
is the extension's bundle identifier that you enter
into the Extension Builder, and the other is the identifier
that's associated with your developer certificate.
These two identifiers together
uniquely identify your extension.
Now next you need two version numbers.
The first is the internal version number that Safari
uses to decide whether one version is newer than another.
And the other is the display version that's
actually shown to the user in the UI.
And then finally all you need to list is the URL
where Safari can download this
particular version of your extension.
So if you make this file and put it on your web server
Safari will download it periodically, and any versions
or extensions that the user has installed that
are newer in the update manifest will be presented
in Safari's preferences making it very easy
for users to discover and install your updates.
So you've seen a lot of demos in the last session and
in this one that have shown you little bits of each part
of the API, the extensions and how
to use them and implement them.
But also in the State of the Union on Monday you saw
some great demos of some really awesome extensions
that kind of pull all these topics together.
And you might be wondering how can I write one of those.
So to show you that we actually have Cabel Sasser here
from Panic to show you how Code of Notes was constructed.
[ Applause ]
>> Cabel Sasser: All right after listening to all these
super smart Safari people I think you guys know probably 90%
of what you need to do to develop an extension.
I'm going to talk a little bit about the remaining 10%,
the stuff that we learned through developing the code
of notes extensions that we didn't expect, some
tips, some tricks, little things like that.
Code of Notes is a website annotation tool which
provides a new toolbar in Safari and allows the user
to basically scribble directly on a web page.
We have a highlighter and we have
sticky notes and things like that.
So this was kind of an interesting thing to build
and there are a lot of pieces going on here.
First of all we have the toolbar item that we install;
we also have our custom toolbar which you can see here.
And one thing that we did with that toolbar that's kind
of cool is that the bulk of that with the exception
of the glyphs on the buttons it's all CSS style;
so those buttons are all border radius, gradients,
so they can scale well and they're super flexible so I would
encourage you guys to use CSS styling as much as possible.
Let's see if I can show you that actually.
Here's what the bar looks like.
If I zoom that up you see that
everything sort of scales appropriately;
so that allowed us to really quickly develop this toolbar
without having to do a lot of graphics work in Photoshop.
So we've got the bar; we've got a global HTML
page which I know has been already talked about;
and then we've got some JavaScript
that we're injecting into every page.
The global HTML page mostly controls
whether or not this bar should be displayed.
One thing that we learned that was a surprise for us of
course we're injecting our JavaScript into every page
because we never know when a user
is going to want to start drawing.
So one thing we learned that a lot of
pages imbed a lot of iirames in their pages
and your injected JavaScript will also be injected into all
of those iirames so we were seeing a lot of duplicate events
and surprisingly weird stuff happening until we finally
realized that oh it's that little ad in the corner.
So beware of that if you're injecting
JavaScript that it will go everywhere.
And what we did as a suggestion from these guys we're
checking if window top equals window just to make sure
that we're only injecting into the top most element.
So how are we doing this drawing?
Basically, when you activate this
tool, actually open the bar,
there's a giant canvas element
transparent on top of the entire page.
And the canvass is-- if you guys have ever
done any work with canvas it's basically--
gives you drawing context and we're just drawing,
we're capturing the mouse event and
we're scribbling on that canvas.
Now one thing that's kind of interesting is
if we've got a canvass over that entire page,
how are we allowing you to do this text tool?
The text tool allows you to actually
edit the text on the page directly.
If the canvas is covering everything you would think
that the pointer wouldn't be able to click through it
to get those text events and edit the text.
We're actually setting a CSS style
which is pointer-events: none.
We set that on the canvas which basically means
that the pointer events have been passed through.
The canvas no longer gets the pointer events so
that was a cool solution to a problem that kind
of had us scratching our heads for a little bit.
Now there's a couple of interesting things-- the sticky
notes that we're doing here are actually dragging
around using the actual drag and drop events.
We set the body to the drop zone so of course you
can drag them around, then things get really hairy
with the notes button we've implemented; because what
happens here is we have our style of our postcard
and we know what we want our text to look like.
But the page that we're injecting this into might have
styles that override that; they might have body text set
to a huge font, or do things that might
mess up the appearance of our postcard.
So we have a crazy little trick where we walk through
all the style sheets and essentially disable them,
but while we're walking through them we're also storing
them and making a note of them so that if a user were
to hit cancel and wants to see
the page as it looked originally,
we now reset all those styles back to what they were before.
So an earlier version of this plug-in actually put the
postcard in a new tab and that was certainly a lot easier
from a developer perspective because
we didn't have to do these tricks.
But having it be in the window not opening a new tab is
much better; but just something to think about that you're
in an environment where there are a lot of styles
and things to consider that might surprise you.
Some people have asked me how we
are doing the screenshot thing--
how we actually get an image of the tab and shrink it down.
If you're looking for the documentation and you're
searching for screenshot, you probably won't find it.
It's actually called visibleContentsAs DataURL and
what that does is take the image contents of the tab
and basically give you a standard data URL, base64
encoded I believe, of a PNG version of the tab
so look for that visibleContentsAsDataURL.
And last but not least you've probably seen in the demo that
we do a little animation at the end that people seem to love
where a stamp comes down here and everything flies off.
I'm sure you've been hearing this a lot this week but it's
all CSS animation; it's all a class that we make called Fly
Out that sets the WebKit transform and when the user hits
the button we just apply that class to that stamp image,
it flies down on the page, it's off and it's running.
So I think that covers most of the stuff that kind of
surprised us but we did do this in a short period of time
and we're super happy with the results
and we think that when you guys dig in--
like I said you've got 90% of what you need
and the remaining 10% is pretty easy to solve.
So I look forward to seeing what you build.
>> Adam Roben: Thanks Cabel that was really great.
I should also mention that we're very excited to be hearing
from Cabel and the other guys at Panic and from all of you
about what things about extensions you find surprising.
Extensions are a developer-only feature at
the moment and so we're looking for a lot
of feedback in ways that we can improve it.
So let's go over a quick summary of what you've seen today.
We've talked about how to modify web pages
using content style sheet and scripts;
we've talked about how to add context menu items in Safari
using the Context Menu and validate and command events;
and we've talked about how to publish
new versions of your extension by opting
into Safari's autoupdate discovery mechanism by
posting an update manifest on your server and putting
that update manifest's URL into the Builder
so it will be embedded into your extension.
Now if you want to find out more information
about all of this you can contact Vicki Murley,
our Safari Technologies Evangelist; you can also go to the
Safari Dev Center where you'll find a lot of documentation.
That's also where you sign up for the free Safari
Developer Program and get your free signing certificate.
And you can also use the Apple Development Forums and we
have Apple engineers looking through those forums and trying
to answer people's questions all the time.