Posts Tagged javascript

How to detect if element is added or removed from DOM?

There is new HTML5 API called MutationObserver that allow to add events when DOM is changing including a case when element is added or removed from DOM. Mutation observer support is pretty good.

Here is example to detect if element is added/removed from DOM with little help from jQuery

var in_dom = !!element.closest('body').length;
var observer = new MutationObserver(function(mutations) {
    if (element.closest('body').length) {
        if (!in_dom) {
            console.log("element inserted");
        }
        in_dom = true;
    } else if (in_dom) {
        in_dom = false;
        console.log("element removed");
    }

});
observer.observe(document.body, {childList: true});

to stop listening to changes you can run:

observer.disconnect();

, ,

1 Comment

Execute javascript code when R shiny output have been rendered

If you have reactive observer like this:

output$resultsTable <- renderTable({
   tableData
});

and you want to modify the table from javascript you can’t put shinyjs::runjs after table is rendered because the table data need to be the last expression of the reactive renderTable observer. The solution is to use new feature of JavaScript, implemented in browsers, which is mutation observer. The browser support is very good, you can check it on caniuse. The solution that will work, look like this (using jQuery plugin):

$.fn.onRender = function(callback, options) {
  if (arguments[0] == 'cancel') {
    return this.each(function() {
      var observer = $(this).data('observer');
      if (observer) {
        observer.disconnect();
      }
    });
  } else {
    var settings = $.extend({
      oneTime: true,
      observer: {childList: true, subtree: true}
    }, options);
    return this.onRender('cancel').each(function() {
      var node = $(this);
      var observer = new MutationObserver(function(mutations) {
        callback.apply(node[0]);
        if (settings.oneTime) {
          observer.disconnect();
        }
      });
      observer.observe(node[0], settings.observer);
    });
  }
};

You can use it like this:

function changeTable(selector) {
   $(selector).onRender(function() {
      $(this).find('thead th').css('color', 'red');
   });
}

if you want to execute code once and use it outside of shiny observer you can use option oneTime: false like this:

function updateTableWhenRender(selector) {
   $(selector).onRender(function() {
      $(this).find('thead th').css('color', 'red');
   }, {oneTime: false});
}

then the function can be put outside of observer like this:

output$resultsTable <- renderTable({
   tableData
});
shinyjs::runjs("updateTableWhenRender('#resultsTable')")

To cancel the mutation observer, that’s create with the plugin, you can execute this line of code:

$(selector).onRender('cancel');

, ,

Leave a comment

Cross-browser directory upload that can have large files

Google Chrome 21 added a feature to upload directories, Firefox also didn’t get too long far behind and added that feature in version 42. Here is a code that will work in both browsers, with help from jQuery and FormData object. It will also work with files larger then the limit:

function Uploader(path, limit) {
    this.path = path;
    this.upload_max_filesize = limit;
}

Uploader.prototype.upload_tree = function upload_tree(tree, path) {
    var defered = $.Deferred();
    var self = this;
    path = path || self.path;
    function process(entries, callback) {
        entries = entries.slice();
        (function recur() {
            var entry = entries.shift();
            if (entry) {
                callback(entry).then(recur).fail(function() {
                    defered.reject();
                });
            } else {
                defered.resolve();
            }
        })();
    }
    function upload_files(entries) {
        process(entries, function(entry) {
            return self.upload_tree(entry, path + "/" + tree.name);
        });
    }
    function upload_file(file) {
        self.upload(file, path).then(function() {
            defered.resolve();
        }).fail(function() {
            defered.reject();
        });
    }
    if (typeof Directory != 'undefined' && tree instanceof Directory) { // firefox
        tree.getFilesAndDirectories().then(function(entries) {
            upload_files(entries);
        });
    } else if (typeof File != 'undefined' && tree instanceof File) { // firefox
        upload_file(tree);
    } else if (tree.isFile) { // chrome
        tree.file(upload_file);
    } else if (tree.isDirectory) { // chrome
        var dirReader = tree.createReader();
        dirReader.readEntries(function(entries) {
            upload_files(entries);
        });
    }
    return defered.promise();
};

Uploader.prototype.upload = function upload(file, path) {
    var self = this;
    var defered = $.Deferred();
    var file_name = path + '/' + file.name;
    if (file.size > self.upload_max_filesize) {
        if (!(file.slice || file.webkitSlice)) {
            console.log('Exceeded filesize limit.');
            defered.resolve(); // next file
        } else {
            self.upload_by_chunks(file, path).then(function() {
                defered.resolve();
            }).fail(function() {
                defered.reject();
            });
        }
    } else {
        self.upload_file(file, path).then(function() {
            defered.resolve();
        }).fail(function() {
            defered.reject();
        });
    }
    return defered.promise();
};

