<commit_before>import component, {state} from 'kompo';
import {empty, isFunction, addClasses} from 'kompo-util';

import tableRow from './tableRow';
import tableHead from './tableHead';

export default component.construct('table', function({
    appendRow,
    rowFilter,
    columnFilter,
    on,
    classes,
    oddRowClass,
    evenRowClass,
    selectedClass,
    appendHead,
    headRowClass
}) {
    const head = document.createElement('thead'),
        body = document.createElement('tbody'),
        props = component.getProps(this),
        frag = document.createDocumentFragment();

    if(isFunction(on)) on(this);

    addClasses(this, classes);

    props.rows = [];

    component.react(this, state => {
        const data = state.data,
            offset = state.offset,
            limit = state.limit,
            minimize = state.minimize,
            rowFilter = props.rowFilter,
            columnFilter = props.columnFilter;

        if(!Array.isArray(data) || data.length < 1) return;

        const hasRowFilter = isFunction(rowFilter),
            hasColumnFilter = isFunction(columnFilter);

        appendHead(
            this,
            head,
            props,
            {
                defaultClass: headRowClass,
                minimizeWhitelist: props.minimizeWhitelist,
                hasColumnFilter,
                columnFilter
            }
        );

        const dataLength = data.length;
        let os = offset && offset <= dataLength? offset: 0;
        const l = typeof limit !== 'undefined' && os+limit < dataLength? limit: dataLength;

        for(os; os < l; ++os) {
            if (hasRowFilter && !rowFilter(data[os])) continue;
            appendRow(
                this,
                frag,
                props.rows,
                os,
                {
                    defaultClass: os % 2 == 0? evenRowClass: oddRowClass,
                    selectedClass: selectedClass,
                    hasColumnFilter,
                    columnFilter
                }
            );
        }

        empty(body);
        body.appendChild(frag);
    });

    this.appendChild(head);
    this.appendChild(body);
}, {
    classes: [],
    oddRowClass: '',
    evenRowClass: '',
    headRowClass: '',
    minimizeWhitelist: undefined,
    appendRow(
        table,
        frag,
        rows,
        key,
        props
    ) {
        if(rows[key]) {
            frag.appendChild(rows[key]);
            return;
        }

        const tr = tableRow(props);
        rows[key] = tr;
        component.mount(table, frag, tr, s => {
            return table.kompo.selector?
                table.kompo.selector(s).data[key]:
                s.data[key];
        });
    },
    appendHead(
        table,
        head,
        tableProps,
        props
    ) {
        if(tableProps.headRow) return;

        const tr = tableHead(props);
        tableProps.headRow = tr;
        component.mount(table, head, tr, s => {
            return table.kompo.selector?
                table.kompo.selector(s).data[0]:
                s.data[0];
        });
    }
});

export const tableActions = {
    addLimit(Element, add) {
        state.dispatch(Element, s => {
            s.limit = s.limit + add > s.data.length?
                s.data.length:
                s.limit + add;
        });
    },
    subLimit(Element, sub) {
        state.dispatch(Element, s => {
            s.limit = s.limit - sub <= 0?
                0:
                s.limit - sub;
        });
    },
    setLimit(Element, limit) {
        state.dispatch(Element, s => {
            if(limit <= 0) {
                s.limit = 0;
                return
            }

            if(limit > s.data.length) {
                s.limit = s.data.length;
                return
            }

            s.limit = limit;
        });
    },
    addOffset(Element, add) {
        state.dispatch(Element, s => {
            s.offset = s.offset + add >= s.data.length?
                s.data.length:
                s.offset + add;
        });
    },
    subOffset(Element, sub) {
        state.dispatch(Element, s => {
            s.offset = s.offset - sub <= 0?
                0:
                s.offset - sub;
        });
    },
    setOffset(Element, offset) {
        state.dispatch(Element, s => {
            if(offset <= 0) {
                s.offset = 0;
                return
            }

            if(offset > s.data.length) {
                s.offset = s.data.length;
                return
            }

            s.offset = offset;
        });
    },
    minimize(Element) {
        state.dispatch(Element, s => {
            s.minimize = true;
        });
    },
    maximize(Element) {
        state.dispatch(Element, s => {
            s.minimize = false;
        });
    }
};<commit_msg>Added getUniqueIdentifier function to table<commit_after>import component, {state} from 'kompo';
import {empty, isFunction, addClasses} from 'kompo-util';

import tableRow from './tableRow';
import tableHead from './tableHead';

export default component.construct('table', function({
    getUniqueIdentifier,
    appendRow,
    on,
    classes,
    oddRowClass,
    evenRowClass,
    selectedClass,
    appendHead,
    headRowClass
}) {
    const head = document.createElement('thead'),
        body = document.createElement('tbody'),
        props = component.getProps(this),
        frag = document.createDocumentFragment();

    if(isFunction(on)) on(this);

    addClasses(this, classes);

    props.rows = [];

    component.react(this, state => {
        const data = state.data,
            offset = state.offset,
            limit = state.limit,
            minimize = state.minimize,
            rowFilter = props.rowFilter,
            columnFilter = props.columnFilter;

        if(!Array.isArray(data) || data.length < 1) return;

        const hasRowFilter = isFunction(rowFilter),
            hasColumnFilter = isFunction(columnFilter);

        appendHead(
            this,
            head,
            props,
            {
                defaultClass: headRowClass,
                minimizeWhitelist: props.minimizeWhitelist,
                hasColumnFilter,
                columnFilter
            }
        );

        const dataLength = data.length;
        let os = offset && offset <= dataLength? offset: 0;
        const l = typeof limit !== 'undefined' && os+limit < dataLength? limit: dataLength,
            hasKeyFunction = isFunction(getUniqueIdentifier);

        for(os; os < l; ++os) {
            if (hasRowFilter && !rowFilter(data[os])) continue;
            appendRow(
                this,
                frag,
                props.rows,
                hasKeyFunction? getUniqueIdentifier(): os,
                {
                    defaultClass: os % 2 == 0? evenRowClass: oddRowClass,
                    selectedClass: selectedClass,
                    hasColumnFilter,
                    columnFilter
                }
            );
        }

        empty(body);
        body.appendChild(frag);
    });

    this.appendChild(head);
    this.appendChild(body);
}, {
    classes: [],
    oddRowClass: '',
    evenRowClass: '',
    headRowClass: '',
    minimizeWhitelist: undefined,
    getUniqueIdentifier: undefined,
    appendRow(
        table,
        frag,
        rows,
        key,
        props
    ) {
        if(rows[key]) {
            frag.appendChild(rows[key]);
            return;
        }

        const tr = tableRow(props);
        rows[key] = tr;
        component.mount(table, frag, tr, s => {
            return table.kompo.selector?
                table.kompo.selector(s).data[key]:
                s.data[key];
        });
    },
    appendHead(
        table,
        head,
        tableProps,
        props
    ) {
        if(tableProps.headRow) return;

        const tr = tableHead(props);
        tableProps.headRow = tr;
        component.mount(table, head, tr, s => {
            return table.kompo.selector?
                table.kompo.selector(s).data[0]:
                s.data[0];
        });
    }
});

export const tableActions = {
    addLimit(Element, add) {
        state.dispatch(Element, s => {
            s.limit = s.limit + add > s.data.length?
                s.data.length:
                s.limit + add;
        });
    },
    subLimit(Element, sub) {
        state.dispatch(Element, s => {
            s.limit = s.limit - sub <= 0?
                0:
                s.limit - sub;
        });
    },
    setLimit(Element, limit) {
        state.dispatch(Element, s => {
            if(limit <= 0) {
                s.limit = 0;
                return
            }

            if(limit > s.data.length) {
                s.limit = s.data.length;
                return
            }

            s.limit = limit;
        });
    },
    addOffset(Element, add) {
        state.dispatch(Element, s => {
            s.offset = s.offset + add >= s.data.length?
                s.data.length:
                s.offset + add;
        });
    },
    subOffset(Element, sub) {
        state.dispatch(Element, s => {
            s.offset = s.offset - sub <= 0?
                0:
                s.offset - sub;
        });
    },
    setOffset(Element, offset) {
        state.dispatch(Element, s => {
            if(offset <= 0) {
                s.offset = 0;
                return
            }

            if(offset > s.data.length) {
                s.offset = s.data.length;
                return
            }

            s.offset = offset;
        });
    },
    minimize(Element) {
        state.dispatch(Element, s => {
            s.minimize = true;
        });
    },
    maximize(Element) {
        state.dispatch(Element, s => {
            s.minimize = false;
        });
    }
};<|endoftext|>


<commit_before>import { addClass, removeClass } from "./lazyload.class";
import { callbackIfSet } from "./lazyload.callback";

const genericLoadEventName = "load";
const mediaLoadEventName = "loadeddata";
const errorEventName = "error";

const addEventListener = (element, eventName, handler) => {
	element.addEventListener(eventName, handler);
};

const removeEventListener = (element, eventName, handler) => {
	element.removeEventListener(eventName, handler);
};

const addAllEventListeners = (element, loadHandler, errorHandler) => {
	addEventListener(element, genericLoadEventName, loadHandler);
	addEventListener(element, mediaLoadEventName, loadHandler);
	addEventListener(element, errorEventName, errorHandler);
};

const removeAllEventListeners = (element, loadHandler, errorHandler) => {
	removeEventListener(element, genericLoadEventName, loadHandler);
	removeEventListener(element, mediaLoadEventName, loadHandler);
	removeEventListener(element, errorEventName, errorHandler);
};

const eventHandler = function(event, success, settings) {
	const className = success ? settings.class_loaded : settings.class_error;
	const callback = success ? settings.callback_load : settings.callback_error;
	const element = event.target;

	removeClass(element, settings.class_loading);
	addClass(element, className);
	callbackIfSet(callback, element);
};

export const addOneShotEventListeners = (element, settings) => {
	const loadHandler = event => {
		eventHandler(event, true, settings);
		removeAllEventListeners(element, loadHandler, errorHandler);
	};
	const errorHandler = event => {
		eventHandler(event, false, settings);
		removeAllEventListeners(element, loadHandler, errorHandler);
	};
	addAllEventListeners(element, loadHandler, errorHandler);
};

export const addOneShotPromiseEventListners = (element, resolve, reject) => {
	const loadHandler = () => {
		resolve(element);
		removeAllEventListeners(element, loadHandler, errorHandler);
	};
	const errorHandler = () => {
		reject(element);
		removeAllEventListeners(element, loadHandler, errorHandler);
	};
	addAllEventListeners(element, loadHandler, errorHandler);
};
<commit_msg>Renamed event add/remove multiple event listeners function<commit_after>import { addClass, removeClass } from "./lazyload.class";
import { callbackIfSet } from "./lazyload.callback";

const genericLoadEventName = "load";
const mediaLoadEventName = "loadeddata";
const errorEventName = "error";

const addEventListener = (element, eventName, handler) => {
	element.addEventListener(eventName, handler);
};

const removeEventListener = (element, eventName, handler) => {
	element.removeEventListener(eventName, handler);
};

const addEventListeners = (element, loadHandler, errorHandler) => {
	addEventListener(element, genericLoadEventName, loadHandler);
	addEventListener(element, mediaLoadEventName, loadHandler);
	addEventListener(element, errorEventName, errorHandler);
};

const removeEventListeners = (element, loadHandler, errorHandler) => {
	removeEventListener(element, genericLoadEventName, loadHandler);
	removeEventListener(element, mediaLoadEventName, loadHandler);
	removeEventListener(element, errorEventName, errorHandler);
};

const eventHandler = function(event, success, settings) {
	const className = success ? settings.class_loaded : settings.class_error;
	const callback = success ? settings.callback_load : settings.callback_error;
	const element = event.target;

	removeClass(element, settings.class_loading);
	addClass(element, className);
	callbackIfSet(callback, element);
};

export const addOneShotEventListeners = (element, settings) => {
	const loadHandler = event => {
		eventHandler(event, true, settings);
		removeEventListeners(element, loadHandler, errorHandler);
	};
	const errorHandler = event => {
		eventHandler(event, false, settings);
		removeEventListeners(element, loadHandler, errorHandler);
	};
	addEventListeners(element, loadHandler, errorHandler);
};

export const addOneShotPromiseEventListners = (element, resolve, reject) => {
	const loadHandler = () => {
		resolve(element);
		removeEventListeners(element, loadHandler, errorHandler);
	};
	const errorHandler = () => {
		reject(element);
		removeEventListeners(element, loadHandler, errorHandler);
	};
	addEventListeners(element, loadHandler, errorHandler);
};
<|endoftext|>


<commit_before>'use strict';

var path = require('path');
var fs = require('fs');

function getFileNameNoExt(context) {
    let filename = path.basename(context.filename);
    let ext = path.extname(filename);

    if (ext === '.js') {
        return false;
    }

    if (ext) {
        filename = filename.slice(0, 0 - ext.length);
    }

    return filename;
}

const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');

function isModuleExports(node) {
    return node.type === 'AssignmentExpression' &&
           node.operator === '=' &&
           node.left.type === 'MemberExpression' &&
           node.left.object.type === 'Identifier' &&
           node.left.object.name === 'module' &&
           node.left.property.type === 'Identifier' &&
           node.left.property.name === 'exports';
}

function isModuleExportsStatement(node) {
    return node.type === 'ExpressionStatement' &&
           isModuleExports(node.expression);
}

function handleScriptElement(scriptEl, transformHelper) {
    let builder = transformHelper.builder;

    let hasExport = false;
    let needsComponentVar;

    var scriptCode = scriptEl.bodyText;

    let tree = esprima.parse(scriptCode, { sourceType:'module' });
    let updatedTree = estraverse.replace(tree, {
        enter: function(node) {
            if (isModuleExports(node)) {
                hasExport = true;
                needsComponentVar = true;
                node.left = {
                    type: 'Identifier',
                    name: 'marko_component'
                };
                this.break();
            } else if (isModuleExportsStatement(node)) {
                hasExport = true;
                return createComponentDeclaration(node.expression.right);
            } else if (node.type === 'ExportDefaultDeclaration') {
                hasExport = true;
                return createComponentDeclaration(node.declaration);
            }
        }
    });

    function createComponentDeclaration(value) {
        return {
            type: 'VariableDeclaration',
            declarations: [
                {
                    type: 'VariableDeclarator',
                    id: {
                        type: 'Identifier',
                        name: 'marko_component'
                    },
                    init: value
                }
            ],
            kind: 'var'
        };
    }

    if (hasExport) {
        transformHelper.context.deprecate('Using <script> with an export to create a single file component will be removed in the next release candidate.  Use class instead. \nSee https://github.com/marko-js/marko/issues/547');

        let componentVar;

        if (needsComponentVar) {
            componentVar = transformHelper.context.addStaticVar('marko_component');
        } else {
            componentVar = builder.identifier('marko_component');
        }

        transformHelper.setHasBoundWidgetForTemplate();
        transformHelper.setInlineComponent(componentVar);
        transformHelper.context.addStaticCode(escodegen.generate(updatedTree));
        scriptEl.detach();
    }
}

function handleStyleElement(styleEl, transformHelper) {
    if (styleEl.bodyText) {
        return;
    }

    var attrs = styleEl.attributes;

    var styleCode;
    var lang = 'css';

    var hasStyleBlock = false;

    for (var i=attrs.length-1; i>=0; i--) {
        var attr = attrs[i];
        var name = attr.name;
        if (name.startsWith('{')) {
            hasStyleBlock = true;

            styleCode = name.slice(1, -1);
        } else if (name === 'class') {
            if (attr.value.type !== 'Literal' || typeof attr.value.value !== 'string') {
                return;
            }

            lang = attr.value.value;
        } else {
            if (hasStyleBlock) {
                transformHelper.context.addError(styleEl, 'Unsupported attribute on the component style tag: ' + attr.name);
                return;
            }
        }
    }

    if (styleCode == null) {
        return;
    }

    var context = transformHelper.context;
    context.addDependency({
        type: lang,
        code: styleCode.trim(),
        virtualPath: './'+path.basename(context.filename)+'.'+lang,
        path: './'+path.basename(context.filename)
    });

    styleEl.detach();
}

function methodToProperty(method) {
    return {
        type: 'Property',
        key: method.key,
        computed: false,
        value: method.value,
        kind: 'init',
        method: true,
        shorthand: false
    };
}

function classToObject(cls, transformHelper) {
    return {
        type: 'ObjectExpression',
        properties: cls.body.body.map((method) => {
            if(method.type != 'MethodDefinition') {
                throw Error('Only methods are allowed on single file component class definitions.');
            }
            return methodToProperty(method);
        })
    };
}

function handleClassDeclaration(classEl, transformHelper) {
    if(!/^class\s*\{/.test(classEl.tagString)) return;

    let tree = esprima.parse('('+classEl.tagString+')');
    let expression = tree.body[0].expression;
    let object = classToObject(expression);
    let componentVar = transformHelper.context.addStaticVar('marko_component', escodegen.generate(object));

    transformHelper.setHasBoundWidgetForTemplate();
    transformHelper.setInlineComponent(componentVar);
    classEl.detach();
}

module.exports = function handleRootNodes() {
    var context = this.context;
    var builder = this.builder;
    var filename = getFileNameNoExt(context);
    var isEntry = 'index' === filename;

    if(!filename) {
        return; // inline component
    }

    var filematch = '('+filename.replace(/\./g, '\\.') + '\\.' + (isEntry ? '|' : '') + ')';
    var stylematch = new RegExp('^'+filematch+'style\\.\\w+$');
    var componentmatch = new RegExp('^'+filematch+'component\\.\\w+$');
    var widgetmatch = new RegExp('^'+filematch+'widget\\.\\w+$');

    var templateRoot = this.el;

    var dirname = this.dirname;
    var hasBindTarget = false;

    fs.readdirSync(dirname).forEach(file => {
        if(stylematch.test(file)) {
            context.addDependency('./' + file);
        } else if(componentmatch.test(file) || widgetmatch.test(file)) {
            hasBindTarget = true;
        }
    });

    var rootNodes = [];
    var hasExplicitBind = false;
    var hasIdCount = 0;
    var nodeWithAssignedId;
    var assignedId;
    var transformHelper = this;

    let walker = context.createWalker({
        enter(node) {
            var tagName = node.tagName && node.tagName.toLowerCase();

            if (node.type === 'TemplateRoot' || !node.type) {
                // Don't worry about the TemplateRoot or an Container node
            } else if (node.type === 'HtmlElement') {
                if (node.hasAttribute('w-bind')) {
                    transformHelper.setHasBoundWidgetForTemplate();
                    hasExplicitBind = true;
                } else {
                    if (node.hasAttribute('id')) {
                        hasIdCount++;
                        nodeWithAssignedId = node;
                        assignedId = node.getAttributeValue('id');
                    }

                    if (isEntry && tagName === 'script') {
                        handleScriptElement(node, transformHelper);
                    } else if (tagName === 'style') {
                        handleStyleElement(node, transformHelper);
                    } else {
                        rootNodes.push(node);
                    }
                }
                walker.skip();

                return;
            } else if (node.type === 'CustomTag') {
                rootNodes.push(node);

                walker.skip();
                return;
            } else {
                if (tagName === 'class') {
                    handleClassDeclaration(node, transformHelper);
                }

                walker.skip();
                return;
            }
        }
    });

    walker.walk(templateRoot);

    if (this.hasInlineComponent()) {
        hasBindTarget = true;
    }

    if (hasExplicitBind) {
        //There is an explicit bind so nothing to do
        return;
    }

    if (!hasBindTarget) {
        return;
    }

    if (rootNodes.length === 0) {
        return;
    }

    if (rootNodes.length > 1 && hasIdCount > 0) {
        // We can only bind a widget to multiple top-level elements if we can assign
        // all of the IDs
        return;
    }

    transformHelper.setHasBoundWidgetForTemplate();

    var nextRef = 0;

    rootNodes.forEach((curNode, i) => {
        curNode.setAttributeValue('_widgetbind');

        if (!curNode.hasAttribute('ref')) {
            if (curNode.type === 'CustomTag' || rootNodes.length > 1) {
                curNode.setAttributeValue('ref', builder.literal('_r' + (nextRef++)));
            }
        }
    });
};<commit_msg>Change to ensure that methods are not written out as shorthand methods<commit_after>'use strict';

var path = require('path');
var fs = require('fs');

function getFileNameNoExt(context) {
    let filename = path.basename(context.filename);
    let ext = path.extname(filename);

    if (ext === '.js') {
        return false;
    }

    if (ext) {
        filename = filename.slice(0, 0 - ext.length);
    }

    return filename;
}

const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');

function isModuleExports(node) {
    return node.type === 'AssignmentExpression' &&
           node.operator === '=' &&
           node.left.type === 'MemberExpression' &&
           node.left.object.type === 'Identifier' &&
           node.left.object.name === 'module' &&
           node.left.property.type === 'Identifier' &&
           node.left.property.name === 'exports';
}

function isModuleExportsStatement(node) {
    return node.type === 'ExpressionStatement' &&
           isModuleExports(node.expression);
}

function handleScriptElement(scriptEl, transformHelper) {
    let builder = transformHelper.builder;

    let hasExport = false;
    let needsComponentVar;

    var scriptCode = scriptEl.bodyText;

    let tree = esprima.parse(scriptCode, { sourceType:'module' });
    let updatedTree = estraverse.replace(tree, {
        enter: function(node) {
            if (isModuleExports(node)) {
                hasExport = true;
                needsComponentVar = true;
                node.left = {
                    type: 'Identifier',
                    name: 'marko_component'
                };
                this.break();
            } else if (isModuleExportsStatement(node)) {
                hasExport = true;
                return createComponentDeclaration(node.expression.right);
            } else if (node.type === 'ExportDefaultDeclaration') {
                hasExport = true;
                return createComponentDeclaration(node.declaration);
            }
        }
    });

    function createComponentDeclaration(value) {
        return {
            type: 'VariableDeclaration',
            declarations: [
                {
                    type: 'VariableDeclarator',
                    id: {
                        type: 'Identifier',
                        name: 'marko_component'
                    },
                    init: value
                }
            ],
            kind: 'var'
        };
    }

    if (hasExport) {
        transformHelper.context.deprecate('Using <script> with an export to create a single file component will be removed in the next release candidate.  Use class instead. \nSee https://github.com/marko-js/marko/issues/547');

        let componentVar;

        if (needsComponentVar) {
            componentVar = transformHelper.context.addStaticVar('marko_component');
        } else {
            componentVar = builder.identifier('marko_component');
        }

        transformHelper.setHasBoundWidgetForTemplate();
        transformHelper.setInlineComponent(componentVar);
        transformHelper.context.addStaticCode(escodegen.generate(updatedTree));
        scriptEl.detach();
    }
}

function handleStyleElement(styleEl, transformHelper) {
    if (styleEl.bodyText) {
        return;
    }

    var attrs = styleEl.attributes;

    var styleCode;
    var lang = 'css';

    var hasStyleBlock = false;

    for (var i=attrs.length-1; i>=0; i--) {
        var attr = attrs[i];
        var name = attr.name;
        if (name.startsWith('{')) {
            hasStyleBlock = true;

            styleCode = name.slice(1, -1);
        } else if (name === 'class') {
            if (attr.value.type !== 'Literal' || typeof attr.value.value !== 'string') {
                return;
            }

            lang = attr.value.value;
        } else {
            if (hasStyleBlock) {
                transformHelper.context.addError(styleEl, 'Unsupported attribute on the component style tag: ' + attr.name);
                return;
            }
        }
    }

    if (styleCode == null) {
        return;
    }

    var context = transformHelper.context;
    context.addDependency({
        type: lang,
        code: styleCode.trim(),
        virtualPath: './'+path.basename(context.filename)+'.'+lang,
        path: './'+path.basename(context.filename)
    });

    styleEl.detach();
}

function methodToProperty(method) {
    return {
        type: 'Property',
        key: method.key,
        computed: false,
        value: method.value,
        kind: 'init',
        method: false,
        shorthand: false
    };
}

function classToObject(cls, transformHelper) {
    return {
        type: 'ObjectExpression',
        properties: cls.body.body.map((method) => {
            if(method.type != 'MethodDefinition') {
                throw Error('Only methods are allowed on single file component class definitions.');
            }
            return methodToProperty(method);
        })
    };
}

function handleClassDeclaration(classEl, transformHelper) {
    if(!/^class\s*\{/.test(classEl.tagString)) return;

    let tree = esprima.parse('('+classEl.tagString+')');
    let expression = tree.body[0].expression;
    let object = classToObject(expression);
    let componentVar = transformHelper.context.addStaticVar('marko_component', escodegen.generate(object));

    transformHelper.setHasBoundWidgetForTemplate();
    transformHelper.setInlineComponent(componentVar);
    classEl.detach();
}

module.exports = function handleRootNodes() {
    var context = this.context;
    var builder = this.builder;
    var filename = getFileNameNoExt(context);
    var isEntry = 'index' === filename;

    if(!filename) {
        return; // inline component
    }

    var filematch = '('+filename.replace(/\./g, '\\.') + '\\.' + (isEntry ? '|' : '') + ')';
    var stylematch = new RegExp('^'+filematch+'style\\.\\w+$');
    var componentmatch = new RegExp('^'+filematch+'component\\.\\w+$');
    var widgetmatch = new RegExp('^'+filematch+'widget\\.\\w+$');

    var templateRoot = this.el;

    var dirname = this.dirname;
    var hasBindTarget = false;

    fs.readdirSync(dirname).forEach(file => {
        if(stylematch.test(file)) {
            context.addDependency('./' + file);
        } else if(componentmatch.test(file) || widgetmatch.test(file)) {
            hasBindTarget = true;
        }
    });

    var rootNodes = [];
    var hasExplicitBind = false;
    var hasIdCount = 0;
    var nodeWithAssignedId;
    var assignedId;
    var transformHelper = this;

    let walker = context.createWalker({
        enter(node) {
            var tagName = node.tagName && node.tagName.toLowerCase();

            if (node.type === 'TemplateRoot' || !node.type) {
                // Don't worry about the TemplateRoot or an Container node
            } else if (node.type === 'HtmlElement') {
                if (node.hasAttribute('w-bind')) {
                    transformHelper.setHasBoundWidgetForTemplate();
                    hasExplicitBind = true;
                } else {
                    if (node.hasAttribute('id')) {
                        hasIdCount++;
                        nodeWithAssignedId = node;
                        assignedId = node.getAttributeValue('id');
                    }

                    if (isEntry && tagName === 'script') {
                        handleScriptElement(node, transformHelper);
                    } else if (tagName === 'style') {
                        handleStyleElement(node, transformHelper);
                    } else {
                        rootNodes.push(node);
                    }
                }
                walker.skip();

                return;
            } else if (node.type === 'CustomTag') {
                rootNodes.push(node);

                walker.skip();
                return;
            } else {
                if (tagName === 'class') {
                    handleClassDeclaration(node, transformHelper);
                }

                walker.skip();
                return;
            }
        }
    });

    walker.walk(templateRoot);

    if (this.hasInlineComponent()) {
        hasBindTarget = true;
    }

    if (hasExplicitBind) {
        //There is an explicit bind so nothing to do
        return;
    }

    if (!hasBindTarget) {
        return;
    }

    if (rootNodes.length === 0) {
        return;
    }

    if (rootNodes.length > 1 && hasIdCount > 0) {
        // We can only bind a widget to multiple top-level elements if we can assign
        // all of the IDs
        return;
    }

    transformHelper.setHasBoundWidgetForTemplate();

    var nextRef = 0;

    rootNodes.forEach((curNode, i) => {
        curNode.setAttributeValue('_widgetbind');

        if (!curNode.hasAttribute('ref')) {
            if (curNode.type === 'CustomTag' || rootNodes.length > 1) {
                curNode.setAttributeValue('ref', builder.literal('_r' + (nextRef++)));
            }
        }
    });
};<|endoftext|>


<commit_before>/* global document beforeEach afterEach describe it window */
const proxyquireStrict = require('proxyquire').noCallThru();
const mockDOMSliderStub = {};

jest.mock('dom-slider', () => mockDOMSliderStub);

describe('Toggle init', () => {
  const { toggle } = require('../components/toggle');

  afterEach(() => {
    document.querySelectorAll.restore();
  });

  it('should call getElements once with the correct selector', () => {
    sinon.spy(document, 'querySelectorAll');
    toggle();
    expect(document.querySelectorAll
      .getCall(0).args[0]).toBe('[data-sprk-toggle="container"]');
  });
});

describe('Toggle tests', () => {
  let container;
  let trigger;
  let triggerAccordion;
  let content;
  let contentAccordion;
  let icon;
  let containerAccordion;
  let iconAccordion;
  let iconAccordionUseElement;
  const mockDOMSliderStub = {};

  const {
    toggleIconType,
    toggleAriaExpanded,
    handleToggleClick,
    bindToggleUIEvents,
  } = proxyquireStrict('../components/toggle', { 'dom-slider': mockDOMSliderStub });

  beforeEach(() => {
    container = document.createElement('div');
    container.setAttribute('data-sprk-toggle', 'container');

    containerAccordion = document.createElement('li');
    containerAccordion.setAttribute('data-sprk-toggle', 'container');
    containerAccordion.classList.add('sprk-c-Accordion__item');

    trigger = document.createElement('button');
    trigger.setAttribute('data-sprk-toggle', 'trigger');
    trigger.setAttribute('aria-expanded', 'false');
    trigger.setAttribute('aria-controls', 'toggle-1');
    trigger.textContent = 'My Toggle Link Text';
    trigger.classList.add(
      'sprk-b-TypeBodyThree',
      'sprk-u-BareButton',
      'sprk-u-TextCrop--none',
      'sprk-b-Link',
      'sprk-b-Link--has-icon',
      'sprk-b-Link--simple',
    );

    triggerAccordion = document.createElement('a');
    triggerAccordion.setAttribute('data-sprk-toggle', 'trigger');
    triggerAccordion.setAttribute('data-sprk-toggle-type', 'accordion');
    triggerAccordion.setAttribute('aria-expanded', 'false');
    triggerAccordion.textContent = 'My Toggle Accordion Link Text';
    triggerAccordion.classList.add(
      'sprk-b-TypeBodyThree',
      'sprk-b-Link',
      'sprk-b-Link--simple',
    );

    content = document.createElement('div');
    content.setAttribute('data-sprk-toggle', 'content');
    content.setAttribute('id', 'toggle-1');
    content.textContent = 'This is the toggle content..';
    content.classList.add('sprk-b-TypeBodyFour', 'sprk-u-pts');
    content.slideToggle = () => new Promise((resolve) => {
      resolve();
    });

    contentAccordion = document.createElement('p');
    contentAccordion.setAttribute('data-sprk-toggle', 'content');
    contentAccordion.textContent = 'This is the toggle accordion content.';
    contentAccordion.classList.add(
      'sprk-b-TypeBodyTwo',
      'sprk-c-Accordion__content',
    );
    contentAccordion.style.display = 'none';
    contentAccordion.slideToggle = () => new Promise((resolve) => {
      resolve();
    });

    icon = document.createElement('svg');
    icon.setAttribute('data-sprk-toggle', 'icon');

    iconAccordion = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    iconAccordion.classList.add('sprk-c-Icon');
    iconAccordion.setAttribute('data-sprk-toggle', 'icon');

    iconAccordionUseElement = document.createElement('use');
    iconAccordionUseElement.setAttribute('xlink:href', '#chevron-down');
    iconAccordionUseElement.setAttribute('data-sprk-toggle', 'svgUse');
    iconAccordion.appendChild(iconAccordionUseElement);

    trigger.append(icon);
    container.append(trigger);
    container.append(content);

    triggerAccordion.append(iconAccordion);
    containerAccordion.append(triggerAccordion);
    containerAccordion.append(contentAccordion);
  });

  it('should toggle aria-expanded attribute for toggles', () => {
    toggleAriaExpanded(trigger);
    expect(trigger.hasAttribute('aria-expanded')).toBe(true);
  });

  it('should add aria-expanded attribute set to false if not found on'
    + ' trigger', () => {
    triggerAccordion.removeAttribute('aria-expanded');
    toggleAriaExpanded(triggerAccordion);
    expect(triggerAccordion.getAttribute('aria-expanded')).toBe('false');
  });

  it('should toggle aria-expanded attribute to false', () => {
    toggleAriaExpanded(trigger); // Is true, makes it false
    toggleAriaExpanded(trigger); // Is false, makes it true
    expect(trigger.hasAttribute('aria-expanded')).toBe(true);
  });

  it('should toggle accordion item open', () => {
    handleToggleClick(
      contentAccordion,
      iconAccordion,
      iconAccordionUseElement,
      triggerAccordion,
    );
    expect(containerAccordion.classList
      .contains('sprk-c-Accordion__item--open')).toBe(true);
    expect(iconAccordionUseElement.getAttribute('xlink:href')).toBe('#chevron-up-circle-two-color');
  });

  it('should toggle accordion open', () => {
    triggerAccordion
      .setAttribute('data-sprk-toggle-type', 'masthead-accordion');
    handleToggleClick(
      contentAccordion,
      iconAccordion,
      iconAccordionUseElement,
      triggerAccordion,
    );
    expect(containerAccordion.classList
      .contains('sprk-c-MastheadAccordion__item--open')).toBe(true);
    expect(iconAccordionUseElement
      .getAttribute('xlink:href')).toBe('#chevron-up-circle-two-color');
  });

  it('should toggle accordion icon open', () => {
    toggleIconType(
      iconAccordion,
      iconAccordionUseElement,
      'chevron-up-circle-two-color',
      'chevron-down-circle-two-color',
    );
    expect(iconAccordionUseElement.getAttribute('xlink:href')).toBe('#chevron-down-circle-two-color');
  });

  it('should add listener to toggle trigger', () => {
    sinon.spy(trigger, 'addEventListener');
    bindToggleUIEvents(container);
    expect(trigger.addEventListener.getCall(0).args[0]).toBe('click');
  });

  it('should add listener to Accordion toggle trigger', () => {
    sinon.spy(triggerAccordion, 'addEventListener');
    bindToggleUIEvents(containerAccordion);
    expect(triggerAccordion.addEventListener.getCall(0).args[0]).toBe('click');
  });

  it('should show details when clicked', () => {
    bindToggleUIEvents(containerAccordion);
    const event = new window.Event('click');
    triggerAccordion.dispatchEvent(event);
    expect(containerAccordion.classList
      .contains('sprk-c-Accordion__item--open')).toBe(true);
  });

  it('should disable clicks until after slide toggle animation runs', () => {
    bindToggleUIEvents(container);
    trigger.dispatchEvent(new window.Event('click'));
    expect(trigger.style.pointerEvents).toBe('none');
  });

  it('should enable clicks after slide toggle animation runs', (cb) => {
    bindToggleUIEvents(container);
    const event = new window.Event('click');
    trigger.dispatchEvent(event);
    trigger.dispatchEvent(event);
    setTimeout(() => {
      expect(trigger.style.pointerEvents).toBe('auto');
      cb();
    }, 100);
  });

  it('should still toggle content if icon is not present', () => {
    handleToggleClick(contentAccordion, null, null, triggerAccordion);
    expect(containerAccordion.classList
      .contains('sprk-c-Accordion__item--open')).toBe(true);
  });

  it('should not add Accordion__item class if toggle is not an'
    + 'accordion', () => {
    handleToggleClick(content, null, null, trigger);
    expect(container.classList.contains('sprk-c-Accordion__item--open')).toBe(false);
  });
});
<commit_msg>tests<commit_after>/* global document beforeEach afterEach describe it window */
const proxyquireStrict = require('proxyquire').noCallThru();

// when something tries to import dom-slider,
// return an empty object
const mockDOMSliderStub = {};
jest.mock('dom-slider', () => mockDOMSliderStub);

// when a test tries to access window.domSlider.slideToggle,
// return a Promise that resolves immediately so we can test
// the .then()
const mockDOMSliderWindow = {
  slideToggle : () => {
    return new Promise((resolve) => { resolve() })
  }
};

window.domSlider = mockDOMSliderWindow;

describe('Toggle init', () => {
  const { toggle } = require('../components/toggle');

  afterEach(() => {
    document.querySelectorAll.restore();
  });

  it('should call getElements once with the correct selector', () => {
    sinon.spy(document, 'querySelectorAll');
    toggle();
    expect(document.querySelectorAll
      .getCall(0).args[0]).toBe('[data-sprk-toggle="container"]');
  });
});

describe('Toggle tests', () => {
  let container;
  let trigger;
  let triggerAccordion;
  let content;
  let contentAccordion;
  let icon;
  let containerAccordion;
  let iconAccordion;
  let iconAccordionUseElement;
  const mockDOMSliderStub = {};

  const {
    toggleIconType,
    toggleAriaExpanded,
    handleToggleClick,
    bindToggleUIEvents,
  } = proxyquireStrict('../components/toggle', { 'dom-slider': mockDOMSliderStub });

  beforeEach(() => {
    container = document.createElement('div');
    container.setAttribute('data-sprk-toggle', 'container');

    containerAccordion = document.createElement('li');
    containerAccordion.setAttribute('data-sprk-toggle', 'container');
    containerAccordion.classList.add('sprk-c-Accordion__item');

    trigger = document.createElement('button');
    trigger.setAttribute('data-sprk-toggle', 'trigger');
    trigger.setAttribute('aria-expanded', 'false');
    trigger.setAttribute('aria-controls', 'toggle-1');
    trigger.textContent = 'My Toggle Link Text';
    trigger.classList.add(
      'sprk-b-TypeBodyThree',
      'sprk-u-BareButton',
      'sprk-u-TextCrop--none',
      'sprk-b-Link',
      'sprk-b-Link--has-icon',
      'sprk-b-Link--simple',
    );

    triggerAccordion = document.createElement('a');
    triggerAccordion.setAttribute('data-sprk-toggle', 'trigger');
    triggerAccordion.setAttribute('data-sprk-toggle-type', 'accordion');
    triggerAccordion.setAttribute('aria-expanded', 'false');
    triggerAccordion.textContent = 'My Toggle Accordion Link Text';
    triggerAccordion.classList.add(
      'sprk-b-TypeBodyThree',
      'sprk-b-Link',
      'sprk-b-Link--simple',
    );

    content = document.createElement('div');
    content.setAttribute('data-sprk-toggle', 'content');
    content.setAttribute('id', 'toggle-1');
    content.textContent = 'This is the toggle content..';
    content.classList.add('sprk-b-TypeBodyFour', 'sprk-u-pts');
    content.slideToggle = () => new Promise((resolve) => {
      resolve();
    });

    contentAccordion = document.createElement('p');
    contentAccordion.setAttribute('data-sprk-toggle', 'content');
    contentAccordion.textContent = 'This is the toggle accordion content.';
    contentAccordion.classList.add(
      'sprk-b-TypeBodyTwo',
      'sprk-c-Accordion__content',
    );
    contentAccordion.style.display = 'none';
    contentAccordion.slideToggle = () => new Promise((resolve) => {
      resolve();
    });

    icon = document.createElement('svg');
    icon.setAttribute('data-sprk-toggle', 'icon');

    iconAccordion = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    iconAccordion.classList.add('sprk-c-Icon');
    iconAccordion.setAttribute('data-sprk-toggle', 'icon');

    iconAccordionUseElement = document.createElement('use');
    iconAccordionUseElement.setAttribute('xlink:href', '#chevron-down');
    iconAccordionUseElement.setAttribute('data-sprk-toggle', 'svgUse');
    iconAccordion.appendChild(iconAccordionUseElement);

    trigger.append(icon);
    container.append(trigger);
    container.append(content);

    triggerAccordion.append(iconAccordion);
    containerAccordion.append(triggerAccordion);
    containerAccordion.append(contentAccordion);
  });

  it('should toggle aria-expanded attribute for toggles', () => {
    toggleAriaExpanded(trigger);
    expect(trigger.hasAttribute('aria-expanded')).toBe(true);
  });

  it('should add aria-expanded attribute set to false if not found on'
    + ' trigger', () => {
    triggerAccordion.removeAttribute('aria-expanded');
    toggleAriaExpanded(triggerAccordion);
    expect(triggerAccordion.getAttribute('aria-expanded')).toBe('false');
  });

  it('should toggle aria-expanded attribute to false', () => {
    toggleAriaExpanded(trigger); // Is true, makes it false
    toggleAriaExpanded(trigger); // Is false, makes it true
    expect(trigger.hasAttribute('aria-expanded')).toBe(true);
  });

  it('should toggle accordion item open', () => {
    handleToggleClick(
      contentAccordion,
      iconAccordion,
      iconAccordionUseElement,
      triggerAccordion,
    );
    expect(containerAccordion.classList
      .contains('sprk-c-Accordion__item--open')).toBe(true);
    expect(iconAccordionUseElement.getAttribute('xlink:href')).toBe('#chevron-up-circle-two-color');
  });

  it('should toggle accordion open', () => {
    triggerAccordion
      .setAttribute('data-sprk-toggle-type', 'masthead-accordion');
    handleToggleClick(
      contentAccordion,
      iconAccordion,
      iconAccordionUseElement,
      triggerAccordion,
    );
    expect(containerAccordion.classList
      .contains('sprk-c-MastheadAccordion__item--open')).toBe(true);
    expect(iconAccordionUseElement
      .getAttribute('xlink:href')).toBe('#chevron-up-circle-two-color');
  });

  it('should toggle accordion icon open', () => {
    toggleIconType(
      iconAccordion,
      iconAccordionUseElement,
      'chevron-up-circle-two-color',
      'chevron-down-circle-two-color',
    );
    expect(iconAccordionUseElement.getAttribute('xlink:href')).toBe('#chevron-down-circle-two-color');
  });

  it('should add listener to toggle trigger', () => {
    sinon.spy(trigger, 'addEventListener');
    bindToggleUIEvents(container);
    expect(trigger.addEventListener.getCall(0).args[0]).toBe('click');
  });

  it('should add listener to Accordion toggle trigger', () => {
    sinon.spy(triggerAccordion, 'addEventListener');
    bindToggleUIEvents(containerAccordion);
    expect(triggerAccordion.addEventListener.getCall(0).args[0]).toBe('click');
  });

  it('should show details when clicked', () => {
    bindToggleUIEvents(containerAccordion);
    const event = new window.Event('click');
    triggerAccordion.dispatchEvent(event);
    expect(containerAccordion.classList
      .contains('sprk-c-Accordion__item--open')).toBe(true);
  });

  it('should disable clicks until after slide toggle animation runs', () => {
    bindToggleUIEvents(container);
    trigger.dispatchEvent(new window.Event('click'));
    expect(trigger.style.pointerEvents).toBe('none');
  });

  it('should enable clicks after slide toggle animation runs', (cb) => {
    bindToggleUIEvents(container);
    const event = new window.Event('click');
    trigger.dispatchEvent(event);
    trigger.dispatchEvent(event);
    setTimeout(() => {
      expect(trigger.style.pointerEvents).toBe('auto');
      cb();
    }, 100);
  });

  it('should still toggle content if icon is not present', () => {
    handleToggleClick(contentAccordion, null, null, triggerAccordion);
    expect(containerAccordion.classList
      .contains('sprk-c-Accordion__item--open')).toBe(true);
  });

  it('should not add Accordion__item class if toggle is not an'
    + ' accordion', () => {
    handleToggleClick(content, null, null, trigger);
    expect(container.classList.contains('sprk-c-Accordion__item--open')).toBe(false);
  });
});
<|endoftext|>


<commit_before>var NRS = (function(NRS, $, undefined) {
	NRS.loadContacts = function() {
		NRS.contacts = {};

		NRS.database.select("contacts", null, function(error, contacts) {
			if (contacts && contacts.length) {
				$.each(contacts, function(index, contact) {
					NRS.contacts[contact.account] = contact;
				});
			}
		});
	}

	NRS.pages.contacts = function() {
		NRS.pageLoading();

		if (!NRS.databaseSupport) {
			$("#contact_page_database_error").show();
			$("#contacts_table_container").hide();
			$("#add_contact_button").hide();
			NRS.pageLoaded();
			return;
		}

		$("#contacts_table_container").show();
		$("#contact_page_database_error").hide();

		NRS.database.select("contacts", null, function(error, contacts) {
			if (contacts && contacts.length) {
				var rows = "";

				contacts.sort(function(a, b) {
					if (a.name.toLowerCase() > b.name.toLowerCase()) {
						return 1;
					} else if (a.name.toLowerCase()  100) {
						contactDescription = contactDescription.substring(0, 100) + "...";
					} else if (!contactDescription) {
						contactDescription = "-";
					}

					rows += "
" + contact.name.escapeHTML() + "	" + NRS.getAccountFormatted(contact, "account") + "	" + (contact.email ? contact.email.escapeHTML() : "-") + "	" + contactDescription.escapeHTML() + "	Send Nxt Message Delete
";
				});

				$("#contacts_table tbody").empty().append(rows);
				NRS.dataLoadFinished($("#contacts_table"));

				NRS.pageLoaded();
			} else {
				$("#contacts_table tbody").empty();
				NRS.dataLoadFinished($("#contacts_table"));

				NRS.pageLoaded();
			}
		});
	}

	NRS.forms.addContact = function($modal) {
		var data = NRS.getFormData($modal.find("form:first"));

		data.account_id = String(data.account_id);

		if (!data.name) {
			return {
				"error": "Contact name is a required field."
			};
		} else if (!data.account_id) {
			return {
				"error": "Account ID is a required field."
			};
		}

		if (/^\d+$/.test(data.name) || /^NXT\-/i.test(data.name)) {
			return {
				"error": "Contact name must contain alphabetic characters."
			};
		}

		if (data.email && !/@/.test(data.email)) {
			return {
				"error": "Email address is incorrect."
			};
		}

		if (data.account_id.charAt(0) == '@') {
			var convertedAccountId = $modal.find("input[name=converted_account_id]").val();
			if (convertedAccountId) {
				data.account_id = convertedAccountId;
			} else {
				return {
					"error": "Invalid account ID."
				};
			}
		}

		if (/^NXT\-/i.test(data.account_id)) {
			data.account_rs = data.account_id;

			var address = new NxtAddress();

			if (address.set(data.account_rs)) {
				data.account = address.account_id();
			} else {
				return {
					"error": "Invalid account ID."
				};
			}
		} else {
			var address = new NxtAddress();

			if (address.set(data.account_id)) {
				data.account_rs = address.toString();
			} else {
				return {
					"error": "Invalid account ID."
				};
			}
		}

		NRS.sendRequest("getAccount", {
			"account": data.account_id
		}, function(response) {
			if (!response.errorCode) {
				if (response.account != data.account || response.accountRS != data.account_rs) {
					return {
						"error": "Invalid account ID."
					};
				}
			}
		}, false);

		var $btn = $modal.find("button.btn-primary:not([data-dismiss=modal], .ignore)");

		NRS.database.select("contacts", [{
			"account": data.account_id
		}, {
			"name": data.name
		}], function(error, contacts) {
			if (contacts && contacts.length) {
				if (contacts[0].name == data.name) {
					$modal.find(".error_message").html("A contact with this name already exists.").show();
				} else {
					$modal.find(".error_message").html("A contact with this account ID already exists.").show();
				}
				$btn.button("reset");
				$modal.modal("unlock");
			} else {
				NRS.database.insert("contacts", {
					name: data.name,
					email: data.email,
					account: data.account_id,
					accountRS: data.account_rs,
					description: data.description
				}, function(error) {
					NRS.contacts[data.account_id] = {
						name: data.name,
						email: data.email,
						account: data.account_id,
						accountRS: data.account_rs,
						description: data.description
					};

					setTimeout(function() {
						$btn.button("reset");
						$modal.modal("unlock");
						$modal.modal("hide");
						$.growl("Contact added successfully.", {
							"type": "success"
						});

						if (NRS.currentPage == "contacts") {
							NRS.pages.contacts();
						} else if (NRS.currentPage == "messages" && NRS.selectedContext) {
							var heading = NRS.selectedContext.find("h4.list-group-item-heading");
							if (heading.length) {
								heading.html(data.name.escapeHTML());
							}
							NRS.selectedContext.data("context", "messages_sidebar_update_context");
						}
					}, 50);
				});
			}
		});
	}

	$("#update_contact_modal").on('show.bs.modal', function(e) {
		var $invoker = $(e.relatedTarget);

		var contactId = parseInt($invoker.data("contact"), 10);

		if (!contactId && NRS.selectedContext) {
			var accountId = NRS.selectedContext.data("account");

			var dbKey = (/^NXT\-/i.test(accountId) ? "accountRS" : "account");

			var dbQuery = {};
			dbQuery[dbKey] = accountId;

			NRS.database.select("contacts", [dbQuery], function(error, contact) {
				contact = contact[0];

				$("#update_contact_id").val(contact.id);
				$("#update_contact_name").val(contact.name);
				$("#update_contact_email").val(contact.email);
				if (NRS.settings["reed_solomon"]) {
					$("#update_contact_account_id").val(contact.accountRS);
				} else {
					$("#update_contact_account_id").val(contact.account);
				}
				$("#update_contact_description").val(contact.description);
			});
		} else {
			$("#update_contact_id").val(contactId);

			NRS.database.select("contacts", [{
				"id": contactId
			}], function(error, contact) {
				contact = contact[0];

				$("#update_contact_name").val(contact.name);
				$("#update_contact_email").val(contact.email);
				if (NRS.settings["reed_solomon"]) {
					$("#update_contact_account_id").val(contact.accountRS);
				} else {
					$("#update_contact_account_id").val(contact.account);
				}
				$("#update_contact_description").val(contact.description);
			});
		}
	});

	NRS.forms.updateContact = function($modal) {
		var data = NRS.getFormData($modal.find("form:first"));

		data.account_id = String(data.account_id);

		if (!data.name) {
			return {
				"error": "Contact name is a required field."
			};
		} else if (!data.account_id) {
			return {
				"error": "Account ID is a required field."
			};
		}

		if (data.account_id.charAt(0) == '@') {
			var convertedAccountId = $modal.find("input[name=converted_account_id]").val();
			if (convertedAccountId) {
				data.account_id = convertedAccountId;
			} else {
				return {
					"error": "Invalid account ID."
				};
			}
		}

		var contactId = parseInt($("#update_contact_id").val(), 10);

		if (!contactId) {
			return {
				"error": "Invalid contact."
			};
		}

		if (/^NXT\-/i.test(data.account_id)) {
			data.account_rs = data.account_id;

			var address = new NxtAddress();

			if (address.set(data.account_rs)) {
				data.account_id = address.account_id();
			} else {
				return {
					"error": "Invalid account ID."
				};
			}
		} else {
			var address = new NxtAddress();

			if (address.set(data.account_id)) {
				data.account_rs = address.toString();
			} else {
				return {
					"error": "Invalid account ID."
				};
			}
		}

		NRS.sendRequest("getAccount", {
			"account": data.account_id
		}, function(response) {
			if (!response.errorCode) {
				if (response.account != data.account_id || response.accountRS != data.account_rs) {
					return {
						"error": "Invalid account ID."
					};
				}
			}
		}, false);

		var $btn = $modal.find("button.btn-primary:not([data-dismiss=modal])");

		NRS.database.select("contacts", [{
			"account": data.account_id
		}], function(error, contacts) {
			if (contacts && contacts.length && contacts[0].id != contactId) {
				$modal.find(".error_message").html("A contact with this account ID already exists.").show();
				$btn.button("reset");
				$modal.modal("unlock");
			} else {
				NRS.database.update("contacts", {
					name: data.name,
					email: data.email,
					account: data.account_id,
					accountRS: data.account_rs,
					description: data.description
				}, [{
					"id": contactId
				}], function(error) {
					if (contacts.length && data.account_id != contacts[0].accountId) {
						delete NRS.contacts[contacts[0].accountId];
					}

					NRS.contacts[data.account_id] = {
						name: data.name,
						email: data.email,
						account: data.account_id,
						accountRS: data.account_rs,
						description: data.description
					};

					setTimeout(function() {
						$btn.button("reset");
						$modal.modal("unlock");
						$modal.modal("hide");
						$.growl("Contact updated successfully.", {
							"type": "success"
						});

						if (NRS.currentPage == "contacts") {
							NRS.pages.contacts();
						} else if (NRS.currentPage == "messages" && NRS.selectedContext) {
							var heading = NRS.selectedContext.find("h4.list-group-item-heading");
							if (heading.length) {
								heading.html(data.name.escapeHTML());
							}
						}
					}, 50);
				});
			}
		});
	}

	$("#delete_contact_modal").on('show.bs.modal', function(e) {
		var $invoker = $(e.relatedTarget);

		var contactId = $invoker.data("contact");

		$("#delete_contact_id").val(contactId);

		NRS.database.select("contacts", [{
			"id": contactId
		}], function(error, contact) {
			contact = contact[0];

			$("#delete_contact_name").html(contact.name.escapeHTML());
			$("#delete_contact_account_id").val(contact.accountId);
		});
	});

	NRS.forms.deleteContact = function($modal) {
		var id = parseInt($("#delete_contact_id").val(), 10);

		NRS.database.delete("contacts", [{
			"id": id
		}], function() {
			delete NRS.contacts[$("#delete_contact_account_id").val()];

			setTimeout(function() {
				$.growl("Contact deleted successfully.", {
					"type": "success"
				});

				if (NRS.currentPage == "contacts") {
					NRS.pages.contacts();
				}
			}, 50);
		});

		return {
			"stop": true
		};
	}

	return NRS;
}(NRS || {}, jQuery));<commit_msg>Make sure deleted contact is no longer shown in dropdown.<commit_after>var NRS = (function(NRS, $, undefined) {
	NRS.loadContacts = function() {
		NRS.contacts = {};

		NRS.database.select("contacts", null, function(error, contacts) {
			if (contacts && contacts.length) {
				$.each(contacts, function(index, contact) {
					NRS.contacts[contact.account] = contact;
				});
			}
		});
	}

	NRS.pages.contacts = function() {
		NRS.pageLoading();

		if (!NRS.databaseSupport) {
			$("#contact_page_database_error").show();
			$("#contacts_table_container").hide();
			$("#add_contact_button").hide();
			NRS.pageLoaded();
			return;
		}

		$("#contacts_table_container").show();
		$("#contact_page_database_error").hide();

		NRS.database.select("contacts", null, function(error, contacts) {
			if (contacts && contacts.length) {
				var rows = "";

				contacts.sort(function(a, b) {
					if (a.name.toLowerCase() > b.name.toLowerCase()) {
						return 1;
					} else if (a.name.toLowerCase()  100) {
						contactDescription = contactDescription.substring(0, 100) + "...";
					} else if (!contactDescription) {
						contactDescription = "-";
					}

					rows += "
" + contact.name.escapeHTML() + "	" + NRS.getAccountFormatted(contact, "account") + "	" + (contact.email ? contact.email.escapeHTML() : "-") + "	" + contactDescription.escapeHTML() + "	Send Nxt Message Delete
";
				});

				$("#contacts_table tbody").empty().append(rows);
				NRS.dataLoadFinished($("#contacts_table"));

				NRS.pageLoaded();
			} else {
				$("#contacts_table tbody").empty();
				NRS.dataLoadFinished($("#contacts_table"));

				NRS.pageLoaded();
			}
		});
	}

	NRS.forms.addContact = function($modal) {
		var data = NRS.getFormData($modal.find("form:first"));

		data.account_id = String(data.account_id);

		if (!data.name) {
			return {
				"error": "Contact name is a required field."
			};
		} else if (!data.account_id) {
			return {
				"error": "Account ID is a required field."
			};
		}

		if (/^\d+$/.test(data.name) || /^NXT\-/i.test(data.name)) {
			return {
				"error": "Contact name must contain alphabetic characters."
			};
		}

		if (data.email && !/@/.test(data.email)) {
			return {
				"error": "Email address is incorrect."
			};
		}

		if (data.account_id.charAt(0) == '@') {
			var convertedAccountId = $modal.find("input[name=converted_account_id]").val();
			if (convertedAccountId) {
				data.account_id = convertedAccountId;
			} else {
				return {
					"error": "Invalid account ID."
				};
			}
		}

		if (/^NXT\-/i.test(data.account_id)) {
			data.account_rs = data.account_id;

			var address = new NxtAddress();

			if (address.set(data.account_rs)) {
				data.account = address.account_id();
			} else {
				return {
					"error": "Invalid account ID."
				};
			}
		} else {
			var address = new NxtAddress();

			if (address.set(data.account_id)) {
				data.account_rs = address.toString();
			} else {
				return {
					"error": "Invalid account ID."
				};
			}
		}

		NRS.sendRequest("getAccount", {
			"account": data.account_id
		}, function(response) {
			if (!response.errorCode) {
				if (response.account != data.account || response.accountRS != data.account_rs) {
					return {
						"error": "Invalid account ID."
					};
				}
			}
		}, false);

		var $btn = $modal.find("button.btn-primary:not([data-dismiss=modal], .ignore)");

		NRS.database.select("contacts", [{
			"account": data.account_id
		}, {
			"name": data.name
		}], function(error, contacts) {
			if (contacts && contacts.length) {
				if (contacts[0].name == data.name) {
					$modal.find(".error_message").html("A contact with this name already exists.").show();
				} else {
					$modal.find(".error_message").html("A contact with this account ID already exists.").show();
				}
				$btn.button("reset");
				$modal.modal("unlock");
			} else {
				NRS.database.insert("contacts", {
					name: data.name,
					email: data.email,
					account: data.account_id,
					accountRS: data.account_rs,
					description: data.description
				}, function(error) {
					NRS.contacts[data.account_id] = {
						name: data.name,
						email: data.email,
						account: data.account_id,
						accountRS: data.account_rs,
						description: data.description
					};

					setTimeout(function() {
						$btn.button("reset");
						$modal.modal("unlock");
						$modal.modal("hide");
						$.growl("Contact added successfully.", {
							"type": "success"
						});

						if (NRS.currentPage == "contacts") {
							NRS.pages.contacts();
						} else if (NRS.currentPage == "messages" && NRS.selectedContext) {
							var heading = NRS.selectedContext.find("h4.list-group-item-heading");
							if (heading.length) {
								heading.html(data.name.escapeHTML());
							}
							NRS.selectedContext.data("context", "messages_sidebar_update_context");
						}
					}, 50);
				});
			}
		});
	}

	$("#update_contact_modal").on('show.bs.modal', function(e) {
		var $invoker = $(e.relatedTarget);

		var contactId = parseInt($invoker.data("contact"), 10);

		if (!contactId && NRS.selectedContext) {
			var accountId = NRS.selectedContext.data("account");

			var dbKey = (/^NXT\-/i.test(accountId) ? "accountRS" : "account");

			var dbQuery = {};
			dbQuery[dbKey] = accountId;

			NRS.database.select("contacts", [dbQuery], function(error, contact) {
				contact = contact[0];

				$("#update_contact_id").val(contact.id);
				$("#update_contact_name").val(contact.name);
				$("#update_contact_email").val(contact.email);
				if (NRS.settings["reed_solomon"]) {
					$("#update_contact_account_id").val(contact.accountRS);
				} else {
					$("#update_contact_account_id").val(contact.account);
				}
				$("#update_contact_description").val(contact.description);
			});
		} else {
			$("#update_contact_id").val(contactId);

			NRS.database.select("contacts", [{
				"id": contactId
			}], function(error, contact) {
				contact = contact[0];

				$("#update_contact_name").val(contact.name);
				$("#update_contact_email").val(contact.email);
				if (NRS.settings["reed_solomon"]) {
					$("#update_contact_account_id").val(contact.accountRS);
				} else {
					$("#update_contact_account_id").val(contact.account);
				}
				$("#update_contact_description").val(contact.description);
			});
		}
	});

	NRS.forms.updateContact = function($modal) {
		var data = NRS.getFormData($modal.find("form:first"));

		data.account_id = String(data.account_id);

		if (!data.name) {
			return {
				"error": "Contact name is a required field."
			};
		} else if (!data.account_id) {
			return {
				"error": "Account ID is a required field."
			};
		}

		if (data.account_id.charAt(0) == '@') {
			var convertedAccountId = $modal.find("input[name=converted_account_id]").val();
			if (convertedAccountId) {
				data.account_id = convertedAccountId;
			} else {
				return {
					"error": "Invalid account ID."
				};
			}
		}

		var contactId = parseInt($("#update_contact_id").val(), 10);

		if (!contactId) {
			return {
				"error": "Invalid contact."
			};
		}

		if (/^NXT\-/i.test(data.account_id)) {
			data.account_rs = data.account_id;

			var address = new NxtAddress();

			if (address.set(data.account_rs)) {
				data.account_id = address.account_id();
			} else {
				return {
					"error": "Invalid account ID."
				};
			}
		} else {
			var address = new NxtAddress();

			if (address.set(data.account_id)) {
				data.account_rs = address.toString();
			} else {
				return {
					"error": "Invalid account ID."
				};
			}
		}

		NRS.sendRequest("getAccount", {
			"account": data.account_id
		}, function(response) {
			if (!response.errorCode) {
				if (response.account != data.account_id || response.accountRS != data.account_rs) {
					return {
						"error": "Invalid account ID."
					};
				}
			}
		}, false);

		var $btn = $modal.find("button.btn-primary:not([data-dismiss=modal])");

		NRS.database.select("contacts", [{
			"account": data.account_id
		}], function(error, contacts) {
			if (contacts && contacts.length && contacts[0].id != contactId) {
				$modal.find(".error_message").html("A contact with this account ID already exists.").show();
				$btn.button("reset");
				$modal.modal("unlock");
			} else {
				NRS.database.update("contacts", {
					name: data.name,
					email: data.email,
					account: data.account_id,
					accountRS: data.account_rs,
					description: data.description
				}, [{
					"id": contactId
				}], function(error) {
					if (contacts.length && data.account_id != contacts[0].accountId) {
						delete NRS.contacts[contacts[0].accountId];
					}

					NRS.contacts[data.account_id] = {
						name: data.name,
						email: data.email,
						account: data.account_id,
						accountRS: data.account_rs,
						description: data.description
					};

					setTimeout(function() {
						$btn.button("reset");
						$modal.modal("unlock");
						$modal.modal("hide");
						$.growl("Contact updated successfully.", {
							"type": "success"
						});

						if (NRS.currentPage == "contacts") {
							NRS.pages.contacts();
						} else if (NRS.currentPage == "messages" && NRS.selectedContext) {
							var heading = NRS.selectedContext.find("h4.list-group-item-heading");
							if (heading.length) {
								heading.html(data.name.escapeHTML());
							}
						}
					}, 50);
				});
			}
		});
	}

	$("#delete_contact_modal").on("show.bs.modal", function(e) {
		var $invoker = $(e.relatedTarget);

		var contactId = $invoker.data("contact");

		$("#delete_contact_id").val(contactId);

		NRS.database.select("contacts", [{
			"id": contactId
		}], function(error, contact) {
			contact = contact[0];

			$("#delete_contact_name").html(contact.name.escapeHTML());
			$("#delete_contact_account_id").val(NRS.getAccountFormatted(contact, "account"));
		});
	});

	NRS.forms.deleteContact = function($modal) {
		var id = parseInt($("#delete_contact_id").val(), 10);

		NRS.database.delete("contacts", [{
			"id": id
		}], function() {
			delete NRS.contacts[$("#delete_contact_account_id").val()];

			setTimeout(function() {
				$.growl("Contact deleted successfully.", {
					"type": "success"
				});

				if (NRS.currentPage == "contacts") {
					NRS.pages.contacts();
				}
			}, 50);
		});

		return {
			"stop": true
		};
	}

	return NRS;
}(NRS || {}, jQuery));<|endoftext|>


<commit_before>// datatables-tool.js

// Handle AJAX type errors
var handle_ajax_error = function(jqXHR, textStatus, errorThrown) {
  scraperwiki.alert(errorThrown, $(jqXHR.responseText).text(), "error")
}

// Links clickable etc. in one row of data
var prettifyRow = function( tr, array, iDisplayIndex, iDisplayIndexFull ) {
  $('td', tr).each(function(){
      $(this).html(
          $(this).html()
          // first add links onto URLs:
          .replace(
              /((http|https|ftp):\/\/[a-zA-Z0-9-_~#:\.\?%&\/\[\]@\!\$'\(\)\*\+,;=]+)/g,
              '$1'
          )
          // then convert images to themselves embedded.
          // XXX _normal is to match Twitter images, watch for it causing trouble
	  // e.g. https://si0.twimg.com/profile_images/2559953209/pM981LrS_normal - remove it
          .replace(
              />((http|https|ftp):\/\/[a-zA-Z0-9-_~#:\.\?%&\/\[\]@\!\$'\(\)\*\+,;=]+(\.jpeg|\.png|\.jpg|\.gif|\.bmp|_normal))</ig,
              '><'
          )
	  // match LinkedIn image URLs, which always have "licdn.com/mpr/mpr" in them.
	  // e.g. http://m3.licdn.com/mpr/mprx/0_oCf8SHoyvJ0Wq_CEo87xSEoAvRHIq5CEe_R0SEw2EOpRI3voQk0uio0GUveqBC_QITDYCDvcT0rm
          .replace(
              />((http|https|ftp):\/\/[a-z0-9\.]+licdn.com\/mpr\/mpr[a-zA-Z0-9-_~#:\.\?%&\/\[\]@\!\$'\(\)\*\+,;=]+)</ig,
              '><'
          )
          // shorten displayed part of any URLs longer than 30 characters, down to 30
          .replace(
              />((http|https|ftp):\/\/[a-zA-Z0-9-_~#:\.\?%&\/\[\]@\!\$'\(\)\*\+,;=]{31,})</g,
              function (str, p1, offset, s) {
                 return ">" + p1.substr(0,30) + "…<"
              }
          )
      )
  })
  return tr
}

// Save current active tab/table, and its status to the filesystem in the view's box
var saveState = function (oSettings, oData) {
  var j = JSON.stringify(oData)
  var fname = escapeshell("settings_" + currentActiveTable + ".json")
  scraperwiki.exec("echo -n <<ENDOFJSON >" + fname + ".new.$$ " + escapeshell(j) + "\nENDOFJSON\n" +
    "mv " + fname + ".new.$$ " + fname,
    function(content) {
      if (content != "") {
        scraperwiki.alert("Unexpected saveState response!", content, "error")
      }
      saveActiveTable()
    }, handle_ajax_error
  )
}

// Save just the active table
var saveActiveTable = function () {
  scraperwiki.exec("echo -n " + escapeshell(currentActiveTable) + " >active_table.txt",
    function(content) {
      if (content != "") {
        scraperwiki.alert("Unexpected saveActiveTable response!", content, "error")
      }
    }, handle_ajax_error
  )

}

// Add this in, needed for loadState which must return asynchronously
scraperwiki.async_exec = function(cmd, success, error) {
  var options, settings;
  settings = scraperwiki.readSettings();
  options = {
    url: "" + window.location.protocol + "//" + window.location.host + "/" + scraperwiki.box + "/exec",
    async: false,
    type: "POST",
    dataType: "text",
    data: {
      apikey: settings.source.apikey,
      cmd: cmd
    }
  };
  if (success != null) {
    options.success = success;
  }
  if (error != null) {
    options.error = error;
  }
  return $.ajax(options);
};

// Restore column status from the view's box's filesystem
var loadState = function (oSettings) {
  var fname = escapeshell("settings_" + currentActiveTable + ".json")
  var oData = false
  scraperwiki.async_exec("touch " + fname + "; cat " + fname,
    function(content) {
      try {
        oData = JSON.parse(content)
      } catch (e) {
	oData = false
      }
    }, handle_ajax_error
  )
  return oData
}

// Read active table from the box's filesystem and pass it on to callback
var loadActiveTable = function(callback) {
  scraperwiki.exec("touch active_table.txt; cat active_table.txt",
    function(content) {
      callback(content)
    }, handle_ajax_error
  )
}

// Escape identifiers
var escapeSQL = function(column_name) {
  return '"' + column_name.replace(/"/g, '""') + '"'
}
var escapeshell = function(cmd) {
    return "'"+cmd.replace(/'/g,"'\\''")+"'";
};

// Function to map JSON data between DataTables format and ScraperWiki's SQL endpoint format.
// It returns a function for the fnServerData parameter
var convertData = function(table_name, column_names) {
  // This is a wrapper round the GET request DataTables makes to get more data
  // sSource - the URL, we don't use it, we hard code it instead
  // aoData - contains the URL parameters, e.g. what page, what to filter, what order and so on
  // fnCallback - where to call with the data you get back
  // oSettings - settings object for the whole DataTables, see http://datatables.net/docs/DataTables/1.9.0/DataTable.models.oSettings.html
  return function ( sSource, aoData, fnCallback, oSettings ) {
    // convert aoData into a normal hash (called ps)
    var params = {}
    for (var i=0;i<aoData.length;i++) {
      params[aoData[i].name] = aoData[i].value
    }

    // construct SQL query needed according to the parameters
    var order_by = ""
    if (params.iSortingCols >= 1) {
      var order_parts = []
      for (var i = 0; i < params.iSortingCols; i++) {
        order_part = escapeSQL(column_names[params["iSortCol_" + i]])
        if (params["sSortDir_" + i] == 'desc') {
          order_part += " desc"
        } else if (params["sSortDir_" + i] != 'asc') {
          scraperwiki.alert("Got unknown sSortDir_" + i + " value in table " + table_name)
        }
        order_parts.push(order_part)
      }
      order_by = " order by " + order_parts.join(",")
    }
    var where = ""
    if (params.sSearch) {
      // XXX no idea if this bog standard Javascript escape really does what we want with SQL databases.
      // There's no security risk (as endpoint is sandboxed). There could be user experience pain though.
      var search = "'%" + escape(params.sSearch.toLowerCase()) + "%'"
      where = " where " + _.map(column_names, function(n) { return "lower(" + escapeSQL(n) + ") like " + search }).join(" or ")
    }
    var query = "select * " +
           " from " + escapeSQL(table_name) +
         where +
         order_by +
           " limit " + params.iDisplayLength +
           " offset " + params.iDisplayStart

    // get column counts
    scraperwiki.sql("select (select count(*) from " + escapeSQL(table_name) + ") as total, (select count(*) from " + escapeSQL(table_name) + where + ") as display_total", function (data) {
      var counts = data[0]

      oSettings.jqXHR = $.ajax( {
        "dataType": 'json',
        "type": "GET",
        "url": sqliteEndpoint,
        "data": { q: query },
        "success": function ( response ) {
          // ScraperWiki returns a list of dicts. This converts it to a list of lists.
          var rows = []
          for (var i=0;i<response.length;i++) {
            var row = []
            _.each(meta.table[table_name].columnNames, function(col) {
              row.push(response[i][col])
            })
            rows.push(row)
          }
          // Send the data to dataTables
          fnCallback({
            "aaData" : rows,
            "iTotalRecords": data[0].total, // without filtering
            "iTotalDisplayRecords": data[0].display_total // after filtering
          })
        },
        "error": handle_ajax_error
      } );
    }, handle_ajax_error)
  }
}

// Make one of the DataTables (in one tab)
// 'i' should be the integer position of the datatable in the list of all tables
// 'table_name' is obviously the name of the active table
var constructDataTable = function(i, table_name) {
  // Find or make the table
  $(".maintable").hide()
  var id = "table_" + i
  var $outer = $("#" + id)
  if ($outer.length == 0) {
    $outer = $('

')
    $('body').append($outer)
  } else {
    $outer.show()
    return
  }
  var $t = $outer.find("table")

  // Find out the column names
  column_names = meta.table[table_name].columnNames
  if (column_names.length == 0) {
    scraperwiki.alert("No columns in the table", jqXHR.responseText)
    return
  }

  // Make the column headings
      var thead = '

'
  _.each(column_names, function(column_name) {
    thead += '' + column_name + '	'
  })
  thead += '

'
  $t.append(thead)

  // Fill in the datatables object
  $t.dataTable({
    "bProcessing": true,
    "bServerSide": true,
    "bDeferRender": true,
    "bPaginate": true,
    "bFilter": true,
    "iDisplayLength": 500,
    "bScrollCollapse": true,
    "sDom": '<"table_controls"pfi>r<"table_wrapper"t>',
    "sPaginationType": "bootstrap",
    "fnServerData": convertData(table_name, column_names),
    "fnRowCallback": prettifyRow,
    "fnInitComplete": function(){
      // Really hackily replace their rubbish search input with a nicer one
      var $copy = $('.dataTables_filter label input').clone(true).addClass('search-query')
      $('.dataTables_filter').empty().append($copy)
    },
    "bStateSave": true,
    "fnStateSave": saveState,
    "fnStateLoad": loadState,
    "oLanguage": {
      "sEmptyTable": "This table is empty"
     }
  })
}

// Create and insert spreadsheet-like tab bar at bottom of page.
// 'tables' should be a list of table names.
// 'active_table' should be the one you want to appear selected.
var constructTabs = function(tables, active_table){
  var $tabs = $('
').addClass('tabs-below').appendTo('body')
  var $ul = $('
').addClass('nav nav-tabs').appendTo($tabs)
  $.each(tables, function(i, table_name){
    var li = '
'
    if (table_name == active_table){
      var li = '
'
      currentActiveTable = table_name
      currentActiveTableIndex = i
    }
    $(li).append('' + table_name + '').bind('click', function(e){
      e.preventDefault()
      $(this).addClass('active').siblings('.active').removeClass('active')
      currentActiveTable = table_name
      currentActiveTableIndex = i
      constructDataTable(i, table_name)
    }).appendTo($ul)
  })
}

// Make all the DataTables and their tabs
var constructDataTables = function(first_table_name) {
  if (!first_table_name) {
    first_table_name = tables[0]
  }
  constructTabs(tables, first_table_name)
  $("#tab_" + currentActiveTableIndex).trigger('click')
}

// Main entry point, make the data table
var settings
var sqliteEndpoint
var tables
var currentActiveTable
var currentActiveTableIndex
var meta
$(function(){
  settings = scraperwiki.readSettings()
  sqliteEndpoint = settings.target.url + '/sqlite'

  scraperwiki.sql.meta(function(newMeta) {
    meta = newMeta
    tables = _.keys(meta.table)
    loadActiveTable(function(saved_active_table) {
      constructDataTables(saved_active_table)
    })
  }, handle_ajax_error)
});

<commit_msg>If too many columns, give nice error message<commit_after>// datatables-tool.js

// Handle AJAX type errors
var handle_ajax_error = function(jqXHR, textStatus, errorThrown) {
  scraperwiki.alert(errorThrown, $(jqXHR.responseText).text(), "error")
}

// Links clickable etc. in one row of data
var prettifyRow = function( tr, array, iDisplayIndex, iDisplayIndexFull ) {
  $('td', tr).each(function(){
      $(this).html(
          $(this).html()
          // first add links onto URLs:
          .replace(
              /((http|https|ftp):\/\/[a-zA-Z0-9-_~#:\.\?%&\/\[\]@\!\$'\(\)\*\+,;=]+)/g,
              '$1'
          )
          // then convert images to themselves embedded.
          // XXX _normal is to match Twitter images, watch for it causing trouble
	  // e.g. https://si0.twimg.com/profile_images/2559953209/pM981LrS_normal - remove it
          .replace(
              />((http|https|ftp):\/\/[a-zA-Z0-9-_~#:\.\?%&\/\[\]@\!\$'\(\)\*\+,;=]+(\.jpeg|\.png|\.jpg|\.gif|\.bmp|_normal))</ig,
              '><'
          )
	  // match LinkedIn image URLs, which always have "licdn.com/mpr/mpr" in them.
	  // e.g. http://m3.licdn.com/mpr/mprx/0_oCf8SHoyvJ0Wq_CEo87xSEoAvRHIq5CEe_R0SEw2EOpRI3voQk0uio0GUveqBC_QITDYCDvcT0rm
          .replace(
              />((http|https|ftp):\/\/[a-z0-9\.]+licdn.com\/mpr\/mpr[a-zA-Z0-9-_~#:\.\?%&\/\[\]@\!\$'\(\)\*\+,;=]+)</ig,
              '><'
          )
          // shorten displayed part of any URLs longer than 30 characters, down to 30
          .replace(
              />((http|https|ftp):\/\/[a-zA-Z0-9-_~#:\.\?%&\/\[\]@\!\$'\(\)\*\+,;=]{31,})</g,
              function (str, p1, offset, s) {
                 return ">" + p1.substr(0,30) + "…<"
              }
          )
      )
  })
  return tr
}

// Save current active tab/table, and its status to the filesystem in the view's box
var saveState = function (oSettings, oData) {
  var j = JSON.stringify(oData)
  var fname = escapeshell("settings_" + currentActiveTable + ".json")
  scraperwiki.exec("echo -n <<ENDOFJSON >" + fname + ".new.$$ " + escapeshell(j) + "\nENDOFJSON\n" +
    "mv " + fname + ".new.$$ " + fname,
    function(content) {
      if (content != "") {
        scraperwiki.alert("Unexpected saveState response!", content, "error")
      }
      saveActiveTable()
    }, handle_ajax_error
  )
}

// Save just the active table
var saveActiveTable = function () {
  scraperwiki.exec("echo -n " + escapeshell(currentActiveTable) + " >active_table.txt",
    function(content) {
      if (content != "") {
        scraperwiki.alert("Unexpected saveActiveTable response!", content, "error")
      }
    }, handle_ajax_error
  )

}

// Add this in, needed for loadState which must return asynchronously
scraperwiki.async_exec = function(cmd, success, error) {
  var options, settings;
  settings = scraperwiki.readSettings();
  options = {
    url: "" + window.location.protocol + "//" + window.location.host + "/" + scraperwiki.box + "/exec",
    async: false,
    type: "POST",
    dataType: "text",
    data: {
      apikey: settings.source.apikey,
      cmd: cmd
    }
  };
  if (success != null) {
    options.success = success;
  }
  if (error != null) {
    options.error = error;
  }
  return $.ajax(options);
};

// Restore column status from the view's box's filesystem
var loadState = function (oSettings) {
  var fname = escapeshell("settings_" + currentActiveTable + ".json")
  var oData = false
  scraperwiki.async_exec("touch " + fname + "; cat " + fname,
    function(content) {
      try {
        oData = JSON.parse(content)
      } catch (e) {
	oData = false
      }
    }, handle_ajax_error
  )
  return oData
}

// Read active table from the box's filesystem and pass it on to callback
var loadActiveTable = function(callback) {
  scraperwiki.exec("touch active_table.txt; cat active_table.txt",
    function(content) {
      callback(content)
    }, handle_ajax_error
  )
}

// Escape identifiers
var escapeSQL = function(column_name) {
  return '"' + column_name.replace(/"/g, '""') + '"'
}
var escapeshell = function(cmd) {
    return "'"+cmd.replace(/'/g,"'\\''")+"'";
};

// Function to map JSON data between DataTables format and ScraperWiki's SQL endpoint format.
// It returns a function for the fnServerData parameter
var convertData = function(table_name, column_names) {
  // This is a wrapper round the GET request DataTables makes to get more data
  // sSource - the URL, we don't use it, we hard code it instead
  // aoData - contains the URL parameters, e.g. what page, what to filter, what order and so on
  // fnCallback - where to call with the data you get back
  // oSettings - settings object for the whole DataTables, see http://datatables.net/docs/DataTables/1.9.0/DataTable.models.oSettings.html
  return function ( sSource, aoData, fnCallback, oSettings ) {
    // convert aoData into a normal hash (called ps)
    var params = {}
    for (var i=0;i<aoData.length;i++) {
      params[aoData[i].name] = aoData[i].value
    }

    // construct SQL query needed according to the parameters
    var order_by = ""
    if (params.iSortingCols >= 1) {
      var order_parts = []
      for (var i = 0; i < params.iSortingCols; i++) {
        order_part = escapeSQL(column_names[params["iSortCol_" + i]])
        if (params["sSortDir_" + i] == 'desc') {
          order_part += " desc"
        } else if (params["sSortDir_" + i] != 'asc') {
          scraperwiki.alert("Got unknown sSortDir_" + i + " value in table " + table_name)
        }
        order_parts.push(order_part)
      }
      order_by = " order by " + order_parts.join(",")
    }
    var where = ""
    if (params.sSearch) {
      // XXX no idea if this bog standard Javascript escape really does what we want with SQL databases.
      // There's no security risk (as endpoint is sandboxed). There could be user experience pain though.
      var search = "'%" + escape(params.sSearch.toLowerCase()) + "%'"
      where = " where " + _.map(column_names, function(n) { return "lower(" + escapeSQL(n) + ") like " + search }).join(" or ")
      if (where.length > 1500) {
        scraperwiki.alert("Filtering is unavailable.", "Your dataset has too many columns")
        $(".search-query").val("").trigger("keyup")
        return
      }
    }
    var query = "select * " +
           " from " + escapeSQL(table_name) +
         where +
         order_by +
           " limit " + params.iDisplayLength +
           " offset " + params.iDisplayStart

    // get column counts
    scraperwiki.sql("select (select count(*) from " + escapeSQL(table_name) + ") as total, (select count(*) from " + escapeSQL(table_name) + where + ") as display_total", function (data) {
      var counts = data[0]

      oSettings.jqXHR = $.ajax( {
        "dataType": 'json',
        "type": "GET",
        "url": sqliteEndpoint,
        "data": { q: query },
        "success": function ( response ) {
          // ScraperWiki returns a list of dicts. This converts it to a list of lists.
          var rows = []
          for (var i=0;i<response.length;i++) {
            var row = []
            _.each(meta.table[table_name].columnNames, function(col) {
              row.push(response[i][col])
            })
            rows.push(row)
          }
          // Send the data to dataTables
          fnCallback({
            "aaData" : rows,
            "iTotalRecords": data[0].total, // without filtering
            "iTotalDisplayRecords": data[0].display_total // after filtering
          })
        },
        "error": handle_ajax_error
      } );
    }, handle_ajax_error)
  }
}

// Make one of the DataTables (in one tab)
// 'i' should be the integer position of the datatable in the list of all tables
// 'table_name' is obviously the name of the active table
var constructDataTable = function(i, table_name) {
  // Find or make the table
  $(".maintable").hide()
  var id = "table_" + i
  var $outer = $("#" + id)
  if ($outer.length == 0) {
    $outer = $('

')
    $('body').append($outer)
  } else {
    $outer.show()
    return
  }
  var $t = $outer.find("table")

  // Find out the column names
  column_names = meta.table[table_name].columnNames
  if (column_names.length == 0) {
    scraperwiki.alert("No columns in the table", jqXHR.responseText)
    return
  }

  // Make the column headings
      var thead = '

'
  _.each(column_names, function(column_name) {
    thead += '' + column_name + '	'
  })
  thead += '

'
  $t.append(thead)

  // Fill in the datatables object
  $t.dataTable({
    "bProcessing": true,
    "bServerSide": true,
    "bDeferRender": true,
    "bPaginate": true,
    "bFilter": true,
    "iDisplayLength": 500,
    "bScrollCollapse": true,
    "sDom": '<"table_controls"pfi>r<"table_wrapper"t>',
    "sPaginationType": "bootstrap",
    "fnServerData": convertData(table_name, column_names),
    "fnRowCallback": prettifyRow,
    "fnInitComplete": function(){
      // Really hackily replace their rubbish search input with a nicer one
      var $copy = $('.dataTables_filter label input').clone(true).addClass('search-query')
      $('.dataTables_filter').empty().append($copy)
    },
    "bStateSave": true,
    "fnStateSave": saveState,
    "fnStateLoad": loadState,
    "oLanguage": {
      "sEmptyTable": "This table is empty"
     }
  })
}

// Create and insert spreadsheet-like tab bar at bottom of page.
// 'tables' should be a list of table names.
// 'active_table' should be the one you want to appear selected.
var constructTabs = function(tables, active_table){
  var $tabs = $('
').addClass('tabs-below').appendTo('body')
  var $ul = $('
').addClass('nav nav-tabs').appendTo($tabs)
  $.each(tables, function(i, table_name){
    var li = '
'
    if (table_name == active_table){
      var li = '
'
      currentActiveTable = table_name
      currentActiveTableIndex = i
    }
    $(li).append('' + table_name + '').bind('click', function(e){
      e.preventDefault()
      $(this).addClass('active').siblings('.active').removeClass('active')
      currentActiveTable = table_name
      currentActiveTableIndex = i
      constructDataTable(i, table_name)
    }).appendTo($ul)
  })
}

// Make all the DataTables and their tabs
var constructDataTables = function(first_table_name) {
  if (!first_table_name) {
    first_table_name = tables[0]
  }
  constructTabs(tables, first_table_name)
  $("#tab_" + currentActiveTableIndex).trigger('click')
}

// Main entry point, make the data table
var settings
var sqliteEndpoint
var tables
var currentActiveTable
var currentActiveTableIndex
var meta
$(function(){
  settings = scraperwiki.readSettings()
  sqliteEndpoint = settings.target.url + '/sqlite'

  scraperwiki.sql.meta(function(newMeta) {
    meta = newMeta
    tables = _.keys(meta.table)
    loadActiveTable(function(saved_active_table) {
      constructDataTables(saved_active_table)
    })
  }, handle_ajax_error)
});

<|endoftext|>


<commit_before>import { shallowMount } from "@vue/test-utils";
import VueSelect from "../../src/components/Select";

describe("Single value options", () => {
  it("should reset the search input on focus lost", () => {
    const Select = shallowMount(VueSelect);
    Select.vm.open = true;

    Select.vm.search = "t";
    expect(Select.vm.search).toEqual("t");

    Select.vm.onSearchBlur();
    expect(Select.vm.search).toEqual("");
  });

  //   it('should apply the "hidden" class to the search input when a value is present', () => {
  //     const vm = new Vue({
  //       template:
  //         '
<v-select ref="select" :options="options" :value="value"></v-select>
',
  //       data: {
  //         value: "one",
  //         options: ["one", "two", "three"]
  //       }
  //     }).$mount();
  //
  //     expect(vm.$children[0].inputClasses.hidden).toEqual(true);
  //   });
  //
  //   it('should not apply the "hidden" class to the search input when a value is present, and the dropdown is open', done => {
  //     const vm = new Vue({
  //       template:
  //         '
<v-select ref="select" :options="options" :value="value"></v-select>
',
  //       data: {
  //         value: "one",
  //         options: ["one", "two", "three"],
  //         open: true
  //       }
  //     }).$mount();
  //     vm.$children[0].toggleDropdown({ target: vm.$children[0].$refs.search });
  //     Vue.nextTick(() => {
  //       Vue.nextTick(() => {
  //         expect(vm.$children[0].open).toEqual(true);
  //         expect(vm.$children[0].inputClasses.hidden).toEqual(false);
  //         done();
  //       });
  //     });
  //   });
  //
  //   it("should not reset the search input on focus lost when clearSearchOnSelect is false", done => {
  //     const vm = new Vue({
  //       template:
  //         '
<v-select ref="select" :options="options" :value="value" :clear-search-on-select="false"></v-select>
',
  //       data: {
  //         value: "one",
  //         options: ["one", "two", "three"]
  //       }
  //     }).$mount();
  //     expect(vm.$refs.select.clearSearchOnSelect).toEqual(false);
  //
  //     vm.$children[0].open = true;
  //     vm.$refs.select.search = "t";
  //     expect(vm.$refs.select.search).toEqual("t");
  //
  //     vm.$children[0].onSearchBlur();
  //     Vue.nextTick(() => {
  //       expect(vm.$refs.select.search).toEqual("t");
  //       done();
  //     });
  //   });
});
<commit_msg>Complete Layout specs<commit_after>import { shallowMount } from "@vue/test-utils";
import VueSelect from "../../src/components/Select";

describe("Single value options", () => {
  it("should reset the search input on focus lost", () => {
    const Select = shallowMount(VueSelect);
    Select.vm.open = true;

    Select.vm.search = "t";
    expect(Select.vm.search).toEqual("t");

    Select.vm.onSearchBlur();
    expect(Select.vm.search).toEqual("");
  });

  it('should apply the "hidden" class to the search input when a value is present', () => {
    const Select = shallowMount(VueSelect, { propsData: { value: "foo" } });
    expect(Select.vm.inputClasses.hidden).toEqual(true);
  });

  it('should not apply the "hidden" class to the search input when a value is present, and the dropdown is open', () => {
    const Select = shallowMount(VueSelect, { propsData: { value: "foo" } });
    Select.vm.toggleDropdown({ target: Select.vm.$refs.search });

    expect(Select.vm.open).toEqual(true);
    expect(Select.vm.inputClasses.hidden).toEqual(false);
  });

  it("should not reset the search input on focus lost when clearSearchOnSelect is false", () => {
    const Select = shallowMount(VueSelect, {
      propsData: { value: "foo", clearSearchOnSelect: false }
    });

    expect(Select.vm.clearSearchOnSelect).toEqual(false);

    Select.vm.open = true;
    Select.vm.search = "t";
    expect(Select.vm.search).toEqual("t");

    Select.vm.onSearchBlur();
    expect(Select.vm.search).toEqual("t");
  });
});
<|endoftext|>


<commit_before>const blockstack = require('blockstack')
const jsontokens = require('jsontokens')

class ProofChecker {

  constructor ( proofsConfig, logger, storageDriver ){
    this.proofsRequired = proofsConfig.proofsRequired
    this.logger = logger
    this.storageDriver = storageDriver
  }

  static makeProofsHeader ( proofs, name = false ){
    let socialProofObj = { proofs }
    if ( name !== false ) {
      socialProofObj.name = name
    }
    return Buffer(JSON.stringify(socialProofObj)).toString('base64')
  }

  static proofObjectFromHeader ( proofHeader ){
    return JSON.parse(Buffer(proofHeader, 'base64').toString())
  }

  fetchProfile ( address ) {
    let filename = `${address}/0/profile.json`
    let readURL = this.storageDriver.getReadURLPrefix()
    const url = `${readURL}${filename}`

    return fetch( url )
      .then(x => x.json())
      .then(x => x[0])
      .then(x => [jsontokens.decodeToken(x.token).payload, x.token])
      .then(pieces => {
        let x = pieces[0]
        let token = pieces[1]
        let verifier = new jsontokens.TokenVerifier('ES256k', x.subject.publicKey)
        let verified = verifier.verify(token)
        if (verified) {
          return x.claim
        } else {
          return false
        }
      })

  }

  validEnough ( validProofs ) {
    this.logger.debug(validProofs)
    return (validProofs.length >= this.proofsRequired)
  }

  checkProofs ( req ) {
    return new Promise((resolve) => {
      // 1: if we're writing the profile or don't need proofs, let it pass.
      if (this.proofsRequired == 0 || req.params.filename == '0/profile.json') {
        resolve(true)
        return
      }else{
        let address = req.params.address
        // 0: check if we cached the social proofs
        // 1: fetch the profile.json
        this.fetchProfile( address )
          .then( profile =>
                 blockstack.validateProofs(profile, address, undefined) )
          .then( proofs => {
            let validProofs = proofs.filter(
              ( p ) => { return p.valid } )
            resolve( this.validEnough( validProofs ) )
          })
          .catch( x => {
            this.logger.error(x)
            resolve( false )
          })
      }
    })
  }

module.exports = ProofChecker
<commit_msg>cleaner branching there<commit_after>const blockstack = require('blockstack')
const jsontokens = require('jsontokens')

class ProofChecker {

  constructor ( proofsConfig, logger, storageDriver ){
    this.proofsRequired = proofsConfig.proofsRequired
    this.logger = logger
    this.storageDriver = storageDriver
  }

  static makeProofsHeader ( proofs, name = false ){
    let socialProofObj = { proofs }
    if ( name !== false ) {
      socialProofObj.name = name
    }
    return Buffer(JSON.stringify(socialProofObj)).toString('base64')
  }

  static proofObjectFromHeader ( proofHeader ){
    return JSON.parse(Buffer(proofHeader, 'base64').toString())
  }

  fetchProfile ( address ) {
    let filename = `${address}/0/profile.json`
    let readURL = this.storageDriver.getReadURLPrefix()
    const url = `${readURL}${filename}`

    return fetch( url )
      .then(x => x.json())
      .then(x => x[0])
      .then(x => [jsontokens.decodeToken(x.token).payload, x.token])
      .then(pieces => {
        let x = pieces[0]
        let token = pieces[1]
        let verifier = new jsontokens.TokenVerifier('ES256k', x.subject.publicKey)
        let verified = verifier.verify(token)
        if (verified) {
          return x.claim
        } else {
          return false
        }
      })

  }

  validEnough ( validProofs ) {
    this.logger.debug(validProofs)
    return (validProofs.length >= this.proofsRequired)
  }

  checkProofs ( req ) {
    return new Promise((resolve) => {
      // 1: if we're writing the profile or don't need proofs, let it pass.
      if (this.proofsRequired == 0 || req.params.filename == '0/profile.json') {
        resolve(true)
      }else{
        let address = req.params.address
        // 0: check if we cached the social proofs
        // 1: fetch the profile.json
        this.fetchProfile( address )
          .then( profile =>
                 blockstack.validateProofs(profile, address, undefined) )
          .then( proofs => {
            let validProofs = proofs.filter(
              ( p ) => { return p.valid } )
            resolve( this.validEnough( validProofs ) )
          })
          .catch( x => {
            this.logger.error(x)
            resolve( false )
          })
      }
    })
  }

module.exports = ProofChecker
<|endoftext|>