We previously made a panel with a Web page inside. We’ll now work on adding logic to it by making this WebView communicate with Sketch and vice versa. To illustrate passing information from the WebView to Sketch, we’ll trigger Sketch tools when clicking on buttons in the Web page. To illustrate the other way around, we’ll show the selected layer’s CSS style in the Web page.
This is the second part of the article, if you haven’t read the first part already, I recommend you to do so but if you really want to start here, here’s the code of what we made so far.
To trigger an action in Sketch (e.g. open the Rectangle tool) by clicking on a button in the Web page, we need to receive click events in the Web page and then send this information to the Sketch plugin. The only way to do that is quite confusing: we need to use the local Web page’s URL hash. Changing the hash won’t reload the page but we’ll still be able to receive an event for that change thanks to a delegate. Before starting, to make it easier for us to work with delegates, we’re going to use MochaJSDelegate.
@import "MochaJSDelegate.js";
We can now easily create a delegate and listen to URL changes with webView:didChangeLocationWithinPageForFrame:. It will receive the event, but how do we get the actual information? We’ll access the Web page’s JavaScript environment and execute code there, retrieving the results on the same occasion with the WebView’s windowScriptObject property.
// Access the Web page's JavaScript environment
var windowObject = webView.windowScriptObject();
// Create the delegate
var delegate = new MochaJSDelegate({
// Listen for URL changes
"webView:didChangeLocationWithinPageForFrame:": (function(webView, webFrame) {
// Extract the URL hash (without #) by executing JavaScript in the Web page
var hash = windowObject.evaluateWebScript("window.location.hash.substring(1)");
})
})
// Set the delegate on the WebView
webView.setFrameLoadDelegate_(delegate.getClassInstance());
But because we only listen to URL changes, doing the same action multiples times in a row won’t trigger the event. To fix that, we’ll use an object (JSON) that contains the action we want to pass to Sketch, and the current UNIX date to make it unique in every case. To make it easier to add multiple actions with the same event, we’ll store actions (MSRectangleShapeAction, MSOvalShapeAction, MSTriangleShapeAction…) in “data-action” HTML attributes on each button in the Web page.
document.querySelector("button").addEventListener("click", function(e) {
// Create JSON object with the action we want to trigger and the current UNIX date
var data = {
"action": item.getAttribute("data-action"),
"date": new Date().getTime()
}
// Put the JSON as a string in the hash
window.location.hash = JSON.stringify(data);
});
After getting the hash in the delegate, we can just parse it to get back our object that we can work with. We can now use Sketch’s actionsController() to trigger the specific action asked by which button was clicked in the Web page.
// Parse the hash's JSON content
var hash = windowObject.evaluateWebScript("window.location.hash.substring(1)");
var data = JSON.parse(hash);
// Launch a Sketch action and focus the main window
context.document.actionsController().actionForID(data.action).doPerformAction(null);
NSApplication.sharedApplication().mainWindow().makeKeyAndOrderFront(null);
Everything works as expected, but just to make it a bit prettier, we’ll use the Sketch tools icons instead of default buttons.
We’ll now show a CSS preview of the selected layer on a simple div. We’ll use the same delegate as before but with a new event this time: webView:didFinishLoadForFrame:. By listening to when the Web page is done loading, we can set the preview before the interface even shows up.
We’ll first create a very basic function in our Web page that accepts a string of CSS attributes and apply those to a specific div.
function updatePreview(style) {
// Set the attributes as the CSS style of our <div>
document.querySelector(".preview__item").style.cssText = style;
}
In the plugin, we’ll use the windowScriptObject property again to execute that function, passing either a string of CSS attributes from the selection or an empty string if we can’t get any CSS attributes (e.g. multiple layers, images…), making the div look like our selection or invisible if we can’t show anything.
"webView:didFinishLoadForFrame:": (function(webView, webFrame) {
// Get the current selection
var selection = context.selection;
if (selection.length == 1) {
// Send the CSS attributes as a string to the Web page
windowObject.evaluateWebScript("updatePreview('" + selection[0].CSSAttributes().join(" ") + "')");
} else {
// Or send an empty string to the Web page
windowObject.evaluateWebScript("updatePreview(' ')");
}
})
The preview looks nice but it only updates when we start the plugin. To fix that, we can use Actions: a Sketch API that allows us to execute code based on events in Sketch. There’s a lot of events available but we only need one of them for what we want to achieve: SelectionChanged.finish.
We’ll modify our plugin’s manifest.json with a new entry to tell Sketch that we want to listen to that event.
"handlers": {
"run": "onRun",
"actions": {
"SelectionChanged.finish": "onSelectionChanged",
}
}
In our plugin, we can now add an onSelectionChanged function below onRun that will be executed every time the selection changes in Sketch. Because it’s a different function from onRun, we need to find a way to get access to the panel and its content again. We’ll just check for the reference we stored earlier with the same identifier, and if it exists we can access the panel again with it. If it doesn’t exist, that means that there is no panel opened so we shouldn’t do anything.
After accessing the panel, we can also access the WebView by using the subviews property. We can then essentially use the same code that we created for the webView:didFinishLoadForFrame: event earlier to update the preview every time we change our selection in Sketch.
var onSelectionChanged = function(context) {
var threadDictionary = NSThread.mainThread().threadDictionary();
var identifier = "co.awkward.floatingexample";
// Check if there's a panel opened or not
if (threadDictionary[identifier]) {
// Access the panel from the reference and the WebView
var panel = threadDictionary[identifier];
var webView = panel.contentView().subviews()[1];
// Access the Web page's JavaScript environment
var windowObject = webView.windowScriptObject();
// Get the current selection and update the CSS preview accordingly
var selection = context.actionContext.document.selectedLayers().layers();
if (selection.length == 1) {
windowObject.evaluateWebScript("updatePreview('" + selection[0].CSSAttributes().join(" ") + "')");
} else {
windowObject.evaluateWebScript("updatePreview(' ')");
}
}
};
We now have a floating panel that interacts with Sketch and updates when we select new layers, yay!
If something went wrong for you, here’s the code for the final result. This was just the tip of the iceberg, but I hope that this basic example was a good start to inspire you to create better and more advanced Sketch plugins.