<commit_before>import { setupRerender } from 'preact/test-utils';
import { createElement as h, render, Component, Fragment, hydrate } from '../../src/index';
import { setupScratch, teardown } from '../_util/helpers';
import { div, span, input as inputStr } from '../_util/dom';
/* eslint-disable react/jsx-boolean-value */

/** @jsx h */
describe('focus', () => {

	/** @type {HTMLDivElement} */
	let scratch;

	/** @type {() => void} */
	let rerender;

	const List = ({ children }) =>
{children}
;
	const ListItem = ({ children }) => {children};
	const InputWithId = ({ i }) => <input id={`input-${i}`} type="text" />;
	const Input = () => <input type="text" />;

	function focusInput() {
		if (!scratch) return;

		const input = scratch.querySelector('input');
		input.value = 'a word';
		input.focus();
		input.setSelectionRange(2, 5);

		expect(document.activeElement).to.equal(input);

		return input;
	}

	function focusInputById() {
		if (!scratch) return;

		const input = scratch.querySelector('#input-0');
		input.value = 'a word';
		input.focus();
		input.setSelectionRange(2, 5);

		expect(document.activeElement).to.equal(input);

		return input;
	}

	/**
	 * Validate an input tag has maintained focus
	 * @param {HTMLInputElement} input The input to validate
	 * @param {string} [message] Message to show if the activeElement is not
	 * eqaul to the `input` parameter
	 */
	function validateFocus(input, message) {
		// Check `nodeName` first to make cli output less spammy
		expect(document.activeElement.nodeName).to.equal(input.nodeName, message);
		expect(document.activeElement).to.equal(input, message);
		expect(input.selectionStart).to.equal(2);
		expect(input.selectionEnd).to.equal(5);

	}

	beforeEach(() => {
		scratch = setupScratch();
		rerender = setupRerender();
	});

	afterEach(() => {
		teardown(scratch);
	});

	it.skip('should maintain focus when swapping elements', () => {
		render((
			<List>
				<Input />
				<ListItem>fooo</ListItem>
			</List>
		), scratch);

		const input = focusInput();

		render((
			<List>
				<ListItem>fooo</ListItem>
				<Input />
			</List>
		), scratch);
		validateFocus(input);
	});

	it('should maintain focus when moving the input around', () => {
		function App({ showFirst, showLast }) {
			return (
				<List>
					{ showFirst ? <ListItem>1</ListItem> : null }
					<Input />
					{ showLast ? <ListItem>2</ListItem> : null }
				</List>
			);
		}

		render(<App showFirst={true} showLast={true} />, scratch);

		let input = focusInput();
		render(<App showFirst={false} showLast={true} />, scratch);
		validateFocus(input, 'move from middle to beginning');

		input = focusInput();
		render(<App showFirst={true} showLast={true} />, scratch);
		validateFocus(input, 'move from beginning to middle');

		input = focusInput();
		render(<App showFirst={true} showLast={false} />, scratch);
		validateFocus(input, 'move from middle to end');

		input = focusInput();
		render(<App showFirst={true} showLast={true} />, scratch);
		validateFocus(input, 'move from end to middle');
	});

	it('should maintain focus when adding children around input', () => {
		render((
			<List>
				<Input />
			</List>
		), scratch);

		let input = focusInput();

		render((
			<List>
				<ListItem>1</ListItem>
				<Input />
			</List>
		), scratch);
		validateFocus(input, 'insert sibling before');

		input = focusInput();

		render((
			<List>
				<ListItem>1</ListItem>
				<Input />
				<ListItem>2</ListItem>
			</List>
		), scratch);
		validateFocus(input, 'insert sibling after');

		input = focusInput();

		render((
			<List>
				<ListItem>1</ListItem>
				<Input />
				<ListItem>2</ListItem>
				<ListItem>3</ListItem>
			</List>
		), scratch);
		validateFocus(input, 'insert sibling after again');

		input = focusInput();

		render((
			<List>
				<ListItem>0</ListItem>
				<ListItem>1</ListItem>
				<Input />
				<ListItem>2</ListItem>
				<ListItem>3</ListItem>
			</List>
		), scratch);
		validateFocus(input, 'insert sibling before again');

		expect(scratch.innerHTML).to.equal('
01<input type="text">23
');
	});

	it('should maintain focus when removing elements around input', () => {
		render((
			<List>
				<ListItem>0</ListItem>
				<ListItem>1</ListItem>
				<Input />
				<ListItem>2</ListItem>
				<ListItem>3</ListItem>
			</List>
		), scratch);

		let input = focusInput();

		render((
			<List>
				<ListItem>1</ListItem>
				<Input />
				<ListItem>2</ListItem>
				<ListItem>3</ListItem>
			</List>
		), scratch);
		validateFocus(input, 'remove sibling before');

		input = focusInput();

		render((
			<List>
				<ListItem>1</ListItem>
				<Input />
				<ListItem>2</ListItem>
			</List>
		), scratch);
		validateFocus(input, 'remove sibling after');

		input = focusInput();

		render((
			<List>
				<ListItem>1</ListItem>
				<Input />
			</List>
		), scratch);
		validateFocus(input, 'remove sibling after 2');

		input = focusInput();

		render((
			<List>
				<Input />
			</List>
		), scratch);
		validateFocus(input, 'remove sibling before 2');
	});

	it('should maintain focus when adding input next to the current input', () => {
		render((
			<List>
				<InputWithId i={0} />
			</List>
		), scratch);

		let input = focusInputById();

		render((
			<List>
				<Input />
				<InputWithId i={0} />
			</List>
		), scratch);
		validateFocus(input, 'add input before');

		input = focusInputById();

		render((
			<List>
				<Input />
				<InputWithId i={0} />
				<Input />
			</List>
		), scratch);
		validateFocus(input, 'add input after');

		input = focusInputById();

		render((
			<List>
				<Input />
				<Input />
				<InputWithId i={0} />
				<Input />
			</List>
		), scratch);
		validateFocus(input, 'add input first place');

		input = focusInputById();

		render((
			<List>
				<Input />
				<Input />
				<Input />
				<InputWithId i={0} />
				<Input />
			</List>
		), scratch);
		validateFocus(input, 'add input before');
	});

	it('should maintain focus when hydrating', () => {
		const html = div([
			span('1'),
			span('2'),
			span('3'),
			inputStr()
		].join(''));

		scratch.innerHTML = html;
		const input = focusInput();

		hydrate((
			<List>
				<ListItem>1</ListItem>
				<ListItem>2</ListItem>
				<ListItem>3</ListItem>
				<Input />
			</List>
		), scratch);

		expect(scratch.innerHTML).to.equal(html);
		validateFocus(input);
	});

	it('should keep focus in Fragments', () => {

		/** @type {HTMLInputElement} */
		let input;

		/** @type {() => void} */
		let updateState;

		class App extends Component {
			constructor() {
				super();
				this.state = { active: false };
				updateState = () => this.setState(prev => ({ active: !prev.active }));
			}

			render() {
				return (

Heading

						{!this.state.active ? (
							<Fragment>
								foobar
								<Fragment>
									Hello World

yo

								</Fragment>
								<input type="text" ref={i => input = i} />
							</Fragment>
						) : (
							<Fragment>
								<Fragment>
									Hello World

yo

								</Fragment>
								foobar
								<input type="text" ref={i => input = i} />
							</Fragment>
						)}

				);
			}
		}

		render(<App />, scratch);

		input.focus();
		updateState();

		expect(document.activeElement).to.equal(input, 'Before rerender');
		rerender();

		expect(document.activeElement).to.equal(input, 'After rerender');
	});

	it('should keep text selection', () => {

		/** @type {HTMLInputElement} */
		let input;

		/** @type {() => void} */
		let updateState;

		class App extends Component {
			constructor() {
				super();
				this.state = { active: false };
				updateState = () => this.setState(prev => ({ active: !prev.active }));
			}

			render() {
				return (

Heading

						{!this.state.active ? (
							<Fragment>
								foobar
								<Fragment>
									Hello World

yo

								</Fragment>
								<input type="text" ref={i => input = i} value="foobar" />
							</Fragment>
						) : (
							<Fragment>
								<Fragment>
									Hello World

yo

								</Fragment>
								foobar
								<input type="text" ref={i => input = i} value="foobar" />
							</Fragment>
						)}

				);
			}
		}

		render(<App />, scratch);

		input.focus();
		input.setSelectionRange(2, 5);
		updateState();

		expect(document.activeElement).to.equal(input, 'Before rerender');
		rerender();

		expect(input.selectionStart).to.equal(2);
		expect(input.selectionEnd).to.equal(5);
		expect(document.activeElement).to.equal(input, 'After rerender');
	});
});
<commit_msg>chore: update tests<commit_after>import { setupRerender } from 'preact/test-utils';
import { createElement as h, render, Component, Fragment, hydrate } from '../../src/index';
import { setupScratch, teardown } from '../_util/helpers';
import { div, span, input as inputStr } from '../_util/dom';
/* eslint-disable react/jsx-boolean-value */

/** @jsx h */
describe('focus', () => {

	/** @type {HTMLDivElement} */
	let scratch;

	/** @type {() => void} */
	let rerender;

	const List = ({ children }) =>
{children}
;
	const ListItem = ({ children }) => {children};
	const InputWithId = ({ i }) => <input id={`input-${i}`} type="text" />;
	const Input = () => <input type="text" />;

	function focusInput() {
		if (!scratch) return;

		const input = scratch.querySelector('input');
		input.value = 'a word';
		input.focus();
		input.setSelectionRange(2, 5);

		expect(document.activeElement).to.equal(input);

		return input;
	}

	function focusInputById() {
		if (!scratch) return;

		const input = scratch.querySelector('#input-0');
		input.value = 'a word';
		input.focus();
		input.setSelectionRange(2, 5);

		expect(document.activeElement).to.equal(input);

		return input;
	}

	/**
	 * Validate an input tag has maintained focus
	 * @param {HTMLInputElement} input The input to validate
	 * @param {string} [message] Message to show if the activeElement is not
	 * eqaul to the `input` parameter
	 */
	function validateFocus(input, message) {
		// Check `nodeName` first to make cli output less spammy
		expect(document.activeElement.nodeName).to.equal(input.nodeName, message);
		expect(document.activeElement).to.equal(input, message);
		expect(input.selectionStart).to.equal(2);
		expect(input.selectionEnd).to.equal(5);

	}

	beforeEach(() => {
		scratch = setupScratch();
		rerender = setupRerender();
	});

	afterEach(() => {
		teardown(scratch);
	});

	it.skip('should maintain focus when swapping elements', () => {
		render((
			<List>
				<Input />
				<ListItem>fooo</ListItem>
			</List>
		), scratch);

		const input = focusInput();

		render((
			<List>
				<ListItem>fooo</ListItem>
				<Input />
			</List>
		), scratch);
		validateFocus(input);
	});

	it('should maintain focus when moving the input around', () => {
		function App({ showFirst, showLast }) {
			return (
				<List>
					{ showFirst ? <ListItem>1</ListItem> : null }
					<Input />
					{ showLast ? <ListItem>2</ListItem> : null }
				</List>
			);
		}

		render(<App showFirst={true} showLast={true} />, scratch);

		let input = focusInput();
		render(<App showFirst={false} showLast={true} />, scratch);
		validateFocus(input, 'move from middle to beginning');

		input = focusInput();
		render(<App showFirst={true} showLast={true} />, scratch);
		validateFocus(input, 'move from beginning to middle');

		input = focusInput();
		render(<App showFirst={true} showLast={false} />, scratch);
		validateFocus(input, 'move from middle to end');

		input = focusInput();
		render(<App showFirst={true} showLast={true} />, scratch);
		validateFocus(input, 'move from end to middle');
	});

	it('should maintain focus when adding children around input', () => {
		render((
			<List>
				<Input />
			</List>
		), scratch);

		let input = focusInput();

		render((
			<List>
				<ListItem key="1">1</ListItem>
				<Input />
			</List>
		), scratch);
		validateFocus(input, 'insert sibling before');

		input = focusInput();

		render((
			<List>
				<ListItem key="1">1</ListItem>
				<Input />
				<ListItem key="2">2</ListItem>
			</List>
		), scratch);
		validateFocus(input, 'insert sibling after');

		input = focusInput();

		render((
			<List>
				<ListItem key="1">1</ListItem>
				<Input />
				<ListItem key="2">2</ListItem>
				<ListItem key="3">3</ListItem>
			</List>
		), scratch);
		validateFocus(input, 'insert sibling after again');

		input = focusInput();

		render((
			<List>
				<ListItem key="0">0</ListItem>
				<ListItem key="1">1</ListItem>
				<Input />
				<ListItem key="2">2</ListItem>
				<ListItem key="3">3</ListItem>
			</List>
		), scratch);
		validateFocus(input, 'insert sibling before again');

		expect(scratch.innerHTML).to.equal('
01<input type="text">23
');
	});

	it('should maintain focus when conditional elements around input', () => {
		render((
			<List>
				<ListItem>0</ListItem>
				<ListItem>1</ListItem>
				<Input />
				<ListItem>2</ListItem>
				<ListItem>3</ListItem>
			</List>
		), scratch);

		let input = focusInput();

		render((
			<List>
				{false && <ListItem>0</ListItem>}
				<ListItem>1</ListItem>
				<Input />
				<ListItem>2</ListItem>
				<ListItem>3</ListItem>
			</List>
		), scratch);
		validateFocus(input, 'remove sibling before');

		input = focusInput();

		render((
			<List>
				{false && <ListItem>0</ListItem>}
				<ListItem>1</ListItem>
				<Input />
				<ListItem>2</ListItem>
				{false && <ListItem>3</ListItem>}
			</List>
		), scratch);
		validateFocus(input, 'remove sibling after');

		input = focusInput();

		render((
			<List>
				{false && <ListItem>0</ListItem>}
				<ListItem>1</ListItem>
				<Input />
				{false && <ListItem>2</ListItem>}
				{false && <ListItem>3</ListItem>}
			</List>
		), scratch);
		validateFocus(input, 'remove sibling after 2');

		input = focusInput();
		render((
			<List>
				{false && <ListItem>0</ListItem>}
				{false && <ListItem>1</ListItem>}
				<Input />
				{false && <ListItem>2</ListItem>}
				{false && <ListItem>3</ListItem>}
			</List>
		), scratch);
		validateFocus(input, 'remove sibling before 2');
	});

	it('should maintain focus when removing elements around input', () => {
		render((
			<List>
				<ListItem key="0">0</ListItem>
				<ListItem key="1">1</ListItem>
				<Input />
				<ListItem key="2">2</ListItem>
				<ListItem key="3">3</ListItem>
			</List>
		), scratch);

		let input = focusInput();

		render((
			<List>
				<ListItem key="1">1</ListItem>
				<Input />
				<ListItem key="2">2</ListItem>
				<ListItem key="3">3</ListItem>
			</List>
		), scratch);
		validateFocus(input, 'remove sibling before');

		input = focusInput();

		render((
			<List>
				<ListItem key="1">1</ListItem>
				<Input />
				<ListItem key="2">2</ListItem>
			</List>
		), scratch);
		validateFocus(input, 'remove sibling after');

		input = focusInput();

		render((
			<List>
				<ListItem key="1">1</ListItem>
				<Input />
			</List>
		), scratch);
		validateFocus(input, 'remove sibling after 2');

		input = focusInput();

		render((
			<List>
				<Input />
			</List>
		), scratch);
		validateFocus(input, 'remove sibling before 2');
	});

	it('should maintain focus when adding input next to the current input', () => {
		render((
			<List>
				<InputWithId i={0} />
			</List>
		), scratch);

		let input = focusInputById();

		render((
			<List>
				<Input key="1" />
				<InputWithId i={0} />
			</List>
		), scratch);
		validateFocus(input, 'add input before');

		input = focusInputById();

		render((
			<List>
				<Input key="1" />
				<InputWithId i={0} />
				<Input key="2" />
			</List>
		), scratch);
		validateFocus(input, 'add input after');

		input = focusInputById();

		render((
			<List>
				<Input key="0" />
				<Input key="1" />
				<InputWithId i={0} />
				<Input key="2" />
			</List>
		), scratch);
		validateFocus(input, 'add input first place');

		input = focusInputById();

		render((
			<List>
				<Input key="-1" />
				<Input key="0" />
				<Input key="1" />
				<InputWithId i={0} />
				<Input key="2" />
			</List>
		), scratch);
		validateFocus(input, 'add input before');
	});

	it('should maintain focus when hydrating', () => {
		const html = div([
			span('1'),
			span('2'),
			span('3'),
			inputStr()
		].join(''));

		scratch.innerHTML = html;
		const input = focusInput();

		hydrate((
			<List>
				<ListItem>1</ListItem>
				<ListItem>2</ListItem>
				<ListItem>3</ListItem>
				<Input />
			</List>
		), scratch);

		expect(scratch.innerHTML).to.equal(html);
		validateFocus(input);
	});

	it('should keep focus in Fragments', () => {

		/** @type {HTMLInputElement} */
		let input;

		/** @type {() => void} */
		let updateState;

		class App extends Component {
			constructor() {
				super();
				this.state = { active: false };
				updateState = () => this.setState(prev => ({ active: !prev.active }));
			}

			render() {
				return (

Heading

						{!this.state.active ? (
							<Fragment>
								foobar
								<Fragment>
									Hello World

yo

								</Fragment>
								<input type="text" ref={i => input = i} />
							</Fragment>
						) : (
							<Fragment>
								<Fragment>
									Hello World

yo

								</Fragment>
								foobar
								<input type="text" ref={i => input = i} />
							</Fragment>
						)}

				);
			}
		}

		render(<App />, scratch);

		input.focus();
		updateState();

		expect(document.activeElement).to.equal(input, 'Before rerender');
		rerender();

		expect(document.activeElement).to.equal(input, 'After rerender');
	});

	it('should keep text selection', () => {

		/** @type {HTMLInputElement} */
		let input;

		/** @type {() => void} */
		let updateState;

		class App extends Component {
			constructor() {
				super();
				this.state = { active: false };
				updateState = () => this.setState(prev => ({ active: !prev.active }));
			}

			render() {
				return (

Heading

						{!this.state.active ? (
							<Fragment>
								foobar
								<Fragment>
									Hello World

yo

								</Fragment>
								<input type="text" ref={i => input = i} value="foobar" />
							</Fragment>
						) : (
							<Fragment>
								<Fragment>
									Hello World

yo

								</Fragment>
								foobar
								<input type="text" ref={i => input = i} value="foobar" />
							</Fragment>
						)}

				);
			}
		}

		render(<App />, scratch);

		input.focus();
		input.setSelectionRange(2, 5);
		updateState();

		expect(document.activeElement).to.equal(input, 'Before rerender');
		rerender();

		expect(input.selectionStart).to.equal(2);
		expect(input.selectionEnd).to.equal(5);
		expect(document.activeElement).to.equal(input, 'After rerender');
	});
});
<|endoftext|>


<commit_before>// # ceb-feature-template.js

// Presently, this feature does work with IE9 and IE10.

// ## Light DOM
//
// The template can contains a node having the attribute `ceb-content`.
// The marked node is intend to host the light DOM of the current element at the end of the templating process.
//
// If the template doesn't contain this node, the light DOM will be lost.
//
// ## DOM nodes references
//
// The template can contains nodes having the attribute `ceb-ref`.
// The marked nodes will be available at the end of the templating process from the feature function (`feature(el)`).
//
// That means, if a node has the attribute `ceb-ref="header"`.
// It will be available via `feature(el).header`.

(function (g, factory) {
    'use strict';

    /* istanbul ignore next */
    if (typeof exports === 'object') {
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        define('ceb-feature-template', [], factory);
    } else {
        g.cebFeatureTemplate = factory();
    }

}(this, function () {
    'use strict';

    // ## feature function

    // The template feature's function returns the nodes' reference of the template.
    function feature(el) {
        if (!el.__cebTemplateScope) {
            el.__cebTemplateScope = {};
        }
        return el.__cebTemplateScope;
    }

    // ## Templating stuff

    // The counter is used to generate unique DOM's id.
    var counter = 0;

    // Regex to detect the *ceb-ref* attributes
    var nodesRegEx = /ceb\-ref=\W*(\w*)/igm;

    // Regex to detect the *ceb-content* attribute
    var contentRegEx = /ceb\-content/im;

    // Find recursively the content's node of the current element.
    function findContentNode(el) {
        var oldCebContentId = el.getAttribute('ceb-old-content-id');
        if (oldCebContentId) {
            return findContentNode(el.querySelector('[' + oldCebContentId + ']'));
        }
        return el;
    }

    // Apply a template to an element.
    function apply(tpl, el, isHandleLightDOM, isNodeReferences) {
        var lightChildren = [],
            refrencedNodes = [],
            template = tpl;

        if (isNodeReferences) {
            // Update the template to detect the DOM nodes references.
            var result;
            while ((result = nodesRegEx.exec(template)) !== null) {
                var property = result[1];
                // build an id of the reference
                var newAtt = 'ceb-' + (counter++) + '-ref';
                // replace the original attribute name by the idenitifer
                template = template.replace(' ceb-ref', ' ' + newAtt);
                // push the entry
                refrencedNodes.push({
                    attribute: newAtt,
                    property: property
                });
            }
        }

        if (isHandleLightDOM) {
            // Get the current content node having the light DOM nodes,
            // When the node is freshly created, the content node is the element.
            // When the node has been created by clonning, the content node is not anymore the element,
            // but a sub content node linked to one of its descents.

            var oldContentNode = findContentNode(el);

            // Remove the light DOM to keep it.
            while (oldContentNode.childNodes.length > 0) {
                lightChildren.push(oldContentNode.removeChild(oldContentNode.childNodes[0]));
            }
            // lightChildren = Array.prototype.slice.call(oldContentNode.childNodes);

            // Generate the new content's id value.
            var newCebContentId = 'ceb-' + (counter++) + '-content';

            // Replace the original attribute name by the id.
            template = template.replace(' ceb-content', ' ' + newCebContentId);

            // Keep a value of the content's id value if the node is cloned.
            el.setAttribute('ceb-old-content-id', newCebContentId);
        }

        // Transform the template string into an alive DOM nodes.
        el.innerHTML = template;

        if (isHandleLightDOM) {
            // Get the content node to add him the in pending light DOM.
            var newContentNode = findContentNode(el);
            lightChildren.forEach(function (child) {
                newContentNode.appendChild(child);
                // newContentNode.appendChild(child);
            });
        }

        if (isNodeReferences) {
            // Get the reference nodes and attach them to the element templating scope.
            refrencedNodes.forEach(function (entry) {
                feature(el)[entry.property] = el.querySelector('[' + entry.attribute + ']');
            });
        }

    }

    // ## Setup function

    // The templeting process is done before the call of the `createdCallback` method defined in the structure.
    function setup(struct, builder, options) {
        var tpl = options.template || '';
        var isHandleLightDOM = tpl.search(contentRegEx) !== -1;
        var isNodeReferences = tpl.search(nodesRegEx) !== -1;
        // Register a wrapper to the createdCallback callback in order to
        // apply the template before the original call.
        builder.wrap('createdCallback', function (next, el) {
            apply(tpl, el, isHandleLightDOM, isNodeReferences);
            next(arguments);
        });
    }
    feature.setup = setup;

    return feature;
}));
<commit_msg>add comment<commit_after>// # ceb-feature-template.js

// Work on ever green browser and IE9/IE10.
// However the template feature doesn't worl with [document-register-element](https://github.com/WebReflection/document-register-element) on IE9/IE10.

// ## Light DOM
//
// The template can contains a node having the attribute `ceb-content`.
// The marked node is intend to host the light DOM of the current element at the end of the templating process.
//
// If the template doesn't contain this node, the light DOM will be lost.
//
// ## DOM nodes references
//
// The template can contains nodes having the attribute `ceb-ref`.
// The marked nodes will be available at the end of the templating process from the feature function (`feature(el)`).
//
// That means, if a node has the attribute `ceb-ref="header"`.
// It will be available via `feature(el).header`.

(function (g, factory) {
    'use strict';

    /* istanbul ignore next */
    if (typeof exports === 'object') {
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        define('ceb-feature-template', [], factory);
    } else {
        g.cebFeatureTemplate = factory();
    }

}(this, function () {
    'use strict';

    // ## feature function

    // The template feature's function returns the nodes' reference of the template.
    function feature(el) {
        if (!el.__cebTemplateScope) {
            el.__cebTemplateScope = {};
        }
        return el.__cebTemplateScope;
    }

    // ## Templating stuff

    // The counter is used to generate unique DOM's id.
    var counter = 0;

    // Regex to detect the *ceb-ref* attributes
    var nodesRegEx = /ceb\-ref=\W*(\w*)/igm;

    // Regex to detect the *ceb-content* attribute
    var contentRegEx = /ceb\-content/im;

    // Find recursively the content's node of the current element.
    function findContentNode(el) {
        var oldCebContentId = el.getAttribute('ceb-old-content-id');
        if (oldCebContentId) {
            return findContentNode(el.querySelector('[' + oldCebContentId + ']'));
        }
        return el;
    }

    // Apply a template to an element.
    function apply(tpl, el, isHandleLightDOM, isNodeReferences) {
        var lightChildren = [],
            refrencedNodes = [],
            template = tpl;

        if (isNodeReferences) {
            // Update the template to detect the DOM nodes references.
            var result;
            while ((result = nodesRegEx.exec(template)) !== null) {
                var property = result[1];
                // build an id of the reference
                var newAtt = 'ceb-' + (counter++) + '-ref';
                // replace the original attribute name by the idenitifer
                template = template.replace(' ceb-ref', ' ' + newAtt);
                // push the entry
                refrencedNodes.push({
                    attribute: newAtt,
                    property: property
                });
            }
        }

        if (isHandleLightDOM) {
            // Get the current content node having the light DOM nodes,
            // When the node is freshly created, the content node is the element.
            // When the node has been created by clonning, the content node is not anymore the element,
            // but a sub content node linked to one of its descents.

            var oldContentNode = findContentNode(el);

            // Remove the light DOM to keep it.
            while (oldContentNode.childNodes.length > 0) {
                lightChildren.push(oldContentNode.removeChild(oldContentNode.childNodes[0]));
            }
            // lightChildren = Array.prototype.slice.call(oldContentNode.childNodes);

            // Generate the new content's id value.
            var newCebContentId = 'ceb-' + (counter++) + '-content';

            // Replace the original attribute name by the id.
            template = template.replace(' ceb-content', ' ' + newCebContentId);

            // Keep a value of the content's id value if the node is cloned.
            el.setAttribute('ceb-old-content-id', newCebContentId);
        }

        // Transform the template string into an alive DOM nodes.
        el.innerHTML = template;

        if (isHandleLightDOM) {
            // Get the content node to add him the in pending light DOM.
            var newContentNode = findContentNode(el);
            lightChildren.forEach(function (child) {
                newContentNode.appendChild(child);
                // newContentNode.appendChild(child);
            });
        }

        if (isNodeReferences) {
            // Get the reference nodes and attach them to the element templating scope.
            refrencedNodes.forEach(function (entry) {
                feature(el)[entry.property] = el.querySelector('[' + entry.attribute + ']');
            });
        }

    }

    // ## Setup function

    // The templeting process is done before the call of the `createdCallback` method defined in the structure.
    function setup(struct, builder, options) {
        var tpl = options.template || '';
        var isHandleLightDOM = tpl.search(contentRegEx) !== -1;
        var isNodeReferences = tpl.search(nodesRegEx) !== -1;
        // Register a wrapper to the createdCallback callback in order to
        // apply the template before the original call.
        builder.wrap('createdCallback', function (next, el) {
            apply(tpl, el, isHandleLightDOM, isNodeReferences);
            next(arguments);
        });
    }
    feature.setup = setup;

    return feature;
}));
<|endoftext|>


<commit_before>/*
Name:             bolcom.js
Description:      Module for node.js to access Bol.com Open API service
Author:           Franklin van de Meent (https://frankl.in)
Source:           https://github.com/fvdm/nodejs-bolcom
Bugs & feedback:  https://github.com/fvdm/nodejs-bolcom/issues
License:          Unlicense / Public Domain

Service name:     Bol.com
Service docs:     https://developers.bol.com
*/

var http = require ('httpreq');

var settings = {
  apikey: null,
  sessionId: null,
  timeout: 5000
};

/**
 * Clean up product data
 *
 * @param product {object} - Object to clean
 * @returns product {object} - Cleaned object
 */

function cleanProduct (product) {
  var purls = {};
  var pimgs = {};
  var groups = {};
  var group;
  var image;
  var attrib;
  var url;
  var i;
  var a;

  // urls
  try {
    for (i = 0; i < product.urls.length; i++) {
      url = product.urls [i];
      purls [url.key] = url;
    }
    product.urls = purls;
  } catch (e) {
    // skip
  }

  // images
  try {
    for (i = 0; i < product.images.length; i++) {
      image = product.images [i];
      pimgs [image.key] = image;
    }

    product.images = pimgs;
  } catch (e) {
    // skip
  }

  // includeattributes: true
  try {
    if (product.attributeGroups) {
      for (i = 0; i < product.attributeGroups.length; i++) {
        group = product.attributeGroups [i];
        groups [group.title] = {
          title: group.title
        };

        if (group.attributes) {
          groups [group.title] .attributes = {};

          for (a = 0; a < group.attributes.length; a++) {
            attrib = group.attributes [a];
            groups [group.title] .attributes [attrib.key] = attrib;
          }
        }
      }

      product.attributeGroups = groups;
    }
  } catch (e) {
    // skip
  }

  return product;
}

/**
 * Process HTTP response
 *
 * @callback callback
 * @param err {Error,null} - Error instance
 * @param res {object} - Response object
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function processResponse (err, res, callback) {
  var data = '';
  var error = null;

  res = res || {
    body: '',
    statusCode: null
  };

  if (err) {
    error = new Error ('request failed');
    error.err = err;
    callback (error);
    return;
  }

  if (res.statusCode >= 300) {
    error = new Error ('API error');
  }

  try {
    data = JSON.parse (res.body);
  } catch (e) {
    error = new Error ('invalid response');
    error.err = e;
  }

  if (error) {
    error.code = res.statusCode;
    error.api = data instanceof Object ? data : {};
    error.body = data instanceof Object ? null : res.body;

    callback (error);
    return;
  }

  callback (null, data);
}

/**
 * Communication with API
 *
 * @callback callback
 * @param cat {string} - api.bol.com/:CAT/v4/method
 * @param method {string} - api.bol.com/cat/v4/:METHOD
 * @param [params] {object} - Request paramaters
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function talk (cat, method, params, callback) {
  var options = {
    url: 'https://api.bol.com/' + cat + '/v4/' + method,
    method: 'GET',
    timeout: settings.timeout,
    headers: {
      'User-Agent': 'bolcom.js (https://www.npmjs.com/package/bolcom)'
    }
  };

  // params is optional
  if (typeof params === 'function') {
    callback = params;
    params = {};
  }

  // check api key
  if (!settings.apikey) {
    callback (new Error ('missing apikey'));
    return;
  }

  // check session ID
  if (settings.sessionId) {
    options.headers ['X-OpenAPI-Session-ID'] = settings.sessionId;
  }

  params = params instanceof Object ? params : {};
  params.apikey = settings.apikey;
  params.format = 'json';
  options.parameters = params;

  // process response
  http.doRequest (options, function (err, res) {
    processResponse (err, res, callback);
  });
}

/**
 * Method: utils.ping
 *
 * @callback callback
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function methodUtilsPing (callback) {
  talk ('utils', 'ping', callback);
}

/**
 * Method: account.sessions
 *
 * @callback callback
 * @returns {void}
 */

function methodAccountSessions (callback) {
  talk ('accounts', 'sessions', callback);
}

/**
 * Method: catalog.search
 *
 * @callback callback
 * @param props {object} - Method parameters
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function methodCatalogSearch (props, callback) {
  talk ('catalog', 'search', props, function (err, data) {
    var i;

    if (err) {
      callback (err);
      return;
    }

    if (data.products instanceof Array) {
      for (i = 0; i < data.products.length; i++) {
        data.products [i] = cleanProduct (data.products [i]);
      }
    }

    callback (err, data);
  });
}

/**
 * Method: catalog.lists
 *
 * @callback callback
 * @param props {object} - Method parameters
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function methodCatalogLists (props, callback) {
  talk ('catalog', 'lists', props, function (err, data) {
    var i;

    if (err) {
      callback (err);
      return;
    }

    if (data.products instanceof Array) {
      for (i = 0; i < data.products.length; i++) {
        data.products [i] = cleanProduct (data.products [i]);
      }
    }

    callback (err, data);
  });
}

/**
 * Method: catalog.products
 *
 * @callback callback
 * @param productId {string} - Product ID
 * @param [props] {object} - Method parameters
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function methodCatalogProducts (productId, props, callback) {
  if (typeof props === 'function') {
    callback = props;
    props = {};
  }

  talk ('catalog', 'products/' + productId, props, function (err, data) {
    var i;

    if (err) {
      callback (err);
      return;
    }

    if (data.products instanceof Array) {
      for (i = 0; i < data.products.length; i++) {
        data.products [i] = cleanProduct (data.products [i]);
      }
    }

    callback (err, data);
  });
}

/**
 * Method: catalog.offers
 *
 * @callback callback
 * @param productId {string} - Product ID
 * @param [props] {object} - Method parameters
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function methodCatalogOffers (productId, props, callback) {
  if (typeof props === 'function') {
    callback = props;
    props = {};
  }

  talk ('catalog', 'offers/' + productId, props, function (err, data) {
    if (err) {
      callback (err);
      return;
    }

    if (data.offerData) {
      data = data.offerData;
    }

    callback (err, data);
  });
}

/**
 * Method: catalog.recommendations
 *
 * @callback callback
 * @param productId {string} - Product ID
 * @param [props] {object} - Method parameters
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function methodCatalogRecommendations (productId, props, callback) {
  if (typeof props === 'function') {
    callback = props;
    props = {};
  }

  talk ('catalog', 'recommendations/' + productId, props, function (err, data) {
    var i;

    if (err) {
      callback (err);
      return;
    }

    if (data.products) {
      data = data.products;

      if (data instanceof Array && data.length >= 1) {
        for (i = 0; i < data.length; i++) {
          data [i] = cleanProduct (data [i]);
        }
      }
    }

    callback (err, data);
  });
}

/**
 * Method: catalog.relatedproducts
 *
 * @callback callback
 * @param productId {string} - Product ID
 * @param [props] {object} - Method parameters
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function methodCatalogRelatedProducts (productId, props, callback) {
  if (typeof props === 'function') {
    callback = props;
    props = {};
  }

  talk ('catalog', 'relatedproducts/' + productId, props, function (err, data) {
    var tmp = {};
    var tmp2 = {};
    var i;
    var m;

    if (err) {
      callback (err);
      return;
    }

    if (data.productFamilies) {
      data = data.productFamilies;

      if (data instanceof Array && data.length >= 1 && data [0] .key) {
        for (i = 0; i < data.length; i++) {
          tmp [data [i] .key] = data [i];

          if (data [i] .productFamilyMembers instanceof Array) {
            tmp2 = {};

            for (m = 0; m < data [i] .productFamilyMembers.length; m++) {
              tmp2 [data [i] .productFamilyMembers [m] .label] = data [i] .productFamilyMembers [m];
            }

            tmp [data [i] .key] .productFamilyMembers = tmp2;
          }
        }

        data = tmp;
      }
    }

    callback (err, data);
  });
}

function methodAccountWishlists (callback) {
  talk ('accounts', 'wishlists', callback);
}

/**
 * Module setup
 *
 * @param apikey {string} - Your Bol.com API key
 * @param [timeout] {number=5000} - Request time out in ms
 * @returns {object} - Module interface
 */

module.exports = function (apikey, sessionId, timeout) {
  if (typeof sessionId === 'number') {
    timeout = sessionId;
    sessionId = null;
  }

  settings.apikey = apikey;
  settings.sessionId = sessionId;
  settings.timeout = timeout || settings.timeout;

  return {
    catalog: {
      search: methodCatalogSearch,
      lists: methodCatalogLists,
      products: methodCatalogProducts,
      offers: methodCatalogOffers,
      recommendations: methodCatalogRecommendations,
      relatedproducts: methodCatalogRelatedProducts
    },
    utils: {
      ping: methodUtilsPing
    },
    account: {
      sessions: methodAccountSessions,
      wishlists: methodAccountWishlists
    }
  };
};
<commit_msg>JSdoc methodAccountWishlists<commit_after>/*
Name:             bolcom.js
Description:      Module for node.js to access Bol.com Open API service
Author:           Franklin van de Meent (https://frankl.in)
Source:           https://github.com/fvdm/nodejs-bolcom
Bugs & feedback:  https://github.com/fvdm/nodejs-bolcom/issues
License:          Unlicense / Public Domain

Service name:     Bol.com
Service docs:     https://developers.bol.com
*/

var http = require ('httpreq');

var settings = {
  apikey: null,
  sessionId: null,
  timeout: 5000
};

/**
 * Clean up product data
 *
 * @param product {object} - Object to clean
 * @returns product {object} - Cleaned object
 */

function cleanProduct (product) {
  var purls = {};
  var pimgs = {};
  var groups = {};
  var group;
  var image;
  var attrib;
  var url;
  var i;
  var a;

  // urls
  try {
    for (i = 0; i < product.urls.length; i++) {
      url = product.urls [i];
      purls [url.key] = url;
    }
    product.urls = purls;
  } catch (e) {
    // skip
  }

  // images
  try {
    for (i = 0; i < product.images.length; i++) {
      image = product.images [i];
      pimgs [image.key] = image;
    }

    product.images = pimgs;
  } catch (e) {
    // skip
  }

  // includeattributes: true
  try {
    if (product.attributeGroups) {
      for (i = 0; i < product.attributeGroups.length; i++) {
        group = product.attributeGroups [i];
        groups [group.title] = {
          title: group.title
        };

        if (group.attributes) {
          groups [group.title] .attributes = {};

          for (a = 0; a < group.attributes.length; a++) {
            attrib = group.attributes [a];
            groups [group.title] .attributes [attrib.key] = attrib;
          }
        }
      }

      product.attributeGroups = groups;
    }
  } catch (e) {
    // skip
  }

  return product;
}

/**
 * Process HTTP response
 *
 * @callback callback
 * @param err {Error,null} - Error instance
 * @param res {object} - Response object
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function processResponse (err, res, callback) {
  var data = '';
  var error = null;

  res = res || {
    body: '',
    statusCode: null
  };

  if (err) {
    error = new Error ('request failed');
    error.err = err;
    callback (error);
    return;
  }

  if (res.statusCode >= 300) {
    error = new Error ('API error');
  }

  try {
    data = JSON.parse (res.body);
  } catch (e) {
    error = new Error ('invalid response');
    error.err = e;
  }

  if (error) {
    error.code = res.statusCode;
    error.api = data instanceof Object ? data : {};
    error.body = data instanceof Object ? null : res.body;

    callback (error);
    return;
  }

  callback (null, data);
}

/**
 * Communication with API
 *
 * @callback callback
 * @param cat {string} - api.bol.com/:CAT/v4/method
 * @param method {string} - api.bol.com/cat/v4/:METHOD
 * @param [params] {object} - Request paramaters
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function talk (cat, method, params, callback) {
  var options = {
    url: 'https://api.bol.com/' + cat + '/v4/' + method,
    method: 'GET',
    timeout: settings.timeout,
    headers: {
      'User-Agent': 'bolcom.js (https://www.npmjs.com/package/bolcom)'
    }
  };

  // params is optional
  if (typeof params === 'function') {
    callback = params;
    params = {};
  }

  // check api key
  if (!settings.apikey) {
    callback (new Error ('missing apikey'));
    return;
  }

  // check session ID
  if (settings.sessionId) {
    options.headers ['X-OpenAPI-Session-ID'] = settings.sessionId;
  }

  params = params instanceof Object ? params : {};
  params.apikey = settings.apikey;
  params.format = 'json';
  options.parameters = params;

  // process response
  http.doRequest (options, function (err, res) {
    processResponse (err, res, callback);
  });
}

/**
 * Method: utils.ping
 *
 * @callback callback
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function methodUtilsPing (callback) {
  talk ('utils', 'ping', callback);
}

/**
 * Method: account.sessions
 *
 * @callback callback
 * @returns {void}
 */

function methodAccountSessions (callback) {
  talk ('accounts', 'sessions', callback);
}

/**
 * Method: catalog.search
 *
 * @callback callback
 * @param props {object} - Method parameters
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function methodCatalogSearch (props, callback) {
  talk ('catalog', 'search', props, function (err, data) {
    var i;

    if (err) {
      callback (err);
      return;
    }

    if (data.products instanceof Array) {
      for (i = 0; i < data.products.length; i++) {
        data.products [i] = cleanProduct (data.products [i]);
      }
    }

    callback (err, data);
  });
}

/**
 * Method: catalog.lists
 *
 * @callback callback
 * @param props {object} - Method parameters
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function methodCatalogLists (props, callback) {
  talk ('catalog', 'lists', props, function (err, data) {
    var i;

    if (err) {
      callback (err);
      return;
    }

    if (data.products instanceof Array) {
      for (i = 0; i < data.products.length; i++) {
        data.products [i] = cleanProduct (data.products [i]);
      }
    }

    callback (err, data);
  });
}

/**
 * Method: catalog.products
 *
 * @callback callback
 * @param productId {string} - Product ID
 * @param [props] {object} - Method parameters
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function methodCatalogProducts (productId, props, callback) {
  if (typeof props === 'function') {
    callback = props;
    props = {};
  }

  talk ('catalog', 'products/' + productId, props, function (err, data) {
    var i;

    if (err) {
      callback (err);
      return;
    }

    if (data.products instanceof Array) {
      for (i = 0; i < data.products.length; i++) {
        data.products [i] = cleanProduct (data.products [i]);
      }
    }

    callback (err, data);
  });
}

/**
 * Method: catalog.offers
 *
 * @callback callback
 * @param productId {string} - Product ID
 * @param [props] {object} - Method parameters
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function methodCatalogOffers (productId, props, callback) {
  if (typeof props === 'function') {
    callback = props;
    props = {};
  }

  talk ('catalog', 'offers/' + productId, props, function (err, data) {
    if (err) {
      callback (err);
      return;
    }

    if (data.offerData) {
      data = data.offerData;
    }

    callback (err, data);
  });
}

/**
 * Method: catalog.recommendations
 *
 * @callback callback
 * @param productId {string} - Product ID
 * @param [props] {object} - Method parameters
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function methodCatalogRecommendations (productId, props, callback) {
  if (typeof props === 'function') {
    callback = props;
    props = {};
  }

  talk ('catalog', 'recommendations/' + productId, props, function (err, data) {
    var i;

    if (err) {
      callback (err);
      return;
    }

    if (data.products) {
      data = data.products;

      if (data instanceof Array && data.length >= 1) {
        for (i = 0; i < data.length; i++) {
          data [i] = cleanProduct (data [i]);
        }
      }
    }

    callback (err, data);
  });
}

/**
 * Method: catalog.relatedproducts
 *
 * @callback callback
 * @param productId {string} - Product ID
 * @param [props] {object} - Method parameters
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function methodCatalogRelatedProducts (productId, props, callback) {
  if (typeof props === 'function') {
    callback = props;
    props = {};
  }

  talk ('catalog', 'relatedproducts/' + productId, props, function (err, data) {
    var tmp = {};
    var tmp2 = {};
    var i;
    var m;

    if (err) {
      callback (err);
      return;
    }

    if (data.productFamilies) {
      data = data.productFamilies;

      if (data instanceof Array && data.length >= 1 && data [0] .key) {
        for (i = 0; i < data.length; i++) {
          tmp [data [i] .key] = data [i];

          if (data [i] .productFamilyMembers instanceof Array) {
            tmp2 = {};

            for (m = 0; m < data [i] .productFamilyMembers.length; m++) {
              tmp2 [data [i] .productFamilyMembers [m] .label] = data [i] .productFamilyMembers [m];
            }

            tmp [data [i] .key] .productFamilyMembers = tmp2;
          }
        }

        data = tmp;
      }
    }

    callback (err, data);
  });
}

/**
 * Method: account.wishlists
 *
 * @callback callback
 * @param callback {function} - `function (err, data) {}`
 * @returns {void}
 */

function methodAccountWishlists (callback) {
  talk ('accounts', 'wishlists', callback);
}

/**
 * Module setup
 *
 * @param apikey {string} - Your Bol.com API key
 * @param [timeout] {number=5000} - Request time out in ms
 * @returns {object} - Module interface
 */

module.exports = function (apikey, sessionId, timeout) {
  if (typeof sessionId === 'number') {
    timeout = sessionId;
    sessionId = null;
  }

  settings.apikey = apikey;
  settings.sessionId = sessionId;
  settings.timeout = timeout || settings.timeout;

  return {
    catalog: {
      search: methodCatalogSearch,
      lists: methodCatalogLists,
      products: methodCatalogProducts,
      offers: methodCatalogOffers,
      recommendations: methodCatalogRecommendations,
      relatedproducts: methodCatalogRelatedProducts
    },
    utils: {
      ping: methodUtilsPing
    },
    account: {
      sessions: methodAccountSessions,
      wishlists: methodAccountWishlists
    }
  };
};
<|endoftext|>


<commit_before>"use strict";

/*global module, require*/

var _ = require("lodash"),
    d3 = require("d3"),
    interopModule = require("gitit-interop"),
    layerPrefix = "layers/",
    layerFileExt = ".geojson",
    prefixLen = layerPrefix.length,
    extLen = layerFileExt.length,
    selectedLimit = 10,
    mapPrefix = "maps/";

var layerName = function(path) {
    return path.slice(prefixLen, path.length - extLen);
};

var mapName = function(path) {
    return path.indexOf(mapPrefix) === 0 ? path.slice(mapPrefix.length) : path;
};

module.exports = function(errors, container, toolbar, layers, worksheet, selection, title, findShapesByName, redraw) {
    var interop = interopModule(errors.warnUser),

	s = interop.schema,
	pageAsMarkdown = s.pageAsMarkdown,
	multiple = s.multiple,
	optional = s.optional,
	boolean = s.boolean,
	float = s.float,
	text = s.text,
	pageLink = s.pageLink,
	fileLink = s.fileLink,

	schema = {
	    layers: multiple({
		layer: fileLink,
		opacity: optional(float(0, 1), 1)
	    }),
	    selection: multiple({
		selection: text
	    }),
	    selectionPage: {
		"selection page": pageLink
	    },
	    sort: multiple({
		sort: text,
		reverse: boolean
	    })
	};

    var wikiLoad = function(page) {
	interop.parser.loadPagesStartingFrom(
	    page,
	    schema,
	    function(pageData, fileData) {
		fileData.entries().forEach(function(e) {
		    layers.create(
			layerName(e.key),
			e.value.features,
			e.value.bbox);
		});

		var pages = pageData.entries().forEach(function(e) {
		    var loaded = e.value;

		    if (loaded.has("layers")) {
			loaded.get("layers").forEach(function(l) {
			    var layer = layers.get(layerName(l.get("layer")));
			    layer.setOpacity(l.get("opacity"));
			});
		    }

		    if (loaded.has("selection")) {
			selection.select(
			    findShapesByName(
				d3.set(
				    loaded.get("selection").map(function(s) {
					return s.get("selection");
				    }))));
		    }

		    if (loaded.has("sort")) {
			loaded.get("sort").forEach(function(s) {
			    worksheet.sortProperty(s.get("sort"), true);
			    if (s.get("reverse")) {
				// Additional sort on the same property reverses it.
				worksheet.sortProperty(s.get("sort"), true);
			    }
			});
		    }

		});

		title.title(mapName(page));

		redraw();
	    },
	    errors.warnUser
	);
    },

	savePages = function() {
	    var mapPage = mapPrefix + title.title();

	    var selectedThings = selection.names()
		    .map(function(s) {
			return {selection: s};
		    }),

		frontPage = {
		    name: mapPage,
		    content: {
			layers: layers.names().map(function(layerName) {
			    var l = layers.get(layerName);
			    return {
				layer: layerPrefix + layerName + layerFileExt,
				opacity: l.options.opacity
			    };
			}),

			sort: _.zip(
			    worksheet.getSortProperties().properties,
			    worksheet.getSortProperties().reverse)
			    .map(function(pair) {
				return {
				    sort: pair[0],
				    reverse: pair[1]
				};
			    })
		    }
		},
		data = [frontPage];

	    if (selectedThings.length > 10) {
		frontPage.content.selectionPage = {
		    "selection page": mapPage + "/selection"
		};

		data.push({
		    name: mapPage + "/selection",
		    content: {
			selection: selectedThings
		    }
		});

	    } else {
		frontPage.content.selection = selectedThings;
	    }

	    return data.map(function(d) {
		return {
		    name: d.name,
		    content: interop.parser.pageAsMarkdown(d.content, schema)
		};
	    });
	},

	display = interop.makeDisplay(
	    container,
	    toolbar,
	    function onWikiSave(logMessage) {
		var files = layers.names().map(function(layerName) {
		    var layer = layers.get(layerName);
		    return {
			name: layerPrefix + layerName + layerFileExt,
			content: JSON.stringify({
			    type: "FeatureCollection",
			    features: layer.geometry(),
			    bbox: layer.boundingbox()
			})
		    };
		});

		interop.requests.save(
		    savePages(),
		    files,
		    logMessage,
		    errors.informUser,
		    errors.warnUser
		);
	    },
	    wikiLoad,
	    errors.informUser,
	    "Map of " + title.title
	);

    title.onChange(display.wikiPage);
    display.wikiPage(title.title());

    return {
	loadPage: function(page) {
	    wikiLoad(page);
	}
    };
};<commit_msg>Fixed embedding.<commit_after>"use strict";

/*global module, require*/

var _ = require("lodash"),
    d3 = require("d3"),
    interopModule = require("gitit-interop"),
    layerPrefix = "layers/",
    layerFileExt = ".geojson",
    prefixLen = layerPrefix.length,
    extLen = layerFileExt.length,
    selectedLimit = 10,
    mapPrefix = "maps/";

var layerName = function(path) {
    return path.slice(prefixLen, path.length - extLen);
};

var mapName = function(path) {
    return path.indexOf(mapPrefix) === 0 ? path.slice(mapPrefix.length) : path;
};

module.exports = function(errors, container, toolbar, layers, worksheet, selection, title, findShapesByName, redraw) {
    var interop = interopModule(errors.warnUser),

	s = interop.schema,
	pageAsMarkdown = s.pageAsMarkdown,
	multiple = s.multiple,
	optional = s.optional,
	boolean = s.boolean,
	float = s.float,
	text = s.text,
	pageLink = s.pageLink,
	fileLink = s.fileLink,

	schema = {
	    layers: multiple({
		layer: fileLink,
		opacity: optional(float(0, 1), 1)
	    }),
	    selection: multiple({
		selection: text
	    }),
	    selectionPage: {
		"selection page": pageLink
	    },
	    sort: multiple({
		sort: text,
		reverse: boolean
	    })
	};

    var wikiLoad = function(page) {
	interop.parser.loadPagesStartingFrom(
	    page,
	    schema,
	    function(pageData, fileData) {
		fileData.entries().forEach(function(e) {
		    layers.create(
			layerName(e.key),
			e.value.features,
			e.value.bbox);
		});

		var pages = pageData.entries().forEach(function(e) {
		    var loaded = e.value;

		    if (loaded.has("layers")) {
			loaded.get("layers").forEach(function(l) {
			    var layer = layers.get(layerName(l.get("layer")));
			    layer.setOpacity(l.get("opacity"));
			});
		    }

		    if (loaded.has("selection")) {
			selection.select(
			    findShapesByName(
				d3.set(
				    loaded.get("selection").map(function(s) {
					return s.get("selection");
				    }))));
		    }

		    if (loaded.has("sort")) {
			loaded.get("sort").forEach(function(s) {
			    worksheet.sortProperty(s.get("sort"), true);
			    if (s.get("reverse")) {
				// Additional sort on the same property reverses it.
				worksheet.sortProperty(s.get("sort"), true);
			    }
			});
		    }

		});

		title.title(mapName(page));

		redraw();
	    },
	    errors.warnUser
	);
    },

	savePages = function() {
	    var mapPage = mapPrefix + title.title();

	    var selectedThings = selection.names()
		    .map(function(s) {
			return {selection: s};
		    }),

		frontPage = {
		    name: mapPage,
		    content: {
			layers: layers.names().map(function(layerName) {
			    var l = layers.get(layerName);
			    return {
				layer: layerPrefix + layerName + layerFileExt,
				opacity: l.options.opacity
			    };
			}),

			sort: _.zip(
			    worksheet.getSortProperties().properties,
			    worksheet.getSortProperties().reverse)
			    .map(function(pair) {
				return {
				    sort: pair[0],
				    reverse: pair[1]
				};
			    })
		    }
		},
		data = [frontPage];

	    if (selectedThings.length > 10) {
		frontPage.content.selectionPage = {
		    "selection page": mapPage + "/selection"
		};

		data.push({
		    name: mapPage + "/selection",
		    content: {
			selection: selectedThings
		    }
		});

	    } else {
		frontPage.content.selection = selectedThings;
	    }

	    return data.map(function(d) {
		return {
		    name: d.name,
		    content: interop.parser.pageAsMarkdown(d.content, schema)
		};
	    });
	},

	display = interop.makeDisplay(
	    container,
	    toolbar,
	    function onWikiSave(logMessage) {
		var files = layers.names().map(function(layerName) {
		    var layer = layers.get(layerName);
		    return {
			name: layerPrefix + layerName + layerFileExt,
			content: JSON.stringify({
			    type: "FeatureCollection",
			    features: layer.geometry(),
			    bbox: layer.boundingbox()
			})
		    };
		});

		interop.requests.save(
		    savePages(),
		    files,
		    logMessage,
		    errors.informUser,
		    errors.warnUser
		);
	    },
	    wikiLoad,
	    errors.informUser,
	    function() {
		return "Map of " + title.title();
	    }
	);

    title.onChange(display.wikiPage);
    display.wikiPage(title.title());

    return {
	loadPage: function(page) {
	    wikiLoad(page);
	}
    };
};<|endoftext|>


<commit_before>var babel = require('babel-core')

module.exports = function (wallaby) {
  return {
    // set `load: false` to all of the browserified source files and tests,
    // as they should not be loaded in browser,
    // their browserified versions will be loaded instead
    files: [
      {pattern: 'src/**/*.js', load: true}
    ],

    tests: [
      {pattern: 'test/**/*.js', load: true}
    ],

    compilers: {
      "**/*.js": wallaby.compilers.babel({
        babel: babel,
        presets: [
          'es2015'
        ]
      })
    },

    env: {
      type: 'node'
    },

    testFramework: 'mocha',

    debug: true
  };
};
<commit_msg>chore(wallaby) remove wallaby.js config<commit_after><|endoftext|>


<commit_before>  var fs = require('fs');

  function addDeps(names) {
    var pkg
      , deps
      ;

    try {
      pkg = JSON.parse(fs.readFileSync('package.json'));
    } catch(e) {
      pkg = {
          name: __dirname.split('/').pop()
        , version: '0.1.0'
        , dependencies: {}
        , browserDependencies: {}
        , devDependencies: {}
      }
    }

    deps = pkg.browserDependencies = pkg.browserDependencies || {}

    if (!names.length) {
      return
    }

    names.forEach(function (name) {
      var nameParts
        , version
        ;

      nameParts = name.split('@')
      version = nameParts.pop()
      // strictly speaking, `@' shouldn't be allowed in a name
      // but no harm in allowing it if it is there I supposed
      name = nameParts.join('@')

      if (deps[name] && !version) {
        return;
      }

      deps[name] = version || '*';
    });

    fs.writeFileSync('package.json', JSON.stringify(pkg, null, '  '), 'utf8');
  }

  addDep(['futures@ ~v 0.1']);
<commit_msg>parsing package version correctly(-ish), finally<commit_after>/*jshint strict:true node:true es5:true onevar:true laxcomma:true laxbreak:true eqeqeq:true immed:true latedef:true*/
(function () {
  "use strict";

  var fs = require('fs')
    , semver = require('semver')
    , forEachAsync = require('forEachAsync')
    , npm = require('npm')
    , reNoMaxVer = /^\s*>=?\s*[^<>=\s]+\s*$/
    ;

  function prettyVer(ver) {
    return semver.validRange(ver).replace(/-\s+/g, ' ').replace(/-$/, '') || '*';
  }

  function fixDeps(deps) {
    var depsObj = {}
      ;

    if (!Array.isArray(deps)) {
      if ('object' === typeof deps) {
        return deps;
      }

      console.error('dependencies or browserDependencies was neither array nor object:');
      console.error(deps);
      console.error('Replacing with empty object: {}');
      return {};
    }

    deps.forEach(function (dep) {
      var depParts = dep.split('@')
        ;

      depsObj[depParts[0]] = prettyVer(depParts[1]);
    });

    return depsObj;
  }

  function promoteVersion(ver) {
    var vers = semver.parse(ver)
      ;

    if (Number(vers[1])) {
      return '>=' + ver + ' <' + (Number(vers[1]) + 1);
    }

    if (Number(vers[2])) {
      return '>=' + ver + ' < 0.' + (Number(vers[2]) + 1);
    }

    return  '=0.0.' + vers[3];
  }

  // prefer cli value over package.json value
  // add max if not present
  function getSafeVer(orig, current) {
    var name = orig.split('@').shift()
      , origVer = orig.split('@').pop()
      , curVer = current.split('@').pop()
      , version
      ;

    if (origVer) {
      version = origVer;
    } else {
      version = curVer;
    }

    version = version.replace(/v/g, '');

    // find >= without
    if (reNoMaxVer.test(version)) {
      version = curVer.replace(/>=?/g, ' ');
      return prettyVer(promoteVersion(version));
    }

    // already has more than one
    // TODO might be max-only (no min)
    if (/[~<>]=?/.test(version)) {
      return prettyVer(version);
    }

    if ('*' === version) {
      version = curVer;
    }
    return prettyVer('~' + (curVer));
  }

  function installEach(next, nameAndVer) {
    /*jshint validthis:true*/
    var deps = this
      ;

    //npm.commands.view([nameAndVer], function (err, mapByVersion) {
    npm.commands.install([nameAndVer], function (err, arr, obj, str) {
      var pkgName = nameAndVer.split('@').shift().trim()
        , safeVer
        , installed
        , installedDetails
        ;

      if (err) {
        console.error(err);
        return;
      }

      arr.some(function (details) {
        if (details[0].match(pkgName)) {
          installed = details[0];
          return true;
        }
      });
      Object.keys(obj).some(function (dir) {
        if (installed === obj[dir].what) {
          installedDetails = obj[dir];
          return true;
        }
      });

      if (!installedDetails) {
        console.error(pkgName, installed, obj);
      }

      safeVer = getSafeVer(nameAndVer, installedDetails.what);
      deps[pkgName] = safeVer;

      next();
    });
  }

  function addDeps(names) {
    var pkg
      , deps
      ;

    // fix unsafe version schemes
    function addLaxVersions(name) {
      var version = deps[name]
        ;

      delete deps[name];
      name = name.trim();
      version = version.trim();
      deps[name] = version;

      // fix invalid versions (including *)
      if ('*' === version || !semver.validRange(version)) {
        console.error('invalid version for', name, '@', version, 'assuming ', name, '@ *');
        version = '';
      }

      // fix any >= entries without <
      if (!version || reNoMaxVer.test(version)) {
        console.error('bad version:', name, version);
        // avoid duplicates
        if (names.every(function (_name) {
          return name.trim() !== _name.trim();
        })) {
          if (version) {
            names.push(name + '@' + prettyVer(version));
          } else {
            names.push(name);
          }
        }
      }
    }

    try {
      pkg = JSON.parse(fs.readFileSync('package.json'));
    } catch(e) {
      pkg = {
          name: __dirname.split('/').pop()
        , version: '0.1.0'
        , maintainers: []
        , dependencies: {}
        , devDependencies: {}
      };
      console.error(e);
    }

    if (pkg.dependencies) {
      deps = pkg.dependencies = fixDeps(pkg.dependencies);
    } else if (pkg.browserDependencies) {
      deps = pkg.browserDependencies = fixDeps(pkg.browserDependencies);
    } else {
      deps = pkg.dependencies = {};
    }

    Object.keys(deps).forEach(addLaxVersions);
    names.forEach(function (nameAndVer, i) {
      var parts = nameAndVer.split('@')
        , name = parts.shift()
        , ver = prettyVer(parts.pop() || '*')
        ;

      names[i] = name.trim() + '@' + ver.trim();
    });

    forEachAsync(names, installEach, deps).then(function () {
      fs.writeFile('package.json', JSON.stringify(pkg, null, '  '), 'utf8', function (err) {
        if (err) {
          console.error(err);
        }

        console.log(deps);
      });
    });
  }

  function onNpmReady() {
    var pkgs = Array.prototype.splice.call(process.argv, 2)
      ;

    if (pkgs.length < 2) {
      pkgs.push('futures@ ~v1.9');
      pkgs.push('sequence @ 2.x');
    }

    addDeps(pkgs);
  }

  npm.load({ loglevel: 'silent' }, onNpmReady);
}());
<|endoftext|>


<commit_before>/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

/**
 * Tests that chrome debugging works.
 */

var gClient, gThreadClient;
var gNewGlobal = promise.defer();
var gNewChromeSource = promise.defer();

var { DevToolsLoader } = ChromeUtils.import(
  "resource://devtools/shared/Loader.jsm",
  {}
);
var customLoader = new DevToolsLoader();
customLoader.invisibleToDebugger = true;
var { DebuggerServer } = customLoader.require("devtools/server/main");
var { DebuggerClient } = require("devtools/shared/client/debugger-client");

function initDebuggerClient() {
  DebuggerServer.init();
  DebuggerServer.registerAllActors();
  DebuggerServer.allowChromeProcess = true;

  let transport = DebuggerServer.connectPipe();
  return new DebuggerClient(transport);
}

async function attachThread(client, actor) {
  let [response, tabClient] = await client.attachTarget(actor);
  let [response2, threadClient] = await tabClient.attachThread(null);
  return threadClient;
}

function onNewGlobal() {
  ok(true, "Received a new chrome global.");
  gClient.removeListener("newGlobal", onNewGlobal);
  gNewGlobal.resolve();
}

function onNewSource(event, packet) {
  if (packet.source.url.startsWith("chrome:")) {
    ok(true, "Received a new chrome source: " + packet.source.url);
    gThreadClient.removeListener("newSource", onNewSource);
    gNewChromeSource.resolve();
  }
}

function resumeAndCloseConnection() {
  return new Promise(resolve => {
    gThreadClient.resume(() => resolve(gClient.close()));
  });
}

registerCleanupFunction(function() {
  gClient = null;
  gThreadClient = null;
  gNewGlobal = null;
  gNewChromeSource = null;

  customLoader = null;
  DebuggerServer = null;
});

add_task(async function() {
  gClient = initDebuggerClient();

  const [type] = await gClient.connect();
  is(type, "browser", "Root actor should identify itself as a browser.");

  const response = await gClient.getProcess();
  let actor = response.form.actor;
  gThreadClient = await attachThread(gClient, actor);
  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:mozilla");

  // listen for a new source and global
  gThreadClient.addListener("newSource", onNewSource);
  gClient.addListener("newGlobal", onNewGlobal);
  await promise.all([gNewGlobal.promise, gNewChromeSource.promise]);

  await resumeAndCloseConnection();
});
<commit_msg>[sync] Bug 1494319 - Remove unused "newGlobal" event r=yulia<commit_after>/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

/**
 * Tests that chrome debugging works.
 */

var gClient, gThreadClient;
var gNewChromeSource = promise.defer();

var { DevToolsLoader } = ChromeUtils.import(
  "resource://devtools/shared/Loader.jsm",
  {}
);
var customLoader = new DevToolsLoader();
customLoader.invisibleToDebugger = true;
var { DebuggerServer } = customLoader.require("devtools/server/main");
var { DebuggerClient } = require("devtools/shared/client/debugger-client");

function initDebuggerClient() {
  DebuggerServer.init();
  DebuggerServer.registerAllActors();
  DebuggerServer.allowChromeProcess = true;

  let transport = DebuggerServer.connectPipe();
  return new DebuggerClient(transport);
}

async function attachThread(client, actor) {
  let [response, tabClient] = await client.attachTarget(actor);
  let [response2, threadClient] = await tabClient.attachThread(null);
  return threadClient;
}

function onNewSource(event, packet) {
  if (packet.source.url.startsWith("chrome:")) {
    ok(true, "Received a new chrome source: " + packet.source.url);
    gThreadClient.removeListener("newSource", onNewSource);
    gNewChromeSource.resolve();
  }
}

function resumeAndCloseConnection() {
  return new Promise(resolve => {
    gThreadClient.resume(() => resolve(gClient.close()));
  });
}

registerCleanupFunction(function() {
  gClient = null;
  gThreadClient = null;
  gNewChromeSource = null;

  customLoader = null;
  DebuggerServer = null;
});

add_task(async function() {
  gClient = initDebuggerClient();

  const [type] = await gClient.connect();
  is(type, "browser", "Root actor should identify itself as a browser.");

  const response = await gClient.getProcess();
  let actor = response.form.actor;
  gThreadClient = await attachThread(gClient, actor);
  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:mozilla");

  // listen for a new source and global
  gThreadClient.addListener("newSource", onNewSource);
  await gNewChromeSource.promise;

  await resumeAndCloseConnection();
});
<|endoftext|>


<commit_before>/* eslint-env node */
module.exports = {
  browsers: ['ie 9', 'last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions'],
};
<commit_msg>Drop IE9 from targets (only target latest Chrome, FF, Safari).<commit_after>/* eslint-env node */
module.exports = {
  browsers: ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions'],
};
<|endoftext|>


<commit_before>export default {

    /**
     * Search the document for an element with a particular CSS selector
     * @param {string} selector - the CSS selector to match against
     * @return {HTMLElement} the first element that matches
     */
    el: function (selector) {
        return document.querySelector(selector)
    },

    /**
     * Create a new element
     * @param {string} tag - the type/tag of the element to create
     * @param {Object} attributes - {"key": "value", ...} attributes the
     *          element has after creation
     * @param {HTMLElement[]|string[]} children - elements to append to the
     *          new element as children. Strings are automatically converted
     *          to Text nodes
     * @return {HTMLElement} the newly constructed element
     */
    h: function (tag, attributes, children) {
        const el = document.createElement(tag)
        for (const prop in attributes) {
            el.setAttribute(prop, attributes[prop])
        }
        append(el, children)
        return el
    },

    /**
     * Append elements to another element as children
     * @param {HTMLElement} el - the parent element
     * @param {HTMLElement[]|string[]} children - elements to append to el
     *          as children. Strings are automatically converted
     *          to Text nodes
     */
    append: function (el, children) {
        append(el, children)
    },

    /**
     * Search the document for an element with a particular CSS selector and
     * delete all of its child elements
     * @param {string} selector - the CSS selector to match against
     * @return the first element that matches
     */
    clear: function (selector) {
        const el = this.el(selector)
        return clearElement(el)
    },

    /**
     * Delete all of the supplied element's child elements
     * @param {HTMLElement} el - the parent element
     * @return el
     */
    clearElement: function (el) {
        return clearElement(el)
    },
}

function append(el, children) {
    for (var i = 0; i < children.length; ++i) {
        if (typeof children[i] === 'string' || typeof children[i] === 'number') {
            el.appendChild(document.createTextNode(children[i]))
        } else {
            el.appendChild(children[i])
        }
    }
}

function clearElement(el) {
    while (el.firstChild) {
        el.removeChild(el.firstChild)
    }
    return el
}
<commit_msg>Remove use of var<commit_after>export default {

    /**
     * Search the document for an element with a particular CSS selector
     * @param {string} selector - the CSS selector to match against
     * @return {HTMLElement} the first element that matches
     */
    el: function (selector) {
        return document.querySelector(selector)
    },

    /**
     * Create a new element
     * @param {string} tag - the type/tag of the element to create
     * @param {Object} attributes - {"key": "value", ...} attributes the
     *          element has after creation
     * @param {HTMLElement[]|string[]} children - elements to append to the
     *          new element as children. Strings are automatically converted
     *          to Text nodes
     * @return {HTMLElement} the newly constructed element
     */
    h: function (tag, attributes, children) {
        const el = document.createElement(tag)
        for (const prop in attributes) {
            el.setAttribute(prop, attributes[prop])
        }
        append(el, children)
        return el
    },

    /**
     * Append elements to another element as children
     * @param {HTMLElement} el - the parent element
     * @param {HTMLElement[]|string[]} children - elements to append to el
     *          as children. Strings are automatically converted
     *          to Text nodes
     */
    append: function (el, children) {
        append(el, children)
    },

    /**
     * Search the document for an element with a particular CSS selector and
     * delete all of its child elements
     * @param {string} selector - the CSS selector to match against
     * @return the first element that matches
     */
    clear: function (selector) {
        const el = this.el(selector)
        return clearElement(el)
    },

    /**
     * Delete all of the supplied element's child elements
     * @param {HTMLElement} el - the parent element
     * @return el
     */
    clearElement: function (el) {
        return clearElement(el)
    },
}

function append(el, children) {
    for (let i = 0; i < children.length; ++i) {
        if (typeof children[i] === 'string' || typeof children[i] === 'number') {
            el.appendChild(document.createTextNode(children[i]))
        } else {
            el.appendChild(children[i])
        }
    }
}

function clearElement(el) {
    while (el.firstChild) {
        el.removeChild(el.firstChild)
    }
    return el
}
<|endoftext|>


<commit_before>/**
 * Built-in Log Configuration
 * (sails.config.log)
 *
 * Configure the log level for your app, as well as the transport
 * (Underneath the covers, Sails uses Winston for logging, which
 * allows for some pretty neat custom transports/adapters for log messages)
 *
 * For more information on the Sails logger, check out:
 * http://sailsjs.org/#/documentation/concepts/Logging
 */

module.exports.log = {

  /***************************************************************************
  *                                                                          *
  * Valid `level` configs: i.e. the minimum log level to capture with        *
  * sails.log.*()                                                            *
  *                                                                          *
  * The order of precedence for log levels from lowest to highest is:        *
  * silly, verbose, info, debug, warn, error                                 *
  *                                                                          *
  * You may also set the level to "silent" to suppress all logs.             *
  *                                                                          *
  ***************************************************************************/

  // level: 'info'

};
<commit_msg>feat(config): update log config<commit_after>/**
 * Built-in Log Configuration
 * (sails.config.log)
 *
 * Configure the log level for your app, as well as the transport
 * (Underneath the covers, Sails uses Winston for logging, which
 * allows for some pretty neat custom transports/adapters for log messages)
 *
 * For more information on the Sails logger, check out:
 * http://sailsjs.org/#/documentation/concepts/Logging
 */

module.exports.log = {

  /***************************************************************************
  *                                                                          *
  * Valid `level` configs: i.e. the minimum log level to capture with        *
  * sails.log.*()                                                            *
  *                                                                          *
  * The order of precedence for log levels from lowest to highest is:        *
  * silly, verbose, info, debug, warn, error                                 *
  *                                                                          *
  * You may also set the level to "silent" to suppress all logs.             *
  *                                                                          *
  ***************************************************************************/

  level: 'info'

};
<|endoftext|>


<commit_before>// Name:          HeroTracker, version 1.2
// Author:        Darren
// Last Updated:  2/19/2022
//
// --------------------------------------------------------------------------------------------
// - Change Log
// --------------------------------------------------------------------------------------------
// - 02/19/2022 eepjr24 - Adding back to one click after update by Steve K. to fix turn order
// -                      bug. See inline for fix location.
// -
// --------------------------------------------------------------------------------------------
//
// Purpose:
//
//   HeroTracker implements the turn sequence of the Hero Games system.  The Hero Games system
//   utilizes a segment chart with 12 'phases'.  A character's speed determines how many and on
//   which phases he/she gets to act.  HeroTracker will add a token to the Roll20 tracker
//   multiple times to simulate this mechanic.
//
//
// Usage:
//
//   !herotracker <parms>
//
// Parameters:
//
//   --help
//   --add
//   --remove
//   --id <token_id>
//   --tag
//   --speed <number>
//   --dex <number>
//   --segment <number>
//   --speed_field <name>
//   --dex_field <name>
//   --back
//   --start

var HeroTracker = HeroTracker || {
  TURN: [
    //segment 0  1  2  3  4  5  6  7  8  9 10 11 12    speed
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], //  0
    [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], //  1
    [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1], //  2
    [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], //  3
    [0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1], //  4
    [0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1], //  5
    [0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], //  6
    [0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1], //  7
    [0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1], //  8
    [0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1], //  9
    [0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1], // 10
    [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], // 11
    [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  ], // 12

  write: function (s, who, style, from) {
    "use strict";

    if (who) {
      who = "/w " + who.split(" ", 1)[0] + " ";
    }
    sendChat(
      from,
      who +
        s
          .replace(/</g, "&" + "lt;")
          .replace(/>/g, "&" + "gt;")
          .replace(/\n/g, "
")
    );
  },

  handleTrackerMessage: function (argv, msg) {
    "use strict";

    var gm;
    var pageid = Campaign().get("playerpageid");
    var who = msg.who;
    var token_id = "";
    var tokens;
    var add = 0;
    var remove = 0;
    var back = 0;
    var tag = "";
    var speed_field = "SPD";
    var dex_field = "DEX";
    var speed; // if undefined, get value from the token
    var dex; // if undefined, get value from the token
    var segment;
    var start;
    var random;

    gm = playerIsGM(msg.playerid);

    // parse command parameters

    if (msg.selected) {
      tokens = _.reject(msg.selected, (o) => o._type !== "graphic");
    }

    //		tokens = msg.selected;
    msg = msg.content;

    argv.splice(0, 1); // remove !HeroTracker

    if (argv.length === 0) {
      return HeroTracker.showTrackerHelp(who);
    }

    while (argv.length > 0) {
      switch (argv[0]) {
        case "--id":
          if (argv.length < 2) {
            return HeroTracker.write("--id parameter must be followed by a token_id", who, "", "HeroTracker");
          }
          token_id = argv[1];
          argv.splice(0, 2);
          break;

        case "--add":
          add = 1;
          argv.splice(0, 1);
          break;

        case "--remove":
          if (gm) {
            remove = 1;
          } else {
            return HeroTracker.write("Only the GM can use the --remove parameter.", who, "", "HeroTracker");
          }
          argv.splice(0, 1);
          break;

        case "--tag":
          if (argv.length < 2) {
            return HeroTracker.write("--tag parameter must be followed by a string", who, "", "HeroTracker");
          }
          tag = argv[1];
          argv.splice(0, 2);
          break;

        case "--speed":
          if (argv.length < 2) {
            return HeroTracker.write("--speed parameter must be followed by a number", who, "", "HeroTracker");
          }
          speed = argv[1];
          if (isNaN(parseInt(speed))) {
            return HeroTracker.write("speed was not a number", who, "", "HeroTracker");
          }
          argv.splice(0, 2);
          break;

        case "--dex":
          if (argv.length < 2) {
            return HeroTracker.write("--dex parameter must be followed by a number", who, "", "HeroTracker");
          }
          dex = argv[1];
          if (isNaN(parseInt(dex))) {
            return HeroTracker.write("dex was not a number", who, "", "HeroTracker");
          }
          argv.splice(0, 2);
          break;

        case "--segment":
          if (argv.length < 2) {
            return HeroTracker.write("--segment parameter must be followed by a number", who, "", "HeroTracker");
          }
          segment = argv[1];
          if (isNaN(parseInt(segment))) {
            return HeroTracker.write("segment was not a number", who, "", "HeroTracker");
          }
          argv.splice(0, 2);
          break;

        case "--back":
          back = 1;
          argv.splice(0, 1);
          break;

        case "--speed_field":
          if (argv.length < 2) {
            return HeroTracker.write("--speed_field parameter requires an attribute name", who, "", "HeroTracker");
          }
          speed_field = argv[1];
          argv.splice(0, 2);
          break;

        case "--dex_field":
          if (argv.length < 2) {
            return HeroTracker.write("--dex_field parameter requires an attribute name", who, "", "HeroTracker");
          }
          dex_field = argv[1];
          argv.splice(0, 2);
          break;

        case "--help":
          return HeroTracker.showTrackerHelp(who);
          break;

        case "--start":
          start = 1;
          argv.splice(0, 1);
          break;

        // hidden option used for testing
        case "--random":
          random = 1;
          argv.splice(0, 1);
          break;

        default:
          return HeroTracker.write("unknown parameter", who, "", "HeroTracker");
          break;
      }
    }

    // we have parsed all chat parameters

    // only the gm can to a 'back' action
    if (back === 1 && !gm) {
      return HeroTracker.write("only a gm can do the 'back' action", who, "", "HeroTracker");
    }

    // can't do more than one action: add, remove, back
    if (add + remove + back > 1) {
      return HeroTracker.write("can't do more than one action: 'add', 'remove', 'back'", who, "", "HeroTracker");
    }

    // must have a token or tag to add or remove or what are we even doing here?
    if (add + remove === 1 && token_id === "" && tag === "" && (tokens === undefined || tokens.length === 0)) {
      return HeroTracker.write("nothing selected", who, "", "HeroTracker");
    }

    // cannot specify both a token id and a tag
    if (!token_id == "" && tag == "") {
      return HeroTracker.write("can't specify both a token id and a tag", who, "", "HeroTracker");
    }

    // if we are doing an "add", we must have a speed or a segment.
    // if we have selected tokens, we can get the token's speed attribute.
    // but if we have no selected tokens, a speed or segment must have
    // been provided as part of the command parameters (--speed / --segment).
    if (add == 1 && !speed && !segment && (tokens == undefined || tokens.length == 0)) {
      return HeroTracker.write("no speed or segment specified", who, "", "HeroTracker");
    }

    // cannot specify both a specified token id and a tag
    if (token_id !== "" && tag !== "") {
      return HeroTracker.write("can't specify both a token id and a tag", who, "", "HeroTracker");
    }

    // cannot specify both a speed and a segment
    if (speed && segment) {
      return HeroTracker.write("can't specify both a speed and a segment", who, "", "HeroTracker");
    }

    // ok.  error checking done.

    if (add === 1) {
      if (!gm) sendChat("HeroTracker", "/w gm " + who + " add");

      Campaign().set("initiativepage", true); // display initiative tracker

      if (tag !== "") {
        // add custom tag to tracker
        HeroTracker.addToTracker("-1", speed ? speed : 0, dex ? dex : 0, segment, tag, who);
      } else if (token_id !== "") {
        // add a token specified with the --id parameter
        var t; // individual token
        var c; // character_id
        var s; // speed attribute
        var d; // dex attribute
        t = getObj("graphic", token_id);
        if (t) {
          if (t.get("pageid") !== pageid)
            return HeroTracker.write("token is not on the player page", who, "", "HeroTracker");
          c = t.get("represents");
        }
        if (c) {
          s = speed ? speed : getAttrByName(c, speed_field);
          d = dex ? dex : getAttrByName(c, dex_field);
          HeroTracker.removeFromTracker(token_id);
          HeroTracker.addToTracker(token_id, s ? s : 0, d ? d : 0, segment, tag, who);
        }
      } else {
        // add tokens based on current selection
        _.each(tokens, function (obj) {
          var t; // individual token
          var c; // character_id
          var s; // speed attribute
          var d; // dex attribute

          t = getObj("graphic", obj._id);

          if (t) {
            if (t.get("pageid") !== pageid)
              return HeroTracker.write("token is not on the player page", who, "", "HeroTracker");
            c = t.get("represents");
          }

          if (c) {
            s = speed ? speed : getAttrByName(c, speed_field);
            if (!s) return HeroTracker.write("could not find attribute: " + speed_field, who, "", "HeroTracker");
            d = dex ? dex : getAttrByName(c, dex_field);
            if (!d) return HeroTracker.write("could not find attribute: " + dex_field, who, "", "HeroTracker");
            HeroTracker.removeFromTracker(obj._id);
            HeroTracker.addToTracker(obj._id, s ? s : 0, d ? d : 0, segment, tag, who);
          }
        });
      }
    }

    if (remove === 1) {
      if (!gm) sendChat("HeroTracker", "/w gm " + who + " remove");

      if (token_id === "") {
        // remove a token specified with the --id parameter
        HeroTracker.removeFromTracker(token_id);
      } else {
        // remove multiple tokens based on current selection
        _.each(tokens, function (obj) {
          HeroTracker.removeFromTracker(obj._id);
        });
      }
    }

    if (back === 1) {
      HeroTracker.backTrack(who);
    }

    if (random === 1) {
      HeroTracker.randomize();
    }

    if (start === 1) {
      HeroTracker.startTurn();
    }
  },

  showTrackerHelp: function (who) {
    "use strict";

    if (who) {
      who = "/w " + who.split(" ", 1)[0] + " ";
    }

    sendChat(
      "HeroTracker",
      who +
        " " +
        '
' +
        '
' +
        "HeroTracker" +
        "
" +
        '
' +
        '
HeroTracker implements the turn sequence of the Hero Games system.  ' +
        "The Hero Games system utilizes a segment chart with 12 phases. A character &" +
        "apos;s speed " +
        "determines how many and on which phases he/she gets to act. HeroTracker will add a token to the " +
        "Roll20 tracker multiple times to simulate this mechanic.
" +
        "
" +
        '
Syntax
' +
        '
' +
        "
!herotracker &" +
        "lt;parms&" +
        "gt;
" +
        "
!ht &" +
        "lt;parms&" +
        "gt;
" +
        "
" +
        '
Parameters
' +
        '
' +
        "
" +
        "
" +
        "--help" +
        "
 " +
        '
' +
        "--add" +
        "
 " +
        '
' +
        "--remove" +
        "
 " +
        '
' +
        "--id &" +
        "lt;token_id&" +
        "gt;" +
        "
 " +
        '
' +
        "--tag &" +
        "lt;label&" +
        "gt;" +
        "
 " +
        '
' +
        "--speed &" +
        "lt;number&" +
        "gt;" +
        "
 " +
        '
' +
        "--dex &" +
        "lt;number&" +
        "gt;" +
        "
 " +
        '
' +
        "--segment &" +
        "lt;number&" +
        "gt;" +
        "
 " +
        '
' +
        "--speed_field &" +
        "lt;name&" +
        "gt;" +
        "
 " +
        '
' +
        "--dex_field &" +
        "lt;name&" +
        "gt;" +
        "
 " +
        '
' +
        "--back" +
        "
 " +
        '
' +
        "--start" +
        "
 " +
        "
" +
        "
" +
        '
' +
        '
To add tokens to the initiative tracker, select one or more tokens and then enter the chat command:
' +
        "
!ht --add
" +
        '
To remove entries, select one or more tokens and enter the chat command:
' +
        "
!ht --remove
" +
        '
Alternately, you can specify a single token by its ID rather than selecting it.  ' +
        "For example, to add a specific token, selected or not, enter this chat command:
" +
        "
!ht --add --id -L9sBx-soK0CQzQYCiFA --speed 5
" +
        '
You can also add a static label to the tracker by entering this chat command:
' +
        "
!ht --tag GAS-EFFECT --speed 4
" +
        '
When adding a token, the speed and dex will be automatically derived from the ' +
        "token&" +
        "apos;s attributes.  However, you can override this by specifying your own speed and dex:
" +
        "
!ht --add --speed 6 --dex 18
" +
        '
You can add a token or label to just one segment by using this chat command:
' +
        "
!ht --add --tag POST --segment 13
" +
        '
Because the tracker can end up with a great many entries, it would normally create ' +
        "a cumbersome problem to cycle through the entire list should you accidentally move past a turn that isn&" +
        "apos;t complete.  As such, HeroTracker " +
        "includes an option to rollback the tracker one turn:
" +
        "
!ht --back
" +
        '
Hero System begins a turn at segment 12.  You can sort the tracker, ' +
        "with segment 12 at the top of the order, by using this command:
" +
        "
!ht --start
" +
        '
HeroTracker uses dex to break ties by appending a decimal value to the ' +
        "token&" +
        "apos;s speed.  The value is just the dex subtracted from 100.  If you do not want to use this feature, you can specify a dex " +
        "of zero using this command:
" +
        "
!ht --add --dex 0
" +
        '
By default, HeroTracker uses the character sheet fields SPD and DEX ' +
        "to derive the speed and dex values.  You can override and choose your own field names using the parameters --speed_field and --dex_field.  " +
        "This can also be used for characters that use a different attribute to break ties, such as ego based characters.  The command would look like this:
" +
        "
!ht --add --dex_field EGO
" +
        "
" +
        '
Notes
' +
        '<div style="padding-left: 15px; padding-right: 10px"
' +
        "
The speed and dex of a selected token will be derived from the " +
        "token&" +
        "apos;s attributes SPD and DEX respectively.
" +
        "
If you specify a single token using the --id parameter, or create a custom label using " +
        "the --tag parameter, you must also provide a speed or segment, and optionally a dex.
" +
        "
" +
        "
"
    );
  },

  addToTracker: function (token_id, speed, dex, segment, custom, who) {
    ("use strict");

    var turnorder;
    var tiebreaker = 0;

    /*
		HeroTracker.write("token_id: " + token_id, who, "", "HeroTracker" );
		HeroTracker.write("speed: " + speed, who, "", "HeroTracker" );
		HeroTracker.write("dex: " + dex, who, "", "HeroTracker" );
		HeroTracker.write("segment: " + segment, who, "", "HeroTracker" );
		HeroTracker.write("custom: " + custom, who, "", "HeroTracker" );
		*/

    if (speed) speed = parseInt(speed);
    if (dex) dex = parseInt(dex);

    // compute the tiebreaker value
    if (dex > 0) tiebreaker = 1 - dex / 100;

    turnorder = JSON.parse(Campaign().get("turnorder") || "[]");

    if (token_id === "") {
      token_id = "-1";
    } // if no token id is present, set to -1 to use the custom arg

    const token = token_id !== "-1" ? getObj("graphic", token_id) : undefined;
    const page_id = token ? token.get("pageid") : undefined;

    // if a segment was provided, we skip speed and dex entirely
    if (segment) {
      var i = HeroTracker.getSortIndex(turnorder, segment);
      // Update 2/19/22, added page_id to comply with new turn tracker requirement
      turnorder.splice(i, 0, { id: token_id, pr: segment, custom: custom, _pageid: page_id });
      Campaign().set("turnorder", JSON.stringify(turnorder));
      return;
    }
    // handle speeds greater than 12

    while (speed > 12) {
      var i;
      for (var s = 1; s <= 12; s++) {
        var p = Math.round((s + tiebreaker + 0.00001) * 100) / 100;
        i = HeroTracker.getSortIndex(turnorder, p);
        turnorder.splice(i, 0, { id: token_id, pr: p, custom: custom, _pageid: page_id });
      }
      speed = speed - 12;
    }

    for (var s = 1; s <= 12; s++) {
      var i;
      var p = Math.round((s + tiebreaker + 0.00001) * 100) / 100;
      if (HeroTracker.TURN[speed][s] == 1) {
        i = HeroTracker.getSortIndex(turnorder, p);
        // Update 2/19/22, added page_id to comply with new turn tracker requirement
        turnorder.splice(i, 0, { id: token_id, pr: p, custom: custom, _pageid: page_id });
      }
    }

    Campaign().set("turnorder", JSON.stringify(turnorder));
  },

  getSortIndex: function (turnorder, pr) {
    "use strict";

    // if the current turnorder is zero or one entry, we always
    // add to the end of the array.
    if (turnorder.length <= 1) {
      return turnorder.length;
    }

    // new entries are NEVER added to the top of the list
    var a = 1; // start the second entry

    // if the current top of the list is greater than the entry
    // we're trying to add, we will want to skip down to the
    // turn break.
    if (turnorder[0].pr >= pr) {
      // scan down to find the turn 'break'
      do {
        if (turnorder[a].pr < turnorder[a - 1].pr) break;
        else a++;
      } while (turnorder.length > a);

      // if we're at the end of the list, append the entry to the end
      if (turnorder.length === a) return a;
    }

    // we should be sitting at the top of the turn break.
    // now we scan down to find our insert point.
    do {
      if (turnorder[a].pr > pr) break;
      else a++;
    } while (turnorder.length > a);

    return a;
  },

  removeFromTracker: function (token_id) {
    "use strict";

    var turnorder;

    if (Campaign().get("turnorder") == "") return;
    else turnorder = JSON.parse(Campaign().get("turnorder"));

    for (var i = 0; i < turnorder.length; ) {
      if (turnorder[i]["id"] === token_id) {
        turnorder.splice(i, 1);
      } else {
        i++;
      }
    }

    Campaign().set("turnorder", JSON.stringify(turnorder));
  },

  backTrack: function () {
    "use strict";

    var turnorder;
    var turn;

    turnorder = JSON.parse(Campaign().get("turnorder") || "[]");

    if (turnorder.length <= 1) return;

    turnorder.splice(0, 0, turnorder.pop());

    Campaign().set("turnorder", JSON.stringify(turnorder));
  },

  newTurnSort: function (a, b) {
    "use strict";

    const aFloat = parseFloat(a.pr) || 0;
    const bFloat = parseFloat(b.pr) || 0;
    const cFloat = Math.round((aFloat - bFloat + 0.00001) * 100) / 100;

    if (cFloat === 0.0) return -1;
    if (12.0 <= aFloat && 12.0 > bFloat) return -1;
    if (12.0 > aFloat && 12.0 <= bFloat) return 1;

    return cFloat;
  },

  startTurn: function () {
    let turns = JSON.parse(Campaign().get("turnorder") || "[]");
    turns.sort(HeroTracker.newTurnSort);
    Campaign().set("turnorder", JSON.stringify(turns));
  },

  sortRandom: function (a, b) {
    return 0.5 - Math.random();
  },

  randomize: function () {
    let turns = JSON.parse(Campaign().get("turnorder") || "[]");
    turns.sort(HeroTracker.sortRandom);
    Campaign().set("turnorder", JSON.stringify(turns));
  },

  handleChatMessage: function (msg) {
    "use strict";

    if (msg.type !== "api") {
      return;
    }

    if (
      msg.content === "!herotracker" ||
      msg.content.indexOf("!herotracker ") === 0 ||
      msg.content === "!ht" ||
      msg.content.indexOf("!ht ") === 0
    ) {
      return HeroTracker.handleTrackerMessage(msg.content.split(" "), msg);
    }
  },

  registerHeroTracker: function () {
    "use strict";
    // HeroTracker.initConfig();
    on("chat:message", HeroTracker.handleChatMessage);
  },
};

on("ready", function () {
  HeroTracker.registerHeroTracker();
});

<commit_msg>Delete HeroTracker.js<commit_after><|endoftext|>