Modify greasemonkey script - greasemonkey

It create links to files, that I then can save. But the issue is, file default name always bunch of id numbers, but I want it to be replaced with text content from certain area of the website.
Here is the code:
// ==UserScript==
// #name SC Download helper
// #include *://*.somesite.com/*
// #require http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js
// #require https://gist.github.com/raw/2625891/waitForKeyElements.js
// #version 3
// #grant GM_addStyle
// #run-at document-start
// ==/UserScript==
function addLinks(jNode){
for (var i = 0, len = jNode.length; i < len; i++){
var title
var id = jNode[i].parentNode.parentNode
if(jNode[i].className=='video-title'){
if(id.nodeName=="A" && id.parentNode.nodeName=="LI"){return
}else{
title=jNode[i].innerHTML
id=id.rel}
}else if(jNode[i].className=='vr-video-title'){
title=jNode[i].firstElementChild.innerHTML
id=id.firstElementChild.firstElementChild.rel
}else{
title=jNode[i].innerHTML
id=document.getElementsByTagName('script')[12].innerHTML.match('content_id="(.*)"')[1]}
jNode[i].innerHTML+='<a download="'+ title.trim() +' ('+ id +').mp4" href=http://d3qx1p4lbtwbt8.cloudfront.net/encoded/'+ id +'_mp4_best.mp4?e=1497022873&h='+ id +'><img src=https://i.imgur.com/5hEBJ2N.png></a>'
}
}
waitForKeyElements('div.video-title, div.vr-video-title, h1.page-title.video-title', addLinks)

Related

Chrome.tabs.sendMessage is not sending a message to content script

I have looked at a lot of different posts about using execute script and send message to execute the content script on a specified tab, but it doesn't execute the content script until I do a hard refresh and then the response from the content script is successful. Attached below is the call back function for button clicked this is in a popup.js file.
function buttonClicked() {
// Get an object for the active tab
console.log("print dymo has been clicked");
chrome.tabs.query({active: true, currentWindow: true}, function(tab_array){
// send a messege to the contentscript that will then scrape the web
// it will then call the popup.js receiveURL() method with the url it makes
console.log("Messege sent to conntent script");
alert("print has been clicked")
alert(tab_array[0].id)
chrome.tabs.sendMessage(tab_array[0].id, {getHTML: true}, receiveURL);
});
Content Script:
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
// When there is a request (that will come in as true)
if (request) {
/*********************************************************************
IF YOU NEED TO CHANGE THE WEB SCRAPER START HERE
**********************************************************************/
// Take out everything in the html before the tag "<label>Job:</label>"
// Selects only the inner elements between the style row
var matches = document.querySelectorAll('ul.list-unstyled')
//Gives us the inner text of the elements of information
//Gives us an array of all the information split up by new lines
information = matches[1].innerText.split(/\r?\n/)
//Iterate ansd store everything in a dictionary split by a :
var dict_info = {}
for (index = 0; index < information.length; index++) {
parts = information[index].split(": ")
ans = ""
for(i = 1; i < parts.length; i++) {
ans += parts[i]
}
dict_info[parts[0]] = ans
}
var name = dict_info['Requestor']
var job = dict_info['Job']
if (job != undefined) {
job = job.match(JOB_REGEX)[1]
}
var request = dict_info['Request']
if (request != undefined) {
request = request.match(REQ_REGEX)[1]
}
var file = dict_info["File"]
if (file.length > 10) {
file = file.substring(0,file.length-9)
}
if (file.length > 20) {
file = file.substring(0, 20)
}
var email = dict_info['Requestor Email']
var cost = dict_info['Estimated Cost']
if(cost == undefined) {
cost = dict_info['Cost']
}
if (cost != undefined) {
cost = cost.match(COST_REGEX)[1]
}
name = name.split(" ")
name = name[0] + "/" + name[name.length-1]
var url = "https:// test"
sendResponse(url);
return true;
}
}
);

Update link to heading in google docs

In google docs one can easily add headings and link to them from inside of the document. But when the heading text changes, the link text does not change.
Is there a way to change that behavior or update the link text automatically?
I know it is about 1 1/2 years, but maybe this will help. I have had the exact same problem and wrote a function that will update all the links to the headings in a document. Since I could not find any built-in functions or add-ons, the only way was to script it.
Some things to consider:
This needs a current table of contents to work. If you don't have (or do not want) a TOC, you can insert one, run that function and delete it afterwards. Also, I have only tested it with a TOC that contains page numbers.
It will update ALL texts of links to headings in the document. However, links to everything else remain untouched.
Please use at your own risk (maybe try it out in a copy of your document). I have tested it, but the testing could have been more thorough. Also, this is my first in scripting Docs.
Paste this in the Script editor of your doc and run replaceHeadingLinks. Links that the script could not update (because they link to a heading that does not exist anymore) will be output in the console.
function replaceHeadingLinks() {
var curDoc = DocumentApp.getActiveDocument();
var links = getAllLinks_(curDoc.getBody());
var headings = getAllHeadings_(curDoc.getBody());
var deprecatedLinks = []; // holds all links to headings that do not exist anymore.
links.forEach(function(link) {
if(link.url.startsWith('#heading')) {
// get the new heading text
var newHeadingText = headings.get(link.url);
// if the link does not exist anymore, we cannot update it.
if(typeof newHeadingText !== "undefined") {
var newOffset = link.startOffset + newHeadingText.length - 1;
// delete the old text, insert new one and set link
link.element.deleteText(link.startOffset, link.endOffsetInclusive);
link.element.insertText(link.startOffset, newHeadingText);
link.element.setLinkUrl(link.startOffset, newOffset, link.url);
} else {
deprecatedLinks.push(link);
}
}
}
)
// error handling: show deprecated links:
if(deprecatedLinks.length > 0) {
Logger.log("Links we could not update:");
for(var i = 0; i < deprecatedLinks.length; i++) {
var link = deprecatedLinks[i];
var oldText = link.element.getText().substring(link.startOffset, link.endOffsetInclusive);
Logger.log("heading: " + link.url + " / description: " + oldText);
}
} else {
Logger.log("all links updated");
}
}
/**
* Get an array of all LinkUrls in the document. The function is
* recursive, and if no element is provided, it will default to
* the active document's Body element.
*
* #param {Element} element The document element to operate on.
* .
* #returns {Array} Array of objects, vis
* {element,
* startOffset,
* endOffsetInclusive,
* url}
*
* Credits: https://stackoverflow.com/questions/18727341/get-all-links-in-a-document/40730088
*/
function getAllLinks_(element) {
var links = [];
element = element || DocumentApp.getActiveDocument().getBody();
if (element.getType() === DocumentApp.ElementType.TEXT) {
var textObj = element.editAsText();
var text = element.getText();
var inUrl = false;
var curUrl = {};
for (var ch=0; ch < text.length; ch++) {
var url = textObj.getLinkUrl(ch);
if (url != null) {
if (!inUrl) {
// We are now!
inUrl = true;
curUrl = {};
curUrl.element = element;
curUrl.url = String( url ); // grab a copy
curUrl.startOffset = ch;
}
else {
curUrl.endOffsetInclusive = ch;
}
}
else {
if (inUrl) {
// Not any more, we're not.
inUrl = false;
links.push(curUrl); // add to links
curUrl = {};
}
}
}
// edge case: link is at the end of a paragraph
// check if object is empty
if(inUrl && (Object.keys(curUrl).length !== 0 || curUrl.constructor !== Object)) {
links.push(curUrl); // add to links
curUrl = {};
}
}
else {
// only traverse if the element is traversable
if(typeof element.getNumChildren !== "undefined") {
var numChildren = element.getNumChildren();
for (var i=0; i<numChildren; i++) {
// exclude Table of Contents
child = element.getChild(i);
if(child.getType() !== DocumentApp.ElementType.TABLE_OF_CONTENTS) {
links = links.concat(getAllLinks_(element.getChild(i)));
}
}
}
}
return links;
}
/**
* returns a map of all headings within an element. The map key
* is the heading ID, such as h.q1xuchg2smrk
*
* THIS REQUIRES A CURRENT TABLE OF CONTENTS IN THE DOCUMENT TO WORK PROPERLY.
*
* #param {Element} element The document element to operate on.
* .
* #returns {Map} Map with heading ID as key and the heading element as value.
*/
function getAllHeadings_(element) {
var headingsMap = new Map();
var p = element.findElement(DocumentApp.ElementType.TABLE_OF_CONTENTS).getElement();
if(p !== null) {
var toc = p.asTableOfContents();
for (var ti = 0; ti < toc.getNumChildren(); ti++) {
var itemToc = toc.getChild(ti).asParagraph().getChild(0).asText();
var itemText = itemToc.getText();
var itemUrl = itemToc.getLinkUrl(0);
var itemDesc = null;
// strip the line numbers if TOC contains line numbers
var itemText = itemText.match(/(.*)\t/)[1];
headingsMap.set(itemUrl,itemText);
}
}
return headingsMap;
}

Where to retrieve audio file? -- Arduino - Photon project

