Console.log for all or selected function calls with stack trace

I needed to debug some function calls, I needed to know which function is called and when, I decide to not to use debugger and step because it would be slow so I’ve written the function that logs all function calls, and include stack trace if needed. The function can be called in few ways:

  1. globals('createUser', true)

    will log one function call and include stack trace, if second argument is omitted or false it will not print stack trace

  2. globals(['createUser', 'deleteUser'], true)

    will log both functions and include stack trace

  3. globals({'createUser': true, 'deleteUser': false})

    will log both but only the first one will have stack trace

  4. globals()

    will log all global function calls

  5. globals(true)

    will log all function calls and include stack trace for all functions

Here is the function itself:

function globals(arg, stack) {
  var show_stack;
  if (typeof arg == 'boolean') {
    stack = arg;
    arg = undefined;
  }
  var is_valid_name;
  if ($.isPlainObject(arg)) {
    var keys = Object.keys(arg);
    is_valid_name = function(name) { return keys.indexOf(name) != -1; };
    show_stack = function(name) { return arg[name]; };
  } else {
    show_stack = function() { return stack; };
    if (arg === undefined) {
      is_valid_name = function() { return true };
    } else if (arguments[0] instanceof Array) {
      is_valid_name = function(name) { return arg.indexOf(name) != -1; };
    } else {
      is_valid_name = function(name) { return arg == name; };
    }
  }
  document.addEventListener("DOMContentLoaded", function() {
    Object.keys(window).forEach(function(key) {
      var original = window[key];
      if (typeof original == 'function' &&
          !original.toString().match(/\[native code\]/) &&
          'globals' != key && is_valid_name(key)) {
        window[key] = function() {
          var args = [].map.call(arguments, function(arg) {
            if (arg instanceof $.fn.init) {
              return '[Object jQuery]';
            } else if (arg === undefined) {
              return 'undefined';
            } else {
              return JSON.stringify(arg);
            }
          }).join(', ');
          console.log(key + '(' + args + ')');
          if (show_stack(key)) {
            console.log(new Error().stack);
          }
          return original.apply(this, arguments);
        };
        // just in case some code parse function as strings
        window[key].toString = function() {
          return original.toString();
        };
      }
    });
  });
}

this function can be easily extended to log methods calls on an object just use object instead of window. Here is example of higher order function that create debug functions, here is the code with few improvements:

function debug(object, name) {
  var fn = function(arg, stack) {
    var show_stack;
    if (typeof arg == 'boolean') {
      stack = arg;
      arg = undefined;
    }
    var is_valid_name;
    if ($.isPlainObject(arg)) {
      var keys = Object.keys(arg);
      is_valid_name = function(name) { return keys.indexOf(name) != -1; };
      show_stack = function(name) { return arg[name]; };
    } else {
      show_stack = function() { return stack; };
      if (arg === undefined) {
        is_valid_name = function() { return true };
      } else if (arguments[0] instanceof Array) {
        is_valid_name = function(name) { return arg.indexOf(name) != -1; };
      } else {
        is_valid_name = function(name) { return arg == name; };
      }
    }
    document.addEventListener("DOMContentLoaded", function() {
      var functions = Object.keys(object).filter(function(name) {
        return typeof object[name] == 'function';
      });
      functions.forEach(function(key) {
        var original = object[key];
        var str = original.toString();
        if (!str.match(/\[native code\]/) && !str.match(/<#debug>/) &&
            key != 'debug' && !original.__debug &&
            is_valid_name(key)) {
          object[key] = function() {
            var args = [].map.call(arguments, function(arg) {
              if (arg instanceof HTMLElement) {
                return '[NODE "' + arg.nodeName + '"]';
              } else if (arg instanceof $.fn.init) {
                return '[Object jQuery]';
              } else if (arg === undefined) {
                return 'undefined';
              } else if (arg === window) {
                return '[Object Window]';
              } else if (arg == document) {
                return '[Object document]';
              } else {
                return JSON.stringify(arg);
              }
            }).join(', ');
            console.log((name?name + '.': '') + key + '(' + args + ')');
            if (show_stack(key)) {
              console.log(new Error().stack);
            }
            return original.apply(this, arguments);
          };
          object[key].toString = function() {
            return str;
          };
          object[key].__debug = true;
          object[key].prototype = original.prototype;
          for (var i in original) {
            if (original.hasOwnProperty(i)) {
              object[key][i] = original[i];
            }
          }
        }
      });
    });
  };
  fn.toString = function() {
    return '<#debug>';
  };
  return fn;
}

You can debug all jQuery methods using:

debug(jQuery, '$')();

second argument is label for log

the function that returned accept the same arguments as the first function. You can call the function with window object to get the same function so if you want log only one function with stack trace will look like this:

var globals = debug(window);
globals('addExcludedWarning', true)

Another cool idea is to include time of the function invocation, that is left as an exercise, Hint you can use performance.now() function to check current time in milliseconds.

Advertisements

, ,

Leave a comment

How get jQuery data without jQuery

Sometimes there is a need to get access to data value when jQuery is not available, this may happen when the website is using jQuery.noConflict(true) and don’t give access to jQuery it uses. Adding your own jQuery don’t work because data (added by calling .data('name', value) not by data-name attribute) is accessible only from same jQuery in which data was added.

But there is solution for this. First we need helper function (if we don’t want to include another jQuery):

var $$ = function(selector) {
  return [].slice.call(document.querySelectorAll(selector));
};

Looking at the source code for jQuery you will see this code:

function Data() {
    this.expando = jQuery.expando + Data.uid++;
}

Data.uid = 1;

Data.prototype = {

    cache: function( owner ) {

        // Check if the owner object already has a cache
        var value = owner[ this.expando ];
        ...

and each jQuery have it’s own jQuery.expando which is string jQuery + version + random number.

So we need to extract expando values from DOM node, here is the code:

function expando(item) {
    return Object.keys(item).filter(function(key) {
        return key.match(/^jQuery/);
    }).map(function(key) {
        return item[key];
    });
}

in most cases there are two expando attributes not only for data but also for events. Next step is to find data value:

function data(item, name) {
    return expando(item).map(function get(expando) {
        return expando[name];
    }).filter(Boolean);
}

You can test this approach in this pen, the terminal is created inside IIFE and jQuery.noConflict(true) is called, (in Google Chrome you need to select from dropdown result(gGwROe) frame to interact with the page).

You can test by calling inject_jquery() function (that’s defined in pen) that $('.terminal').data('terminal') don’t work (you either can’t call $('.terminal').terminal() unless you include jQuery Terminal again.

But if you copy/paste $$, expando and data functions into console and call:

var term = data($$('.terminal')[0], 'terminal')[0];

you will have access to terminal instance, you can call term.clear() to clear the terminal or any other terminal method.

,

1 Comment

How to create Server File Explorer using jQuery and PHP

I’ve created plugin called jQuery File Browser that can be used to created browser for your server files. Here we will use php as server side script but it can be replaced with any other server side language like Ruby On Rails, Python or Node.js.

TL;DR you can find whole code in this gist

We will use JSON-RPC as our protocol to communicate with the server and my library that implement the protocol in php and JavaScript.

After we clone json-rpc and jQuery File Browser, we need to create our service.php file:

<?php

require('json-rpc/json-rpc.php');


class Service {
    function username() {
        return get_current_user();
    }
    function ls($path) {
        $files = array();
        $dirs = array();
        foreach (new DirectoryIterator($path) as $file) {
            $name = $file->getBasename();
            if ($name != "." && $name != "..") {
                if ($file->isFile() || $file->isLink()) {
                    $files[] = $name;
                } else {
                    $dirs[] = $name;
                }
            }
        }
        return array(
            "files" => $files,
            "dirs" => $dirs
        );
    }
}

echo handle_json_rpc(new Service());

?>

Then we need to create base html file with imported libraries including jQuery and jQuery UI (we will use file browser using jQuery UI dialog):

<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta charset="utf-8" />
  <title>File Explorer using jQuery</title>
  <!-- remove underscore from sc_ript (wordpress replace skrypt tag with a tag)-->
  <sc_ript src="https://code.jquery.com/jquery-3.1.1.min.js"></sc_ript>
  <sc_ript src="http://code.jquery.com/ui/1.12.0/jquery-ui.js"></sc_ript>
  <sc_ript src="jquery.filebrowser/js/jquery.filebrowser.min.js"></sc_ript>
  <sc_ript src="json-rpc/json-rpc.js"></sc_ript>

  <link href="http://code.jquery.com/ui/1.12.1/themes/dark-hive/jquery-ui.css" rel="stylesheet"/>
  <link href="jquery.filebrowser/css/jquery.filebrowser.min.css" rel="stylesheet"/>

Then we need to create instance of file browser inside jQuery UI dialog (we will call your service to get the username):

   $(function() {
       rpc({
           url: 'service.php',
           // this will make json-rpc library return promises instead of function that need to be called
           promisify: true
       }).then(function(service) {
           service.username().then(function(username) {
               var browser = $('<div/>').appendTo('body').dialog({
                   width: 800,
                   height: 600
               }).browse({
                   root: '/home/' + username + '/',
                   dir: function(path) {
                       return service.ls(path);
                   }
               });
           });
       });
   });

And now you should have file explorer window open with content of your server user home directory. If you don’t see the output you may want to change root to ‘/home/username’ and don’t use server to get the username, which may be the user that run http server.

The icons that are inside jQuery File Browser are taken from my Clarity icons for GNU/Linux with GTK+.

Next you can add editor that will open on double click on the icon, we will use CodeMirror for this.

First we need to include codemirror:

  <link href="codemirror-5.29.0/lib/codemirror.css" rel="stylesheet"/>
  <link href="codemirror-5.29.0/theme/base16-dark.css" rel="stylesheet"/>
  <sc_ript src="codemirror-5.29.0/lib/codemirror.js"></sc_ript>
  <sc_ript src="codemirror-5.29.0/keymap/emacs.js"></sc_ript>

We use base16-dark theme (to match dark theme of the jQuery UI) and emacs keymap, but you can pick different one.

Then we need to open the editor, we will create open function that look like this:

function open(path) {
    var fname = path.replace(/.*\//, '');
    var readonly = true;
    var mtime;
    service.is_writable(path).then(function(is_writable) {
        readonly = !is_writable;
    });
    service.filemtime(path).then(function(time) {
        mtime = time;
    });
    var ext = fname.replace(/^.*\./, '');
    var mode;
    // you can add more modes here:
    switch(ext) {
        case 'js':
            mode = 'javascript';
            break;
        case 'rb':
            mode = 'ruby';
            break;
        case 'py':
            mode = 'python';
            break;
        case 'sql':
            mode = 'mysql';
            break;
        case 'svg':
            mode = 'xml';
            break;
        case 'xml':
        case 'css':
        case 'php':
            mode = ext;
            break;
    }
    var scripts = $('script').map(function() {
        return ($(this).attr('src') || '').replace(/.*\//, '');
    }).get();
    if (scripts.indexOf(mode + '.js') == -1) {
        var name = 'codemirror-5.29.0/mode/' + mode + '/' + mode + '.js';
        service.file_exists(name).then(function(exists) {
            if (exists) {
                $('<sc' + 'ript/>').attr('src', name).appendTo('head');
            }
        });
    }
    service.file(path).then(function(content) {
        var unsaved = false;
        var div = $('<div><textarea/><div>');
        var textarea = div.find('textarea').hide();
        textarea.val(content);
        div.dialog({
            title: fname,
            width: 650,
            beforeClose: function(e, ui) {
                if (unsaved) {
                    if (confirm('Unsaved file, are you sure?')) {
                        $(this).dialog('destroy');
                    } else {
                        return false;
                    }
                } else {
                    $(this).dialog('destroy');
                }
            },
            resize: function(e, ui) {
                editor.editor.setSize(textarea.width(), textarea.height());
            }
        });
        var editor = {
            dialog: div,
            editor: CodeMirror.fromTextArea(textarea[0], {
                lineNumbers: true,
                matchBrackets: true,
                mode: mode,
                readOnly: readonly,
                theme: 'base16-dark',
                indentUnit: 4,
                keyMap: 'emacs',
                extraKeys: {
                    "Ctrl-S": function(cm) {
                        if (unsaved) {
                            function saveFile() {
                                editor.editor.save();
                                var content = textarea.val();
                                service.save(path, content).then(function() {
                                    unsaved = false;
                                    div.dialog('option', 'title', fname);
                                });
                            }
                            service.filemtime(path).then(function(time) {
                                if (mtime != time) {
                                    if (confirm('File `' + fname + '\' changed ' +
                                                'on Disk are you sure?')) {
                                        saveFile();
                                    }
                                } else {
                                    saveFile();
                                }
                            });
                        }
                    },
                    "Tab": "indentAuto"
                }
            })
        };
        editor.editor.on('change', function(editor) {
            console.log('change');
            unsaved = true;
            div.dialog('option', 'title', '*' + fname);
        });
    });
}

it require few more server side functions:

class Service {
    function file_exists($path) {
        return file_exists($path);
    }
    function username() {
        return get_current_user();
    }
    function is_writable($path) {
        return is_writable($path);
    }
    function file($path) {
        return file_get_contents($path);
    }
    function save($path, $content) {
        $file = fopen($path, 'w');
        $ret = fwrite($file, $content);
        fclose($file);
        return $ret;
    }
    function filemtime($path) {
        return filemtime($path);
    }
    function ls($path) {
        $files = array();
        $dirs = array();
        foreach (new DirectoryIterator($path) as $file) {
            $name = $file->getBasename();
            if ($name != "." && $name != "..") {
                if ($file->isFile() || $file->isLink()) {
                    $files[] = $name;
                } else {
                    $dirs[] = $name;
                }
            }
        }
        return array(
            "files" => $files,
            "dirs" => $dirs
        );
    }
}

and then we need to add this function to our file explorer, we can do that by adding open option, (we can also embed whole function instead of adding it as a value):

   $(function() {
       rpc({
           url: 'service.php',
           promisify: true
       }).then(function(service) {
           service.username().then(function(username) {
               var browser = $('<div/>').appendTo('body').dialog({
                   width: 800,
                   height: 600
               }).browse({
                   root: '/home/' + username + '/',
                   dir: function(path) {
                       return service.ls(path);
                   },
                   open: open
               });
           });
       });
   });

we also need some css tweaks to make icons take more space and to make editor resize properly.

body {
    font-size: 10px;
}
.ui-dialog .ui-dialog-content {
    padding: 0;
    overflow: hidden;
}
.browser {
    width: 800px;
    height: 600px;
}
.browser-widget .content .selection {
    border-color: #fff;
}
.browser-widget li.file,
.browser-widget li.directory {
    width: 60px;
}
.ui-dialog textarea {
    width: 100%;
    height: 100%;
    padding: 0;
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    resize: none;
}

, ,

Leave a comment

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

How to add vignette in GIMP ScriptFu

Besides wrting code I’m also amateur photographer (you can see my photos on flickr most of them released under creative commons attribution share alike license) and start adding Vignette in GIMP (the only option because I’m working on GNU/Linux).

To add vignette you can do this steps:

  1. Add ellipse selection
  2. Feather it by 1000 pixels (I have 24MP camera) for your case it may be larger or smaller
  3. Inverse the Mask
  4. Add new black layer
  5. Add layer mask from selection
  6. Make it 50% opacity
  7. remove the selection

And now you have vignette on the image. It was fine but I needed to repeat those steps on each image, and since GIMP have support for scripting using script-fu (variant of scheme), I thought that I add simple script for the steps, here is result:

(define (make-vignette size opacity img)
  (let* ((width (car (gimp-image-width img)))
         (height (car (gimp-image-height img)))
         (layer (car (gimp-layer-new img width height 0 &quot;vignette&quot; 100 DARKEN-ONLY-MODE))))
    (gimp-selection-clear img)
    (gimp-image-select-ellipse img 0 0 0 width height)
    (gimp-selection-feather img size)
    (gimp-selection-invert img)
    (gimp-layer-set-opacity layer opacity)
    (gimp-layer-add-mask layer (car (gimp-layer-create-mask layer ADD-SELECTION-MASK)))
    (gimp-selection-clear img)
    (gimp-image-insert-layer img layer 0 -1)))

(script-fu-register
  &quot;make-vignette&quot;
  &quot;Vignette&quot;
  &quot;Create Vignette for the image&quot;
  &quot;Jakub Jankiewicz&quot;
  &quot;Copyright (c) 2017 Jakub Jankiewicz &lt;http://jcubic.pl/me&gt;&quot;
  &quot;May 20, 2017&quot;
  RGB
  SF-VALUE &quot;Size&quot; &quot;1000&quot;
  SF-VALUE &quot;Opacity&quot; &quot;50&quot;
  SF-IMAGE &quot;image&quot; 0
)
(script-fu-menu-register &quot;make-vignette&quot; &quot;&lt;Image&gt;/Filters/Light and Shadow&quot;)

I’ve save it in ~/gimp-2.8/scripts/vignette.scm and it added menu item Vignette to menu Light and Shadow in Filters menu

Vignette Menu

and after I’ve executed the script I’ve get the same vignette layer with mask like this:

Vignette Layer

, ,

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:

var in_dom = document.body.contains(element);
var observer = new MutationObserver(function(mutations) {
    if (document.body.contains(element)) {
        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();

DEMO

, ,

1 Comment

%d bloggers like this: