Posts Tagged jquery

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

Working with SVG in jQuery

I recently notice that if you create element like a circle in SVG (inline SVG embedded into HTML), using inspector/firebug or jQuery, your circle is not visible on SVG until you refresh the SVG, I found a hack to force refresh of the SVG, I just get text of the SVG and insert it again into the DOM, and all elements that were not visible like circle mention before will be rendered.

Here is referesh function, as jQuery plugin

$.fn.xml = function() {
    return (new XMLSerializer()).serializeToString(this[0]);
};

$.fn.DOMRefresh = function() {
    return $($(this.xml()).replaceAll(this));
};

Those plugins should work with every XML embeded into HTML not only SVG. Don’t look very nice but it work, but I found a better way fix jQuery to work with SVG. I found that when I use function document.createElementNS instead of document.createElement elements added to inline SVG using for instance appendChild function everything work fine. So only thing that need to be done for jQuery to work with inline SVG is to replace this function if element is SVG. First I wrote method in jQuery object that test if element is SVG element, there are only three elements that have the same name in HTML and SVG is a, script, style and title tags so I didn’t put them in.

isSVGElement: function( o ) {
        if (o instanceof SVGElement) {
            return true;
        } else {
            if (typeof o === 'string') {
                return $.inArray(o, ['altGlyph', 'altGlyphDef',
                                     'altGlyphItem', 'animate',
                                     'animateColor', 'animateMotion',
                                     'animateTransform', 'circle',
                                     'clipPath', 'color-profile',
                                     'cursor', 'defs', 'desc', 'ellipse',
                                     'feBlend', 'feColorMatrix',
                                     'feComponentTransfer',
                                     'feComposite', 'feConvolveMatrix',
                                     'feDiffuseLighting',
                                     'feDisplacementMap',
                                     'feDistantLight', 'feFlood',
                                     'feFuncA', 'feFuncB', 'feFuncG',
                                     'feFuncR', 'feGaussianBlur',
                                     'feImage', 'feMerge', 'feMergeNode',
                                     'feMorphology', 'feOffset',
                                     'fePointLight',
                                     'feSpecularLighting', 'feSpotLight',
                                     'feTile', 'feTurbulence', 'filter',
                                     'font', 'font-face',
                                     'font-face-format',
                                     'font-face-name', 'font-face-src',
                                     'font-face-uri', 'foreignObject',
                                     'g', 'glyph', 'glyphRef', 'hkern',
                                     'image', 'line', 'linearGradient',
                                     'marker', 'mask', 'metadata',
                                     'missing-glyph', 'mpath', 'path',
                                     'pattern', 'polygon', 'polyline',
                                     'radialGradient', 'rect',
                                     'set', 'stop', 'svg',
                                     'switch', 'symbol', 'text',
                                     'textPath', 'tref',
                                     'tspan', 'use', 'view',
                                     'vkern']) !== -1;
            }
        }
    }

I get the list of elements from Mozilla MDN.

