Posts Tagged javascript

Create javascript interface for all methods in cordova plugin

When you create a cordova plugin you need to create execute method and check for action parameter to do different task and in each task you need to create json object and call callbackContext. It would be nice if if you could just create a class with methods and each method is map to javascript method. Here is tutorial how to do this:

You need to create this ReflectService.java file:

package com.example.package;

import android.content.Context;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.PrintWriter;

import java.util.List;
import java.util.ArrayList;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaWebView;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class ReflectService extends CordovaPlugin {

    protected Method getMethod(String name) {
        Class aClass = this.getClass();
        Method[] methods = aClass.getMethods();
        Method method = null;
        for (int i=0; i<methods.length; ++i) {
            if (methods[i].getName().equals(name)) {
                method = methods[i];
                break;
            }
        }
        return method;
    }

    public String[] getMethods() {
        Class aClass = this.getClass();
        Method[] methods = aClass.getDeclaredMethods();
        List<String> list = new ArrayList<String>();
        int len = methods.length;
        String[] result = new String[len];
        for (Method method : methods) {
            String name = method.getName();
            if (Modifier.isPublic(method.getModifiers()) &&
                !name.equals("initialize")) {
                list.add(name);
            }
        }
        return list.toArray(new String[list.size()]);
    }

    protected String[] jsonArrayToString(JSONArray args) {
        String[] result = {};
        try {
            int len = args.length();
            result = new String[len];
            for (int i=0; i<len; ++i) {
                result[i] = args.getString(i);
            }
        } catch (JSONException e) {}
        return result;
    }

    protected String[] getStackTrace(Exception e) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        e.printStackTrace(pw);
        return sw.toString().split("\n");
    }

    @Override
    public boolean execute(String action,
                           JSONArray args,
                           final CallbackContext callbackContext) throws JSONException {
        if (!action.equals("execute")) {
            final Method method = this.getMethod(action);
            if (method == null) {
                return false;
            }
            final Object[] arguments = this.jsonArrayToString(args);
            cordova.getThreadPool().execute(new Runnable() {
                public void run() {
                    JSONObject json;
                    Object result;
                    try {
                        result = method.invoke(ReflectService.this, arguments);
                        json = new JSONObject();
                        if (result instanceof Object[]) {
                            json.put("result", new JSONArray(result));
                        } else {
                            json.put("result", result);
                        }
                        json.put("error", null);
                        callbackContext.success(json);
                    } catch(JSONException e) {
                        callbackContext.success();
                    } catch(Exception e) {
                        try {
                            json = new JSONObject();
                            JSONObject error = new JSONObject();
                            error.put("error", "Exception");
                            error.put("code", 200);
                            error.put("message", e.getMessage());
                            String[] trace = ReflectService.this.getStackTrace(e);
                            error.put("trace", new JSONArray(trace));
                            json.put("error", error);
                            json.put("result", null);
                            callbackContext.success(json);
                        } catch(JSONException ee) {
                            callbackContext.success();
                        }
                    }
                }
            });
            return true;
        }
        return false;
    }
}

You probably don’t need to use threads but I’ve seen this in cordova-plugin-shell-exec.

and then you can write your Service like this:

package com.example.package;

import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaWebView;

public class Service extends ReflectService {

    public void initialize(CordovaInterface cordova, CordovaWebView webView) {
        super.initialize(cordova, webView);
        // your init code here
    }
    public String echo(String input) {
        if (input.equals("ping")) {
            return "pong";
        } else {
            return null;
        }
    }
}

You will be able to execute each method you create from execute in ReflectService class.

Then you need to create js file to create function for each method in the class. We will use getMethods action (that will execute getMethods method) to get list of methods and create function for each name.

window.Service = function Service(callback) {
    function call(method, args, callback) {
        return cordova.exec(function(response) {
            callback(null, response);
        }, function(error) {
            callback(error);
        }, "Service", method, args || []);
    }
    var service = {};
    call("getMethods", [], function(err, response) {
        if (response.result instanceof Array) {
            response.result.forEach(function(method) {
                service[method] = function() {
                    var args = [].slice.call(arguments);
                    return function(callback) {
                        return call(method, args, function(err, response) {
                            err = err || response.error;
                            if (err) {
                                callback(err);
                            } else {
                                callback(null, response.result);
                            }
                        });
                    };
                };
            });
            callback(service);
        }
    });
};

Next you need to create plugin.xml file that look like this:

<?xml version="1.0" encoding="UTF-8"?>
<plugin id="pl.jcubic.leash.service" version="1.0.0"
        xmlns="http://apache.org/cordova/ns/plugins/1.0">
  <name>Service</name>
  <description>Apache Cordova Leash shell service plugin</description>
  <license>Apache 2.0</license>
  <keywords>cordova,exec,runtime,process,shell,command</keywords>
  <js-module name="service" src="www/service.js">
    <clobbers target="service"/>
  </js-module>
  <platform name="android">
    <config-file parent="/*" target="res/xml/config.xml">
      <feature name="Service">
        <param name="android-package" value="com.example.package.Service" />
        <param name="onload" value="true" />
      </feature>
    </config-file>
    <source-file src="src/com/example/package/Service.java"
                 target-dir="src/com/example/package/Service" />
    <source-file src="src/com/example/package/ReflectService.java"
                 target-dir="src/com/example/package/Service" />
  </platform>
</plugin>

When you add this plugin you can call it in javascript like this:

window.Service(function(service) {
    service.echo('ping')(function(err, result) {
        var pre = document.createElement('pre');
        pre.innerHTML = JSON.stringify(result, null, 4);
        document.body.appendChild(pre);
    });
});

NOTE: Instead of callbacks you can use promises.

You can find whole code in this gist.

, ,

Leave a comment

Execute callback in javascript when a div change visibility with html5

There is new html5 API that can be used to trigger a callback when element change visibility. That api is called IntersectionObserver it’s supported by Chrome, Edge and Opera, its similar to MutationObserver.

The code look like this:

var element = document.querySelector('selector');
if (window.IntersectionObserver) {
  var observer = new IntersectionObserver(function(entries) {
    if (entries[0].intersectionRatio) {
      console.log('visible');
    } else {
      console.log('hidden');
    }
  }, {
    root: document.body
  });
  observer.observe(element);
}

NOTE: intersectionRatio will not work if the element have position: fixed so if you can use jQuery you can check $(element).is(':visible');.

If you don’t use jQuery you can take :visible code from jQuery which look like this:

elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length;

To remove the observer you need to call:

observer.unobserve(element);

The element will intersect with body only when it’s visible, and the observer is fired when intersection is changed, so it will fire when element will become visible or not visible. You can read about IntersectionObserver on MDN.

If you’re are using jQuery you can wrap this using special event:

if (window.IntersectionObserver) {
  $.event.special.visibility = {
    setup: function() {
      function event(visibility) {
        var e = $.Event("visibility");
        e.visible = visibility;
        return e;
      }
      var element = this;
      var $element = $(this);
      var observer = new IntersectionObserver(function(entries) {
        var e = event($element.is(':visible'));
        ($.event.dispatch || $.event.handle).call(element, e);
      }, {
        root: document.body
      });
      observer.observe(this);
      $.data(this, 'observer', observer);
    },
    teardown: function() {
      var observer = $.data(this, 'observer');
      if (observer) {
        observer.unobserve(this);
        $.removeData(this, 'observer');
      }
    }
  };
}

and call it like this:

$('div').on('visibility', function(e) {
  $('pre').html(e.visible ? 'visible' : 'hidden');
});

Here is Codepen DEMO.

The event will fire when intersection observer is added, if you don’t want that you may want to disable first check.

      var first_time = true;
      var observer = new IntersectionObserver(function(entries) {
        if (!first_time) {
          var e = event(!!entries[0].intersectionRatio);
          ($.event.dispatch || $.event.handle).call(element, e);
        }
        first_time = false;
      }, {
        root: document.body
      });

,

Leave a comment

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 self = $(this);
      var render = self.data('render');
      if (render) {
        render.observer.disconnect();
        self.removeData('render');
      }
    });
  } else {
    var settings = $.extend({
      oneTime: true,
      observer: {childList: true, subtree: true}
    }, options);
    return this.each(function() {
      var node = $(this);
      var render = node.data('render');
      if (!render) {
        render = {
          callbacks: $.Callbacks(),
          observer: new MutationObserver(function(mutations) {
            render.callbacks.fire(node[0]);
            if (settings.oneTime) {
              node.onRender('cancel');
            }
          })
        };
        render.observer.observe(node[0], settings.observer);
        node.data('render', render);
      }
      render.callbacks.add(callback);
    });
  }
};

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');

If you have multiple renders and have onRender on each render call you should call cancel before you call onRender again.

, ,

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

%d bloggers like this: