So I'm using the HTML5 history management for adding the ability to navigate back and forward within a website with AJAX loaded subcontent.
Now I would like to store javascript functions within the state object, to callback at the state popping. More or less like the following code:
$(window).bind("popstate", function(event) {
var state = event.originalEvent.state;
if (!state) {
return;
}
state.callback(state.argument);
}
function beforeLoad() {
var resourceId = "xyz";
var func;
if (case1) {
func = switchPageToMode1;
} else { // case 2
func = swithPageToMode2;
}
func(resourceId); // run the function
window.history.pushState({ callback: func, resourceId: resourceId }, "newTitle", "newURL"); // and push it to history
}
function switchPageToMode1(resourceId) {
alterPageLayoutSomeWay();
loadResource(resourceId);
}
function swithPageToMode2(resourceId) {
alterPageLayoutSomeOtherWay();
loadResource(resourceId);
}
function loadResource(resourceId) {
...
}
All right. So what I'm trying to do is storing a reference to a javascript function. But when pushing the state (the actual window.history.pushState call) the browser files a complaint, namely Error: "DATA_CLONE_ERR: DOM Exception 25"
Anybody knows what I'm doing wrong? Is it at all possible to store function calls within the state?
No, it's not possible, not directly anyway. According to MDC the "state object," i.e. the first argument to pushState, "can be anything that can be serialized." Unfortunately, you can't serialize a function. The WHATWG spec says basically the same thing but in many more words, the gist of which is that functions are explicitly disallowed in the state object.
The solution would be to store either a string you can eval or the name of the function in the state object, e.g.:
$(window).bind("popstate", function(event) {
var state = event.originalEvent.state;
if ( !state ) { return; }
window[ state.callback ]( state.argument ); // <-- look here
}
function beforeLoad() {
var resourceId = "xyz",
func
;
if ( case1 ) {
func = "switchPageToMode1"; // <-- string, not function
} else {
// case 2
func = "swithPageToMode2";
}
window[ func ]( resourceId ); // <-- same here
window.history.pushState(
{ callback : func,
argument : resourceId
},
"newTitle", "newURL"
);
}
Of course that's assuming switchPageToMode1 and -2 are in the global context (i.e. window), which isn't the best practice. If not they'll have to be accessible somehow from the global context, e.g. [window.]MyAppGlobal.switchPageToMode1, in which case you would call MyAppGlobal[ func ]( argument ).
I came up with a slightly different solution.
I added two variables to the window variable
window.history.uniqueStateId = 0;
window.history.data = {}.
Each time I perform a pushstate, all I do is push a unique id for the first parameter
var data = { /* non-serializable data */ };
window.history.pushState({stateId : uniqueStateId}, '', url);
window.history.data[uniqueStateId] = data;
On the popstate event, I then just grab the id from the state object and look it up from the data object.
Here is what I do:
Each HTML page contains one or more components that can create new History entries.
Each component implements three methods:
getId() which returns its unique DOM id.
getState() that returns the component's state:
{
id: getId(),
state: componentSpecificState
}
setState(state) that updates the component's state using the aforementioned value.
On page load, I initialize a mapping from component id to the component like so:
this.idToComponent[this.loginForm.getId()] = this.loginForm;
this.idToComponent[this.signupForm.getId()] = this.signupForm;
Components save their state before creating new History entries:
history.replaceState(this.getState(), title, href);
When the popstate event is fired I invoke:
var component = this.idToComponent[history.state.id];
component.setState(history.state);
To summarize: instead of serializing a function() we serialize the component id and fire its setState() function. This approach survives page loads.
Related
I'm currently working in a page with parent/child components. Somehow my child component gets updated when I manage its variables in the parent component.
What I'm trying to do:
My child component has a 'site' variable with all the data i need to send via API
My parent component has a Save button to send the child data to the Back-end
When 'site' changes in the child component, I'm emitting an event #change to the parent component
The #change event contains all the data I need, but not in the format I want
There is a function submit() that gets this data and modify the one of the arrays so that this: ['foo','bar'] becomes this 'foo,bar'
The problem when I do the step '5' my child component gets updated
The child component inside the parent component
<configuracoes :configuracoes="configuracoes" #change="setData"
v-if="currentPage === 'configs'"/>
The change event emitted by the child component
this.$emit("change", this.site);
The important part of 'site' var
site: {
seo: {
keywords: [],
...
},
...
},
The setData() function
setData(data) {
this.data = data;
},
The submitData() function
submitData() {
if (this.currentPage === "configs") {
let data = ({}, Object.assign(this.data))
let keywords = data.seo.keywords.join(',')
data.seo.keywords = keywords
this.$store.dispatch("sites/updateSite", {
empresa_id: this.user.empresa_id,
site_id: this.siteId,
dados: data,
});
}
}
As you can see, I'm declaring another variable let data to avoid updating this.site variable, but no success
First of all, there is an issue with how you're "copying" your this.data object.
let data = ({}, Object.assign(this.data)); // this doesn't work
console.log(data === this.data); // true
const dataCopy = Object.assign({}, this.data); // this works
console.log(dataCopy === this.data); // false
The way Object.assign works, all the properties will be copied over into the first argument. Since you only pass a single argument, it doesn't change and is still pointing to the same old object.
If you use the correct way, you will most likely still run into the same issue. The reason is that data.seo is not a primitive value (a number or a string), but is an object.
This means that the whole seo object will be copied over into the new copy. In other words, even though dataCopy !== this.data, dataCopy.seo === this.data.seo. This is known as "shallow copy".
You want to make sure you DO NOT modify the original seo object, here are a few ways to do that.
let goodCopy;
const newKeywords = this.data.seo.keywords.join(',');
// use object spread syntax
goodCopy = {
...this.data,
seo: {
...this.data.seo,
keywords: newKeywords,
},
};
// use Object.assign
goodCopy = Object.assign(
{},
this.data,
{
seo: Object.assign(
{},
this.data.seo,
{keywords: newKeywords}),
});
// create a copy of "seo", and then change it to your liking
const seoCopy = {...this.data.seo};
seoCopy.keywords = newKeywords;
goodCopy = Object.assign({}, this.data, {seo: seoCopy});
this.$store.dispatch('sites/updateSite', {
empresa_id: this.user.empresa_id,
site_id: this.siteId,
dados: goodCopy,
});
If you want to read up on ways to copy a JavaScript object, here's a good question.
I have a segment of code where I am getting some weird output. The parameter being used in the function is changing when I would not think it would.
entry point to the code.
handleAction(action : HAction){
this.openForm("marksForm","Form");
}
method to open the form.
public openForm(name : string, type : string){
console.log("Name",name)
let cb = this.createComponentInitCallback(this.compService.getComponentType(type),
name);
let itemconfig ={
type: 'row',
content: [{
type: 'component',
title: 'Form Test',
componentName: 'h-form',
componentState: {}
}]
}
let tryRegister = false;
try{
this.goldenLayout.getComponent(name);
}catch(e){console.log("registering component",name); tryRegister=true;}
if(tryRegister)
this.goldenLayout.registerComponent(name,cb);
if(this.goldenLayout.root.contentItems[0])
this.goldenLayout.root.contentItems[ 0 ].addChild(itemconfig);
else
this.goldenLayout.root.addChild(itemconfig);
}
This method creates the defined callback function.
public createComponentInitCallback(componentType: Type<any>, name : string ): ComponentInitCallback {
console.log("1Name",name);
let f = (container: GoldenLayout.Container, componentState: any) => {
console.log("2Name",name);
this.ngZone.run(() => {
console.log("3Name",name);
// Create an instance of the angular component.
const factory = this.componentFactoryResolver.resolveComponentFactory(componentType);
const injector = this._createComponentInjector(container, componentState);
const componentRef = this.viewContainer.createComponent(factory, undefined, injector);
console.log("4Name",name)
componentRef.instance.name=name;
// Bind the new component to container's client DOM element.
container.getElement().append($(componentRef.location.nativeElement));
this._bindEventHooks(container, componentRef.instance);
// Store a ref to the compoenentRef in the container to support destruction later on.
(container as any)[COMPONENT_REF_KEY] = componentRef;
});
};
return f;
}
You will see my log statements. This callback gets executed inside the GoldenLayout library. However, I was pretty sure this should work.
Below are the outputs:
Name marksForm
1Name marksForm
2Name h-form
3Name h-form
4Name h-form
The first console output is logging what is passed into this method. You can see that it is obviously changing on me so I have to be doing something wrong. Oddly enough, the componentType parameter is working perfectly fine.
What am I doing wrong here?
A function that relies on lexical this and is supposed to be passed as callback should always be bound to the context.
createComponentInitCallback method can be bound to the context, either with bind or an arrow function (see this explanation on bound prototype methods vs arrow instance methods):
constructor() {
this.createComponentInitCallback = this.createComponentInitCallback.bind(this);
}
Or resulting callback can be bound to the context:
let cb = this.createComponentInitCallback(this.compService.getComponentType(type),
name).bind(this);
Considering there are no scenarios where this should differ from current class instance, the first option is preferable.
As for function scope, it cannot be lost under no circumstances. If name was passed as an argument in parent function, it will remain unchanged in nested function.
Unless you are using Angular HttpModule to make calls, any async call made with an external library will result in running your call back out of the original scope.
To mitigate this you need to assign this to a local variable the callback can use.
public createComponentInitCallback(componentType: Type<any>, name : string ): ComponentInitCallback {
console.log("1Name",name);
let self = this;
let f = (container: GoldenLayout.Container, componentState: any) => {
console.log("2Name",name);
this.ngZone.run(() => {
console.log("3Name",name);
// Create an instance of the angular component.
const factory = self.componentFactoryResolver.resolveComponentFactory(componentType);
const injector = self._createComponentInjector(container, componentState);
const componentRef = self.viewContainer.createComponent(factory, undefined, injector);
console.log("4Name",name)
componentRef.instance.name=name;
// Bind the new component to container's client DOM element.
container.getElement().append($(componentRef.location.nativeElement));
self._bindEventHooks(container, componentRef.instance);
// Store a ref to the compoenentRef in the container to support destruction later on.
(container as any)[COMPONENT_REF_KEY] = componentRef;
});
};
return f;
}
I'm having an issue getting a value that is set in a callback. I initially make a call to get Quest data, then call game.state.setNPCs after the quest data has returned.
I want to get the NPC object after it has been set, but the get is returning an empty array even though setNPCs() seems to be setting the array.
You can see below, after the callback to set the result.npcs, I log out getNPCs(), and it is an Empty Array.
Even more weird, I call game.state.getNPCs() from within the GameState object after the value has been set, but it is still an empty array.
EDIT: I find if I pass in game.state.getNPCs as a callback into the initial callback setNPCs(), like so:
callback(result.npcs, game.state.getNPCs);
Then this works... But I don't want to have to pass in another callback. See below.
Initial call with game.state.setNPCs callback:
Utilities.game.quest.getQuestData({ id : stat.quest_id }, game.state.setNPCs);
Call to getQuestData:
getQuestData : function (params, setNPCcallback) {
API.Quest.getQuestData(params).done(function (result) {
if (game.state) {
game.state.setQuest(result); //Object received successfully
setNPCcallback(result.npcs, game.state.getNPCs);
console.log('NPCs', game.state.getNPCs()); //Empty array
}
});
},
GameState object:
var GameState = function(args) {
this.npcs = [];
...
};
GameState.prototype = {
constructor : GameState,
getNPCs : function () {
console.log(this.npcs); //Empty array
return this.npcs;
},
setNPCs : function (npcsArray, getNPCcallback) {
this.npcs = npcsArray;
console.log(this.npcs); //Contains Object
console.log(game.state.getNPCs()); //Empty array
console.log(getNPCcallback()); //Contains Object
},
I made a small demo to test the issue.
var Person = function () {
this.name = "someone";
}
Person.prototype.walk = function () {
console.log(this)
};
var p = new Person;
function exec(callback) {
callback(); //context is Window
callback.call(p); //context is Person {name: "someone"}
}
p.walk(); //context is Person {name: "someone"}
exec(p.walk);
When you invoke the callback from getQuestData, the context is not GameState instance. Invoking the callback with the correct context using call or apply methods, or using a callback which is bound to correct context using bind method should fix the issue.
Else you can pass the GameState instance itself and invoke gameState.callbackMethod()
which should look like the following according to previous example
function exec(instance) {
instance.walk(); //context is Person {name: "someone"}
}
exec(p);
I want initialize a module with some default values and change them later if required. To do this I have a module/singleton which contains a _state value. I have exposed a setter method to update that value. However, the setter does not update the _state member variable. The code looks like this:
var StateObject = function () {
var _state = { a: 1 };
return {
state : _state,
setState : function (s) {
_state = s;
}
};
}();
modules.export = StateObject;
and the calling function:
var SO = require('./state-object');
console.log(SO.state.a); // prints 1
SO.setState({a: 2});
console.log(SO.state.a); // still prints 1
Can anyone explain what would cause this and if there is a work around?
The potential pitfall of a solution like this is if some piece of code stores SO.state locally and references that. If that happens and you call setState() some time later, the reference won't be updated in that piece of code. That's just something to be aware of when you replace the whole state and not just individual values in the state.
The problem here has to do with references. When you execute StateObject(), the state variable stores the initial reference to _state. Then when you call setState(), you overwrite _state, but state is still holding on to the previous reference.
You might try something like this:
modules.export = {
state: { a: 1 },
setState: function(v) {
this.state = v;
}
};
Well, the problem is with the reference not being updated, as mscdex mentioned.
In my opinion the main problem is actually your logic: why have a setter if you don't have a getter?
var SO = function () {
var _state = { a: 1 };
return {
getState : function () {
return _state;
},
setState : function (s) {
_state = s;
}
};
}();
console.log(SO.getState().a); // prints 1
SO.setState({a: 2});
console.log(SO.getState().a); // prints 2
This works, as it is also returning the reference to the latest set object. And this has actually nothing to do with node or modules, you can run your example in the browser's JavaScript console.
First off, I know I can copy "this" on instantiation, but that doesn't work here.
Basically I'm writing something to track people interacting with Youtube videos.
I got this working fine for one video at a time. But I want it to work on pages with multiple Youtube videos as well, so I converted the code to a class so I can create a new instance of it for each video on the page.
The problem is when trying to bind to the Youtube event listener for state changes. For "non-class" code, it looks like this:
var o = document.getElementById( id );
o.addEventListener("onStateChange", "onPlayerStateChange" );
(onPlayerStateChange being the function I wrote to track state changes in the video)
(I'm also aware that addEventListener won't work with MSIE but I'm not worrying about that yet)
But when I'm inside a class, I have to use "this" to refer to another function in that class. Here's what the code looks like:
this.o = document.getElementById( id );
this.o.addEventListener("onStateChange", "this.onPlayerStateChange" );
When it's written like this, this.onPlayerStateChange is never called. I've tried copying "this" into another variable, e.g. "me", but that doesn't work either. The onPlayerStateChange function is defined within the "this" scope before I do this:
var me = this;
this.o = document.getElementById( id );
this.o.addEventListener("onStateChange", "me.onPlayerStateChange" );
Any insights?
Looking through other similar questions here, all of them are using jQuery, and I think doing it that way might work if I did it that way. But I don't want to use jQuery, because this is going to be deployed on random third party sites. I love jQuery but I don't want it to be a requirement to use this.
You need a global way to access the onPlayerStateChange method of your object. When you assign me as var me = this;, the variable me is only valid inside the object method where it is created. However, the Youtube player API requires a function that is accessible globally, since the actual call is coming from Flash and it has no direct reference to your JavaScript function.
I found a very helpful blog post by James Coglan in which he discussed a nice way to communicate with the Youtube's JavaScript API and manage events for multiple videos.
I have released a JavaScript wrapper library using his ideas at http://github.com/AnuragMishra/YoutubePlayer. Feel free to checkout the code. The underlying idea is simple - store all instances of the player object on the constructor. For example:
function Player(id) {
// id of the placeholder div that gets replaced
// the <object> element in which the flash video resides will
// replace the placeholder div and take over its id
this.id = id;
Player.instances.push(this);
}
Player.instances = [];
When passing a string as a callback, use a string of the form:
"Player.dispatchEvent('playerId')"
When the flash player evals this string, it should return a function. That function is the callback that will ultimately receive the playback event id.
Player.dispatchEvent = function(id) {
var player = ..; // search player object using id in "instances"
return function(eventId) { // this is the callback that Flash talks to
player.notify(eventId);
};
};
When the flash player has loaded the video, the global onYoutubePlayerReady function is called. Inside that method, setup the event handlers for listening to playback events.
function onYouTubePlayerReady(id) {
var player = ..; // find player in "instances"
// replace <id> with player.id
var callback = "YoutubePlayer.dispatchEvent({id})";
callback = callback.replace("{id}", player.id);
player.addEventListener('onStateChange', callback);
}
See a working example here..
You can use a technique called currying to achieve this. For that you need a currying function. Here's one I wrote some time back
/**
* Changes the scope of function "fn" to the "scope" parameter specified or
* if not, defaults to window scope. The scope of the function determines what
* "this" inside "fn" evaluates to, inside the function "fn". Any additional arguments
* specified in this are passed to the underlying "curried" function. If the underlying
* function is already passed some arguments, the optional arguments are appended
* to the argument array of the underlying function. To explain this lets take
* the example below:
*
* You can pass any number of arguments that are passed to the underlying (curried)
* function
* #param {Function} fn The function to curry
* #param {Object} scope The scope to be set inside the curried function, if
* not specified, defaults to window
* #param arguments {...} Any other optional arguments ot be passed to the curried function
*
*/
var curry = function(fn, scope /*, arguments */) {
scope = scope || window;
var actualArgs = arguments;
return function() {
var args = [];
for(var j = 0; j < arguments.length; j++) {
args.push(arguments[j]);
}
for(var i = 2; i < actualArgs.length; i++) {
args.push(actualArgs[i]);
}
return fn.apply(scope, args);
};
};
You can use it to curry other functions and maintain the 'this' scope inside the functions.
Check out this article on currying
this.o.addEventListener("onStateChange", curry(onPlayerStateChange, this));
Edit:
var curriedFunc = curry(onPlayerStateChange, this);
this.o.addEventListener("onStateChange", "curriedFunc");
Edit:
Okay lets say this is your custom class you create:
function MyCustomClass() {
var privateVar = "x"; // some variables;
this.onPlayerStateChange = function() { //instance method on your custom class
// do something important
}
}
On a global level you create an instance of MyCustomClass
var myCustom = new MyCustomClass(); // create a new instance of your custom class
var curriedFunc = curry(myCustom.onplayerStageChange, myCustom); // curry its onplayerstateChange
// now add it to your event handler
o.addEventListener("onStateChange", "curriedFunc");
You should be using the following to attach an event:
this.o.addEventListener("statechange", this.onPlayerStateChange);
For addEventListener, you don't need to add the on prefix.
What I posted above is correct for standard javascript, but because this passes it to the YT flash object, it's expecting onStateChange which is correct.
HTH
EDIT: Try the method in this post to help.
TheCloudlessSky was partly right and Sean was partly right. You can continue to use "onStateChange" as the event name, but don't put this.onPlayerStateChange in quotations - doing so removes the special meaning of this and javascript will look for a function named "this.onPlayerStateChange" rather than looking for a function "onPlayerStateChange" within this object.
this.o.addEventListener("onStateChange", this.onPlayerStateChange);
After looking at the Youtube Api, it looks like the addEventListener only accepts a String for the event handler function. That means there's no clean way to register a unique event handler for each object.
An alternative is to register a global handler for all youtube state changes, and then let that handler pass the state change onto all your objects. Assuming you have an array of "tracker" objects:
function globalOnPlayerStateChange() {
for (tracker in myTrackerObjects) {
tracker.playerStateChange();
}
}
Each tracker object can then figure out by itself whether or not a state change actually occured (using the API's getPlayerState function):
function MyYoutubeTracker() {
this.currentState = ...
// Determine if state changed happened or not
this.playerStateChange = function() {
var newState = this.o.getPlayerState();
if (newState != this.currentState) {
// State has changed
this.currentState = newState;
}
}
// Register global event handler for this youtube object
this.o.addEventListener("onStateChange", "globalOnPlayerStateChange");
}
Ok, I got this all working. It's a bit of an ugly hack but it works. Basically I'm storing each new instance of the class in an array, and I'm passing the array key (1, 2, etc) into the class, so it can refer to itself externally as needed in a few key places.
The places I need the class to refer to itself externally are the string I pass to addEventListener, and within a few setTimeout functions, where "this" apparently loses its context (as far as I can tell anyways, because the only way I could them working was changing "this" to use external references instead.
Here's the full code.
On the page that has Youtube videos, they are injected using swfobject. The _ytmeta object stores the titles for each video. It's optional, but it's the only way to log the title of a video, because Youtube's API does not give it to you. This means you have to know the title up front, but the point is simply that if you want the title to show up in our reports, you have to create this object:
<div id='yt1'></div>
<script src='youtube.js'></script>
<script src='swfobject.js'></script>
<script>
var _ytmeta = {}
_ytmeta.yt1 = { 'title': 'Moonwalking in Walmart' };
var params = { allowScriptAccess: "always" };
swfobject.embedSWF("http://www.youtube.com/v/gE1ZvCnwkYk?enablejsapi=1&playerapiid=yt1", "yt1", "425", "356", "8", null, null, params );
</script>
So we're including the swfobject javascript code, as well as the youtube.js file, which will be hosted on our server and included on the pages you want to track videos.
Here are the contents of youtube.js:
// we're storing each youtube object (video) in an array, and passing the array key into the class, so the class instance can refer to itself externally
// this is necessary for two reasons
// first, the event listener function we pass to Youtube has to be globally accessible, so passing "this.blah" doesn't work
// it has to be passed as a string also, so putting "this" in quotes makes it lose its special meaning
// second, when we create timeout functions, the meaning of "this" inside that function loses its scope, so we have to refer to the class externally from there too.
// _yt is the global youtube array that stores each youtube object. yti is the array key, incremented automatically for each new object created
var _yt = [], _yti = 0;
// this is the function the youtube player calls once it's loaded.
// each time it's called, it creates a new object in the global array, and passes the array key into the class so the class can refer to itself externally
function onYouTubePlayerReady( id ) {
_yti++;
_yt[ _yti ] = new _yta( id, _yti );
}
function _yta( id, i ) {
if( !id || !i ) return;
this.id = id;
this.mytime;
this.scrubTimer;
this.startTimer;
this.last = 'none';
this.scrubbing = false;
this.o = document.getElementById( this.id );
this.o.addEventListener("onStateChange", "_yt["+i+"].onPlayerStateChange" );
this.onPlayerStateChange = function( newState ) {
// some events rely on a timer to determine what action was performed, we clear it on every state change.
if( this.myTime != undefined ) clearTimeout( this.myTime );
// pause - happens when clicking pause, or seeking
// that's why a timeout is used, so if we're seeking, once it starts playing again, we log it as a seek and kill the timer that would have logged the pause
// we're only giving it 2 seconds to start playing again though. that should be enough for most users.
// if we happen to log a pause during the seek - so be it.
if( newState == '2' ) {
this.myTime = setTimeout( function() {
_yt[i].videoLog('pause');
_yt[i].last = 'pause';
_yt[i].scrubbing = false;
}, 2000 );
if( this.scrubbing == false ){
this.last = 'pre-scrub';
this.scrubbing = true;
}
}
// play
else if( newState == '1' ) {
switch( this.last ) {
case 'none':
this.killTimers();
this.startTimer = setInterval( this.startRun, 200 );
break;
case 'pause':
this.myTime = setTimeout( function() {
_yt[i].videoLog('play');
_yt[i].last = 'play';
}, 2000 );
break;
case 'pre-scrub':
this.killTimers();
this.scrubTimer = setInterval( this.scrubRun, 200 );
break;
}
}
// end
else if( newState == '0' ) {
this.last = 'none';
this.videoLog('end');
}
}
// have to use external calls here because these are set as timeouts, which makes "this" change context (apparently)
this.scrubRun = function() {
_yt[i].videoLog('seek');
_yt[i].killTimers();
_yt[i].last = 'scrub';
_yt[i].scrubbing = false;
}
this.startRun = function() {
_yt[i].videoLog('play');
_yt[i].killTimers();
_yt[i].last = 'start';
}
this.killTimers = function() {
if( this.startTimer ) {
clearInterval( this.startTimer );
this.startTimer = null;
}
if( this.scrubTimer ){
clearInterval( this.scrubTimer );
this.scrubTimer = null;
}
}
this.videoLog = function( action ) {
clicky.video( action, this.videoTime(), this.videoURL(), this.videoTitle());
}
this.videoTime = function() {
return Math.round( this.o.getCurrentTime() );
}
this.videoURL = function() {
return this.o.getVideoUrl().split('&')[0]; // remove any extra parameters - we just want the first one, which is the video ID.
}
this.videoTitle = function() {
// titles have to be defined in an external object
if( window['_ytmeta'] ) return window['_ytmeta'][ this.id ].title || '';
}
}
Hopefully, someone in the future will find this helpful, because it was a serious pain in the ass to get it working!
Thank you everyone who posted their ideas here. :)