<commit_before>/**
 * @file mdjs
 * @author xiaowu
 * @email fe.xiaowu@gmail.com
 */

'use strict';

import template from 'art-template/node/template-native';
import {slugify} from 'transliteration';
import express from 'express';
import serve_static from 'serve-static';
import serve_index from 'serve-index';

import {existsSync, readdirSync, statSync, readFileSync} from 'fs';
import {parse, format} from 'url';
import {resolve, dirname, basename, extname, sep} from 'path';

import Key_cache from 'key-cache';

import marked from 'marked';
import highlight from 'highlight.js';

import OPTIONS from './options';

export default class Mdjs {
    /**
     * 构造器
     *
     * @param  {Object} options 配置参数
     */
    constructor(options = {}) {
        let package_options;
        try {
            package_options = require(resolve('./package.json')).mdjs;
        }
        catch (e) {
            package_options = {};
        }

        // 合并默认配置
        // 合并的顺序是： 参数 > package.mdjs > 默认 （由左向右合并）
        options = this.options = {...OPTIONS, ...package_options, ...options};

        options.root = resolve(options.root);
        options.cache_path = resolve(options.cache_path);

        // 缓存当前运行的目录
        this.__dirname = dirname(__dirname);

        // 缓存express
        this.express = express();

        // 初始化express
        this._init_express();
    }

    /**
     * 获取渲染后的导航html代码
     *
     * @param  {string|undefined} uri 当前高亮的路径，如果为空则全不高亮， 高亮即展开
     *
     * @return {string}     html代码
     */
    get_render_nav(id) {
        let data = this.get_list();
        let str = '';

        if (!data || !data.length) {
            return str;
        }

        if (id) {
            id = decodeURIComponent(id);
        }

        let filter = (filepath, type) => {
            if (!id) {
                return false;
            }

            if (type === 'dir') {
                return id.indexOf(filepath + '/') === 0;
            }
            return id === filepath;
        };

        console.log(id)

        let fn = (res) => {
            res.forEach((val) => {
                val.icon = false;
                val.uri = slugify(val.text, {separator: ''});

                if (!val.children || !val.children.length) {
                    if (filter(val.id, 'file')) {
                        val.state = {
                            // opened: true,
                            selected: true
                        }
                    }
                }
                else {
                    if (filter(val.id, 'dir')) {
                        val.state = {
                            opened: true
                        }
                    }

                    fn(val.children);
                }
            });
        };

        fn(data);

        return data;
    }

    /**
     * 清空缓存
     *
     * @return {Object} this
     */
    clear_cache() {
        // 调用缓存对象清空缓存
        new Key_cache({
            dir: this.options.cache_path
        }).remove();

        return this;
    }

    /**
     * 获取navtree使用数据，会追加options.links
     *
     * @description 会先读取缓存
     * @return {Array} 数组
     */
    get_list() {
        return this.cache('nav_data', () => {
            let data = this._get_list();

            if (!data.children) {
                data.children = [];
            }

            // 如果有链接，则追加
            if (this.options.links && this.options.links.length) {
                this.options.links.forEach(val => {
                    if (val.type === 'after') {
                        data.children.push(val);
                    }
                    else {
                        data.children.unshift(val);
                    }
                });
            }

            return data.children;
        });
    }

