Using WebComponents in GreaseMonkey

77 views
Skip to first unread message

Sebastian Simon

unread,
Oct 22, 2019, 11:22:19 AM10/22/19
to greasemonkey-users
I’m writing a user-script that uses WebComponents, i.e. class TestElement extends HTMLElement, this.attachShadow({ mode: "open" }), customElements.define("test-element", TestElement), etc. But it seems, that due to the security system of GreaseMonkey, I can’t fully use WebComponents: instance methods, getters and setters aren’t applied to my <test-element> elements. Yet, I have found a work-around: the only method actually running is the constructor, within which I can define methods, getters and setters, which do work.

Here is my minimal example code, demonstrating the issue:

// ==UserScript==
// @name     WebComponents test
// @include  http://example.com/
// @run-at   document-start
// ==/UserScript==

addEventListener
("DOMContentLoaded", function(){ "use strict";
 
class TestElement extends HTMLElement{
    constructor
(){
     
super();
     
     
const shadow = this.attachShadow({
          mode
: "open"
       
});
     
     
this.content = shadow.appendChild(document.createElement("div"));
     
Object.defineProperty(this, "testGetter", {
       
get(){
         
return this.content.textContent || "something";
       
},
       
set(value){
         
this.content.textContent = value;
       
}
     
});
     
Object.defineProperty(this, "testMethod", {
        value
(){
          console
.log("Hello, World");
       
}
     
});
     
      console
.log("Constructor actually runs."); // It does.
   
}
   
   
get dataGetter(){
     
return this.content.textContent || "something";
   
}
   
   
set dataGetter(value){
     
this.content.textContent = value;
   
}
   
    someMethod
(){
      console
.log("Hello, World");
   
}
 
}
 
  customElements
.define("test-element", TestElement);
 
 
const testElement = document.createElement("test-element");
 
  document
.body.append(testElement);
 
  console
.log("instance", testElement instanceof TestElement); // false
  console
.log("dataGetter in", "dataGetter" in testElement); // false
  console
.log("dataGetter property", testElement.dataGetter); // undefined
  testElement
.dataGetter = "Test"; // Silently fails.
 
Object.assign(testElement, {
    dataGetter
: "Test"
 
}); // Silently fails.
  console
.log("dataGetter property", testElement.dataGetter); // "Test", but not from the Getter.
 
  console
.log("testGetter in", "testGetter" in testElement); // true
  console
.log("testGetter property", testElement.testGetter); // "something"
  testElement
.testGetter = "Test"; // Works.
 
Object.assign(testElement, {
    testGetter
: "Test"
 
}); // Works.
  console
.log("testGetter property", testElement.testGetter); // "Test"
 
  testElement
.testMethod(); // Logs "Hello, World"
 
  testElement
.someMethod(); // TypeError: testElement.someMethod is not a function
});

The comments explain what’s going on. As the code is running on the webpage http://example.com/, the JavaScript context doesn’t seem to be allowed to access TestElement instance properties that were defined on the class itself. But since the constructor runs, I can define new instance properties, using Object.defineProperty. To be clear, all of this would work perfectly fine, if the script was a normal content script within a <script>, not using a user-script.

What I ended up using in my real script is this one-liner after super();—it takes all instance properties of the class (which are accessible, if the class is accessed directly), and redefines every property on the current instance, except constructor:

Object.defineProperties(this, Object.fromEntries(Object.entries(Object.getOwnPropertyDescriptors(MyCustomElement.prototype))
 
.filter(([key]) => key !== "constructor")));

This is the easiest work-around I came up with, but is it really not possible to use WebComponents normally in GreaseMonkey user-scripts? The work-around still doesn’t account for testElement instanceof TestElement.

Sebastian Simon

unread,
Oct 22, 2019, 11:37:04 PM10/22/19
to greasemonkey-users
Note: replacing MyCustomElement by this.constructor in the workaround didn’t work—this.constructor.prototype appears to be referring to the HTMLElement prototype rather than the expected (confirmed in content script) MyCustomElement prototype.

Sebastian Simon

unread,
Oct 25, 2019, 7:44:51 AM10/25/19
to greasemonkey-users
And now, I had to switch from WebComponents back to regular DOM manipulation. For some reason, when I clicked my element, no further mouse events were applied to the rest of the page. Again, this is something that works outside of user-scripts.

T Z

unread,
Jan 20, 2020, 1:43:51 PM1/20/20
to greasemonkey-users
Reply all
Reply to author
Forward
0 new messages