I am writing a userscript that will run whenever I visit our JIRA server. It will swap out the avatar images for users with images from our company's ID badging photo store. This way, the entire organization does not need to upload their images onto the JIRA server separately for the user icons to be readily recognizable. I can use jQuery to alter the 'src' attribute of images already rendered into the DOM. However, the JIRA pages load dynamic content into divs and so on, so I am wondering if there is a way to attach an event handler that would trigger whenever the browser tries to fetch an image, and serve a different image from company server instead. The img tag contains the data attribute needed for me to map the image correctly. I just don't know what event to hook into.
The closest I could find is the "Grumpy cat" example on this page (landed there from this Stackoverflow comment), but it seems Mobify is no longer being maintained. What are the present-day options to tackle this use case?
Thanks!
You can use a deep (subtree) MutationObserver to watch for elements being added to the DOM. Whenever an element is added, look through it and its children for <img>s, and perform the required replacement on each:
const transform = img => img.src = img.src.replace(/200x100/, '100x100');
new MutationObserver((mutations) => {
for (const { addedNodes } of mutations) {
for (const addedNode of addedNodes) {
if (addedNode.nodeType !== 1) continue;
const imgs = addedNode.querySelectorAll('img');
for (const img of imgs) {
transform(img);
}
if (addedNode.tagName === 'IMG') {
transform(addedNode);
}
}
}
})
.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
document.body.innerHTML += '<div><img src="https://via.placeholder.com/200x100"><div>';
});
Related
I'm working on a messaging service that allows users to send messages with attachments. I was using an RTE but it added a lot of unnecessary complexity and so now my textbox is just react-textarea-autosize. It is working as expected but when a user pastes/drops an image in to the text box I am trying to capture it and store it in a separate array of files that appears just below the message.
I started using the "onPaste" event, and logging the event shows event.clipboardData as:
{dropEffect: "none",
effectAllowed: "uninitialized",
files: FileList {length: 0},
items: DataTransferItemList {length: 0},
types: []}
using event.clipboardData.getData("text") returns the correct result if text is copied, and an empty string if an image is copied.
I then attempted to add a separate div that was set to "contentEditable:true" and listen to a paste on that to determine if using a textarea was the issue, and although I could visibly see the image being pasted, but the event still contained no useful data. This also does not work for dropping image files, or dragging and dropping images from other websites.
What gives? I don't understand because the paste event is still firing, I've been testing this mainly in chrome, but firefox also seemed to face difficulties, although it was returning an array of types in the cliboardData object.
Is there anyway to receive an image file that is captured from the paste/drop event?
I don't know why the files and items show a length of zero, but what I discovered is that the DataTransferItemList doesn't display properties in logs, instead you have to use getter functions as referenced in the MDN Docs. I found a great small example of different Inputs with data transfers.
https://codepen.io/tech_query/pen/MqGgap
form.onpaste = form.ondrop = event => {
if (event.type === 'drop') event.preventDefault();
const target = event.target;
const cell = target.nextElementSibling.querySelectorAll('td');
cell[0].textContent = event.type;
const transfer = event.clipboardData || event.dataTransfer;
cell[1].firstChild.innerHTML = Array.from(
transfer.items, item => `<li>${item.kind} ${item.type}</li>`
).join('\n');
};
To give you some background, many (if not all) websites load their images one by one, so if there are a lot of images, and/or you have a slow computer, most of the images wont show up. This is avoidable for the most part, however if you're running a script to exact image URLs, then you don't need to see the image, you just want its URL. My question is as follows:
Is it possible to trick a webpage into thinking an image is done loading so that it will start loading the next one?
Typically browser will not wait for one image to be downloaded before requesting the next image. It will request all images simultaneously, as soon as it gets the srcs of those images.
Are you sure that the images are indeed waiting for previous image to download or are they waiting for a specific time interval?
In case if you are sure that it depends on download of previous image, then what you can do is, route all your requests through some proxy server / firewall and configure it to return an empty file with HTTP status 200 whenever an image is requested from that site.
That way the browser (or actually the website code) will assume that it has downloaded the image successfully.
how do I do that? – Jack Kasbrack
That's actually a very open ended / opinion based question. It will also depend on your OS, browser, system permissions etc. Assuming you are using Windows and have sufficient permissions, you can try using Fiddler. It has an AutoResponder functionality that you can use.
(I've no affiliation with Fiddler / Telerik as such. I'm suggesting it only as an example and because I've used it in the past and know that it can be used for the aforementioned purpose. There will be many more products that provide similar functionality and you should use the product of your choice.)
use a plugin called lazy load. what it does is it will load the whole webpage and will just load the image later on. it will only load the image when the user scroll on it.
To extract all image URLs to a text file maybe you could use something like this,
If you execute this script inside any website it will list the URLs of the images
document.querySelectorAll('*[src]').forEach((item) => {
const isImage = item.src.match(/(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|jpeg|gif|png|svg)/g);
if (isImage) console.log(item.src);
});
You could also use the same idea to read Style from elements and get images from background url or something, like that:
document.querySelectorAll('*').forEach((item) => {
const computedItem = getComputedStyle(item);
Object.keys(computedItem).forEach((attr) => {
const style = computedItem[attr];
const image = style.match(/(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|jpeg|gif|png|svg)/g);
if (image) console.log(image[0]);
});
});
So, at the end of the day you could do some function like that, which will return an array of all images on the site
function getImageURLS() {
let images = [];
document.querySelectorAll('*').forEach((item) => {
const computedItem = getComputedStyle(item);
Object.keys(computedItem).forEach((attr) => {
const style = computedItem[attr];
const image = style.match(/(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|jpeg|gif|png|svg)/g);
if (image) images.push(image[0]);
});
});
document.querySelectorAll('*[src]').forEach((item) => {
const isImage = item.src.match(/(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|jpeg|gif|png|svg)/g);
if (isImage) images.push(item.src);
});
return images;
}
It can probably be optimized but, well you get the idea..
If you just want to extract images once. You can use some tools like
1) Chrome Extension
2) Software
3) Online website
If you want to run it multiple times. Probably use the above code https://stackoverflow.com/a/53245330/4674358 wrapped in if condition
if(document.readyState === "complete") {
extractURL();
}
else {
//Add onload or DOMContentLoaded event listeners here: for example,
window.addEventListener("onload", function () {
extractURL();
}, false);
//or
/*document.addEventListener("DOMContentLoaded", function () {
extractURL();
}, false);*/
}
extractURL() {
//code mentioned above
}
You want the "DOMContentLoaded" event docs. It fires as soon as the document is fully parsed, but before everything has been loaded.
let addIfImage = (list, image) => image.src.match(/(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|jpeg|gif|png|svg)/g) ?
[image.src, ...list] :
list;
let getSrcFromTags= (tag = 'img') => Array.from(document.getElementsByTagName(tag))
.reduce(addIfImage, []);
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", doSomething);
} else { // `DOMContentLoaded` already fired
doSomething();
}
I am using this, works as expected:
var imageLoading = function(n) {
var image = document.images[n];
var downloadingImage = new Image();
downloadingImage.onload = function(){
image.src = this.src;
console.log('Image ' + n + ' loaded');
if (document.images[++n]) {
imageLoading(n);
}
};
downloadingImage.src = image.getAttribute("data-src");
}
document.addEventListener("DOMContentLoaded", function(event) {
setTimeout(function() {
imageLoading(0);
}, 0);
});
And change every src attribute of image element to data-src
Problem: I am working on an extension in javascript which needs to be able to view the source HTML of a page after everything is rendered.
The problem is that no matter what method I use, I can only seem to retrieve the pre-rendered source. The website is using emberjs for generating the content of the page.
Example:
Site: https://www.playstation.com/en-us/explore/games/ps4-games/?console=ps4
When I right click and view source, I get the page before the content is loaded.When I right click and inspect element, I want to get the source after the content has loaded.
What I've tried:
background.js
var acceptedURLPattern = "playstation.com";
tabUpdatedCallback = function(tabID, changeInfo, tab) {
if(tab.url.indexOf(acceptedURLPattern) == -1) return;
var eventJsonScript = {
code: "console.log(\"Script Injected\"); window.addEventListener(\"load\", (event) => { " + browserString + ".runtime.sendMessage({ \"html\": document.documentElement.outerHTML });});"
};
browser.tabs.executeScript(tabID, eventJsonScript);
}
handleHTMLMessage = function(request, sender, sendResponse) {
console.log(request);
}
browser.tabs.onUpdated.addListener(tabUpdatedCallback);
browser.runtime.onMessage.addListener(handleHTMLMessage);
The above script is injecting an eventListener onto the page I want to grab the source of after it fires the "load" event which will then send a message back to background.js containing that source.
I've tried changing the documentElement to innerHTML/outerHTML as well as changing the eventListener to document.addEventListener(\"DOMContentLoaded\"), but none of these changes seemed to have any effect.
I've also tried using these: Get javascript rendered html source using phantomjs and get a browser rendered html+javascript but they are using phantomjs to load and execute the page, then return the html. In my solution, I need to be able to grab the already rendered page.
Thanks for the help in advance!
Edit #1:
I took a look at MutationObserver as mentioned by #wOxxOm and changed the eventJsonScript variable to look like this:
var eventJsonScript = {
code: "console.log(\"Script Injected\"); var mutationObserver = new MutationObserver( (mutations) => { mutations.forEach((mutation) => {if( JSON.stringify(mutation).indexOf(\"Yakuza\") != -1) { console.log(mutation); } });}); mutationObserver.observe(document.documentElement, {attributes: true, characterData: true, childList: true, subtree: true, attributeOldValue: true, characterDataOldValue: true}); mutationObserver.takeRecords()"
};
however despite the site clearly having a section for Yakuza 6, the event doesn't get fired. I did remove the if condition in the injected script to verify that events do get fired normally, it just doesn't seem to contain information that I'm looking for.
So the good news is that someone has already written the code to do this in Ember, you can find it here:
https://github.com/emberjs/ember-test-helpers/blob/031969d016fb0201fd8504ac275526f3a0ab2ecd/addon-test-support/%40ember/test-helpers/settled.js
This is the code Ember tests use to wait until everything is rendered and complete, or "settled".
The bad news is it is a nontrivial task to extract it correctly for your extension.
Basically, you will want to:
Wait till the page is loaded (window.load event)
setTimeout at least 200 ms to ensure the Ember app has booted.
Wait until settled, using code linked above.
Wait until browser is idle (requestIdleCallback in latest Chrome, or get a polyfill).
Hope this helps get you started.
I am trying to detect if a folder or a file is dragged in the dragover or dragenter events.
For example:
In the ondrop event, there is an argument called MouseEvent, which has a field named dataTransfer, where are listed files (.files) or items (.items), depending on the browser, and I can read that in both Chrome and Firefox. However, for the dragover and dragenter events those fields (.files and .items) are empty. The problem is that I need that information while dragging, not dropping.
NOTE: For both files and folders event.dataTransfer.types[i] === "Files" is true.
Background Research
I found the following answer to partially fit for my question:
WebKit, and hence Chrome, is quite restrictive on when you can call getData. You're not allowed to do it inside dragstart or dragover. I think this is the canonical bug.
But that answer is from 2012, and I can't find actual updated information on the topic, so I am looking for updated information on this.
TL;DR you can't.
If you're wondering why this question still hasn't got an accepted answer, you can read this meta question created by OP, and my answer.
File drag/drop in HTML5
I made some research in different pieces of documentation for this topic and tested it by myself on various browsers, so I decided to summarize all I know about drag and drop of files here.
Dragging
When you drag a file you can use some listeners, such as:
dragenter
dragover
dragend
dragleave
Given that these are drag events, the files property of event.dataTransfer will either have length == 0 or be empty (null).
You can't read files details in a drag event and you can't check if they are folders. This is not a bug, it's a security feature.
Imagine you could read files on a drag event: you would be able to read everything even if the user doesn't want to upload files to your site. It would make no sense, seriously. Imagine you are dragging a file from your desktop to another folder and you accidentally drag it through a web page: now the web page reads your file and stores your personal information on its server... that would be a huge security flaw.
However, you will still be able to detect whether the user is dragging files (and by files I mean folders too, because folders are files) or not by iterating over the array event.dataTransfer.types. You can create a function that checks if the drag event contains files, and then call it in the event handler.
Example:
function containsFiles(event) {
if (event.dataTransfer.types) {
for (var i=0; i<event.dataTransfer.types.length; i++) {
if (event.dataTransfer.types[i] == "Files") {
return true;
}
}
}
return false;
}
function handleDragEnter(e) {
e.preventDefault();
if (containsFiles(e)) {
// The drag event contains files
// Do something
} else {
// The drag event doesn't contain files
// Do something else
}
}
Dropping
When you drop a file into the drop <div> (or whatever element you're using as dropzone), you will use a listener for the event drop to read some file properties such as name, size, type and last modification date.
To detect if a file is a folder, you are going to:
Check if the file has type == "", because folders have no type.
Check if the file size is a multiple of 4096: size%4096 == 0, because folders always have a size multiple of 4096 bytes (which is 4KiB).
Example:
function handleDrop(e) {
e.stopPropagation();
e.preventDefault();
var files = e.dataTransfer.files;
for (var i = 0, f; f = files[i]; i++) { // iterate in the files dropped
if (!f.type && f.size%4096 == 0) {
// The file is a folder
// Do something
} else {
// The file is not a folder
// Do something else
}
}
}
KNOWN ISSUE: Since that folders are actually files, this is the only way to distinguish them from another kind of file. Although this method doesn't give you absolute certainty that a file is a folder: it might be a file without extension and with a size of 0 or exactly N x 4096B.
Working examples
Here are some working examples to see what I said above in action and test it by yourself. Before running them, make sure that your browser supports drag and drop features. Have fun:
File drop display info (made by me)
File/folder recognize (made by me)
File drag detect (from css-tricks)
This is work on Dropping -on drop event- (please note that this doesn't work on dragover event):
isDraggedItemIsFile = function(e) {
// handle FF
if (e.originalEvent.dataTransfer.files.length == 0) {
return false;
}
// handle Chrome
if (e.originalEvent.dataTransfer.items) {
if (typeof (e.originalEvent.dataTransfer.items[0].webkitGetAsEntry) == "function") {
return e.originalEvent.dataTransfer.items[0].webkitGetAsEntry().isFile;
} else if (typeof (e.originalEvent.dataTransfer.items[0].getAsEntry) == "function") {
return e.originalEvent.dataTransfer.items[0].getAsEntry().isFile;
}
}
return true;
};
$forms.on('drop', function(e) {
if (isDraggedItemIsFile(e)) {
// do something if file
} else{
// is directory
}
});
Tested on FF V49, Chrome V55, Edge V25
I was able to get the entire Mimetype of the thing being dragged over my page. Mimetype appears to be blank for folders, so maybe you can distinguish it that way.
Partial code (extracted from React):
function handleDragOver(ev: DragEvent) {
ev.preventDefault();
ev.dataTransfer!.dropEffect = 'copy';
console.log(Array.from(ev.dataTransfer.items).map(i => [i.kind,i.type].join('|')).join(', '));
}
document.addEventListener('dragover',handleDragOver);
Output looks like:
file|image/x-icon, file|image/jpeg, file|application/vnd.ms-excel
When I drag 3 files over my page.
Not sure if it only works on localhost, I haven't uploaded this anywhere yet, but it's totally working.
MDN docs on DataTransferItem
You can separate files from folders by using FileReader or with webkitGetAsEntry()
The webkitGetAsEntry() is not supported by ie11, so keep that in mind!
The code will look like:
onDrop(event) {
let files = event.dataTransfer ? event.dataTransfer.files : 'null';
for(let i = 0, file; file = files[i]; i++) {
var reader = new FileReader();
reader.onload = function (e) {
console.log('it is a file!');
};
reader.onerror = function (e) {
console.log('it is a folder!');
};
reader.readAsText(file);
}
}
What i need to do is copy the content of a div with id #logo to a form field with id #input_2_15.
The content in the div is an image (<img src.../>), but this changes... I have the code to copy the content to the input field when the page loads, but i need a code which copies the content every time the image changes (and it does so without refreshing the page). How can i do this?
Also, is it possible to get the function to only copy the image name eg. 12345.png rather than the whole <img src=..../>?
Miro
You mean
function getUrl(id) {
// function to return the source of an image inside an object with given ID
return $("#"+id).find("img").attr("src");
}
$(document).ready(function() {
var currentImage = getUrl("logo"); // get the url of the div now (empty I guess)
$("#ajaxtrigger").click(function() { // some link with ID ajaxtrigger
$("#logo").load("someurlreturningsomehmtl",function(){ // loads the image
if (currentImage != getUrl("logo")) { // did it change?
currentImage = getUrl("logo"); // save the name
$("#input_2_15").val(currentImage); // update the field
}
})
});
});
$('#logo').find('img').attr('src');
This will give you the image location.. now just append this to your img src in the second div
I see this as a case for a "publish and subscribe" ("Pub/Sub") approach.
In this article, we learn four ways to do Pub/Sub with jQuery 1.7, and I chose Option 1 which exploits a new feature of jquery 1.7, namely its $.Callbacks feature. The article gives a good understanding, which I will not try to better here.
The code below is a slightly modified version of Option 1, avoiding need for a global var:
$.Topic = function(id) {
var callbacks, topic = id && $.Topic.topics[id];
if (!topic) {
callbacks = $.Callbacks();
topic = {
publish: callbacks.fire,
subscribe: callbacks.add,
unsubscribe: callbacks.remove
};
if (id) {
$.Topic.topics[id] = topic;
}
}
return topic;
};
$.Topic.topics = {};//avoid global var by making `topics` a property of the static function `jQuery.Topic`.
$(function() {
//A function to change the logo and fire a publisher.
function changeLogoSrc(src) {
$("#logo img").attr('src', src);
$.Topic('logoSrcChanged').publish(src);
}
// A subscriber which listenes for the 'logoSrcChanged' publisher
// and responds by writing the src string to the required form field
$.Topic('logoSrcChanged').subscribe(function(src) {
$("#input_2_15").val(src);
});
});
Demo here
Thus the code for changing the logo and for updating the form field are, to use the correct jargon, effectively "decoupled".
This approach is arguably overblown for something simple, but would be useful in a more extensive environment where many pub/subs are required.