I added this method to main jQuery.extend({ that add methods to jQuery object. Next thing is to replace createElement with createElementNS, there are only 2 places with this in parseHTML (in the same jQuery.extend) and createSafeFragment function, only one is responsible for inserting elements – parseHTML. Below is the code for that function.

    parseHTML: function( data, context, keepScripts ) {
        if ( !data || typeof data !== "string" ) {
            return null;
        }
        if ( typeof context === "boolean" ) {
            keepScripts = context;
            context = false;
        }
        context = context || document;

        var parsed = rsingleTag.exec( data ),
            scripts = !keepScripts && [];

        // Single tag
        if ( parsed ) {
            if ( jQuery.isSVGElement( parsed[1] ) ) {
                return [ context.createElementNS(
                    "http://www.w3.org/2000/svg",
                    parsed[1]) ];
            } else {
                return [ context.createElement( parsed[1] ) ];
            }
        }

        parsed = jQuery.buildFragment( [ data ], context, scripts );
        if ( scripts ) {
            jQuery( scripts ).remove();
        }
        return jQuery.merge( [], parsed.childNodes );
    },

Unfortunetnly not all manupulation methods that create new elements will work but it’s better then refresh hack. The stuff that don’t work is when you create more then one element from a string or if you put some attributes. In this case jQuery use document fragments and innerHTML to create the DOM. If we will want to fix that too we will need to write a parser that will call createElementNS.

You can also include those function after you load jQuery so original code will not be changed.

The other way is to use document.createElementNS and element.setAttributeNS that need to be used if you want to add xlink:href attribute (it use http://www.w3.org/1999/xlink namespace).

,

Leave a comment

Image color picker in javascript that work without canvas in every browser

If you want to create color picker from image in JavaScript you probably will use canvas, but what if you need it work in IE8 as I needed. You can use some server side help to fetch pixels data from the server. I use php for that and GD library. The code is below.

cross-browser color picker

Server side code that return json pixel data

<?php

function rgb($color) {
    $result[] = ($color >> 16) & 0xFF;
    $result[] = ($color >> 8) & 0xFF;
    $result[] = $color & 0xFF;
    return $result;
}

function imagecreatefrom($filename) {
    $path = pathinfo($filename);
    switch($path['extension']) {
        case 'png':
            return imagecreatefrompng($filename);
        case 'jpg':
            return imagecreatefromjpeg($filename);
        case 'gif':
            return imagecreatefromgif($filename);
        default:
            return null;
    }
}

function getImageData($filename) {
    list($width, $height) = getimagesize($filename);
    $image = imagecreatefrom($filename);
    $image_data = array();
    for ($y = 0; $y < $height; ++$y) {
        $row = array();
        for ($x = 0; $x < $width; ++$x) {
            $row[] = rgb(imagecolorat($image, $x, $y));
        }
        $image_data[] = $row;
    }
    return $image_data;
}

if (isset($_GET['filename'])) {
    $filename = $_GET['filename'];
    if (file_exists($filename)) {
        $result = array(
            'error' => null,
            'result' => getImageData($filename)
        );
    } else {
        $result = array(
            'error' => "The file '$filename' don't exist",
            'result' => null
        );
    }
} else {
    $result = array(
        'error' => "You need to put filename",
        'result' => null
    );
}
header('Content-Type: application/json');
echo json_encode($result);

?>

Then you can fetch the data using ajax and add get invidial pixels on mousemove

    var img = $('img');
    $.getJSON('image_data.php', {filename: img.attr('src')}, function(data) {
        if (data.error) {
            alert(data.error);
        } else {
            $('.eyedropper').click(function() {
                picker = true;
                return false;
            });
            img.mousemove(function(e) {
                if (picker) {
                    var x = Math.round(e.pageX - offset.left);
                    var y = Math.round(e.pageY - offset.top);
                    if (x >= 0) {
                        $('.color').css('background-color',
                                        'rgb('+data.result[y][x].join(',')+')');
                    }
                }
            }).click(function(e) {
                picker = false;
            });
        }
    });

Now all you need to have is element with class eyedropper like a link in your html:


<a href="#" class="eyedropper">pick the color</a>

, ,

3 Comments

jQuery splitter – split container

This is my latest jquery plugin — “splitter” which splits content vertically or horizontally with movable element between them, that allow to change the proportion of two element. You can get it from github. The demo is here.

, ,

25 Comments

JQuery Terminal Emulator Plugin

My new project JQuery Terminal Emulator. It’s a plug-in which can be used to add Command Line interface to your application. You can use it to easily create server configuration tool or can be help in debugging or testing server side of AJAX applications. You can put lots of options in one place.

You can create command line interface to JSON-RPC in one line of code. Just set the path to rpc service.

$('body').terminal("json-rpc-service.php");

If you want to use authentication.

$('body').terminal("json-rpc-service.php", {
    login:true
});

And when user type user and password it will call login rpc method, get the token and pass that token to all methods on the server when user type command. So when user type for example add-user foo foo@bar.com it will call json-rpc add-user with parameters [token, “foo”, “foo@bar.com”].

, , , , , , ,

1 Comment

%d bloggers like this: