When putting a recent talk together about Asynchronous JavaScript, I was looking to build a browser controlled presentation that allowed the controlling browser tab to control the presentation tab. In particular, I was looking at managing three things:
- Slide Position
- Slide Font Size
- Slide Actions
For the third one, Slide Actions, I was looking to trigger display of some code (preferably in Developer Tools > Console) as well as potentially running the code.
As a long-time front-end developer, I know that browser tabs are sandboxed, but have seen this type of functionality over time ... but remembering where was daunting. I also wanted to do the research and not look into (felt like cheating) some of the presentation tools (such as reveal.js) that have this functionality.
What I came across was BroadcastChannel and it is supported in Firefox and Chrome per caniuse.com. Since I can't imaging trying to give a presentation using IE or Edge, I considered this great information.
Setup Channels
Use of this functionality wound up being pretty simple ... this code initiated the process in the index.html JavaScript code (_functionality.js) ...
const pnChannel = new BroadcastChannel('le-slides-position');
const fsChannel = new BroadcastChannel('le-slides-font-size');
const anChannel = new BroadcastChannel('le-slides-actions');
In the _navigation.js, _font-sizing.js, and _code-examples files, there are matching declarations ...
// _navigation.js
const channel = new BroadcastChannel('le-slides-position');
// _font-sizing.js
const channel = new BroadcastChannel('le-slides-font-size');
// _code-examples.js
const channel = new BroadcastChannel('le-slides-actions');
NOTE: Each of these lines is in a separate file, hence the use of const channel on each line.
Channel Communication
Here, we'll just examine sending data from the controlling index.html, _functionality,js code ...
const actions = {
init: (force = false) => {
if (!initFired || force) {
fsChannel.postMessage('init');
pnChannel.postMessage('init');
anChannel.postMessage('init');
initFired = true;
}
},
up: () => {
if (!upButton.hasClass('disabled')) {
fsChannel.postMessage('trigger-up');
}
},
reset: () => {
fsChannel.postMessage('trigger-reset');
},
down: () => {
if (!downButton.hasClass('disabled')) {
fsChannel.postMessage('trigger-down');
}
},
previous: () => {
if (!previousButton.hasClass('disabled')) {
pnChannel.postMessage('trigger-previous');
}
},
next: () => {
if (!nextButton.hasClass('disabled')) {
pnChannel.postMessage('trigger-next');
}
},
triggerAction: (action) => {
anChannel.postMessage(action);
}
};
Position Channel
Now, looking at the pnChannel (position channel) ... we can see that the .onmessage funcitonality expects a state. The state sent can include data, in this case what the current index is ... also, additional data is sent, such as previous and next disable states and these buttons can be adjusted appropriately.
pnChannel.onmessage = (states) => {
cardIndex = states.data.currentIndex;
updateContent();
if (states.data.previousDisabled) {
previousButton.addClass('disabled');
} else {
previousButton.removeClass('disabled');
}
if (states.data.nextDisabled) {
nextButton.addClass('disabled');
} else {
nextButton.removeClass('disabled');
}
};
In the _navigation.js file, there it recieves a triggerAction whose data is actually used to execute some functionality ...
channel.onmessage = (triggerAction) => {
actions[triggerAction.data]();
};
const actions = {
init: () => {
nextButton.hide();
previousButton.hide();
},
'trigger-previous': () => {
slideStateMachine.next('previous');
},
'trigger-next': () => {
slideStateMachine.next('next');
},
'report-states': (index) => {
channel.postMessage({
currentIndex: index,
previousDisabled: previousButton.hasClass('disabled'),
nextDisabled: nextButton.hasClass('disabled')
});
}
};
With this code, it should become clear that sending a message is simply a matter of utilizing the .postMessage functionality of a channel.
Font Sizing Channel
Looking at the fsChannel we can see the .onmessage expects a state again, allowing for the button states to be assigned ...
fsChannel.onmessage = (states) => {
if(states.data.upDisabled) {
upButton.addClass('disabled');
} else {
upButton.removeClass('disabled');
}
if(states.data.downDisabled) {
downButton.addClass('disabled');
} else {
downButton.removeClass('disabled');
}
};
This is connected to the **_font-sizing.js* code, which again triggers various actions ...
channel.onmessage = (triggerAction) => {
actions[triggerAction.data]();
};
const actions = {
init: () => {
upButton.hide();
downButton.hide();
resetButton.hide();
},
'trigger-up': () => {
fontStateMachine.next('up');
},
'trigger-reset': () => {
fontStateMachine.next('reset');
},
'trigger-down': () => {
fontStateMachine.next('down');
},
'report-states': () => {
channel.postMessage({
upDisabled: upButton.hasClass('disabled'),
downDisabled: downButton.hasClass('disabled')
});
}
};
Action Channel
Looking at the anChannel we can see that here, the response state data is simply sent to console.log ...
anChannel.onmessage = (states) => {
console.log('action reply:', states.data);
};
The associated code in the _code-examples.js file is a bit more complicated ...
channel.onmessage = (states) => {
const cardAction = cardActions[states.data];
if (states.data === 'init') {
cardAction();
} else {
if (cardAction.showDisplay) {
console.log(cardAction.display);
}
cardAction.fn();
}
};
In this case, I will admit that I "cheated" a bit for a specific purpose ... I used some JSON data ...
"fn": "triggerImage('queues.png', false)"
... and within the _code-examples.js init functionality, I rebuild them as executable functions. Thus, I was able to use a JSON file to control the elements on each screen, as well as what could be "executed" on the presentation screen ...
const name = card.options[j].name;
const optionFn = new Function(card.options[j].fn);
cardActions[name] = {
fn: optionFn,
showDisplay: card.options[j].showFn,
display: card.options[j].fn
};
Conclusions
I learned a lot of exciting things with the project and the code is available on my GitHub account. I'd rather not give it away directly, so I am not going to link to it here.
The content in my article JavaScript Enjoys Your Tears is what I use to present Single-Threaded and Asynchronous JavaScript?.
This was an interesting project and at some point, I can see myself working this into a presentation, in and of itself.