I am embedding some HTML content inside a webpage and I want it to be isolated from outside CSS. My users are able to create HTML content dynamically and these pages can contain some simple JS.
I've been trying to run these JS inside a shadow root. Since the JS is created dynamicaly when the user creates the page I need a way to make it possible to run the code inside the shadowroot. I'm adding content as follows:
const header = document.createElement('header');
const shadowRoot = document.createElement('body')
shadowRoot.innerHTML = pageContent
var script = document.createElement('script')
script.textContent = `document.querySelectorAll('#ii0b7a'); document.createElement(........`
shadowRoot.appendChild(script)
header.attachShadow({
mode: 'open'
}).appendChild(shadowRoot)
document.getElementById('div-box-one').appendChild(header)
So far so good, but the code won't run correctly, because the script is accessing the document elements and not the shadowRoot. So I should first access shadowroot and then I'll be able to access the elements inside it. To do so, i'm wrapping the js code inside a function and renaming document variable like this:
(function(document) {
// code here
}(document.getElementById('shadow_root').shadowRoot));
But this approach is not working correctly, because in some case I have to use the function "document.createElement" which will then throw an error.
My question is: How can I rename document variable only when needed. For example
document.querySelectorAll('#') = document.getElementById('shadow_root').shadowRoot.querySelectorAll('#')
document.createElement = document.createElement
I know it is almost impossible to use libraries inside shadowRoot and iframe is another solution, but iframe brings me other problems with it. (shadow-dom library)
Is it possible to achieve what I want renaming somehow the document variable?
So I've came up with the following solution:
function documentParse() {
var element = document.getElementById('shadow_root').shadowRoot
element.createElement = function createElement(type) {
return document.createElement(type)
}
element.head = document.head
return element
}
script.textContent = `(function(document) {
//script source text
}(documentParse()));`
....
Wrapping the script in this way will bring every search on document to the scope of elements inside the shadowroot dom.
Related
I have to use Google Tag Manager to make a change to fix something temporarily until I gain access to the back-end of a site. I am trying to remove a div container based on class. I have been able to successfully create a custom HTML tag with the following code to add remove the div container like so:
<script>
function changeHtml ()
{
document.getElementsByClassName("signup")[0].style.display = "none";
}
change = changeHtml();
</script>
The issue is that the problematic item in this div container is a form. I quickly found out that if you apply display: none; via css the form still works. The true way to remove this would be to remove it from the DOM. I tried adding the following JS but not having any success:
<script>
const elem = document.getElementsByClassName('signup');
elem.parentNode.removeChild(elem);
</script>
Receiving error:
Javascript Compiler error: Error at line 3, character 1: This language feature is only supported for ECMASCRIPT6 mode of better: const declaration
Use remove() method to remove the selected element from the DOM:
<script>
document.getElementsByClassName('signup')[0].remove();
</script>
You could achieve this easily by trying this instead
<script>
const elem = document.getElementsByClassName('signup')[0];
elem.outerHTML = '';
</script>
As of now, GTM supports ES6 only in custom templates, so the use of const in a custom javascript tag will result in an error. Your description is somewhat generic ("does not work"), so will just assume that this is your problem. Also your tag must be executed only after the element you want to remove actually exists (on DOM ready, or with a visibility trigger).
I have tried to create my first One Note Add In using the JavaScript API. I have tried the example in the MS documentaion (Build your first OneNote task pane add-in). This one works.
Now I want to try to change the formatting of an element in the document. For example I want to change the font colour of a text. However, I have not yet found a way to access the elements in a document.
Can I access elements in a document via a JS Add In to change their "style" property?
How can I do that?
Thanks
Micheal
Finally, I found a way to access the OneNote page content from the JS Add In. You can load the page content using
var page = context.application.getActivePage();
var pageContents = page.contents;
context.load(pageContents);
Now you have access to the page content in the qued commands.
return context.sync().then( function() {
var outline = pageContents.items[0].outline;
outline.appendHtml("<p>new paragraph</p>");
var p = outline.paragraphs;
context.load(p);
...
});
So consequently you can access element by element in document the hirarchy.
Say I have a few js-files stored externally, or I just want to load a new dependency like JQuery or Angular in my shadow-root, is there a way to load it into it?
I know for css-stylesheets you can just do:
var style = document.createElement('style');
style.setAttribute('type','text/css');
style.innerText = '#import "' + csspath + '";';
is there a similar way to do that with js?
Since just doing this:
var script = document.createElement('script');
script.type = "text/javascript";
script.src = jspath;
doesn't work :/
Also I am doing this:
var root = document.getElementById('container').createShadowRoot();
var DOM = new DOMParser().parseFromString(html.responseText,'text/html').getElementsByTagName('html')[0];
DOM.getElementsByTagName('head')[0].appendChild(script);
the "parseFromString(html.responseText,'text/html')" is because I am getting my html from an external source as well.
Here is a plunkr
http://plnkr.co/edit/YM1lXN8QEhjMd4n9u0wf?p=preview
As you figured out, Shadow DOM does not create a new scope for JavaScript, it's only concerned with DOM and CSS scoping.
There are a couple approaches to adding JavaScript to the document. The first is the classic script injection:
<script>
var script = document.createElement('script');
script.src = "//somehost.com/awesome-widget.js";
document.getElementsByTagName('head')[0].appendChild(script);
</script>
You were doing this in your previous example but you were appending the script to the wrong place. Just add it to the main document instead of the html that you loaded. I think it should work in either instance, but it makes more sense for it to follow the typical pattern of appending to the main document's head.
The other approach, if you're using HTML Imports, is to include a link tag, which has a script that points to your resource. This is often what we do with Polymer elements. We link all of our dependencies at the top, that way they get loaded before our element definition is registered.
So I kinda figured this out,
I didn't understand what a #shadow-root is.
Its not like an iFrame where it gets its own #document and has a completely different environment to the rest of the site.
You can load js into a shadow-root by calling its parent element and then its elements,
So say this is my structure:
div [id='app']
| #shadow-root
| | div [id='content']
then in order to work some magic on the '#content' tag, using jquery you can change it by doing this:
$('#app').find('#content').html('hello world')
Even if this code is ran from inside the shadow-root
I'm writing a Javascript file which will be a component in a webpage. I'd like it to be simple to use - just reference the script file in your page, and it is there. To that end however there is a complication - where should the HTML go that the Javascript generates? One approach would be to require a placeholder element in the page with a fixed ID or class or something. But that's an extra requirement. It would be better if the HTML was generated at the location that the script is placed (or, at the start of body, if the script is placed in head). Also, for extra customizability, if the fixed ID was found, the HTML would be placed inside that placeholder.
So I'm wondering - how do I detect my script's location in the page? And how do I place HTML there? document.write() comes to mind, but that is documented as being pretty unreliable. Also it doesn't help if the script is in the head. Not to mention what happens if my script is loaded dynamically via some AJAX call, but I suppose that can be left as an unsupported scenario.
I am doing that with this code...
// This is for Firefox only at the moment.
var thisScriptElement = document.currentScript,
// Generic `a` element for exploiting its ability to return `pathname`.
a = document.createElement('a');
if ( ! thisScriptElement) {
// Iterate backwards, to look for our script.
var scriptElements = document.body.getElementsByTagName('script'),
i = scriptElements.length;
while (i--) {
if ( ! scriptElements[i].src) {
continue;
}
a.href = scriptElements[i].src;
if (a.pathname.replace(/^.*\//, '') == 'name-of-your-js-code.js') {
thisScriptElement = scriptElements[i];
break;
}
}
}
Then, to add your element, it's simple as...
currentScript.parentNode.insertBefore(newElement, currentScript);
I simply add a script element anywhere (and multiple times if necessary) in the body element to include it...
<script type="text/javascript" src="somewhere/name-of-your-js-code.js?"></script>
Ensure the code runs as is, not in DOM ready or window's load event.
Basically, we first check for document.currentScript, which is Firefox only but still useful (if it becomes standardised and/or other browsers implement it, it should be most reliable and fastest).
Then I create a generic a element to exploit some of its functionality, such as extracting the path portion of the href.
I then iterate backwards over the script elements (because in parse order the last script element should be the currently executing script), comparing the filename to what we know ours is called. You may be able to skip this, but I am doing this to be safe.
document.write is very reliable if used as you indicate (a default SharePoint 2010 page uses it 6 times). If placed in the head, it will write content to immediately after the body element. The trick is to build a single string of HTML and write it in one go, don't write snippets of half-formed HTML.
An alternative is to use document.getElementsByTagName('script') while the document is loading and assume the the last one is the current script element. Then you can look at the parent and if it's the head, use the load or DOM ready event to add your elements after the body. Otherwise, just add it before or after the script element as appropriate.
I am writing a script that needs to add DOM elements to the page, at the place where the script is located (widget-like approach).
What is the best way to do this?
Here are the techniques I am considering:
Include an element with an id="Locator" right above the script. Issues:
I don't like the extra markup
If I reuse the widget in the page, several elements will have the same "Locator" id. I was thinking about adding a line in the script to remove the id once used, but still...
Add an id to the script. Issues:
even though it seems to work, the id attribute is not valid for the script element
same issue as above, several elements will have the same id if I reuse the script in the page.
Use getElementsByTagName("script") and pick the last element. This has worked for me so far, it just seems a little heavy and I am not sure if it is reliable (thinking about deferred scripts)
document.write: not elegant, but seems to do the job.
[Edit] Based on the reply from idealmachine, I am thinking about one more option:
Include in the script tag an attribute, for example goal="tabify".
Use getElementsByTagName("script") to get all the scripts.
Loop through the scripts and check the goal="tabify" attribute to find my script.
Remove the goal attribute in case there's another widget in the page.
[Edit] Another idea, also inspired by the replies so far:
Use getElementsByTagName("script") to get all the scripts.
Loop through the scripts and check innerHTML to find my script.
At the end of the script, remove the script tag in case there's another widget in the page.
Out of the box : document.currentScript (not supported by IE)
I've worked for OnlyWire which provides, as their main service, a widget to put on your site.
We use the var scripts = document.getElementsByTagName("script"); var thisScript = scripts[scripts.length - 1]; trick and it seems to work pretty well. Then we use thisScript.parentNode.insertBefore(ga, thisScript); to insert whatever we want before it, in the DOM tree.
I'm not sure I understand why you consider this a "heavy" solution... it doesn't involve iteration, it's a pure cross-browser solution which integrates perfectly.
This works with multiple copies of same code on page as well as with dynamically inserted code:
<script type="text/javascript" class="to-run">
(function(self){
if (self == window) {
var script = document.querySelector('script.to-run');
script.className = '';
Function(script.innerHTML).call(script);
} else {
// Do real stuff here. self refers to current script element.
console.log(1, self);
}
})(this);
</script>
Either document.write or picking the last script element will work for synchronously loaded scripts in the majority of web pages. However, there are some options I can think of that you did not consider to allow for async loading:
Adding a div with class="Locator" before the script. HTML classes has the advantage that duplicates are not invalid. Of course, to handle the multiple widget case, you will want to change the element's class name when done adding the HTML elements so you do not add them twice. (Note that it is also possible for an element to be a member of multiple classes; it is a space-separated list.)
Checking the src of each script element can ensure that tracking code (e.g. Google Analytics legacy tracking code) and other scripts loaded at the very end of the page will not prevent your script from working properly when async loading is used. Again, to handle the multiple widget case, you may need to remove the script elements when done with them (i.e. when the desired code has been added to the page).
One final comment I will make (although you may already be aware of this) is that when coding a widget, you need to declare all your variables using var and enclose all your code within: (JSLint can help check this)
(function(){
...
})();
This has been called a "self-executing function" and will ensure that variables used in your script do not interfere with the rest of the Web page.
Whether you drop a <script> tag in or a <div class="mywidget">, you're adding something to the markup. Personally, I prefer the latter as the script itself is only added once. Too many scripts in the page body can slow down the page load time.
But if you need to add the script tag where the widget is going to be, I don't see what's wrong with using document.write() to place a div.
I just found another method that seems to answer my question:
How to access parent Iframe from javascript
Embedding the script in an iframe allows to locate it anytime, as the script always keeps a reference to its own window.
I vote this the best approach, as it'll always work no matter how many times you add the script to the page (think widget). You're welcome to comment.
What pushed me to consider iframes in the first place was an experiment I did to build a Google gadget.
In many cases this work well (hud.js is the name of the scipt):
var jsscript = document.getElementsByTagName("script");
for (var i = 0; i < jsscript.length; i++) {
var pattern = /hud.js/i;
if ( pattern.test( jsscript[i].getAttribute("src") ) )
{
var parser = document.createElement('a');
parser.href = jsscript[i].getAttribute("src");
host = parser.host;
}
}
Also you can add individual script's name inside them.
either inside some js-script
dataset['my_prefix_name'] = 'someScriptName'
or inside HTML - in the <script> tag
data-my_prefix_name='someScriptName'
and next search appropriate one by looping over document.scripts array:
... function(){
for (var i = 0, n = document.scripts.length; i < n; i++) {
var prefix = document.scripts[i].dataset['my_prefix_name']
if (prefix == 'whatYouNeed')
return prefix
}
}
I haven't had access to internet explorer since forever, but this should work pretty much everywhere:
<script src="script.js"
data-count="30"
data-headline="My headline"
onload="uniqueFunctionName(this)"
defer
></script>
and inside script.js:
window.uniqueFunctionName = function (currentScript) {
var dataset = currentScript.dataset
console.log(dataset['count'])
console.log(dataset['headline'])
}