I have just started with electronics, and doing a project using the Spark Photon, which is based on Arduino. The project website is here: http://hackster.io/middleca/sending-sound-over-the-internet
I uploaded the following two files (.ino and .js) to the Photon, which should then capture and transmit sound (directly I assume). I expected a test.wav would be created. However, where should I find this file so I can check if everything worked?
main.ino file:
#define MICROPHONE_PIN A5
#define AUDIO_BUFFER_MAX 8192
int audioStartIdx = 0, audioEndIdx = 0;
uint16_t audioBuffer[AUDIO_BUFFER_MAX];
uint16_t txBuffer[AUDIO_BUFFER_MAX];
// version without timers
unsigned long lastRead = micros();
char myIpAddress[24];
TCPClient audioClient;
TCPClient checkClient;
TCPServer audioServer = TCPServer(3443);
void setup() {
Serial.begin(115200);
pinMode(MICROPHONE_PIN, INPUT);
// so we know where to connect, try:
// particle get MY_DEVICE_NAME ipAddress
Spark.variable("ipAddress", myIpAddress, STRING);
IPAddress myIp = WiFi.localIP();
sprintf(myIpAddress, "%d.%d.%d.%d", myIp[0], myIp[1], myIp[2], myIp[3]);
// 1/8000th of a second is 125 microseconds
audioServer.begin();
lastRead = micros();
}
void loop() {
checkClient = audioServer.available();
if (checkClient.connected()) {
audioClient = checkClient;
}
//listen for 100ms, taking a sample every 125us,
//and then send that chunk over the network.
listenAndSend(100);
}
void listenAndSend(int delay) {
unsigned long startedListening = millis();
while ((millis() - startedListening) < delay) {
unsigned long time = micros();
if (lastRead > time) {
// time wrapped?
//lets just skip a beat for now, whatever.
lastRead = time;
}
//125 microseconds is 1/8000th of a second
if ((time - lastRead) > 125) {
lastRead = time;
readMic();
}
}
sendAudio();
}
// Callback for Timer 1
void readMic(void) {
uint16_t value = analogRead(MICROPHONE_PIN);
if (audioEndIdx >= AUDIO_BUFFER_MAX) {
audioEndIdx = 0;
}
audioBuffer[audioEndIdx++] = value;
}
void copyAudio(uint16_t *bufferPtr) {
//if end is after start, read from start->end
//if end is before start, then we wrapped, read from start->max, 0->end
int endSnapshotIdx = audioEndIdx;
bool wrapped = endSnapshotIdx < audioStartIdx;
int endIdx = (wrapped) ? AUDIO_BUFFER_MAX : endSnapshotIdx;
int c = 0;
for(int i=audioStartIdx;i<endIdx;i++) {
// do a thing
bufferPtr[c++] = audioBuffer[i];
}
if (wrapped) {
//we have extra
for(int i=0;i<endSnapshotIdx;i++) {
// do more of a thing.
bufferPtr[c++] = audioBuffer[i];
}
}
//and we're done.
audioStartIdx = audioEndIdx;
if (c < AUDIO_BUFFER_MAX) {
bufferPtr[c] = -1;
}
}
// Callback for Timer 1
void sendAudio(void) {
copyAudio(txBuffer);
int i=0;
uint16_t val = 0;
if (audioClient.connected()) {
write_socket(audioClient, txBuffer);
}
else {
while( (val = txBuffer[i++]) < 65535 ) {
Serial.print(val);
Serial.print(',');
}
Serial.println("DONE");
}
}
// an audio sample is 16bit, we need to convert it to bytes for sending over the network
void write_socket(TCPClient socket, uint16_t *buffer) {
int i=0;
uint16_t val = 0;
int tcpIdx = 0;
uint8_t tcpBuffer[1024];
while( (val = buffer[i++]) < 65535 ) {
if ((tcpIdx+1) >= 1024) {
socket.write(tcpBuffer, tcpIdx);
tcpIdx = 0;
}
tcpBuffer[tcpIdx] = val & 0xff;
tcpBuffer[tcpIdx+1] = (val >> 8);
tcpIdx += 2;
}
// any leftovers?
if (tcpIdx > 0) {
socket.write(tcpBuffer, tcpIdx);
}
}
and the waveRecorder.js file:
// make sure you have Node.js Installed!
// Get the IP address of your photon, and put it here:
// CLI command to get your photon's IP address
//
// particle get MY_DEVICE_NAME ipAddress
// Put your IP here!
var settings = {
ip: "192.168.0.54",
port: 3443
};
/**
* Created by middleca on 7/18/15.
*/
//based on a sample from here
// http://stackoverflow.com/questions/19548755/nodejs-write-binary-data-into-writablestream-with-buffer
var fs = require("fs");
var samplesLength = 1000;
var sampleRate = 8000;
var outStream = fs.createWriteStream("test.wav");
var writeHeader = function() {
var b = new Buffer(1024);
b.write('RIFF', 0);
/* file length */
b.writeUInt32LE(32 + samplesLength * 2, 4);
//b.writeUint32LE(0, 4);
b.write('WAVE', 8);
/* format chunk identifier */
b.write('fmt ', 12);
/* format chunk length */
b.writeUInt32LE(16, 16);
/* sample format (raw) */
b.writeUInt16LE(1, 20);
/* channel count */
b.writeUInt16LE(1, 22);
/* sample rate */
b.writeUInt32LE(sampleRate, 24);
/* byte rate (sample rate * block align) */
b.writeUInt32LE(sampleRate * 2, 28);
/* block align (channel count * bytes per sample) */
b.writeUInt16LE(2, 32);
/* bits per sample */
b.writeUInt16LE(16, 34);
/* data chunk identifier */
b.write('data', 36);
/* data chunk length */
//b.writeUInt32LE(40, samplesLength * 2);
b.writeUInt32LE(0, 40);
outStream.write(b.slice(0, 50));
};
writeHeader(outStream);
var net = require('net');
console.log("connecting...");
client = net.connect(settings.port, settings.ip, function () {
client.setNoDelay(true);
client.on("data", function (data) {
try {
console.log("GOT DATA");
outStream.write(data);
//outStream.flush();
console.log("got chunk of " + data.toString('hex'));
}
catch (ex) {
console.error("Er!" + ex);
}
});
});
setTimeout(function() {
console.log('recorded for 10 seconds');
client.end();
outStream.end();
process.exit(0);
}, 10 * 1000);
Thieme! Such a beginner's question... SO unworthy!
Anyway, I will iron my heart and tell you the answer.
First of all, you misunderstood: the .ino file should go to the Photon and the waveRecorder.js file should be stored on your computer (or server) and called whenever you want to retrieve the audio. As you can read in the code, the .ino file makes sure that every millisecond it will check if something wants to connect, and if so, it will stream the sound to the wav.file stored in the same location as your waveRecorder.js file. "Something wants to connect" happens when you launch waveRecorder.js. Make sure you have node installed.
So, to sum it up:
Download the two files (main.ino and waveRecorder.js) to your computer in a folder ../xx/folderName
Then configure the IPAddress in both files using your photon's IPAddress
Upload main.ino to the photon (type 'particle flash abcdefgh123456578 "xx/../folderName/main.ino"' in the terminal)
Then run waveRecorder.js by typing 'node "xx/../folderName/waveRecorder.js"' in your terminal.
That should do it.. Even I got it working :)

How to get more result using node-youtube getbyId() method

I am using node-youtube(data api) to get the result of youtube-saerch by id. When I write
res.render('index',{data:(JSON.stringify(result, null, 2))}); then I get two results. But when i write res.render('index',{data:result}); then i get only when result. How can i get more results by simply writing res.render('index',{data:result});
rather than writing
res.render('index',{data:(JSON.stringify(result, null, 2))});
Here is the code of getbyId() metod.
var YouTube = require('youtube-node');
var youTube = new YouTube();
youTube.setKey('*************************');
youTube.getById('HcwTxRuq-uk', function(error, result) {
if (error) {
console.log(error);
}
else {
res.render('index',{data:(JSON.stringify(result, null, 2))});
}
});
I have also tried JSON.parse() method.
like this
var str=(JSON.stringify(result, null, 3));
var data=JSON.parse(str);
In str there are 3 results but in data there is only 1 result. why it return
one result. Also can i get 3 results using JSON.parse().
I hope this is what you want
Also notice there's no need for JSON.stringify at all
var YouTube = require('youtube-node');
var youTube = new YouTube();
youTube.setKey('************************************');
youTube.search('World War z Trailer', 2, function(error, result) {
if (error) {
console.log(error);
} else {
// result should contain 2 videos with some info
// to get more info use getById
var videos = [];
var video1 = result.items[0].id.videoId;
var video2 = result.items[1].id.videoId;
//get 1st video
youTube.getById(video1, function(error, result) {
if (!error) videos.push(result.items[0]);
// get 2nd video
youTube.getById(video2, function(error, result) {
if (!error) videos.push(result.items[0]);
//console.log(videos[0]);
//console.log(videos[1]);
res.render('index',{data:videos)});
});
});
}
});
I know this is not exactly your question, but I think it could help you to see how I'm doing in one of my projects.
If your videos are in a playlist, you can fetch up to 50 videos at the same time and use the pageToken to fetch more if you want more videos from the playlist.
You can fetch the playlist like this :
/**
* Return an array about the videos contained in the GEM-MECHANIC playlist (based on the selected language).
*
* #param $errorMessage String to return the error message.
* #param $language Int language code for which we must fetch the video (default 1 = english)
* #param $maxVideo Int maximum of video we must fetch with the request (<= 0 mean infinite, 5 if invalid)
* #param $playList Int Playlist which want to fetch (Use PlayList class constants).
*
* #return Bidimensionnal Array
*/
static public function getVideoList(&$errorMessage, $language = 1, $maxVideo = 0, $playList = PlayList::GEM_CAR){
$errorMessage = "";
$list = array();
$gemPlayList = self::getPlayList($playList);
if (array_key_exists($language, $gemPlayList)){
if (!is_numeric($maxVideo) or $maxVideo < 0){
$maxVideo = 5;
}
$list = self::fetchVideoList($errorMessage, $gemPlayList[$language], $maxVideo);
}
elseif(empty($gemPlayList)){
$errorMessage = GeneralDbManager::getInstance()->getErrorMessage("GEM_MECHANIC_INVALID_PLAYLIST_ERR", "The selected playlist doesn't exists.");
}
else{
$errorMessage = GeneralDbManager::getInstance()->getErrorMessage("GEM_MECHANIC_INVALID_LANGUAGE_ERR", 'The selected playlist do not contains videos for the language selected.');
}
return $list;
}
/**
* Return an array about the videos contained in the GEM-MECHANIC playlist (based on the selected language).
*
* #param $errorMessage String to return the error message.
* #param $playListId String id of the youtube playlist for which we want to fetch the video list.
* #param $maxVideo Int maximum of video we must fetch with the request
* #param $maxVideo Int number of videos with fetched so far.
* #param $nextToken String to use to fetch more videos.
*
* #return Bidimensionnal Array
*/
private static function fetchVideoList(&$errorMessage, $playListId, $maxVideo, $currentCount = 0, $nextToken = "", $currentList = array()){
if ($currentCount < $maxVideo or $maxVideo === 0) {
$result = abs($maxVideo - $currentCount);
$param = array('playlistId' => $playListId);
if ($result > 50 or $result === 0){
$param['maxResults'] = 50;
$result = 50;
}
else{
$param['maxResults'] = $result;
}
if (!empty($nextToken)){
$param['pageToken'] = $nextToken;
}
try{
$client = new Google_Client();
$client->setDeveloperKey(self::$apiKey);
$youtube = new Google_Service_YouTube($client);
$playList = $youtube->playlistItems->listPlaylistItems('contentDetails, snippet', $param);
unset($youtube);
unset($client);
foreach($playList as $video){
$currentList[] = array('id' => $video['contentDetails']['videoId'], "title" => $video['snippet']['title'], "check" => 0);
}
$currentCount += $result;
if (empty($errorMessage) and !is_null($playList['nextPageToken']) and $currentCount < $maxVideo){
self::fetchVideoList($errorMessage, $language, $maxVideo, $currentCount, $playList['nextPageToken'], $currentList);
}
unset($playList);
}
catch (Google_Exception $exception) {
ExceptionLogger::logException($exception);
$errorMessage = GeneralDbManager::getInstance()->getErrorMessage("GEM_MECHANIC_CANT_FETCH_VIDEO_ERR", 'We are currently not able to fetch the video list from Youtube.');
}
}
return $currentList;
}

Undo-Redo feature in Fabric.js

Is there any built-in support for for undo/redo in Fabric.js? Can you please guide me on how you used this cancel and repeat in [http://printio.ru/][1]
In http://jsfiddle.net/SpgGV/9/, move the object and change its size. If the object state is changed, and then we do undo/redo, its previous state will be deleted when the next change comes. It makes it easier to do undo/redo. All events of canvas should be called before any element is added to canvas. I didn't add an object:remove event here. You can add it yourself. If one element is removed, the state and list should be invalid if this element is in this array. The simpler way is to set state and list = [] and index = 0.
This will clear the state of your undo/redo queue. If you want to keep all states, such as add/remove, my suggestion is to add more properties to the element of your state array. For instance, state = [{"data":object.originalState, "event": "added"}, ....]. The "event" could be "modified" or "added" and set in a corresponding event handler.
If you have added one object, then set state[index].event="added" so that next time, when you use undo, you check it. If it's "added", then remove it anyway. Or when you use redo, if the target one is "added", then you added it. I've recently been quite busy. I will add codes to jsfiddle.net later.
Update: added setCoords() ;
var current;
var list = [];
var state = [];
var index = 0;
var index2 = 0;
var action = false;
var refresh = true;
canvas.on("object:added", function (e) {
var object = e.target;
console.log('object:modified');
if (action === true) {
state = [state[index2]];
list = [list[index2]];
action = false;
console.log(state);
index = 1;
}
object.saveState();
console.log(object.originalState);
state[index] = JSON.stringify(object.originalState);
list[index] = object;
index++;
index2 = index - 1;
refresh = true;
});
canvas.on("object:modified", function (e) {
var object = e.target;
console.log('object:modified');
if (action === true) {
state = [state[index2]];
list = [list[index2]];
action = false;
console.log(state);
index = 1;
}
object.saveState();
state[index] = JSON.stringify(object.originalState);
list[index] = object;
index++;
index2 = index - 1;
console.log(state);
refresh = true;
});
function undo() {
if (index <= 0) {
index = 0;
return;
}
if (refresh === true) {
index--;
refresh = false;
}
console.log('undo');
index2 = index - 1;
current = list[index2];
current.setOptions(JSON.parse(state[index2]));
index--;
current.setCoords();
canvas.renderAll();
action = true;
}
function redo() {
action = true;
if (index >= state.length - 1) {
return;
}
console.log('redo');
index2 = index + 1;
current = list[index2];
current.setOptions(JSON.parse(state[index2]));
index++;
current.setCoords();
canvas.renderAll();
}
Update: better solution to take edit history algorithm into account. Here we can use Editing.getInst().set(item) where the item could be {action, object, state}; For example, {"add", object, "{JSON....}"}.
/**
* Editing : we will save element states into an queue, and the length of queue
* is fixed amount, for example, 0..99, each element will be insert into the top
* of queue, queue.push, and when the queue is full, we will shift the queue,
* to remove the oldest element from the queue, queue.shift, and then we will
* do push.
*
* So the latest state will be at the top of queue, and the oldest one will be
* at the bottom of the queue (0), and the top of queue is changed, could be
* 1..99.
*
* The initialized action is "set", it will insert item into the top of queue,
* even if it arrived the length of queue, it will queue.shift, but still do
* the same thing, and queue only abandon the oldest element this time. When
* the current is changed and new state is coming, then this time, top will be
* current + 1.
*
* The prev action is to fetch "previous state" of the element, and it will use
* "current" to do this job, first, we will --current, and then we will return
* the item of it, because "current" always represent the "current state" of
* element. When the current is equal 0, that means, we have fetched the last
* element of the queue, and then it arrived at the bottom of the queue.
*
* The next action is to fetch "next state" after current element, and it will
* use "current++" to do the job, when the current is equal to "top", it means
* we have fetched the latest element, so we should stop.
*
* If the action changed from prev/next to "set", then we should reset top to
* "current", and abandon all rest after that...
*
* Here we should know that, if we keep the reference in the queue, the item
* in the queue will never be released.
*
*
* #constructor
*/
function Editing() {
this.queue = [];
this.length = 4;
this.bottom = 0;
this.top = 0;
this.current = 0;
this.empty = true;
// At the Begin of Queue
this.BOQ = true;
// At the End of Queue
this.EOQ = true;
// 0: set, 1: prev, 2: next
this._action = 0;
this._round = 0;
}
Editing.sharedInst = null;
Editing.getInst = function (owner) {
if (Editing.sharedInst === null) {
Editing.sharedInst = new Editing(owner);
}
return Editing.sharedInst;
};
/**
* To set the item into the editing queue, and mark the EOQ, BOQ, so we know
* the current position.
*
* #param item
*/
Editing.prototype.set = function (item) {
console.log("=== Editing.set");
var result = null;
if (this._action != 0) {
this.top = this.current + 1;
}
if (this.top >= this.length) {
result = this.queue.shift();
this.top = this.length - 1;
}
this._action = 0;
this.queue[this.top] = item;
this.current = this.top;
this.top++;
this.empty = false;
this.EOQ = true;
this.BOQ = false;
console.log("==> INFO : ");
console.log(item);
console.log("===========");
console.log("current: ", 0 + this.current);
console.log("start: ", 0 + this.bottom);
console.log("end: ", 0 + this.top);
return result;
};
/**
* To fetch the previous item just before current one
*
* #returns {item|boolean}
*/
Editing.prototype.prev = function () {
console.log("=== Editing.prev");
if (this.empty) {
return false;
}
if (this.BOQ) {
return false;
}
this._action = 1;
this.current--;
if (this.current == this.bottom) {
this.BOQ = true;
}
var item = this.queue[this.current];
this.EOQ = false;
console.log("==> INFO : ");
console.log(item);
console.log("===========");
console.log("current: ", 0 + this.current);
console.log("start: ", 0 + this.bottom);
console.log("end: ", 0 + this.top);
return item;
};
/**
* To fetch the next item just after the current one
*
* #returns {*|boolean}
*/
Editing.prototype.next = function () {
console.log("=== Editing.next");
if (this.empty) {
return false;
}
if (this.EOQ) {
return false;
}
this.current++;
if (this.current == this.top - 1 && this.top < this.length) {
this.EOQ = true;
}
if (this.current == this.top - 1 && this.top == this.length) {
this.EOQ = true;
}
this._action = 2;
var item = this.queue[this.current];
this.BOQ = false;
console.log("==> INFO : ");
console.log(item);
console.log("===========");
console.log("current: ", 0 + this.current);
console.log("start: ", 0 + this.bottom);
console.log("end: ", 0 + this.top);
return item;
};
/**
* To empty the editing and reset all state
*/
Editing.prototype.clear = function () {
this.queue = [];
this.bottom = 0;
this.top = 0;
this.current = 0;
this.empty = true;
this.BOQ = true;
this.EOQ = false;
};
Here is a solution that started with this simpler answer to the similar question, Undo Redo History for Canvas FabricJs.
My answer is along the same lines as Tom's answer and the other answers that are modifications of Tom's answer.
To track the state, I'm using JSON.stringify(canvas) and canvas.loadFromJSON() like the other answers and have an event registered on the object:modified to capture the state.
One important thing is that the final canvas.renderAll() should be called in a callback passed to the second parameter of loadFromJSON(), like this
canvas.loadFromJSON(state, function() {
canvas.renderAll();
}
This is because it can take a few milliseconds to parse and load the JSON and you need to wait until that's done before you render. It's also important to disable the undo and redo buttons as soon as they're clicked and to only re-enable in the same call back. Something like this
$('#undo').prop('disabled', true);
$('#redo').prop('disabled', true);
canvas.loadFromJSON(state, function() {
canvas.renderAll();
// now turn buttons back on appropriately
...
(see full code below)
}
I have an undo and a redo stack and a global for the last unaltered state. When some modification occurs, then the previous state is pushed into the undo stack and the current state is re-captured.
When the user wants to undo, then current state is pushed to the redo stack. Then I pop off the last undo and both set it to the current state and render it on the canvas.
Likewise when the user wants to redo, the current state is pushed to the undo stack. Then I pop off the last redo and both set it to the current state and render it on the canvas.
The Code
// Fabric.js Canvas object
var canvas;
// current unsaved state
var state;
// past states
var undo = [];
// reverted states
var redo = [];
/**
* Push the current state into the undo stack and then capture the current state
*/
function save() {
// clear the redo stack
redo = [];
$('#redo').prop('disabled', true);
// initial call won't have a state
if (state) {
undo.push(state);
$('#undo').prop('disabled', false);
}
state = JSON.stringify(canvas);
}
/**
* Save the current state in the redo stack, reset to a state in the undo stack, and enable the buttons accordingly.
* Or, do the opposite (redo vs. undo)
* #param playStack which stack to get the last state from and to then render the canvas as
* #param saveStack which stack to push current state into
* #param buttonsOn jQuery selector. Enable these buttons.
* #param buttonsOff jQuery selector. Disable these buttons.
*/
function replay(playStack, saveStack, buttonsOn, buttonsOff) {
saveStack.push(state);
state = playStack.pop();
var on = $(buttonsOn);
var off = $(buttonsOff);
// turn both buttons off for the moment to prevent rapid clicking
on.prop('disabled', true);
off.prop('disabled', true);
canvas.clear();
canvas.loadFromJSON(state, function() {
canvas.renderAll();
// now turn the buttons back on if applicable
on.prop('disabled', false);
if (playStack.length) {
off.prop('disabled', false);
}
});
}
$(function() {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Set up the canvas
canvas = new fabric.Canvas('canvas');
canvas.setWidth(500);
canvas.setHeight(500);
// save initial state
save();
// register event listener for user's actions
canvas.on('object:modified', function() {
save();
});
// draw button
$('#draw').click(function() {
var imgObj = new fabric.Circle({
fill: '#' + Math.floor(Math.random() * 16777215).toString(16),
radius: Math.random() * 250,
left: Math.random() * 250,
top: Math.random() * 250
});
canvas.add(imgObj);
canvas.renderAll();
save();
});
// undo and redo buttons
$('#undo').click(function() {
replay(undo, redo, '#redo', this);
});
$('#redo').click(function() {
replay(redo, undo, '#undo', this);
})
});
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.5.0/fabric.min.js" type="text/javascript"></script>
</head>
<body>
<button id="draw">circle</button>
<button id="undo" disabled>undo</button>
<button id="redo" disabled>redo</button>
<canvas id="canvas" style="border: solid 1px black;"></canvas>
</body>
I am allowing the user to remove the last added path (in my painting application), this works fine for me:
var lastItemIndex = (fabricCanvas.getObjects().length - 1);
var item = fabricCanvas.item(lastItemIndex);
if(item.get('type') === 'path') {
fabricCanvas.remove(item);
fabricCanvas.renderAll();
}
But you could also remove the IF statement and let people remove anything.
I know its late to answer this but this is my version of implementing this. Can be useful to someone.
I have implemented this feature by saving Canvas States as JSON. Whenever a user adds or modifies an object in the Canvas, it will save the changed canvas state and maintain it in an array. This array is then manipulated whenever user clicks on Undo or Redo button.
Take a look at this link. I have also provided a working Demo URL.
https://github.com/abhi06991/Undo-Redo-Fabricjs
HTML:
<canvas id="canvas" width="400" height="400"></canvas>
<button type="button" id="undo" >Undo</button>
<button type="button" id="redo" disabled>Redo</button>
JS:
var canvasDemo = (function(){
var _canvasObject = new fabric.Canvas('canvas',{backgroundColor : "#f5deb3"});
var _config = {
canvasState : [],
currentStateIndex : -1,
undoStatus : false,
redoStatus : false,
undoFinishedStatus : 1,
redoFinishedStatus : 1,
undoButton : document.getElementById('undo'),
redoButton : document.getElementById('redo'),
};
_canvasObject.on(
'object:modified', function(){
updateCanvasState();
}
);
_canvasObject.on(
'object:added', function(){
updateCanvasState();
}
);
var addObject = function(){
var rect = new fabric.Rect({
left : 100,
top : 100,
fill : 'red',
width : 200,
height : 200
});
_canvasObject.add(rect);
_canvasObject.setActiveObject(rect);
_canvasObject.renderAll();
}
var updateCanvasState = function() {
if((_config.undoStatus == false && _config.redoStatus == false)){
var jsonData = _canvasObject.toJSON();
var canvasAsJson = JSON.stringify(jsonData);
if(_config.currentStateIndex < _config.canvasState.length-1){
var indexToBeInserted = _config.currentStateIndex+1;
_config.canvasState[indexToBeInserted] = canvasAsJson;
var numberOfElementsToRetain = indexToBeInserted+1;
_config.canvasState = _config.canvasState.splice(0,numberOfElementsToRetain);
}else{
_config.canvasState.push(canvasAsJson);
}
_config.currentStateIndex = _config.canvasState.length-1;
if((_config.currentStateIndex == _config.canvasState.length-1) && _config.currentStateIndex != -1){
_config.redoButton.disabled= "disabled";
}
}
}
var undo = function() {
if(_config.undoFinishedStatus){
if(_config.currentStateIndex == -1){
_config.undoStatus = false;
}
else{
if (_config.canvasState.length >= 1) {
_config.undoFinishedStatus = 0;
if(_config.currentStateIndex != 0){
_config.undoStatus = true;
_canvasObject.loadFromJSON(_config.canvasState[_config.currentStateIndex-1],function(){
var jsonData = JSON.parse(_config.canvasState[_config.currentStateIndex-1]);
_canvasObject.renderAll();
_config.undoStatus = false;
_config.currentStateIndex -= 1;
_config.undoButton.removeAttribute("disabled");
if(_config.currentStateIndex !== _config.canvasState.length-1){
_config.redoButton.removeAttribute('disabled');
}
_config.undoFinishedStatus = 1;
});
}
else if(_config.currentStateIndex == 0){
_canvasObject.clear();
_config.undoFinishedStatus = 1;
_config.undoButton.disabled= "disabled";
_config.redoButton.removeAttribute('disabled');
_config.currentStateIndex -= 1;
}
}
}
}
}
var redo = function() {
if(_config.redoFinishedStatus){
if((_config.currentStateIndex == _config.canvasState.length-1) && _config.currentStateIndex != -1){
_config.redoButton.disabled= "disabled";
}else{
if (_config.canvasState.length > _config.currentStateIndex && _config.canvasState.length != 0){
_config.redoFinishedStatus = 0;
_config.redoStatus = true;
_canvasObject.loadFromJSON(_config.canvasState[_config.currentStateIndex+1],function(){
var jsonData = JSON.parse(_config.canvasState[_config.currentStateIndex+1]);
_canvasObject.renderAll();
_config.redoStatus = false;
_config.currentStateIndex += 1;
if(_config.currentStateIndex != -1){
_config.undoButton.removeAttribute('disabled');
}
_config.redoFinishedStatus = 1;
if((_config.currentStateIndex == _config.canvasState.length-1) && _config.currentStateIndex != -1){
_config.redoButton.disabled= "disabled";
}
});
}
}
}
}
return {
addObject : addObject,
undoButton : _config.undoButton,
redoButton : _config.redoButton,
undo : undo,
redo : redo,
}
})();
canvasDemo.undoButton.addEventListener('click',function(){
canvasDemo.undo();
});
canvasDemo.redoButton.addEventListener('click',function(){
canvasDemo.redo();
});
canvasDemo.addObject();
My use case was drawing simple shapes akin to blueprints, so I didn't have to worry about the overhead of saving the whole canvas state. If you are in the same situation, this is very easy to accomplish. This code assumes you have a 'wrapper' div around the canvas, and that you want the undo/redo functionality bound to the standard windows keystrokes of 'CTRL+Z' and 'CTRL+Y'.
The purpose of the 'pause_saving' variable was to account for the fact that when a canvas is re-rendered it seemingly created each object one by one all over again, and we don't want to catch these events, as they aren't REALLY new events.
//variables for undo/redo
let pause_saving = false;
let undo_stack = []
let redo_stack = []
canvas.on('object:added', function(event){
if (!pause_saving) {
undo_stack.push(JSON.stringify(canvas));
redo_stack = [];
console.log('Object added, state saved', undo_stack);
}
});
canvas.on('object:modified', function(event){
if (!pause_saving) {
undo_stack.push(JSON.stringify(canvas));
redo_stack = [];
console.log('Object modified, state saved', undo_stack);
}
});
canvas.on('object:removed', function(event){
if (!pause_saving) {
undo_stack.push(JSON.stringify(canvas));
redo_stack = [];
console.log('Object removed, state saved', undo_stack);
}
});
//Listen for undo/redo
wrapper.addEventListener('keydown', function(event){
//Undo - CTRL+Z
if (event.ctrlKey && event.keyCode == 90) {
pause_saving=true;
redo_stack.push(undo_stack.pop());
let previous_state = undo_stack[undo_stack.length-1];
if (previous_state == null) {
previous_state = '{}';
}
canvas.loadFromJSON(previous_state,function(){
canvas.renderAll();
})
pause_saving=false;
}
//Redo - CTRL+Y
else if (event.ctrlKey && event.keyCode == 89) {
pause_saving=true;
state = redo_stack.pop();
if (state != null) {
undo_stack.push(state);
canvas.loadFromJSON(state,function(){
canvas.renderAll();
})
pause_saving=false;
}
}
});
You can use "object:added" and/or "object:removed" for that — fabricjs.com/events
You can follow this post:
Do we have canvas Modified Event in Fabric.js?
I know the answer is already chosen but here is my version, script is condensed, also added a reset to original state. After any event you want to save just call saveState(); jsFiddle
canvas = new fabric.Canvas('canvas', {
selection: false
});
function saveState(currentAction) {
currentAction = currentAction || '';
// if (currentAction !== '' && lastAction !== currentAction) {
$(".redo").val($(".undo").val());
$(".undo").val(JSON.stringify(canvas));
console.log("Saving After " + currentAction);
lastAction = currentAction;
// }
var objects = canvas.getObjects();
for (i in objects) {
if (objects.hasOwnProperty(i)) {
objects[i].setCoords();
}
}
}
canvas.on('object:modified', function (e) {
saveState("modified");
});
// Undo Canvas Change
function undo() {
canvas.loadFromJSON($(".redo").val(), canvas.renderAll.bind(canvas));
}
// Redo Canvas Change
function redo() {
canvas.loadFromJSON($(".undo").val(), canvas.renderAll.bind(canvas));
};
$("#reset").click(function () {
canvas.loadFromJSON($("#original_canvas").val(),canvas.renderAll.bind(canvas));
});
var bgnd = new fabric.Image.fromURL('https://s3-eu-west-1.amazonaws.com/kienzle.dev.cors/img/image2.png', function(oImg){
oImg.hasBorders = false;
oImg.hasControls = false;
// ... Modify other attributes
canvas.insertAt(oImg,0);
canvas.setActiveObject(oImg);
myImg = canvas.getActiveObject();
saveState("render");
$("#original_canvas").val(JSON.stringify(canvas.toJSON()));
});
$("#undoButton").click(function () {
undo();
});
$("#redoButton").click(function () {
redo();
});
i developed a small script for you,hope it will help you .see this demo Fiddle
although redo is not perfect you have to click minimum two time at undo button then redo work .you can easily solve this problem with giving simple conditions in redo code.
//Html
<canvas id="c" width="400" height="200" style=" border-radius:25px 25px 25px 25px"></canvas>
<br>
<br>
<input type="button" id="addtext" value="Add Text"/>
<input type="button" id="undo" value="Undo"/>
<input type="button" id="redo" value="redo"/>
<input type="button" id="clear" value="Clear Canvas"/>
//script
var canvas = new fabric.Canvas('c');
var text = new fabric.Text('Sample', {
fontFamily: 'Hoefler Text',
left: 50,
top: 30,
//textAlign: 'center',
fill: 'navy',
});
canvas.add(text);
var vall=10;
var l=0;
var flag=0;
var k=1;
var yourJSONString = new Array();
canvas.observe('object:selected', function(e) {
//yourJSONString = JSON.stringify(canvas);
if(k!=10)
{
yourJSONString[k] = JSON.stringify(canvas);
k++;
}
j = k;
var activeObject = canvas.getActiveObject();
});
$("#undo").click(function(){
if(k-1!=0)
{
canvas.clear();
canvas.loadFromJSON(yourJSONString[k-1]);
k--;
l++;
}
canvas.renderAll();
});
$("#redo").click(function(){
if(l > 1)
{
canvas.clear();
canvas.loadFromJSON(yourJSONString[k+1]);
k++;
l--;
canvas.renderAll();
}
});
$("#clear").click(function(){
canvas.clear();
});
$("#addtext").click(function(){
var text = new fabric.Text('Sample', {
fontFamily: 'Hoefler Text',
left: 100,
top: 100,
//textAlign: 'center',
fill: 'navy',
});
canvas.add(text);
});
I have answer to all your queries :) get a smile
check this link.. its all done ... copy & paste it :P
http://jsfiddle.net/SpgGV/27/
var canvas = new fabric.Canvas('c');
var current;
var list = [];
var state = [];
var index = 0;
var index2 = 0;
var action = false;
var refresh = true;
state[0] = JSON.stringify(canvas.toDatalessJSON());
console.log(JSON.stringify(canvas.toDatalessJSON()));
$("#clear").click(function(){
canvas.clear();
index=0;
});
$("#addtext").click(function(){
++index;
action=true;
var text = new fabric.Text('Sample', {
fontFamily: 'Hoefler Text',
left: 100,
top: 100,
//textAlign: 'center',
fill: 'navy',
});
canvas.add(text);
});
canvas.on("object:added", function (e) {
if(action===true){
var object = e.target;
console.log(JSON.stringify(canvas.toDatalessJSON()));
state[index] = JSON.stringify(canvas.toDatalessJSON());
refresh = true;
action=false;
canvas.renderAll();
}
});
function undo() {
if (index < 0) {
index = 0;
canvas.loadFromJSON(state[index]);
canvas.renderAll();
return;
}
console.log('undo');
canvas.loadFromJSON(state[index]);
console.log(JSON.stringify(canvas.toDatalessJSON()));
canvas.renderAll();
action = false;
}
function redo() {
action = false;
if (index >= state.length - 1) {
canvas.loadFromJSON(state[index]);
canvas.renderAll();
return;
}
console.log('redo');
canvas.loadFromJSON(state[index]);
console.log(JSON.stringify(canvas.toDatalessJSON()));
canvas.renderAll();
canvas.renderAll();
}
canvas.on("object:modified", function (e) {
var object = e.target;
console.log('object:modified');
console.log(JSON.stringify(canvas.toDatalessJSON()));
state[++index] = JSON.stringify(canvas.toDatalessJSON());
action=false;
});
$('#undo').click(function () {
index--;
undo();
});
$('#redo').click(function () {
index++;
redo();
});

Resources