    /**
     * 渲染md文件
     *
     * @param  {string} content md源码
     *
     * @return {Object}         {content:html代码, catalog: h2,3分类}
     */
    render_markdown(content = '') {
        let renderer = new marked.Renderer();
        let cachekey = {};

        let catalog = [];

        // 渲染标题
        renderer.heading = (text, level) => {
            let key;

            // 如果不是h1,h2,h3则直接返回
            if ([1, 2, 3].indexOf(level) === -1) {
                return `<h${level}>${text}</h${level}>`;
            }

            if (cachekey[level] === undefined) {
                cachekey[level] = 0;
            }

            key = ++cachekey[level];

            // 使用text文本来写hash
            // 把text文本里的标签去掉
            let hash = String(text).replace(/<([^>]+?)>/g, '');

            catalog.push({
                text,
                level,
                id: `h${level}-${key}`,
                hash: hash
            });

            return `
                <h${level}>

                        ${text}

                </h${level}>
            `;
        };

        // 渲染代码
        renderer.code = (data, lang) => {
            data = highlight.highlightAuto(data).value;

            // 有语言时
            if (lang) {

                // 超过3行有提示
                if (data.split(/\n/).length >= 3) {
                    let html = `<pre><code class="hljs lang-${lang}"><span class="hljs-lang-tips">${lang}</span>`;
                    return html + `${data}</code></pre>`;
                }

                return `<pre><code class="hljs lang-${lang}">${data}</code></pre>`;
            }

            return `<pre><code class="hljs">${data}</code></pre>`;
        };

        // md => html
        content = marked(content, {
            renderer
        });

        // 兼容todo
        content = content.replace(/
\s*\[ \]\s*/g, '
<input type="checkbox" class="ui-todo" disabled>');
        content = content.replace(/
\s*\[x\]\s*/g, '
<input type="checkbox" disabled checked class="ui-todo">');

        return {
            content,
            catalog
        };
    }

    /**
     * 运行
     *
     * @return {Object} this
     */
    run() {
        // 委托目录浏览
        this.express.use('/', serve_index(this.options.root, {
            icons: true
        }));
        this.express.listen(this.options.port);
        return this;
    }

    /**
     * 缓存
     *
     * @description 如果有key的缓存则直接调用缓存，否则使用fn返回值作为缓存，如果开启了debug则直接使用fn返回值
     * @param  {string}   key  缓存key
     * @param {Function} fn 当没有缓存时执行的回调
     * @return {Object|undefined} 缓存数据或者回调返回值
     */
    cache(key, fn = () => {}) {
        // 如果没有key则返回空
        if (!key) {
            return undefined;
        }

        // 如果有debug
        if (this.options.debug) {
            return fn();
        }

        let cache = new Key_cache({
            dir: this.options.cache_path
        });
        let value = cache.get(key);
        if (value) {
            return value;
        }

        value = fn();

        // 如果不为空则设置缓存
        if (value !== undefined) {
            cache.set(key, value);
        }

        return value;
    }

    /**
     * 初始化express
     *
     * @private
     */
    _init_express() {
        let app = this.express;

        template.config('base', '');
        template.config('extname', '.html');

        app.engine('.html', template.__express);
        app.set('views', resolve(this.__dirname, './views/'));
        app.set('view engine', 'html');

        // 写入变量
        app.use((req, res, next) => {
            // 写入变量
            res.locals.options = this.options;

            return next();
        });

        // 绑定.md文档
        app.get(/([\s\S]+?)\.md$/, ::this._md);

        // 监听以目录结束的，其实是为了解决默认主页为md文档问题
        app.get(/(^\/$|\/$)/, (req, res, next) => {
            let parseUrl = parse(req.url, true);
            let pathname = parseUrl.pathname;

            // 相对文件的路径,用来判断文件是否存在
            let filepath = decodeURIComponent('.' + pathname);

            // 处理默认主页，拿配置的主页文件名去查找实际这个文件是否存在
            let flag = false;
            this.options.default_index.some(val => {
                if (existsSync(resolve(this.options.root, filepath, val))) {
                    flag = val;
                    return true;
                }
            });

            if (flag) {
                req.url = format({
                    hash: parseUrl.hash,
                    search: parseUrl.search,
                    pathname: pathname + flag,
                    query: parseUrl.query
                });

                return this._md(req, res, next);
            }

            next();
        });

        // 委托静态资源
        app.use('/' + this.options.static_prefix, serve_static(resolve(this.__dirname, './static/')));

        // 委托源目录
        app.use('/', serve_static(this.options.root));
    }

    /**
     * 内部获取列表数据
     *
     * @private
     * @param  {string|undefined} dir 目录
     *
     * @return {Object}     {path:'', children:[]}
     */
    _get_list(dir) {
        let options = this.options;
        let result;

        dir = dir || options.root;

        let file_basename = basename(dir);

        result = {
            id: dir.replace(options.root, '') || '/',
            children: [],
            text: options.dir_alias[file_basename] || file_basename
        };

        // 如果目录不存在
        if (!existsSync(dir)) {
            return result;
        }

        // 读取目录里数据
        let data = readdirSync(dir);

        // 定义目录数据和文件数据
        let dir_data = [];
        let file_data = [];

        // 遍历数据，拿出目录、文件的数据
        data.forEach(file => {
            let filepath = resolve(dir, file);
            let stat = statSync(filepath);

            if (stat.isDirectory()) {
                if (options.ignore_dir && options.ignore_dir.indexOf(file) === -1) {
                    dir_data.push({
                        type: 'dir',
                        filepath
                    });
                }
            }
            else {
                if (extname(file) === '.md') {
                    file_data.push({
                        filepath
                    });
                }
            }
        });

        // 合并目录+文件数据
        dir_data.concat(file_data).forEach(file => {
            if (file.type === 'dir') {
                let res = this._get_list(file.filepath);

                // 必须有子集才算
                if (res.children && res.children.length) {
                    result.children.push(res);
                }
            }
            else {
                result.children.push({
                    text: this.get_markdown_title(file.filepath),
                    id: file.filepath.replace(options.root, '').split(sep).join('\/')
                });
            }
        });

        return result;
    }

    /**
     * 渲染md文档
     *
     * @private
     * @param  {Object}   req  express.req
     * @param  {Object}   res  express.res
     * @param  {Function} next 下一个路由
     * @return {Object} res
     */
    _md(req, res, next) {
        let parseUrl = parse(req.url, true);

        // 如果要读取源码
        if (parseUrl.query.source) {
            return next();
        }

        // 加.是为了变成相对路径
        let filepath = resolve(this.options.root, '.' + parseUrl.pathname);

        // 为了中文
        filepath = decodeURIComponent(filepath);

        // 如果md文件不存在
        if (!existsSync(filepath)) {
            return next();
        }

        let markdown_data = this.render_markdown(readFileSync(filepath).toString()).content;

        // 如果是pjax
        if (parseUrl.query.pjax) {
            return res.end(markdown_data);
        }

        // 渲染md
        return res.render('markdown', {
            nav_data: JSON.stringify(this.get_render_nav(parseUrl.pathname)),
            markdown_data,
            title: `${this.get_markdown_title(filepath)} - ${this.options.name}`
        });
    }

    /**
     * 获取md文档的标题
     *
     * @param  {string} filepath 文件路径
     *
     * @return {string}     标题
     */
    get_markdown_title(filepath) {
        // 如果是md扩展
        if (extname(filepath) === '.md') {
            // 获取文件内容
            let filedata = readFileSync(filepath).toString();

            // 正则取出#标题的文字
            if (filedata.match(/^\s*\#+\s?(.+)/)) {
                return String(RegExp.$1).trim();
            }
        }

        return basename(filepath);
    }
}
<commit_msg>update: 修改代码高亮方式<commit_after>/**
 * @file mdjs
 * @author xiaowu
 * @email fe.xiaowu@gmail.com
 */

'use strict';

import template from 'art-template/node/template-native';
import {slugify} from 'transliteration';
import express from 'express';
import serve_static from 'serve-static';
import serve_index from 'serve-index';

import {existsSync, readdirSync, statSync, readFileSync} from 'fs';
import {parse, format} from 'url';
import {resolve, dirname, basename, extname, sep} from 'path';

import Key_cache from 'key-cache';

import marked from 'marked';
import highlight from 'highlight.js';

import OPTIONS from './options';

export default class Mdjs {
    /**
     * 构造器
     *
     * @param  {Object} options 配置参数
     */
    constructor(options = {}) {
        let package_options;
        try {
            package_options = require(resolve('./package.json')).mdjs;
        }
        catch (e) {
            package_options = {};
        }

        // 合并默认配置
        // 合并的顺序是： 参数 > package.mdjs > 默认 （由左向右合并）
        options = this.options = {...OPTIONS, ...package_options, ...options};

        options.root = resolve(options.root);
        options.cache_path = resolve(options.cache_path);

        // 缓存当前运行的目录
        this.__dirname = dirname(__dirname);

        // 缓存express
        this.express = express();

        // 初始化express
        this._init_express();
    }

    /**
     * 获取渲染后的导航html代码
     *
     * @param  {string|undefined} uri 当前高亮的路径，如果为空则全不高亮， 高亮即展开
     *
     * @return {string}     html代码
     */
    get_render_nav(id) {
        let data = this.get_list();
        let str = '';

        if (!data || !data.length) {
            return str;
        }

        if (id) {
            id = decodeURIComponent(id);
        }

        let filter = (filepath, type) => {
            if (!id) {
                return false;
            }

            if (type === 'dir') {
                return id.indexOf(filepath + '/') === 0;
            }
            return id === filepath;
        };

        console.log(id)

        let fn = (res) => {
            res.forEach((val) => {
                val.icon = false;
                val.uri = slugify(val.text, {separator: ''});

                if (!val.children || !val.children.length) {
                    if (filter(val.id, 'file')) {
                        val.state = {
                            // opened: true,
                            selected: true
                        }
                    }
                }
                else {
                    if (filter(val.id, 'dir')) {
                        val.state = {
                            opened: true
                        }
                    }

                    fn(val.children);
                }
            });
        };

        fn(data);

        return data;
    }

    /**
     * 清空缓存
     *
     * @return {Object} this
     */
    clear_cache() {
        // 调用缓存对象清空缓存
        new Key_cache({
            dir: this.options.cache_path
        }).remove();

        return this;
    }

    /**
     * 获取navtree使用数据，会追加options.links
     *
     * @description 会先读取缓存
     * @return {Array} 数组
     */
    get_list() {
        return this.cache('nav_data', () => {
            let data = this._get_list();

            if (!data.children) {
                data.children = [];
            }

            // 如果有链接，则追加
            if (this.options.links && this.options.links.length) {
                this.options.links.forEach(val => {
                    if (val.type === 'after') {
                        data.children.push(val);
                    }
                    else {
                        data.children.unshift(val);
                    }
                });
            }

            return data.children;
        });
    }

    /**
     * 渲染md文件
     *
     * @param  {string} content md源码
     *
     * @return {Object}         {content:html代码, catalog: h2,3分类}
     */
    render_markdown(content = '') {
        let renderer = new marked.Renderer();
        let cachekey = {};

        let catalog = [];

        // 渲染标题
        renderer.heading = (text, level) => {
            let key;

            // 如果不是h1,h2,h3则直接返回
            if ([1, 2, 3].indexOf(level) === -1) {
                return `<h${level}>${text}</h${level}>`;
            }

            if (cachekey[level] === undefined) {
                cachekey[level] = 0;
            }

            key = ++cachekey[level];

            // 使用text文本来写hash
            // 把text文本里的标签去掉
            let hash = String(text).replace(/<([^>]+?)>/g, '');

            catalog.push({
                text,
                level,
                id: `h${level}-${key}`,
                hash: hash
            });

            return `
                <h${level}>

                        ${text}

                </h${level}>
            `;
        };

        // 渲染代码
        renderer.code = (data, lang) => {
            let html;

            data = highlight.highlightAuto(data).value;

            // 有语言时
            if (lang) {

                // 超过3行有提示
                if (data.split(/\n/).length >= 3) {
                    html = `<pre><code class="hljs lang-${lang}" data-lang="${lang}">${data}</code></pre>`;
                }

                html = `<pre><code class="hljs lang-${lang}">${data}</code></pre>`;
            }
            else {
                html = `<pre><code class="hljs">${data}</code></pre>`;
            }

            return html;
        };

        // md => html
        content = marked(content, {
            renderer
        });

        // 兼容todo
        content = content.replace(/
\s*\[ \]\s*/g, '
<input type="checkbox" class="ui-todo" disabled>');
        content = content.replace(/
\s*\[x\]\s*/g, '
<input type="checkbox" disabled checked class="ui-todo">');

        return {
            content,
            catalog
        };
    }

    /**
     * 运行
     *
     * @return {Object} this
     */
    run() {
        // 委托目录浏览
        this.express.use('/', serve_index(this.options.root, {
            icons: true
        }));
        this.express.listen(this.options.port);
        return this;
    }

    /**
     * 缓存
     *
     * @description 如果有key的缓存则直接调用缓存，否则使用fn返回值作为缓存，如果开启了debug则直接使用fn返回值
     * @param  {string}   key  缓存key
     * @param {Function} fn 当没有缓存时执行的回调
     * @return {Object|undefined} 缓存数据或者回调返回值
     */
    cache(key, fn = () => {}) {
        // 如果没有key则返回空
        if (!key) {
            return undefined;
        }

        // 如果有debug
        if (this.options.debug) {
            return fn();
        }

        let cache = new Key_cache({
            dir: this.options.cache_path
        });
        let value = cache.get(key);
        if (value) {
            return value;
        }

        value = fn();

        // 如果不为空则设置缓存
        if (value !== undefined) {
            cache.set(key, value);
        }

        return value;
    }

    /**
     * 初始化express
     *
     * @private
     */
    _init_express() {
        let app = this.express;

        template.config('base', '');
        template.config('extname', '.html');

        app.engine('.html', template.__express);
        app.set('views', resolve(this.__dirname, './views/'));
        app.set('view engine', 'html');

        // 写入变量
        app.use((req, res, next) => {
            // 写入变量
            res.locals.options = this.options;

            return next();
        });

        // 绑定.md文档
        app.get(/([\s\S]+?)\.md$/, ::this._md);

        // 监听以目录结束的，其实是为了解决默认主页为md文档问题
        app.get(/(^\/$|\/$)/, (req, res, next) => {
            let parseUrl = parse(req.url, true);
            let pathname = parseUrl.pathname;

            // 相对文件的路径,用来判断文件是否存在
            let filepath = decodeURIComponent('.' + pathname);

            // 处理默认主页，拿配置的主页文件名去查找实际这个文件是否存在
            let flag = false;
            this.options.default_index.some(val => {
                if (existsSync(resolve(this.options.root, filepath, val))) {
                    flag = val;
                    return true;
                }
            });

            if (flag) {
                req.url = format({
                    hash: parseUrl.hash,
                    search: parseUrl.search,
                    pathname: pathname + flag,
                    query: parseUrl.query
                });

                return this._md(req, res, next);
            }

            next();
        });

        // 委托静态资源
        app.use('/' + this.options.static_prefix, serve_static(resolve(this.__dirname, './static/')));

        // 委托源目录
        app.use('/', serve_static(this.options.root));
    }

    /**
     * 内部获取列表数据
     *
     * @private
     * @param  {string|undefined} dir 目录
     *
     * @return {Object}     {path:'', children:[]}
     */
    _get_list(dir) {
        let options = this.options;
        let result;

        dir = dir || options.root;

        let file_basename = basename(dir);

        result = {
            id: dir.replace(options.root, '') || '/',
            children: [],
            text: options.dir_alias[file_basename] || file_basename
        };

        // 如果目录不存在
        if (!existsSync(dir)) {
            return result;
        }

        // 读取目录里数据
        let data = readdirSync(dir);

        // 定义目录数据和文件数据
        let dir_data = [];
        let file_data = [];

        // 遍历数据，拿出目录、文件的数据
        data.forEach(file => {
            let filepath = resolve(dir, file);
            let stat = statSync(filepath);

            if (stat.isDirectory()) {
                if (options.ignore_dir && options.ignore_dir.indexOf(file) === -1) {
                    dir_data.push({
                        type: 'dir',
                        filepath
                    });
                }
            }
            else {
                if (extname(file) === '.md') {
                    file_data.push({
                        filepath
                    });
                }
            }
        });

        // 合并目录+文件数据
        dir_data.concat(file_data).forEach(file => {
            if (file.type === 'dir') {
                let res = this._get_list(file.filepath);

                // 必须有子集才算
                if (res.children && res.children.length) {
                    result.children.push(res);
                }
            }
            else {
                result.children.push({
                    text: this.get_markdown_title(file.filepath),
                    id: file.filepath.replace(options.root, '').split(sep).join('\/')
                });
            }
        });

        return result;
    }

    /**
     * 渲染md文档
     *
     * @private
     * @param  {Object}   req  express.req
     * @param  {Object}   res  express.res
     * @param  {Function} next 下一个路由
     * @return {Object} res
     */
    _md(req, res, next) {
        let parseUrl = parse(req.url, true);

        // 如果要读取源码
        if (parseUrl.query.source) {
            return next();
        }

        // 加.是为了变成相对路径
        let filepath = resolve(this.options.root, '.' + parseUrl.pathname);

        // 为了中文
        filepath = decodeURIComponent(filepath);

        // 如果md文件不存在
        if (!existsSync(filepath)) {
            return next();
        }

        let markdown_data = this.render_markdown(readFileSync(filepath).toString()).content;

        // 如果是pjax
        if (parseUrl.query.pjax) {
            return res.end(markdown_data);
        }

        // 渲染md
        return res.render('markdown', {
            nav_data: JSON.stringify(this.get_render_nav(parseUrl.pathname)),
            markdown_data,
            title: `${this.get_markdown_title(filepath)} - ${this.options.name}`
        });
    }

    /**
     * 获取md文档的标题
     *
     * @param  {string} filepath 文件路径
     *
     * @return {string}     标题
     */
    get_markdown_title(filepath) {
        // 如果是md扩展
        if (extname(filepath) === '.md') {
            // 获取文件内容
            let filedata = readFileSync(filepath).toString();

            // 正则取出#标题的文字
            if (filedata.match(/^\s*\#+\s?(.+)/)) {
                return String(RegExp.$1).trim();
            }
        }

        return basename(filepath);
    }
}
<|endoftext|>


<commit_before>// The API for this library is modeled off of [Processing](http://processing.org) and only
// supports a single WebGL canvas for simplicity. The global WebGL context is stored in
// `gl`, which is augmented with additional functions for matrix manipulation.
//
// Example usage:
//
//     <script src="lightgl.js"></script>
//     <script>
//
//     var time = 0;
//
//     function setup() {
//         document.body.appendChild(gl.canvas);
//     }
//
//     function update(seconds) {
//         time += seconds;
//     }
//
//     function draw() {
//         gl.clearColor(1, Math.cos(time), Math.sin(time), 1);
//         gl.clear(gl.COLOR_BUFFER_BIT);
//     }
//
//     </script>

// ### Initialization
// When the page is loaded, a WebGL canvas singleton is automatically created. The default
// resolution is 800x600, which can be changed by setting `gl.canvas.width` and `gl.canvas.height`
// and then calling `gl.viewport()`.
window.onload = function() {
    var canvas = document.createElement('canvas');
    canvas.width = 800;
    canvas.height = 600;
    window.gl = null;
    try { gl = canvas.getContext('webgl'); } catch (e) {}
    try { gl = gl || canvas.getContext('experimental-webgl'); } catch (e) {}
    if (!gl) __error('WebGL not supported');

    // ### Mouse Input
    //
    // The interface for mouse input is also taken from Processing. Mouse state
    // can be accessed through the `mouseX`, `mouseY`, `deltaMouseX`, `deltaMouseY`,
    // `mouseDragging`, `mouseButton`, and `mouseButtons` global variables. The
    // four mouse callbacks are:
    //
    //     function mousePressed() {
    //         // Called when any mouse button is pressed
    //     }
    //
    //     function mouseDragged() {
    //         // Called when the mouse moves while pressed
    //     }
    //
    //     function mouseMoved() {
    //         // Called when the mouse moves while released
    //     }
    //
    //     function mouseReleased() {
    //         // Called when any mouse button is released
    //     }

    window.LEFT_BUTTON = 1;
    window.MIDDLE_BUTTON = 2;
    window.RIGHT_BUTTON = 4;

    window.mouseX = window.mouseY = window.deltaMouseX = window.deltaMouseY = 0;
    window.mouseButton = window.mouseButtons = 0;
    window.mouseDragging = false;

    var oldMouseX = 0;
    var oldMouseY = 0;

    function setMouseInfo(e) {
        mouseX = e.pageX;
        mouseY = e.pageY;
        for (var obj = gl.canvas; obj; obj = obj.offsetParent) {
            mouseX -= obj.offsetLeft;
            mouseY -= obj.offsetTop;
        }
        deltaMouseX = mouseX - oldMouseX;
        deltaMouseY = mouseY - oldMouseY;
        oldMouseX = mouseX;
        oldMouseY = mouseY;
        e.preventDefault();
    }

    gl.canvas.onmousedown = function(e) {
        setMouseInfo(e);
        mouseDragging = true;
        mouseButton = 1 << (e.which - 1);
        mouseButtons |= mouseButton;
        if (window.mousePressed) window.mousePressed();
    };

    document.onmousemove = function(e) {
        setMouseInfo(e);
        if (!mouseDragging && window.mouseMoved) window.mouseMoved();
        else if (mouseDragging && window.mouseDragged) window.mouseDragged();
    };

    document.onmouseup = function(e) {
        setMouseInfo(e);
        mouseDragging = false;
        mouseButton = 1 << (e.which - 1);
        mouseButtons &= ~mouseButton;
        if (window.mouseReleased) window.mouseReleased();
    };

    gl.canvas.oncontextmenu = function(e) {
        e.preventDefault();
    };

    window.onblur = function() {
        mouseDragging = false;
        mouseButtons = 0;
        keys = {};
    };

    // ### Keyboard Input
    //
    // The interface for keyboard input is also taken from Processing. Keyboard state
    // can be accessed through the `key` and `keys` global variables. The key code from
    // the last keyboard event is stored in `key`, which will either be a string or a
    // numeric constant. The current boolean state of any key code can be queried using
    // `keys[code]`. The two keyboard callbacks are:
    //
    //     function keyPressed() {
    //     }
    //
    //     function keyReleased() {
    //     }

    window.key = null;
    window.keys = {};

    function mapKeyCode(code) {
        switch (code) {
            case 8: return 'BACKSPACE';
            case 9: return 'TAB';
            case 13: return 'ENTER';
            case 16: return 'SHIFT';
            case 27: return 'ESCAPE';
            case 32: return 'SPACE';
            case 37: return 'LEFT';
            case 38: return 'UP';
            case 39: return 'RIGHT';
            case 40: return 'DOWN';
        }
        return code >= 65 && code <= 90 ? String.fromCharCode(code) : code;
    }

    document.onkeydown = function(e) {
        if (!e.altKey && !e.ctrlKey && !e.metaKey) {
            key = mapKeyCode(e.keyCode);
            keys[key] = true;
            if (window.keyPressed) window.keyPressed();
        }
    };

    document.onkeyup = function(e) {
        if (!e.altKey && !e.ctrlKey && !e.metaKey) {
            key = mapKeyCode(e.keyCode);
            keys[key] = false;
            if (window.keyReleased) window.keyReleased();
        }
    };

    // Provide an easy way to get a fullscreen app running, including an
    // automatic 3D perspective projection matrix by default. This should be
    // called once in setup().
    //
    // Just fullscreen, no camera:
    //
    //     gl.fullscreen({ camera: false });
    //
    // Adjusting field of view, near plane distance, and far plane distance:
    //
    //     gl.fullscreen({ fov: 45, near: 0.1, far: 1000 });
    //
    // Adding padding from the edge of the window:
    //
    //     gl.fullscreen({ paddingLeft: 250, paddingBottom: 60 });
    gl.fullscreen = function(options) {
        options = options || {};
        var top = options.paddingTop || 0;
        var left = options.paddingLeft || 0;
        var right = options.paddingRight || 0;
        var bottom = options.paddingBottom || 0;
        document.body.appendChild(gl.canvas);
        gl.canvas.style.position = 'absolute';
        gl.canvas.style.left = left + 'px';
        gl.canvas.style.top = top + 'px';
        window.onresize = function() {
            gl.canvas.width = window.innerWidth - left - right;
            gl.canvas.height = window.innerHeight - top - bottom;
            gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
            if (options.camera || !('camera' in options)) {
                gl.matrixMode(gl.PROJECTION);
                gl.loadIdentity();
                gl.perspective(options.fov || 45, gl.canvas.width / gl.canvas.height,
                    options.near || 0.1, options.far || 1000);
                gl.matrixMode(gl.MODELVIEW);
            }
            if (setupCalled) draw();
        };
        window.onresize();
    };

    // Provide an implementation of the OpenGL matrix stack (only modelview
    // and projection matrices), as well as some useful GLU matrix functions.
    var ENUM = 0x12340000;
    gl.MODELVIEW = ENUM | 1;
    gl.PROJECTION = ENUM | 2;
    gl.modelviewMatrix = new Matrix();
    gl.projectionMatrix = new Matrix();
    var modelviewStack = [];
    var projectionStack = [];
    var matrix, stack;
    gl.matrixMode = function(mode) {
        switch (mode) {
        case gl.MODELVIEW:
            matrix = 'modelviewMatrix';
            stack = modelviewStack;
            break;
        case gl.PROJECTION:
            matrix = 'projectionMatrix';
            stack = projectionStack;
            break;
        default:
            __error('invalid matrix mode ' + mode);
        }
    };
    gl.loadIdentity = function() {
        gl[matrix].m = new Matrix().m;
    };
    gl.loadMatrix = function(m) {
        gl[matrix].m = m.m.slice();
    };
    gl.multMatrix = function(m) {
        gl[matrix].m = gl[matrix].multiply(m).m;
    };
    gl.perspective = function(fov, aspect, near, far) {
        gl.multMatrix(Matrix.perspective(fov, aspect, near, far));
    };
    gl.frustum = function(l, r, b, t, n, f) {
        gl.multMatrix(Matrix.frustum(l, r, b, t, n, f));
    };
    gl.ortho = function(l, r, b, t, n, f) {
        gl.multMatrix(Matrix.ortho(l, r, b, t, n, f));
    };
    gl.scale = function(x, y, z) {
        gl.multMatrix(Matrix.scale(x, y, z));
    };
    gl.translate = function(x, y, z) {
        gl.multMatrix(Matrix.translate(x, y, z));
    };
    gl.rotate = function(a, x, y, z) {
        gl.multMatrix(Matrix.rotate(a, x, y, z));
    };
    gl.lookAt = function(ex, ey, ez, cx, cy, cz, ux, uy, uz) {
        gl.multMatrix(Matrix.lookAt(ex, ey, ez, cx, cy, cz, ux, uy, uz));
    };
    gl.pushMatrix = function() {
        stack.push(gl[matrix].m.slice());
    };
    gl.popMatrix = function() {
        gl[matrix].m = stack.pop();
    };
    gl.project = function(objX, objY, objZ, modelview, projection, viewport) {
        modelview = modelview || gl.modelviewMatrix;
        projection = projection || gl.projectionMatrix;
        viewport = viewport || gl.getParameter(gl.VIEWPORT);
        var point = projection.transformPoint(modelview.transformPoint(new Vector(objX, objY, objZ)));
        return new Vector(
            viewport[0] + viewport[2] * (point.x * 0.5 + 0.5),
            viewport[1] + viewport[3] * (point.y * 0.5 + 0.5),
            point.z * 0.5 + 0.5
        );
    };
    gl.unProject = function(winX, winY, winZ, modelview, projection, viewport) {
        modelview = modelview || gl.modelviewMatrix;
        projection = projection || gl.projectionMatrix;
        viewport = viewport || gl.getParameter(gl.VIEWPORT);
        var point = new Vector(
            (winX - viewport[0]) / viewport[2] * 2 - 1,
            (winY - viewport[1]) / viewport[3] * 2 - 1,
            winZ * 2 - 1
        );
        return projection.multiply(modelview).inverse().transformPoint(point);
    };
    gl.matrixMode(gl.MODELVIEW);
    gl.autoDraw = true;

    // Set up the animation loop. If your application doesn't need continuous
    // redrawing, set `gl.autoDraw = false` in your `setup()` function.
    var post =
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        function(callback) { setTimeout(callback, 1000 / 60); };
    var time = new Date();
    function frame() {
        var now = new Date();

        // If we're animating, render as fast as possible, otherwise sleep for
        // a bit and check if we're animating again
        if (gl.autoDraw) {
            if (window.update) window.update((now - time) / 1000);
            if (window.draw) window.draw();
            post(frame);
        } else {
            setTimeout(frame, 100);
        }

        time = now;
    }

    // Draw the initial frame and start the animation loop. The setupCalled
    // flag is used so methods called within `setup()` don't run user callbacks
    // until after `setup()` has finished, otherwise the user's data may not be
    // initialized correctly.
    var setupCalled = false;
    if (window.setup) window.setup();
    setupCalled = true;
    frame();
};

// ### Error handling
// If you want to handle WebGL errors, define a function called handleError():
//
//     function handleError(text) {
//         alert('WebGL error occurred: ' + text);
//     }
//
// The error is always thrown afterwards to make sure the current function stops.
function __error(text) {
  if (window.handleError) window.handleError(text);
  throw text;
}
<commit_msg>added a lightweight immediate mode<commit_after>// The API for this library is modeled off of [Processing](http://processing.org) and only
// supports a single WebGL canvas for simplicity. The global WebGL context is stored in
// `gl`, which is augmented with additional functions for matrix manipulation.
//
// Example usage:
//
//     <script src="lightgl.js"></script>
//     <script>
//
//     var time = 0;
//
//     function setup() {
//         document.body.appendChild(gl.canvas);
//     }
//
//     function update(seconds) {
//         time += seconds;
//     }
//
//     function draw() {
//         gl.clearColor(1, Math.cos(time), Math.sin(time), 1);
//         gl.clear(gl.COLOR_BUFFER_BIT);
//     }
//
//     </script>

// ### Initialization
// When the page is loaded, a WebGL canvas singleton is automatically created. The default
// resolution is 800x600, which can be changed by setting `gl.canvas.width` and `gl.canvas.height`
// and then calling `gl.viewport()`.
window.onload = function() {
    var canvas = document.createElement('canvas');
    canvas.width = 800;
    canvas.height = 600;
    window.gl = null;
    try { gl = canvas.getContext('webgl'); } catch (e) {}
    try { gl = gl || canvas.getContext('experimental-webgl'); } catch (e) {}
    if (!gl) __error('WebGL not supported');

    // ### Mouse Input
    //
    // The interface for mouse input is also taken from Processing. Mouse state
    // can be accessed through the `mouseX`, `mouseY`, `deltaMouseX`, `deltaMouseY`,
    // `mouseDragging`, `mouseButton`, and `mouseButtons` global variables. The
    // four mouse callbacks are:
    //
    //     function mousePressed() {
    //         // Called when any mouse button is pressed
    //     }
    //
    //     function mouseDragged() {
    //         // Called when the mouse moves while pressed
    //     }
    //
    //     function mouseMoved() {
    //         // Called when the mouse moves while released
    //     }
    //
    //     function mouseReleased() {
    //         // Called when any mouse button is released
    //     }

    window.LEFT_BUTTON = 1;
    window.MIDDLE_BUTTON = 2;
    window.RIGHT_BUTTON = 4;

    window.mouseX = window.mouseY = window.deltaMouseX = window.deltaMouseY = 0;
    window.mouseButton = window.mouseButtons = 0;
    window.mouseDragging = false;

    var oldMouseX = 0;
    var oldMouseY = 0;

    function setMouseInfo(e) {
        mouseX = e.pageX;
        mouseY = e.pageY;
        for (var obj = gl.canvas; obj; obj = obj.offsetParent) {
            mouseX -= obj.offsetLeft;
            mouseY -= obj.offsetTop;
        }
        deltaMouseX = mouseX - oldMouseX;
        deltaMouseY = mouseY - oldMouseY;
        oldMouseX = mouseX;
        oldMouseY = mouseY;
        e.preventDefault();
    }

    gl.canvas.onmousedown = function(e) {
        setMouseInfo(e);
        mouseDragging = true;
        mouseButton = 1 << (e.which - 1);
        mouseButtons |= mouseButton;
        if (window.mousePressed) window.mousePressed();
    };

    document.onmousemove = function(e) {
        setMouseInfo(e);
        if (!mouseDragging && window.mouseMoved) window.mouseMoved();
        else if (mouseDragging && window.mouseDragged) window.mouseDragged();
    };

    document.onmouseup = function(e) {
        setMouseInfo(e);
        mouseDragging = false;
        mouseButton = 1 << (e.which - 1);
        mouseButtons &= ~mouseButton;
        if (window.mouseReleased) window.mouseReleased();
    };

    gl.canvas.oncontextmenu = function(e) {
        e.preventDefault();
    };

    window.onblur = function() {
        mouseDragging = false;
        mouseButtons = 0;
        keys = {};
    };

    // ### Keyboard Input
    //
    // The interface for keyboard input is also taken from Processing. Keyboard state
    // can be accessed through the `key` and `keys` global variables. The key code from
    // the last keyboard event is stored in `key`, which will either be a string or a
    // numeric constant. The current boolean state of any key code can be queried using
    // `keys[code]`. The two keyboard callbacks are:
    //
    //     function keyPressed() {
    //     }
    //
    //     function keyReleased() {
    //     }

    window.key = null;
    window.keys = {};

    function mapKeyCode(code) {
        switch (code) {
            case 8: return 'BACKSPACE';
            case 9: return 'TAB';
            case 13: return 'ENTER';
            case 16: return 'SHIFT';
            case 27: return 'ESCAPE';
            case 32: return 'SPACE';
            case 37: return 'LEFT';
            case 38: return 'UP';
            case 39: return 'RIGHT';
            case 40: return 'DOWN';
        }
        return code >= 65 && code <= 90 ? String.fromCharCode(code) : code;
    }

    document.onkeydown = function(e) {
        if (!e.altKey && !e.ctrlKey && !e.metaKey) {
            key = mapKeyCode(e.keyCode);
            keys[key] = true;
            if (window.keyPressed) window.keyPressed();
        }
    };

    document.onkeyup = function(e) {
        if (!e.altKey && !e.ctrlKey && !e.metaKey) {
            key = mapKeyCode(e.keyCode);
            keys[key] = false;
            if (window.keyReleased) window.keyReleased();
        }
    };

    // ### Fullscreen
    //
    // Provide an easy way to get a fullscreen app running, including an
    // automatic 3D perspective projection matrix by default. This should be
    // called once in setup().
    //
    // Just fullscreen, no camera:
    //
    //     gl.fullscreen({ camera: false });
    //
    // Adjusting field of view, near plane distance, and far plane distance:
    //
    //     gl.fullscreen({ fov: 45, near: 0.1, far: 1000 });
    //
    // Adding padding from the edge of the window:
    //
    //     gl.fullscreen({ paddingLeft: 250, paddingBottom: 60 });
    //
    gl.fullscreen = function(options) {
        options = options || {};
        var top = options.paddingTop || 0;
        var left = options.paddingLeft || 0;
        var right = options.paddingRight || 0;
        var bottom = options.paddingBottom || 0;
        document.body.appendChild(gl.canvas);
        gl.canvas.style.position = 'absolute';
        gl.canvas.style.left = left + 'px';
        gl.canvas.style.top = top + 'px';
        window.onresize = function() {
            gl.canvas.width = window.innerWidth - left - right;
            gl.canvas.height = window.innerHeight - top - bottom;
            gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
            if (options.camera || !('camera' in options)) {
                gl.matrixMode(gl.PROJECTION);
                gl.loadIdentity();
                gl.perspective(options.fov || 45, gl.canvas.width / gl.canvas.height,
                    options.near || 0.1, options.far || 1000);
                gl.matrixMode(gl.MODELVIEW);
            }
            if (setupCalled) draw();
        };
        window.onresize();
    };

    // ### Matrix stack
    //
    // Provide an implementation of the OpenGL matrix stack (only modelview
    // and projection matrices), as well as some useful GLU matrix functions.
    //
    var ENUM = 0x12340000;
    gl.MODELVIEW = ENUM | 1;
    gl.PROJECTION = ENUM | 2;
    gl.modelviewMatrix = new Matrix();
    gl.projectionMatrix = new Matrix();
    var modelviewStack = [];
    var projectionStack = [];
    var matrix, stack;
    gl.matrixMode = function(mode) {
        switch (mode) {
        case gl.MODELVIEW:
            matrix = 'modelviewMatrix';
            stack = modelviewStack;
            break;
        case gl.PROJECTION:
            matrix = 'projectionMatrix';
            stack = projectionStack;
            break;
        default:
            __error('invalid matrix mode ' + mode);
        }
    };
    gl.loadIdentity = function() {
        gl[matrix].m = new Matrix().m;
    };
    gl.loadMatrix = function(m) {
        gl[matrix].m = m.m.slice();
    };
    gl.multMatrix = function(m) {
        gl[matrix].m = gl[matrix].multiply(m).m;
    };
    gl.perspective = function(fov, aspect, near, far) {
        gl.multMatrix(Matrix.perspective(fov, aspect, near, far));
    };
    gl.frustum = function(l, r, b, t, n, f) {
        gl.multMatrix(Matrix.frustum(l, r, b, t, n, f));
    };
    gl.ortho = function(l, r, b, t, n, f) {
        gl.multMatrix(Matrix.ortho(l, r, b, t, n, f));
    };
    gl.scale = function(x, y, z) {
        gl.multMatrix(Matrix.scale(x, y, z));
    };
    gl.translate = function(x, y, z) {
        gl.multMatrix(Matrix.translate(x, y, z));
    };
    gl.rotate = function(a, x, y, z) {
        gl.multMatrix(Matrix.rotate(a, x, y, z));
    };
    gl.lookAt = function(ex, ey, ez, cx, cy, cz, ux, uy, uz) {
        gl.multMatrix(Matrix.lookAt(ex, ey, ez, cx, cy, cz, ux, uy, uz));
    };
    gl.pushMatrix = function() {
        stack.push(gl[matrix].m.slice());
    };
    gl.popMatrix = function() {
        gl[matrix].m = stack.pop();
    };
    gl.project = function(objX, objY, objZ, modelview, projection, viewport) {
        modelview = modelview || gl.modelviewMatrix;
        projection = projection || gl.projectionMatrix;
        viewport = viewport || gl.getParameter(gl.VIEWPORT);
        var point = projection.transformPoint(modelview.transformPoint(new Vector(objX, objY, objZ)));
        return new Vector(
            viewport[0] + viewport[2] * (point.x * 0.5 + 0.5),
            viewport[1] + viewport[3] * (point.y * 0.5 + 0.5),
            point.z * 0.5 + 0.5
        );
    };
    gl.unProject = function(winX, winY, winZ, modelview, projection, viewport) {
        modelview = modelview || gl.modelviewMatrix;
        projection = projection || gl.projectionMatrix;
        viewport = viewport || gl.getParameter(gl.VIEWPORT);
        var point = new Vector(
            (winX - viewport[0]) / viewport[2] * 2 - 1,
            (winY - viewport[1]) / viewport[3] * 2 - 1,
            winZ * 2 - 1
        );
        return projection.multiply(modelview).inverse().transformPoint(point);
    };
    gl.matrixMode(gl.MODELVIEW);

    // ### Immediate mode
    //
    // Provide an implementation of OpenGL's deprecated immediate mode. This is
    // depricated for a reason: constantly re-specifying the geometry is a bad
    // idea for performance. You should use a `Mesh` instead, which specifies
    // the geometry once and caches it on the graphics card. Still, nothing
    // beats a quick `gl.begin(gl.POINTS); gl.vertex(1, 2, 3); gl.end();` for
    // debugging. This intentionally doesn't implement texture coordinates or
    // normals because it's only meant for quick debugging tasks.
    //
    var immediateMode = {
        mesh: new Mesh({ normals: false, coords: false, triangles: false }),
        mode: null,
        color: new Vector(1, 1, 1),
        shader: new Shader('\
            attribute vec3 color;\
            varying vec3 c;\
            void main() {\
                c = color;\
                gl_Position = gl_ModelViewProjectionMatrix * vec4(gl_Vertex, 1.0);\
            }\
        ', '\
            varying vec3 c;\
            void main() {\
                gl_FragColor = vec4(c, 1.0);\
            }\
        ')
    };
    immediateMode.mesh.addVertexBuffer('colors', 'color');
    gl.begin = function(mode) {
        if (immediateMode.mode) throw 'mismatched gl.begin() and gl.end() calls';
        immediateMode.mode = mode;
        immediateMode.mesh.vertices = [];
        immediateMode.mesh.colors = [];
    };
    gl.color = function(r, g, b) {
        immediateMode.color = (arguments.length == 1) ? r.toArray() : [r, g, b];
    };
    gl.vertex = function(x, y, z) {
        immediateMode.mesh.colors.push(immediateMode.color);
        immediateMode.mesh.vertices.push(arguments.length == 1 ? x.toArray() : [x, y, z]);
    };
    gl.end = function() {
        if (!immediateMode.mode) throw 'mismatched gl.begin() and gl.end() calls';
        immediateMode.mesh.compile();
        immediateMode.shader.draw(immediateMode.mesh, immediateMode.mode);
        immediateMode.mode = null;
    };

    // Set up a better default state, users can still change it if they want
    gl.clearColor(0, 0, 0, 1);
    gl.enable(gl.DEPTH_TEST);

    // Set up the animation loop. If your application doesn't need continuous
    // redrawing, set `gl.autoDraw = false` in your `setup()` function.
    var post =
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        function(callback) { setTimeout(callback, 1000 / 60); };
    var time = new Date();
    function frame() {
        var now = new Date();

        // If we're animating, render as fast as possible, otherwise sleep for
        // a bit and check if we're animating again
        if (gl.autoDraw) {
            if (window.update) window.update((now - time) / 1000);
            if (window.draw) window.draw();
            post(frame);
        } else {
            setTimeout(frame, 100);
        }

        time = now;
    }
    gl.autoDraw = true;

    // Draw the initial frame and start the animation loop. The setupCalled
    // flag is used so methods called within `setup()` don't run user callbacks
    // until after `setup()` has finished, otherwise the user's data may not be
    // initialized correctly.
    var setupCalled = false;
    if (window.setup) window.setup();
    setupCalled = true;
    frame();
};

// ### Error handling
// If you want to handle WebGL errors, define a function called handleError():
//
//     function handleError(text) {
//         alert('WebGL error occurred: ' + text);
//     }
//
// The error is always thrown afterwards to make sure the current function stops.
function __error(text) {
  if (window.handleError) window.handleError(text);
  throw text;
}
<|endoftext|>


<commit_before>/* touchme main */

/*
TODO add support for the 'swipeOnly' option, which keeps swipe events to the mouse.
 */

// Base function.
var root = this;
var document = this.document || {};

//native custom event or polyfill
var CustomEvent = this.CustomEvent || function(){
    var CustomEvent = function ( event, params ) {
        params = params || { bubbles: false, cancelable: false, detail: undefined };
        var evt = document.createEvent( 'CustomEvent' );
        evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
        return evt;
    };

    CustomEvent.prototype = this.Event.prototype;

    this.CustomEvent = CustomEvent;
};

var touchme = function(args) {
    //`this` will be window in browser, and will contain ontouchstart if a mobile device
    var touchDevice = ('ontouchstart' in root);

    //self explanatory...
    var
        touchStart = false,
        tapNumber = 0,
        tapTimer, holdTimer,
        currentX, currentY,
        oldX, oldY,
        holdElement, isHolding,
        originalX, originalY; //for elements that are being held

    //where args is an object that can replace any of the default arguments
    var defaults = {
        swipeThreshold: 80,
        tapThreshold: 200,
        holdThreshold: 550,
        precision: 45,
        onlyTouch: false,
        swipeOnlyTouch: false,
        nTap: false
    };
    //replace any object that belongs in the defaults
    //TODO: a prettier way to write this?
    if(args){
        for(var key in args){
            if (args.hasOwnProperty(key) && defaults.hasOwnProperty(key)) {
                defaults[key] = args[key];
            }
        }
    }
    //reused for tap and hold.
    var isInTapLimits = function(){
        return(
            currentX >= oldX-defaults.precision &&
            currentX <= oldX+defaults.precision &&
            currentY >= oldY-defaults.precision &&
            currentY <= oldY+defaults.precision
            );
    };
    //add event (s) to a given element
    var setListener = function(element, evt, callback){
        var evtArr = evt.split(' ');
        var i = evtArr.length;
        while(i--){
            element.addEventListener(evtArr[i], callback, false);
        }
    };
    //returns the node being touched/clicked
    var getPointer = function(event){
        if(event.targetTouches){
            return event.targetTouches[0];
        }else{
            return event;
        }
    };
    /*
        triggerEvent()
            creates a custom event, and passes any members in the data object to the event
            triggers the custom event on the given element.
     */
    var triggerEvent = function(element, eventName, data){
        data = data || {
            x: currentX,
            y: currentY
        };
        var newEvent = new CustomEvent(
            eventName,
            {
                bubbles: true,
                cancelable:true
            }
        );
        for(var key in data){
            if(data.hasOwnProperty(key)){
                newEvent[key] = data[key];
            }
        }
        element.dispatchEvent(newEvent);
    };

    //if we're only dealing with touch devices, just set touch device to true to prevent mouse fallbacks
    if(defaults.onlyTouch){
        touchDevice = true;
    }

    //tap, dbltap, ntap, hold
    setListener(document, touchDevice ? 'touchstart' : 'mousedown', function(e){
        touchStart = true;
        tapNumber += 1;

        var pointer = getPointer(e);

        oldX = currentX = pointer.pageX;
        oldY = currentY = pointer.pageY;

        //initialize tapTimer
        clearTimeout(tapTimer);
        tapTimer = setTimeout(function(){
            if( isInTapLimits() && !touchStart ){
                //dense code
                //  first check if it's a double tap. Anything over 2 taps considered a double tap
                //  then, check if ntap is enabled, if so and more than 3 taps occcured, trigger ntap.
                var tapEventName = tapNumber>=2 ? 'dbltap' : 'tap';
                if(defaults.ntap) { tapEventName = tapNumber>=3 ? 'ntap' : tapEventName;}
                triggerEvent(e.target, tapEventName, {
                    taps: tapNumber //use case for ntap
                });

                tapNumber = 0;
            }else if( !isInTapLimits() ){
                tapNumber = 0;
            }
        }, defaults.tapThreshold);

        //initialize holdTimer
        clearTimeout(holdTimer);
        holdTimer = setTimeout(function(){
            if( isInTapLimits() && touchStart){
                //user is within the tap region and is still after hold threshold
                isHolding = true;
                //we'll reference this when the user let's go
                holdElement = e;
                originalX = holdElement.pageX;
                originalY = holdElement.pageY;

                triggerEvent(holdElement.target, 'hold', {
                    //having a difficult time deciding actually how to deal with this at the moment
                    holdElement: holdElement
                });
                tapNumber = 0;
            }
        }, defaults.holdThreshold);
    });

    //track the movement
    setListener(document, touchDevice ? 'touchmove' : 'mousemove', function(e){
        var pointer = getPointer(e);
        currentX = pointer.pageX;
        currentY = pointer.pageY;
    });

    /*
        create two seperate listeners for touchend and mouseup.

        the first is generic, it sets touch start to false and checks to see if the user was holding something
        the second is swipe specific, to check if the default 'swipeOnlyTouch' is set
     */
    setListener(document, touchDevice ? 'touchend' : 'mouseup', function(){
        touchStart = false;

        //if the user was holding something...
        if(isHolding){
            isHolding = false;
            triggerEvent(holdElement.target, 'holdrelease', {
                holdElement: holdElement,
                originalX: originalX,
                originalY: originalY
            });
        }
    });
    //swipe specific
    var swipeEventName = touchDevice ? 'touchend' : 'mouseup';
    swipeEventName = defaults.swipeOnlyTouch ? 'touchend' : swipeEventName;
    setListener(document, swipeEventName, function(e){
        touchStart = false;

        var
            swipeEvents = [],
            deltaX = oldX - currentX,
            deltaY = oldY - currentY;

        //calculate radians for direction
        var rads = Math.atan2(currentY-oldY, currentX-oldX);
        if(rads<0){rads+= Math.PI*2;}

        //the user is swiping...
        //why do we need this?
        if(deltaX <= defaults.swipeThreshold || deltaY <= defaults.swipeThreshold){
            swipeEvents.push('swipe');
        }

        //directional swipes
        if( deltaX <= -defaults.swipeThreshold ){
            swipeEvents.push('swiperight');
        }
        if( deltaX >= defaults.swipeThreshold ){
            swipeEvents.push('swipeleft');
        }
        if( deltaY >= defaults.swipeThreshold ){
            swipeEvents.push('swipeup');
        }
        if( deltaY <= -defaults.swipeThreshold ){
            swipeEvents.push('swipedown');
        }

        var i = swipeEvents.length;
        while(i--){
            triggerEvent(e.target, swipeEvents[i], {
                distance:{
                    x: Math.abs(deltaX),
                    y: Math.abs(deltaY)
                },
                direction:{
                    radians: rads,
                    degrees: rads*(180/Math.PI)
                }
            });
        }
    });

    return touchDevice;
};

// Export to the root, which is probably `window`.
if (typeof exports !== 'undefined') {
    if (typeof module !== 'undefined' && module.exports) {
        exports = module.exports = touchme;
    }
    exports.touchme = touchme;
} else {
    root.touchme = touchme;
}

// Version.
touchme.VERSION = '0.1.0';<commit_msg>Removed old TODO.<commit_after>/* touchme main */

// Base function.
var root = this;
var document = this.document || {};

//native custom event or polyfill
var CustomEvent = this.CustomEvent || function(){
    var CustomEvent = function ( event, params ) {
        params = params || { bubbles: false, cancelable: false, detail: undefined };
        var evt = document.createEvent( 'CustomEvent' );
        evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
        return evt;
    };

    CustomEvent.prototype = this.Event.prototype;

    this.CustomEvent = CustomEvent;
};

var touchme = function(args) {
    //`this` will be window in browser, and will contain ontouchstart if a mobile device
    var touchDevice = ('ontouchstart' in root);

    //self explanatory...
    var
        touchStart = false,
        tapNumber = 0,
        tapTimer, holdTimer,
        currentX, currentY,
        oldX, oldY,
        holdElement, isHolding,
        originalX, originalY; //for elements that are being held

    //where args is an object that can replace any of the default arguments
    var defaults = {
        swipeThreshold: 80,
        tapThreshold: 200,
        holdThreshold: 550,
        precision: 45,
        onlyTouch: false,
        swipeOnlyTouch: false,
        nTap: false
    };
    //replace any object that belongs in the defaults
    //TODO: a prettier way to write this?
    if(args){
        for(var key in args){
            if (args.hasOwnProperty(key) && defaults.hasOwnProperty(key)) {
                defaults[key] = args[key];
            }
        }
    }
    //reused for tap and hold.
    var isInTapLimits = function(){
        return(
            currentX >= oldX-defaults.precision &&
            currentX <= oldX+defaults.precision &&
            currentY >= oldY-defaults.precision &&
            currentY <= oldY+defaults.precision
            );
    };
    //add event (s) to a given element
    var setListener = function(element, evt, callback){
        var evtArr = evt.split(' ');
        var i = evtArr.length;
        while(i--){
            element.addEventListener(evtArr[i], callback, false);
        }
    };
    //returns the node being touched/clicked
    var getPointer = function(event){
        if(event.targetTouches){
            return event.targetTouches[0];
        }else{
            return event;
        }
    };
    /*
        triggerEvent()
            creates a custom event, and passes any members in the data object to the event
            triggers the custom event on the given element.
     */
    var triggerEvent = function(element, eventName, data){
        data = data || {
            x: currentX,
            y: currentY
        };
        var newEvent = new CustomEvent(
            eventName,
            {
                bubbles: true,
                cancelable:true
            }
        );
        for(var key in data){
            if(data.hasOwnProperty(key)){
                newEvent[key] = data[key];
            }
        }
        element.dispatchEvent(newEvent);
    };

    //if we're only dealing with touch devices, just set touch device to true to prevent mouse fallbacks
    if(defaults.onlyTouch){
        touchDevice = true;
    }

    //tap, dbltap, ntap, hold
    setListener(document, touchDevice ? 'touchstart' : 'mousedown', function(e){
        touchStart = true;
        tapNumber += 1;

        var pointer = getPointer(e);

        oldX = currentX = pointer.pageX;
        oldY = currentY = pointer.pageY;

        //initialize tapTimer
        clearTimeout(tapTimer);
        tapTimer = setTimeout(function(){
            if( isInTapLimits() && !touchStart ){
                //dense code
                //  first check if it's a double tap. Anything over 2 taps considered a double tap
                //  then, check if ntap is enabled, if so and more than 3 taps occcured, trigger ntap.
                var tapEventName = tapNumber>=2 ? 'dbltap' : 'tap';
                if(defaults.ntap) { tapEventName = tapNumber>=3 ? 'ntap' : tapEventName;}
                triggerEvent(e.target, tapEventName, {
                    taps: tapNumber //use case for ntap
                });

                tapNumber = 0;
            }else if( !isInTapLimits() ){
                tapNumber = 0;
            }
        }, defaults.tapThreshold);

        //initialize holdTimer
        clearTimeout(holdTimer);
        holdTimer = setTimeout(function(){
            if( isInTapLimits() && touchStart){
                //user is within the tap region and is still after hold threshold
                isHolding = true;
                //we'll reference this when the user let's go
                holdElement = e;
                originalX = holdElement.pageX;
                originalY = holdElement.pageY;

                triggerEvent(holdElement.target, 'hold', {
                    //having a difficult time deciding actually how to deal with this at the moment
                    holdElement: holdElement
                });
                tapNumber = 0;
            }
        }, defaults.holdThreshold);
    });

    //track the movement
    setListener(document, touchDevice ? 'touchmove' : 'mousemove', function(e){
        var pointer = getPointer(e);
        currentX = pointer.pageX;
        currentY = pointer.pageY;
    });

    /*
        create two seperate listeners for touchend and mouseup.

        the first is generic, it sets touch start to false and checks to see if the user was holding something
        the second is swipe specific, to check if the default 'swipeOnlyTouch' is set
     */
    setListener(document, touchDevice ? 'touchend' : 'mouseup', function(){
        touchStart = false;

        //if the user was holding something...
        if(isHolding){
            isHolding = false;
            triggerEvent(holdElement.target, 'holdrelease', {
                holdElement: holdElement,
                originalX: originalX,
                originalY: originalY
            });
        }
    });
    //swipe specific
    var swipeEventName = touchDevice ? 'touchend' : 'mouseup';
    swipeEventName = defaults.swipeOnlyTouch ? 'touchend' : swipeEventName;
    setListener(document, swipeEventName, function(e){
        touchStart = false;

        var
            swipeEvents = [],
            deltaX = oldX - currentX,
            deltaY = oldY - currentY;

        //calculate radians for direction
        var rads = Math.atan2(currentY-oldY, currentX-oldX);
        if(rads<0){rads+= Math.PI*2;}

        //the user is swiping...
        //why do we need this?
        if(deltaX <= defaults.swipeThreshold || deltaY <= defaults.swipeThreshold){
            swipeEvents.push('swipe');
        }

        //directional swipes
        if( deltaX <= -defaults.swipeThreshold ){
            swipeEvents.push('swiperight');
        }
        if( deltaX >= defaults.swipeThreshold ){
            swipeEvents.push('swipeleft');
        }
        if( deltaY >= defaults.swipeThreshold ){
            swipeEvents.push('swipeup');
        }
        if( deltaY <= -defaults.swipeThreshold ){
            swipeEvents.push('swipedown');
        }

        var i = swipeEvents.length;
        while(i--){
            triggerEvent(e.target, swipeEvents[i], {
                distance:{
                    x: Math.abs(deltaX),
                    y: Math.abs(deltaY)
                },
                direction:{
                    radians: rads,
                    degrees: rads*(180/Math.PI)
                }
            });
        }
    });

    return touchDevice;
};

// Export to the root, which is probably `window`.
if (typeof exports !== 'undefined') {
    if (typeof module !== 'undefined' && module.exports) {
        exports = module.exports = touchme;
    }
    exports.touchme = touchme;
} else {
    root.touchme = touchme;
}

// Version.
touchme.VERSION = '0.1.0';<|endoftext|>


<commit_before>(function(document, location, history) { /* jshint maxdepth:8, boss:true */
    "use strict";

    // do not enable the plugin for old browsers BUT keep for jasmine
    if (!history.pushState || !("timeout" in XMLHttpRequest.prototype || window.jasmine)) return;

    const identity = (s) => s;
    const states = []; // in-memory storage for states
    var lastState = {};
    var lastFormData = null;

    function attachNonPreventedListener(eventType, callback) {
        document.addEventListener(eventType, function(e) {
            if (!e.defaultPrevented) {
                callback(e);
            }
        }, false);
    }

    function dispatchAjaxifyEvent(el, eventType, eventDetail) {
        const e = document.createEvent("CustomEvent");

        e.initCustomEvent("ajaxify:" + eventType, true, true, eventDetail || null);

        return el.dispatchEvent(e);
    }

    function updateCurrentState(el, title, content) {
        if (dispatchAjaxifyEvent(el, "update", content)) {
            el.parentNode.replaceChild(content, el);
        }

        lastState.body = el;
        lastState.title = document.title;

        if (states.indexOf(lastState) < 0) {
            // if state does not exist - store it in memory
            states.push(lastState);
        }

        document.title = title;
    }

    attachNonPreventedListener("click", (e) => {
        const el = e.target;

        var link;

        if (el.nodeName.toLowerCase() === "a") {
            // detected click on a link
            link = el;
        } else {
            const focusedElement = document.activeElement;

            if (focusedElement.nodeName.toLowerCase() === "a") {
                if (focusedElement.contains(el)) {
                    // detected click on a link inner element
                    link = focusedElement;
                }
            }
        }

        if (link && !link.target) {
            if (link.getAttribute("aria-disabled") === "true") {
                e.preventDefault();
            } else if (link.protocol.slice(0, 4) === "http") {
                // handle only http(s) links
                const targetUrl = link.href;
                const currentUrl = location.href;

                if (targetUrl === currentUrl || targetUrl.split("#")[0] !== currentUrl.split("#")[0]) {
                    if (dispatchAjaxifyEvent(link, "fetch", link.href)) {
                        // override default bahavior for links
                        e.preventDefault();
                    }
                } else {
                    location.hash = link.hash;
                    // override default bahavior for anchors
                    e.preventDefault();
                }
            }
        }
    });

    attachNonPreventedListener("submit", (e) => {
        const el = e.target;

        if (!el.target) {
            if (el.getAttribute("aria-disabled") === "true") {
                e.preventDefault();
            } else {
                const formEnctype = el.enctype;

                var url = el.action;

                if (formEnctype === "multipart/form-data") {
                    lastFormData = new FormData(el);
                } else {
                    const encode = formEnctype === "text/plain" ? identity : encodeURIComponent;
                    const qs = [];

                    for (var i = 0, field; field = el.elements[i]; ++i) {
                        if (field.name && !field.disabled) {
                            const fieldType = field.type;
                            const fieldName = encode(field.name);

                            if (fieldType === "select-multiple") {
                                for (var j = 0, option; option = field.options[j]; ++j) {
                                    if (option.selected) {
                                        qs.push(fieldName + "=" + encode(option.value));
                                    }
                                }
                            } else if ((fieldType !== "checkbox" && fieldType !== "radio") || field.checked) {
                                qs.push(fieldName + "=" + encode(field.value));
                            }
                        }
                    }

                    if (qs.length) {
                        lastFormData = qs.join("&").split(formEnctype === "text/plain" ? " " : "%20").join("+");

                        if (!el.method || el.method.toUpperCase() === "GET") {
                            url += (~url.indexOf("?") ? "&" : "?") + lastFormData;

                            lastFormData = null; // don't send data for GET forms
                        }
                    }
                }

                if (dispatchAjaxifyEvent(el, "fetch", url)) {
                    e.preventDefault();
                }

                lastFormData = null; // cleanup internal reference
            }
        }
    });

    attachNonPreventedListener("ajaxify:fetch", (e) => {
        const el = e.target;
        const xhr = new XMLHttpRequest();
        const method = (el.method || "GET").toUpperCase();

        ["abort", "error", "load", "timeout"].forEach((type) => {
            xhr["on" + type] = () => {
                if (el.nodeType === 1) {
                    el.setAttribute("aria-disabled", "false");
                }

                dispatchAjaxifyEvent(el, type, xhr);
            };
        });

        xhr.open(method, e.detail, true);
        xhr.responseType = "document";
        xhr.data = lastFormData;

        xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");

        if (method !== "GET") {
            xhr.setRequestHeader("Content-Type", el.enctype);
        }

        if (dispatchAjaxifyEvent(el, "send", xhr)) {
            if (el.nodeType === 1) {
                el.setAttribute("aria-disabled", "true");
            }

            xhr.send(xhr.data);
        }
    });

    attachNonPreventedListener("ajaxify:load", (e) => {
        const xhr = e.detail;
        const res = xhr.response;
        const status = xhr.status;

        var el = document.body;
        var content = res.body;
        // replace content of the main element
        // only for successful responses
        if (status >= 200 && status < 300 || status === 304) {
            el = document.querySelector("main,[role=main]");
            content = el.cloneNode(false);
            // move all elements to replacement
            for (var node; node = res.body.firstChild; ) {
                content.appendChild(node);
            }
        }

        const url = xhr.responseURL || xhr.getResponseHeader("Location") || res.URL;
        const title = res.title;

        updateCurrentState(el, title, content);

        lastState = {}; // create a new state object

        if (url !== location.href) {
            history.pushState(states.length, title, url);
        }
    });

    window.addEventListener("popstate", (e) => {
        // numeric value indicates better-ajaxify state
        if (!e.defaultPrevented && e.state >= 0) {
            const state = states[e.state];

            if (state) {
                var el = document.body;

                if (state.body.nodeName.toLowerCase() !== "body") {
                    el = document.querySelector("main,[role=main]");
                }

                if (el) {
                    updateCurrentState(el, state.title, state.body);

                    lastState = state;
                }
            }
        }
    });

    // update initial state address url
    history.replaceState(0, document.title);

}(window.document, window.location, window.history));
<commit_msg>polyfill xhr.responseURL<commit_after>(function(document, location, history) { /* jshint maxdepth:8, boss:true */
    "use strict";

    // do not enable the plugin for old browsers BUT keep for jasmine
    if (!history.pushState || !("timeout" in XMLHttpRequest.prototype || window.jasmine)) return;

    const identity = (s) => s;
    const states = []; // in-memory storage for states
    var lastState = {};
    var lastFormData = null;

    function attachNonPreventedListener(eventType, callback) {
        document.addEventListener(eventType, function(e) {
            if (!e.defaultPrevented) {
                callback(e);
            }
        }, false);
    }

    function dispatchAjaxifyEvent(el, eventType, eventDetail) {
        const e = document.createEvent("CustomEvent");

        e.initCustomEvent("ajaxify:" + eventType, true, true, eventDetail || null);

        return el.dispatchEvent(e);
    }

    function updateCurrentState(el, title, content) {
        if (dispatchAjaxifyEvent(el, "update", content)) {
            el.parentNode.replaceChild(content, el);
        }

        lastState.body = el;
        lastState.title = document.title;

        if (states.indexOf(lastState) < 0) {
            // if state does not exist - store it in memory
            states.push(lastState);
        }

        document.title = title;
    }

    attachNonPreventedListener("click", (e) => {
        const el = e.target;

        var link;

        if (el.nodeName.toLowerCase() === "a") {
            // detected click on a link
            link = el;
        } else {
            const focusedElement = document.activeElement;

            if (focusedElement.nodeName.toLowerCase() === "a") {
                if (focusedElement.contains(el)) {
                    // detected click on a link inner element
                    link = focusedElement;
                }
            }
        }

        if (link && !link.target) {
            if (link.getAttribute("aria-disabled") === "true") {
                e.preventDefault();
            } else if (link.protocol.slice(0, 4) === "http") {
                // handle only http(s) links
                const targetUrl = link.href;
                const currentUrl = location.href;

                if (targetUrl === currentUrl || targetUrl.split("#")[0] !== currentUrl.split("#")[0]) {
                    if (dispatchAjaxifyEvent(link, "fetch", link.href)) {
                        // override default bahavior for links
                        e.preventDefault();
                    }
                } else {
                    location.hash = link.hash;
                    // override default bahavior for anchors
                    e.preventDefault();
                }
            }
        }
    });

    attachNonPreventedListener("submit", (e) => {
        const el = e.target;

        if (!el.target) {
            if (el.getAttribute("aria-disabled") === "true") {
                e.preventDefault();
            } else {
                const formEnctype = el.enctype;

                var url = el.action;

                if (formEnctype === "multipart/form-data") {
                    lastFormData = new FormData(el);
                } else {
                    const encode = formEnctype === "text/plain" ? identity : encodeURIComponent;
                    const qs = [];

                    for (var i = 0, field; field = el.elements[i]; ++i) {
                        if (field.name && !field.disabled) {
                            const fieldType = field.type;
                            const fieldName = encode(field.name);

                            if (fieldType === "select-multiple") {
                                for (var j = 0, option; option = field.options[j]; ++j) {
                                    if (option.selected) {
                                        qs.push(fieldName + "=" + encode(option.value));
                                    }
                                }
                            } else if ((fieldType !== "checkbox" && fieldType !== "radio") || field.checked) {
                                qs.push(fieldName + "=" + encode(field.value));
                            }
                        }
                    }

                    if (qs.length) {
                        lastFormData = qs.join("&").split(formEnctype === "text/plain" ? " " : "%20").join("+");

                        if (!el.method || el.method.toUpperCase() === "GET") {
                            url += (~url.indexOf("?") ? "&" : "?") + lastFormData;

                            lastFormData = null; // don't send data for GET forms
                        }
                    }
                }

                if (dispatchAjaxifyEvent(el, "fetch", url)) {
                    e.preventDefault();
                }

                lastFormData = null; // cleanup internal reference
            }
        }
    });

    attachNonPreventedListener("ajaxify:fetch", (e) => {
        const el = e.target;
        const xhr = new XMLHttpRequest();
        const method = (el.method || "GET").toUpperCase();

        ["abort", "error", "load", "timeout"].forEach((type) => {
            xhr["on" + type] = () => {
                if (el.nodeType === 1) {
                    el.setAttribute("aria-disabled", "false");
                }

                dispatchAjaxifyEvent(el, type, xhr);
            };
        });

        xhr.open(method, e.detail, true);
        xhr.responseType = "document";
        xhr.data = lastFormData;

        xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");

        if (method !== "GET") {
            xhr.setRequestHeader("Content-Type", el.enctype);
        }

        if (dispatchAjaxifyEvent(el, "send", xhr)) {
            if (el.nodeType === 1) {
                el.setAttribute("aria-disabled", "true");
            }

            xhr.send(xhr.data);
        }
    });

    attachNonPreventedListener("ajaxify:load", (e) => {
        const xhr = e.detail;
        const res = xhr.response;
        const status = xhr.status;

        var el = document.body;
        var content = res.body;
        // replace content of the main element
        // only for successful responses
        if (status >= 200 && status < 300 || status === 304) {
            el = document.querySelector("main,[role=main]");
            content = el.cloneNode(false);
            // move all elements to replacement
            for (var node; node = res.body.firstChild; ) {
                content.appendChild(node);
            }
        }

        const title = res.title;
        var url = xhr.responseURL;

        if (!url) {
            url = xhr.getResponseHeader("Location");

            if (url) {
                url = res.URL.split("/").slice(0, 3).join("/") + url;
            } else {
                url = res.URL;
            }
            // polyfill xhr.responseURL
            Object.defineProperty(xhr, "responseURL", {get: () => url});
        }

        updateCurrentState(el, title, content);

        lastState = {}; // create a new state object

        if (url !== location.href) {
            history.pushState(states.length, title, url);
        }
    });

    window.addEventListener("popstate", (e) => {
        // numeric value indicates better-ajaxify state
        if (!e.defaultPrevented && e.state >= 0) {
            const state = states[e.state];

            if (state) {
                var el = document.body;

                if (state.body.nodeName.toLowerCase() !== "body") {
                    el = document.querySelector("main,[role=main]");
                }

                if (el) {
                    updateCurrentState(el, state.title, state.body);

                    lastState = state;
                }
            }
        }
    });

    // update initial state address url
    history.replaceState(0, document.title);

}(window.document, window.location, window.history));
<|endoftext|>