Uploader.prototype.upload_by_chunks = function upload_by_chunks(file, path, chunk_size) {
    var self = this;
    chunk_size = chunk_size || 1048576; // 1MB
    var defered = $.Deferred();
    function slice(start, end) {
        if (file.slice) {
            return file.slice(start, end);
        } else if (file.webkitSlice) {
            return file.webkitSlice(start, end);
        }
    }
    var i = 0;
    function process(start, end) {
        if (start < file.size) {
            var chunk = slice(start, end);
            var formData = new FormData();
            formData.append('file', chunk, file.name);
            formData.append('token', self.token);
            formData.append('path', path);
            $.ajax({
                url: 'lib/upload.php?append=1',
                type: 'POST',
                success: function(response) {
                    if (response.error) {
                        console.log(response.error);
                        defered.reject();
                    } else {
                        process(end, end+chunk_size);
                    }
                },
                error: function(jxhr, error, status) {
                    console.log(jxhr.statusText);
                    defered.reject();
                },
                data: formData,
                cache: false,
                contentType: false,
                processData: false
            });
        } else {
            console.log('File "' + file.name + '" uploaded.');
            defered.resolve();
        }
    }
    var fname = path + '/' + file.name;
    // we need to remove the file if already there
    $.ajax({
        url: 'delete.php',
        data: {file: fname},
        success: function(response) {
            if (response.result) {
                process(0, chunk_size);
            } else if (response.error) {
                console.log(response.error);
                defered.reject();
            }
        }
    });
    return defered.promise();
};

Uploader.prototype.upload_file = function upload_file(file, path) {
    var self = this;
    var defered = $.Deferred();
    var formData = new FormData();
    formData.append('file', file);
    formData.append('path', path);
    $.ajax({
        url: 'lib/upload.php',
        type: 'POST',
        success: function(response) {
            if (response.error) {
                console.log(response.error);
                defered.reject();
            } else {
                console.log('File "' + file.name + '" ' + 'uploaded.');
                defered.resolve();
            }
        },
        error: function(jxhr, error, status) {
            console.log(jxhr.statusText);
            defered.reject();
        },
        data: formData,
        cache: false,
        contentType: false,
        processData: false
    });
    return defered.promise();
};

Here is drag and drop code that use that “class”:

$('div').on('drop', function(e) {
    e.preventDefault();
    var org = e.originalEvent;
    var uploader = new Uploader('/home/user', FILE_LIMIT_TAKEN_FROM_SERVER);
    var items;
    if (org.dataTransfer.items) {
        items = [].slice.call(org.dataTransfer.items);
    }
    var files = (org.dataTransfer.files || org.target.files);
    if (files) {
        files = [].slice.call(files);
    }
    if (items && items.length) {
        if (items[0].webkitGetAsEntry) {
            var entries = [];
            items.forEach(function(item) {
                var entry = item.webkitGetAsEntry();
                if (entry) entries.push(entry);
            });
            (function upload() {
                var entry = entries.shift();
                if (entry) {
                    uploader.upload_tree(entry).then(upload)
                } else {
                    console.log('Finish');
                }
            })();
        }
    } else if (files && files.length) {
        (function upload() {
            var file = files.shift();
            if (file) {
                uploader.upload(file).then(upload)
            } else {
                console.log('Finish');
            }
        });
    } else if (org.dataTransfer.getFilesAndDirectories) {
        org.dataTransfer.getFilesAndDirectories().then(function(items) {
            (function upload() {
                var item = items.shift();
                if (item) {
                    uploader.upload_tree(item).then(upload);
                } else {
                    console.log('Finish');
                }
            })();
        });
    }
}).on('dragover', function(e) {
    e.preventDefault();
}).on('dragenter', function(e) {
    e.preventDefault();
});

upload.php can look like this:

<?php
header('Content-type: application/json');

if (!isset($_FILES['file'])) {
    echo json_encode(array('error' => 'No File'));
} else if (!isset($_POST['path'])) {
    echo json_encode(array('error' => 'Wrong request, no path'));
} else {
    $fname = basename($_FILES['file']['name']);
    switch ($_FILES['file']['error']) {
        case UPLOAD_ERR_OK:
            $path = '';
            // create directories if don't exists
            foreach (explode("/", $_POST['path']) as $folder) {
                if (!is_dir($path . DIRECTORY_SEPARATOR . $folder)) {
                    mkdir($path . DIRECTORY_SEPARATOR . $folder);
                }
                $path .= DIRECTORY_SEPARATOR . $folder;
            }
            $full_name = $_POST['path'] . '/' . $fname;
            if (file_exists($full_name) && !is_writable($full_name)) {
                echo json_encode(array(
                    'error' => 'File "'. $fname . '" is not writable'
                ));
            } else {
                if (isset($_GET['append'])) {
                    $contents = file_get_contents($_FILES['file']['tmp_name']);
                    $file = fopen($full_name, 'a+');
                    if (!$file) {
                        echo json_encode(array('error' => 'Can\'t save file.'));
                    } else if (fwrite($file, $contents) != strlen($contents)) {
                        echo json_encode(array('error' => 'Not all bytes saved.'));
                    } else {
                        echo json_encode(array('success' => true));
                    }
                    fclose($file);
                } else {
                    if (!move_uploaded_file($_FILES['file']['tmp_name'],
                                            $full_name)) {
                        echo json_encode(array('error' => 'Can\'t save file.'));
                    } else {
                        echo json_encode(array('success' => true));
                    }
                }
            }
            break;
        case UPLOAD_ERR_NO_FILE:
            echo json_encode(array('error' => 'File not sent.'));
            break;
        case UPLOAD_ERR_INI_SIZE:
        case UPLOAD_ERR_FORM_SIZE:
            echo json_encode(array('error' => 'Exceeded filesize limit.'));
            break;
        default:
            echo json_encode(array('error' => 'Unknown error.'));
    }
}
?>

The code for delete can just call unlink function if file exist.

Another thing that can be added to the code, is to ask a question if overwrite a file if it exists. This is left as a exercise to the reader.

, ,

Leave a comment

Debugging code that call $resource in Angular with a Proxy

In angular you can add REST service using $resource factory, for instance you can create new REST object like this:

var studies =  $resource('http://example.com/api/studies/:id', {
  id: '@id'
}, {
  get: {
    method: 'GET',
    cache: false,
    interceptor: {
      response: doInterceptStudy
    }
  },
  query: {
    method: 'GET',
    cache: false,
    params: {
      size: 20
    }
  },
  lasarLookup: {
    bypassUiBlockInterceptor: true,
    method: 'GET',
    cache: false,
    params: {
      size: 20
    }
  },
  filteredQueryAll: {
    url: 'http://example.com/api/studies',
    method: 'POST',
    cache: false,
    params: {
      size: 20
    }
  }
});

and then call it using:

studies.get({id:123}, function(user) {
  $scope.user = user;
});

In chrome you can add breakpoint on XHR request but you will probably end up in angular code when you do that.

It’s probably better ot add breakpoint to lines where you call your $resource like studies.get, but if you have responses with lot of endpoints that are called in different places this is troublesome. But there is quick solution to add breakpoint on every call to resource with a ES6 proxy

studies = new Proxy(studies, {
    get: function(target, name) {
        if (typeof target[name] == 'function') {
            return function() {
                return target[name].apply(target, [].slice.call(arguments));
            };
        } else {
            return target[name];
        }
    }
 });

and you can add breakpoint inside function returned by get proxy method or you can add dubbugger statement:

studies = new Proxy(studies, {
    get: function(target, name) {
        if (typeof target[name] == 'function') {
            return function() {
                debugger;
                return target[name].apply(target, [].slice.call(arguments));
            };
        } else {
            return target[name];
        }
    }
 });

The support for Proxies is pretty decent, only IE don’t support it. You can check browser support on can I use.

,

Leave a comment

Get list of github contributors in node.js

Recently I needed to send emails to all contributors to my github project, so I wrote a little script in node.js (actually my first try was ruby, but have problems with https). Here is the script using only native modules:

#!/usr/bin/node

var https = require('https');
var path = require('path');
function get(host, path, callback) {
    var options = {
        host: host,
        path: path,
        headers: {
            'User-Agent': 'Node.js' //required by github api
        }
    }
    https.get(options, function(res) {
        var output = '';
        res.setEncoding('utf8');

        res.on('data', function (chunk) {
            output += chunk;
        });

        res.on('end', function() {
            callback(JSON.parse(output));
        });
    });
}

if (process.argv.length == 4) {
    var user = process.argv[2];
    var repo = process.argv[3];
    var path = '/repos/' + user + '/' + repo + '/contributors'
    get('api.github.com', path, function(contributors) {
        contributors.forEach(function(contributor) {
            if (contributor.login != user) {
                var path = contributor.url.replace(/https:\/\/[^\/]+/, '');
                get('api.github.com', path, function(user) {
                    if (user.name) {
                        var email = user.email ? ' <' + user.email + '>' : ''; 
                        console.log(user.name + email);
                    }
                });
            }
        });
    });
} else {
    console.log('usage: \n' + path.basename(process.argv[1]) + ' user repo');
}

, ,

Leave a comment

Cross-Domain LocalStorage

As you may know, LocalStorage is domain based. You can’t read or write from localstorage that’s on different domain, even if that’s subdomain. But there is iframe trick that you can use to store data from domain to it’s subdomain.

Basically you create an iframe that’s hosted on your other domain, then you send PostMessage to that iframe and inside iframe you set that value of localStorage.
Here is the code:

iFrame

window.onmessage = function(e) {
  if (e.origin !== "http://example.com") {
    return;
  }
  var payload = JSON.parse(e.data);
  localStorage.setItem(payload.key, JSON.stringify(payload.data));
};

main file

window.onload = function() {
    var win = document.getElementsByTagName('iframe')[0].contentWindow;
    var obj = {
       name: "Jack"
    };
    win.postMessage(JSON.stringify({key: 'storage', data: obj}), "*");
};

<a href="http://other.example.com/iframe.html">http://other.example.com/iframe.html</a>

If you want to save and load value from LocalStorage, you can PostMessage with method key that will indicate what action it need to take. You can also send message back from iframe to it’s parent with data read from localStorage.

iframe

document.domain = "example.com";
window.onmessage = function(e) {
    if (e.origin !== "http://example.com") {
        return;
    }
    var payload = JSON.parse(e.data);
    switch(payload.method) {
        case 'set':
            localStorage.setItem(payload.key, JSON.stringify(payload.data));
            break;
        case 'get':
            var parent = window.parent;
            var data = localStorage.getItem(payload.key);
            parent.postMessage(data, "*");
            break;
        case 'remove':
            localStorage.removeItem(payload.key);
            break;
    }
};

Your main page on example.com

window.onload = function() {
    var iframe = document.getElementsByTagName('iframe')[0];
    var win;
    // some browser (don't remember which one) throw exception when you try to access
    // contentWindow for the first time, it work when you do that second time
    try {
        win = iframe.contentWindow;
    } catch(e) {
        win = iframe.contentWindow;
    }
    var obj = {
       name: "Jack"
    };
    // save obj in subdomain localStorage
    win.postMessage(JSON.stringify({key: 'storage', method: "set", data: obj}), "*");
    window.onmessage = function(e) {
        if (e.origin != "http://other.example.com") {
            return;
        }
        // this will log "Jack"
        console.log(JSON.parse(e.data).name);
    };
    // load previously saved data
    win.postMessage(JSON.stringify({key: 'storage', method: "get"}), "*");
};

Here is a demo that use my sysend library to send messages to other tab in same browser (the library use localStorage for this). It use iframe proxy to send message from jcubic.pl to terminal.jcubic.pl and vice versa. Iframe is hosted on both domains.

The only caveat is that in Firefox, it seems that you need to enable CORS on Iframe. If you use apache server you can enable it in .htaccess file with this code:

Header set Access-Control-Allow-Origin "*"
Header set Access-Control-Allow-Methods: "GET"

it will enable CORS for all files, if you want to enable CORS for a single file, this should work:

<Files "iframe.html">
  Header set Access-Control-Allow-Origin "*"
  Header set Access-Control-Allow-Methods: "GET,POST,OPTIONS,DELETE,PUT"
</Files>

, , ,

14 Comments

40 JavaScript libraries you may not know about

Here is a list of useful libraries I had in my bookmarks, libraries that I need to use in some of my future projects.

  • Drawing and animation
  • Language extensions
  • Utilites
    • URI.js – library for handling urls
    • rangy – Text range library
    • color-thief – pick color(s) from image
    • stacktrace.js – get the stacktrace
    • XRegExp – extended regular expressions
    • -prefix-free – CSS3 without prefixes
    • mathjs – An extensive math library for JavaScript and Node.js
    • numeraljs – library for formatting and manipulating numbers
    • String.js – string manipulation library
    • Ocrad.js – Optical Character Recognition in Javascript
    • filer.js – wrapper over HTML5 File API
  • DOM
  • Events
    • jwerty – keyboard events
    • jquery-simulate – simulate keyboard and mouse events
    • Mousetrap – keyboard shortcuts
    • jQuery hashchange – hashchange event
    • jquery-waypoints – Execute a function when you sroll to the element
    • Steady.js – A jank-free module to do logic on the onscroll event without performance regressions in a @media-query like conditions.
    • sysend.js – Send messages between open pages in the browser (this one is actually my own)

,

1 Comment

%d bloggers like this: