<commit_before>jQuery.support = (function() {

	var support, all, a, select, opt, input, fragment, eventName, isSupported, i,
		div = document.createElement("div");

	// Setup
	div.setAttribute( "className", "t" );
	div.innerHTML = "  <link/>

a<input type='checkbox'/>";

	// Support tests won't run in some limited or non-browser environments
	all = div.getElementsByTagName("*");
	a = div.getElementsByTagName("a")[ 0 ];
	if ( !all || !a || !all.length ) {
		return {};
	}

	// First batch of tests
	select = document.createElement("select");
	opt = select.appendChild( document.createElement("option") );
	input = div.getElementsByTagName("input")[ 0 ];

	a.style.cssText = "top:1px;float:left;opacity:.5";
	support = {
		// Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)
		getSetAttribute: div.className !== "t",

		// IE strips leading whitespace when .innerHTML is used
		leadingWhitespace: div.firstChild.nodeType === 3,

		// Make sure that tbody elements aren't automatically inserted
		// IE will insert them into empty tables
		tbody: !div.getElementsByTagName("tbody").length,

		// Make sure that link elements get serialized correctly by innerHTML
		// This requires a wrapper element in IE
		htmlSerialize: !!div.getElementsByTagName("link").length,

		// Get the style information from getAttribute
		// (IE uses .cssText instead)
		style: /top/.test( a.getAttribute("style") ),

		// Make sure that URLs aren't manipulated
		// (IE normalizes it by default)
		hrefNormalized: a.getAttribute("href") === "/a",

		// Make sure that element opacity exists
		// (IE uses filter instead)
		// Use a regex to work around a WebKit issue. See #5145
		opacity: /^0.5/.test( a.style.opacity ),

		// Verify style float existence
		// (IE uses styleFloat instead of cssFloat)
		cssFloat: !!a.style.cssFloat,

		// Check the default checkbox/radio value ("" on WebKit; "on" elsewhere)
		checkOn: !!input.value,

		// Make sure that a selected-by-default option has a working selected property.
		// (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
		optSelected: opt.selected,

		// Tests for enctype support on a form (#6743)
		enctype: !!document.createElement("form").enctype,

		// Makes sure cloning an html5 element does not cause problems
		// Where outerHTML is undefined, this still works
		html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>",

		// jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode
		boxModel: document.compatMode === "CSS1Compat",

		// Will be defined later
		deleteExpando: true,
		noCloneEvent: true,
		inlineBlockNeedsLayout: false,
		shrinkWrapBlocks: false,
		reliableMarginRight: true,
		boxSizingReliable: true,
		pixelPosition: false
	};

	// Make sure checked status is properly cloned
	input.checked = true;
	support.noCloneChecked = input.cloneNode( true ).checked;

	// Make sure that the options inside disabled selects aren't marked as disabled
	// (WebKit marks them as disabled)
	select.disabled = true;
	support.optDisabled = !opt.disabled;

	// Support: IE<9
	try {
		delete div.test;
	} catch( e ) {
		support.deleteExpando = false;
	}

	// Check if we can trust getAttribute("value")
	input = document.createElement("input");
	input.setAttribute( "value", "" );
	support.input = input.getAttribute( "value" ) === "";

	// Check if an input maintains its value after becoming a radio
	input.value = "t";
	input.setAttribute( "type", "radio" );
	support.radioValue = input.value === "t";

	// #11217 - WebKit loses check when the name is after the checked attribute
	input.setAttribute( "checked", "t" );
	input.setAttribute( "name", "t" );

	fragment = document.createDocumentFragment();
	fragment.appendChild( input );

	// Check if a disconnected checkbox will retain its checked
	// value of true after appended to the DOM (IE6/7)
	support.appendChecked = input.checked;

	// WebKit doesn't clone checked state correctly in fragments
	support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;

	// Support: IE<9
	// Opera does not clone events (and typeof div.attachEvent === undefined).
	// IE9-10 clones events bound via attachEvent, but they don't trigger with .click()
	if ( div.attachEvent ) {
		div.attachEvent( "onclick", function() {
			support.noCloneEvent = false;
		});

		div.cloneNode( true ).click();
	}

	// Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event)
	// Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP), test/csp.php
	for ( i in { submit: true, change: true, focusin: true }) {
		div.setAttribute( eventName = "on" + i, "t" );

		support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false;
	}

	div.style.backgroundClip = "content-box";
	div.cloneNode().style.backgroundClip = "";
	support.clearCloneStyle = div.style.backgroundClip === "content-box";

	// Run tests that need a body at doc ready
	jQuery(function() {
		var container, marginDiv, tds,
			divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",
			body = document.getElementsByTagName("body")[0];

		if ( !body ) {
			// Return for frameset docs that don't have a body
			return;
		}

		container = document.createElement("div");
		container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px";

		body.appendChild( container ).appendChild( div );

		// Support: IE8
		// Check if table cells still have offsetWidth/Height when they are set
		// to display:none and there are still other visible table cells in a
		// table row; if so, offsetWidth/Height are not reliable for use when
		// determining if an element has been hidden directly using
		// display:none (it is still safe to use offsets if a parent element is
		// hidden; don safety goggles and see bug #4512 for more information).
		div.innerHTML = "

	t

";
		tds = div.getElementsByTagName("td");
		tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none";
		isSupported = ( tds[ 0 ].offsetHeight === 0 );

		tds[ 0 ].style.display = "";
		tds[ 1 ].style.display = "none";

		// Support: IE8
		// Check if empty table cells still have offsetWidth/Height
		support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );

		// Check box-sizing and margin behavior
		div.innerHTML = "";
		div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;";
		support.boxSizing = ( div.offsetWidth === 4 );
		support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 );

		// Use window.getComputedStyle because jsdom on node.js will break without it.
		if ( window.getComputedStyle ) {
			support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%";
			support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px";

			// Check if div with explicit width and no margin-right incorrectly
			// gets computed margin-right based on width of container. (#3333)
			// Fails in WebKit before Feb 2011 nightlies
			// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
			marginDiv = div.appendChild( document.createElement("div") );
			marginDiv.style.cssText = div.style.cssText = divReset;
			marginDiv.style.marginRight = marginDiv.style.width = "0";
			div.style.width = "1px";

			support.reliableMarginRight =
				!parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight );
		}

		if ( typeof div.style.zoom !== "undefined" ) {
			// Support: IE<8
			// Check if natively block-level elements act like inline-block
			// elements when setting their display to 'inline' and giving
			// them layout
			div.innerHTML = "";
			div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1";
			support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 );

			// Support: IE6
			// Check if elements with layout shrink-wrap their children
			div.style.display = "block";
			div.innerHTML = "

";
			div.firstChild.style.width = "5px";
			support.shrinkWrapBlocks = ( div.offsetWidth !== 3 );

			// Prevent IE 6 from affecting layout for positioned elements #11048
			// Prevent IE from shrinking the body in IE 7 mode #12869
			body.style.zoom = 1;
		}

		body.removeChild( container );

		// Null elements to avoid leaks in IE
		container = div = tds = marginDiv = null;
	});

	// Null elements to avoid leaks in IE
	all = select = fragment = opt = a = input = null;

	return support;
})();

<commit_msg>Opera 12.0 still implements an older version of the DOM so cloneNode cannot be called without an argument<commit_after>jQuery.support = (function() {

	var support, all, a, select, opt, input, fragment, eventName, isSupported, i,
		div = document.createElement("div");

	// Setup
	div.setAttribute( "className", "t" );
	div.innerHTML = "  <link/>

a<input type='checkbox'/>";

	// Support tests won't run in some limited or non-browser environments
	all = div.getElementsByTagName("*");
	a = div.getElementsByTagName("a")[ 0 ];
	if ( !all || !a || !all.length ) {
		return {};
	}

	// First batch of tests
	select = document.createElement("select");
	opt = select.appendChild( document.createElement("option") );
	input = div.getElementsByTagName("input")[ 0 ];

	a.style.cssText = "top:1px;float:left;opacity:.5";
	support = {
		// Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)
		getSetAttribute: div.className !== "t",

		// IE strips leading whitespace when .innerHTML is used
		leadingWhitespace: div.firstChild.nodeType === 3,

		// Make sure that tbody elements aren't automatically inserted
		// IE will insert them into empty tables
		tbody: !div.getElementsByTagName("tbody").length,

		// Make sure that link elements get serialized correctly by innerHTML
		// This requires a wrapper element in IE
		htmlSerialize: !!div.getElementsByTagName("link").length,

		// Get the style information from getAttribute
		// (IE uses .cssText instead)
		style: /top/.test( a.getAttribute("style") ),

		// Make sure that URLs aren't manipulated
		// (IE normalizes it by default)
		hrefNormalized: a.getAttribute("href") === "/a",

		// Make sure that element opacity exists
		// (IE uses filter instead)
		// Use a regex to work around a WebKit issue. See #5145
		opacity: /^0.5/.test( a.style.opacity ),

		// Verify style float existence
		// (IE uses styleFloat instead of cssFloat)
		cssFloat: !!a.style.cssFloat,

		// Check the default checkbox/radio value ("" on WebKit; "on" elsewhere)
		checkOn: !!input.value,

		// Make sure that a selected-by-default option has a working selected property.
		// (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
		optSelected: opt.selected,

		// Tests for enctype support on a form (#6743)
		enctype: !!document.createElement("form").enctype,

		// Makes sure cloning an html5 element does not cause problems
		// Where outerHTML is undefined, this still works
		html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>",

		// jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode
		boxModel: document.compatMode === "CSS1Compat",

		// Will be defined later
		deleteExpando: true,
		noCloneEvent: true,
		inlineBlockNeedsLayout: false,
		shrinkWrapBlocks: false,
		reliableMarginRight: true,
		boxSizingReliable: true,
		pixelPosition: false
	};

	// Make sure checked status is properly cloned
	input.checked = true;
	support.noCloneChecked = input.cloneNode( true ).checked;

	// Make sure that the options inside disabled selects aren't marked as disabled
	// (WebKit marks them as disabled)
	select.disabled = true;
	support.optDisabled = !opt.disabled;

	// Support: IE<9
	try {
		delete div.test;
	} catch( e ) {
		support.deleteExpando = false;
	}

	// Check if we can trust getAttribute("value")
	input = document.createElement("input");
	input.setAttribute( "value", "" );
	support.input = input.getAttribute( "value" ) === "";

	// Check if an input maintains its value after becoming a radio
	input.value = "t";
	input.setAttribute( "type", "radio" );
	support.radioValue = input.value === "t";

	// #11217 - WebKit loses check when the name is after the checked attribute
	input.setAttribute( "checked", "t" );
	input.setAttribute( "name", "t" );

	fragment = document.createDocumentFragment();
	fragment.appendChild( input );

	// Check if a disconnected checkbox will retain its checked
	// value of true after appended to the DOM (IE6/7)
	support.appendChecked = input.checked;

	// WebKit doesn't clone checked state correctly in fragments
	support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;

	// Support: IE<9
	// Opera does not clone events (and typeof div.attachEvent === undefined).
	// IE9-10 clones events bound via attachEvent, but they don't trigger with .click()
	if ( div.attachEvent ) {
		div.attachEvent( "onclick", function() {
			support.noCloneEvent = false;
		});

		div.cloneNode( true ).click();
	}

	// Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event)
	// Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP), test/csp.php
	for ( i in { submit: true, change: true, focusin: true }) {
		div.setAttribute( eventName = "on" + i, "t" );

		support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false;
	}

	div.style.backgroundClip = "content-box";
	div.cloneNode( true ).style.backgroundClip = "";
	support.clearCloneStyle = div.style.backgroundClip === "content-box";

	// Run tests that need a body at doc ready
	jQuery(function() {
		var container, marginDiv, tds,
			divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",
			body = document.getElementsByTagName("body")[0];

		if ( !body ) {
			// Return for frameset docs that don't have a body
			return;
		}

		container = document.createElement("div");
		container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px";

		body.appendChild( container ).appendChild( div );

		// Support: IE8
		// Check if table cells still have offsetWidth/Height when they are set
		// to display:none and there are still other visible table cells in a
		// table row; if so, offsetWidth/Height are not reliable for use when
		// determining if an element has been hidden directly using
		// display:none (it is still safe to use offsets if a parent element is
		// hidden; don safety goggles and see bug #4512 for more information).
		div.innerHTML = "

	t

";
		tds = div.getElementsByTagName("td");
		tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none";
		isSupported = ( tds[ 0 ].offsetHeight === 0 );

		tds[ 0 ].style.display = "";
		tds[ 1 ].style.display = "none";

		// Support: IE8
		// Check if empty table cells still have offsetWidth/Height
		support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );

		// Check box-sizing and margin behavior
		div.innerHTML = "";
		div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;";
		support.boxSizing = ( div.offsetWidth === 4 );
		support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 );

		// Use window.getComputedStyle because jsdom on node.js will break without it.
		if ( window.getComputedStyle ) {
			support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%";
			support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px";

			// Check if div with explicit width and no margin-right incorrectly
			// gets computed margin-right based on width of container. (#3333)
			// Fails in WebKit before Feb 2011 nightlies
			// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
			marginDiv = div.appendChild( document.createElement("div") );
			marginDiv.style.cssText = div.style.cssText = divReset;
			marginDiv.style.marginRight = marginDiv.style.width = "0";
			div.style.width = "1px";

			support.reliableMarginRight =
				!parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight );
		}

		if ( typeof div.style.zoom !== "undefined" ) {
			// Support: IE<8
			// Check if natively block-level elements act like inline-block
			// elements when setting their display to 'inline' and giving
			// them layout
			div.innerHTML = "";
			div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1";
			support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 );

			// Support: IE6
			// Check if elements with layout shrink-wrap their children
			div.style.display = "block";
			div.innerHTML = "

";
			div.firstChild.style.width = "5px";
			support.shrinkWrapBlocks = ( div.offsetWidth !== 3 );

			// Prevent IE 6 from affecting layout for positioned elements #11048
			// Prevent IE from shrinking the body in IE 7 mode #12869
			body.style.zoom = 1;
		}

		body.removeChild( container );

		// Null elements to avoid leaks in IE
		container = div = tds = marginDiv = null;
	});

	// Null elements to avoid leaks in IE
	all = select = fragment = opt = a = input = null;

	return support;
})();

<|endoftext|>


<commit_before>const photo_clipboard = {};

photo_clipboard.clipboard = new Set();
photo_clipboard.on_load_hooks = [];
photo_clipboard.on_save_hooks = [];

// Load save ///////////////////////////////////////////////////////////////////////////////////////

photo_clipboard.clear_clipboard =
function clear_clipboard()
{
    photo_clipboard.clipboard.clear();
    photo_clipboard.save_clipboard();
}

photo_clipboard.load_clipboard =
function load_clipboard(event)
{
    console.log("Loading photo clipboard from localstorage.");
    const stored = localStorage.getItem("photo_clipboard");
    if (stored === null)
    {
        if (photo_clipboard.clipboard.size != 0)
        {
            photo_clipboard.clipboard = new Set();
        }
    }
    else
    {
        photo_clipboard.clipboard = new Set(JSON.parse(stored));
    }

    for (const on_load_hook of photo_clipboard.on_load_hooks)
    {
        on_load_hook();
    }

    return photo_clipboard.clipboard;
}

photo_clipboard.save_clipboard =
function save_clipboard()
{
    console.log("Saving photo clipboard to localstorage.");
    const serialized = JSON.stringify(Array.from(photo_clipboard.clipboard));
    localStorage.setItem("photo_clipboard", serialized);
    photo_clipboard.update_pagestate();

    for (const on_save_hook of photo_clipboard.on_save_hooks)
    {
        on_save_hook();
    }
}

// Card management /////////////////////////////////////////////////////////////////////////////////

photo_clipboard.apply_check =
function apply_check(checkbox)
{
    /*
    Check the checkbox if this photo ID is on the clipboard.

    There are two valid scenarios:
    1. The checkbox is a child of a photo_card div, and that div has data-id
       containing the photo id. That div will receive the CSS class
       photo_card_selected or photo_card_unselected.
       This is the most common usage.
    2. The checkbox has its own data-photo-id, and the parent element will be
       ignored. This is used only if the checkbox needs to be displayed outside
       of a photo card, such as on the /photo/id page where it makes no sense
       to put a photo card of the photo you're already looking at.
    */
    let photo_id;
    let photo_card;
    if (checkbox.dataset.photoId)
    {
        photo_id = checkbox.dataset.photoId;
    }
    else
    {
        photo_card = checkbox.parentElement;
        photo_id = photo_card.dataset.id;
    }
    checkbox.checked = photo_clipboard.clipboard.has(photo_id);
    if (! photo_card)
    {
        return;
    }
    if (checkbox.checked)
    {
        photo_card.classList.remove("photo_card_unselected");
        photo_card.classList.add("photo_card_selected");
    }
    else
    {
        photo_card.classList.remove("photo_card_selected");
        photo_card.classList.add("photo_card_unselected");
    }
}

photo_clipboard.apply_check_all =
function apply_check_all()
{
    /*
    Run through all the photo cards on the page and set their checkbox to the
    correct value.
    */
    const checkboxes = document.getElementsByClassName("photo_card_selector_checkbox");
    for (const checkbox of checkboxes)
    {
        photo_clipboard.apply_check(checkbox);
    }
}

photo_clipboard.previous_photo_select = null;

photo_clipboard.on_photo_select =
function on_photo_select(event)
{
    /*
    Select or unselect the clicked photo, with support for Shift-click to action
    everything between the previous click and this one inclusively.

    Those middle items will be set to the same state as the new state of the
    clicked item.
    */
    const checkbox = event.target;
    let action;
    if (checkbox.checked)
    {
        action = photo_clipboard.clipboard.add.bind(photo_clipboard.clipboard);
    }
    else
    {
        action = photo_clipboard.clipboard.delete.bind(photo_clipboard.clipboard);
    }

    if (! checkbox.parentElement.classList.contains("photo_card"))
    {
        action(checkbox.dataset.photoId);
    }
    else if (event.shiftKey && photo_clipboard.previous_photo_select)
    {
        const current_photo_div = checkbox.parentElement;
        const previous_photo_div = photo_clipboard.previous_photo_select;
        const photo_divs = Array.from(current_photo_div.parentElement.getElementsByClassName("photo_card"));

        const current_index = photo_divs.indexOf(current_photo_div);
        const previous_index = photo_divs.indexOf(previous_photo_div);

        let slice;
        if (current_index == previous_index)
        {
            slice = [current_photo_div];
        }
        else
        {
            const left = Math.min(previous_index, current_index);
            const right = Math.max(previous_index, current_index);
            slice = photo_divs.slice(left, right + 1);
        }

        for (const photo_div of slice)
        {
            action(photo_div.dataset.id);
        }
        photo_clipboard.previous_photo_select = current_photo_div;
    }
    else
    {
        const photo_div = checkbox.parentElement;
        action(photo_div.dataset.id);
        photo_clipboard.previous_photo_select = photo_div;
    }
    photo_clipboard.save_clipboard();
}

photo_clipboard.select_all_photos =
function select_all_photos()
{
    const photo_divs = Array.from(document.getElementsByClassName("photo_card"));
    for (const photo_div of photo_divs)
    {
        photo_clipboard.clipboard.add(photo_div.dataset.id);
    }
    photo_clipboard.apply_check_all();
    photo_clipboard.save_clipboard();
}

photo_clipboard.unselect_all_photos =
function unselect_all_photos()
{
    const photo_divs = Array.from(document.getElementsByClassName("photo_card"));
    for (const photo_div of photo_divs)
    {
        photo_clipboard.clipboard.delete(photo_div.dataset.id);
    }
    photo_clipboard.apply_check_all()
    photo_clipboard.previous_photo_select = null;
    photo_clipboard.save_clipboard();
}

// Tray management /////////////////////////////////////////////////////////////////////////////////

photo_clipboard.clipboard_tray_collapse =
function clipboard_tray_collapse()
{
    const tray_body = document.getElementById("clipboard_tray_body");
    tray_body.classList.add("hidden");
}

photo_clipboard.clipboard_tray_uncollapse =
function clipboard_tray_uncollapse()
{
    const tray_body = document.getElementById("clipboard_tray_body");
    tray_body.classList.remove("hidden");
    photo_clipboard.update_clipboard_tray();
}

photo_clipboard.clipboard_tray_collapse_toggle =
function clipboard_tray_collapse_toggle()
{
    /*
    Show or hide the clipboard.
    */
    const tray_body = document.getElementById("clipboard_tray_body");
    if (!tray_body)
    {
        return;
    }

    if (tray_body.classList.contains("hidden") && photo_clipboard.clipboard.size > 0)
    {
        photo_clipboard.clipboard_tray_uncollapse();
    }
    else
    {
        photo_clipboard.clipboard_tray_collapse();
    }
}

photo_clipboard.ingest_toolbox_items =
function ingest_toolbox_items()
{
    /*
    The page may provide divs with the class "my_clipboard_tray_toolbox", and
    we will migrate all the elements into the real clipboard tray toolbox.
    */
    const toolbox = document.getElementById("clipboard_tray_toolbox");
    const moreboxes = document.getElementsByClassName("my_clipboard_tray_toolbox");

    for (const morebox of moreboxes)
    {
        while (morebox.firstElementChild)
        {
            toolbox.appendChild(morebox.firstElementChild);
        }
        morebox.parentElement.removeChild(morebox);
    }
}

photo_clipboard.on_tray_delete_button =
function on_tray_delete_button(event)
{
    /*
    Remove the clicked row from the clipboard.
    */
    const clipboard_line = event.target.parentElement;
    const photo_id = clipboard_line.dataset.id;
    photo_clipboard.clipboard.delete(photo_id);
    photo_clipboard.save_clipboard();
}

photo_clipboard.update_clipboard_tray =
function update_clipboard_tray()
{
    /*
    Rebuild the rows if the tray is open.
    */
    const clipboard_tray = document.getElementById("clipboard_tray");
    if (clipboard_tray === null)
    {
        return;
    }

    if (photo_clipboard.clipboard.size == 0)
    {
        photo_clipboard.clipboard_tray_collapse();
    }

    const tray_lines = document.getElementById("clipboard_tray_lines");
    if (!clipboard_tray.classList.contains("hidden"))
    {
        common.delete_all_children(tray_lines);
        const photo_ids = Array.from(photo_clipboard.clipboard);
        photo_ids.sort();
        for (const photo_id of photo_ids)
        {
            const clipboard_line = document.createElement("div");
            clipboard_line.classList.add("clipboard_tray_line");
            clipboard_line.dataset.id = photo_id;

            const clipboard_line_delete_button = document.createElement("button");
            clipboard_line_delete_button.classList.add("remove_tag_button_perm");
            clipboard_line_delete_button.classList.add("red_button");
            clipboard_line_delete_button.onclick = photo_clipboard.on_tray_delete_button;

            const clipboard_line_link = document.createElement("a");
            clipboard_line_link.target = "_blank";
            clipboard_line_link.href = "/photo/" + photo_id;
            clipboard_line_link.innerText = photo_id;

            clipboard_line.appendChild(clipboard_line_delete_button);
            clipboard_line.appendChild(clipboard_line_link);
            tray_lines.appendChild(clipboard_line);
        }
    }
}

// Page and event management ///////////////////////////////////////////////////////////////////////

photo_clipboard.open_full_clipboard_tab =
function open_full_clipboard_tab()
{
    window.open("/clipboard");
}

photo_clipboard.update_clipboard_count =
function update_clipboard_count()
{
    const elements = document.getElementsByClassName("clipboard_count");
    for (const element of elements)
    {
        element.innerText = photo_clipboard.clipboard.size;
    }
}

photo_clipboard.on_storage_event =
function on_storage_event()
{
    /*
    Receive storage events from other tabs and update our state to match.
    */
    photo_clipboard.load_clipboard();
    photo_clipboard.update_pagestate();
}

photo_clipboard.update_pagestate =
function update_pagestate()
{
    /*
    Update all relevant DOM elements to match internal state.
    */
    photo_clipboard.update_clipboard_count();
    photo_clipboard.update_clipboard_tray();
    photo_clipboard.apply_check_all();
}

photo_clipboard.on_pageload =
function on_pageload()
{
    window.addEventListener("storage", photo_clipboard.on_storage_event, false);
    hotkeys.register_hotkey("ctrl a", photo_clipboard.select_all_photos, "Select all photos.");
    hotkeys.register_hotkey("ctrl d", photo_clipboard.unselect_all_photos, "Deselect all photos.");
    hotkeys.register_hotkey("c", photo_clipboard.clipboard_tray_collapse_toggle, "Toggle clipboard tray.");
    hotkeys.register_hotkey("shift c", photo_clipboard.open_full_clipboard_tab, "Open full clipboard page.");
    photo_clipboard.ingest_toolbox_items();
    photo_clipboard.load_clipboard();
    photo_clipboard.update_pagestate();
}
document.addEventListener("DOMContentLoaded", photo_clipboard.on_pageload);
<commit_msg>Improve UI performance of selecting / deselecting photo cards.<commit_after>const photo_clipboard = {};

photo_clipboard.clipboard = new Set();
photo_clipboard.on_load_hooks = [];
photo_clipboard.on_save_hooks = [];

// Load save ///////////////////////////////////////////////////////////////////////////////////////

photo_clipboard.clear_clipboard =
function clear_clipboard()
{
    photo_clipboard.clipboard.clear();
    photo_clipboard.save_clipboard();
}

photo_clipboard.load_clipboard =
function load_clipboard(event)
{
    console.log("Loading photo clipboard from localstorage.");
    const stored = localStorage.getItem("photo_clipboard");
    if (stored === null)
    {
        if (photo_clipboard.clipboard.size != 0)
        {
            photo_clipboard.clipboard = new Set();
        }
    }
    else
    {
        photo_clipboard.clipboard = new Set(JSON.parse(stored));
    }

    for (const on_load_hook of photo_clipboard.on_load_hooks)
    {
        on_load_hook();
    }

    return photo_clipboard.clipboard;
}

photo_clipboard.save_clipboard =
function save_clipboard()
{
    console.log("Saving photo clipboard to localstorage.");
    const serialized = JSON.stringify(Array.from(photo_clipboard.clipboard));
    localStorage.setItem("photo_clipboard", serialized);
    photo_clipboard.update_pagestate();

    for (const on_save_hook of photo_clipboard.on_save_hooks)
    {
        on_save_hook();
    }
}

// Card management /////////////////////////////////////////////////////////////////////////////////

photo_clipboard.apply_check =
function apply_check(checkbox)
{
    /*
    Check the checkbox if this photo ID is on the clipboard.

    There are two valid scenarios:
    1. The checkbox is a child of a photo_card div, and that div has data-id
       containing the photo id. That div will receive the CSS class
       photo_card_selected or photo_card_unselected.
       This is the most common usage.
    2. The checkbox has its own data-photo-id, and the parent element will be
       ignored. This is used only if the checkbox needs to be displayed outside
       of a photo card, such as on the /photo/id page where it makes no sense
       to put a photo card of the photo you're already looking at.
    */
    let photo_id;
    let photo_card;
    if (checkbox.dataset.photoId)
    {
        photo_id = checkbox.dataset.photoId;
    }
    else
    {
        photo_card = checkbox.parentElement;
        photo_id = photo_card.dataset.id;
    }
    checkbox.checked = photo_clipboard.clipboard.has(photo_id);
    if (! photo_card)
    {
        // E.g. on the Photo page there is a checkbox for adding it to the
        // clipboard, though it does not reside in a photo_card div.
        return;
    }
    if (checkbox.checked)
    {
        photo_card.classList.remove("photo_card_unselected");
        photo_card.classList.add("photo_card_selected");
    }
    else
    {
        photo_card.classList.remove("photo_card_selected");
        photo_card.classList.add("photo_card_unselected");
    }
}

photo_clipboard.apply_check_all =
function apply_check_all()
{
    /*
    Run through all the photo cards on the page and set their checkbox to the
    correct value.
    */
    const checkboxes = document.getElementsByClassName("photo_card_selector_checkbox");
    for (const checkbox of checkboxes)
    {
        photo_clipboard.apply_check(checkbox);
    }
}

photo_clipboard.previous_photo_select = null;

photo_clipboard.on_photo_select =
function on_photo_select(event)
{
    /*
    Select or unselect the clicked photo, with support for Shift-click to action
    everything between the previous click and this one inclusively.

    Those middle items will be set to the same state as the new state of the
    clicked item.
    */
    const checkbox = event.target;
    let action;
    if (checkbox.checked)
    {
        action = function(photo_div)
        {
            console.log("photo_card_selected");
            photo_clipboard.clipboard.add(photo_div.dataset.id);
            photo_div.classList.remove("photo_card_unselected");
            photo_div.classList.add("photo_card_selected");
        }
    }
    else
    {
        action = function(photo_div)
        {
            console.log("photo_card_unselected");
            photo_clipboard.clipboard.delete(photo_div.dataset.id);
            photo_div.classList.remove("photo_card_selected");
            photo_div.classList.add("photo_card_unselected");
        }
    }

    if (! checkbox.parentElement.classList.contains("photo_card"))
    {
        if (checkbox.checked)
        {
            photo_clipboard.clipboard.add(checkbox.dataset.photoId);
        }
        else
        {
            photo_clipboard.clipboard.delete(checkbox.dataset.photoId);
        }
    }
    else if (event.shiftKey && photo_clipboard.previous_photo_select)
    {
        const current_photo_div = checkbox.parentElement;
        const previous_photo_div = photo_clipboard.previous_photo_select;
        const photo_divs = Array.from(current_photo_div.parentElement.getElementsByClassName("photo_card"));

        const current_index = photo_divs.indexOf(current_photo_div);
        const previous_index = photo_divs.indexOf(previous_photo_div);

        let slice;
        if (current_index == previous_index)
        {
            slice = [current_photo_div];
        }
        else
        {
            const left = Math.min(previous_index, current_index);
            const right = Math.max(previous_index, current_index);
            slice = photo_divs.slice(left, right + 1);
        }

        for (const photo_div of slice)
        {
            action(photo_div);
        }
        photo_clipboard.previous_photo_select = current_photo_div;
    }
    else
    {
        const photo_div = checkbox.parentElement;
        action(photo_div);
        photo_clipboard.previous_photo_select = photo_div;
    }
    setTimeout(() => photo_clipboard.save_clipboard(), 0);
}

photo_clipboard.select_all_photos =
function select_all_photos()
{
    const photo_divs = Array.from(document.getElementsByClassName("photo_card"));
    for (const photo_div of photo_divs)
    {
        photo_clipboard.clipboard.add(photo_div.dataset.id);
        photo_div.classList.remove("photo_card_unselected");
        photo_div.classList.add("photo_card_selected");
        const checkbox = photo_div.getElementsByClassName("photo_card_selector_checkbox")[0];
        if (checkbox)
        {
            checkbox.checked = true;
        }
    }
    photo_clipboard.previous_photo_select = null;
    setTimeout(() => photo_clipboard.save_clipboard(), 0);
}

photo_clipboard.unselect_all_photos =
function unselect_all_photos()
{
    const photo_divs = Array.from(document.getElementsByClassName("photo_card"));
    for (const photo_div of photo_divs)
    {
        photo_clipboard.clipboard.delete(photo_div.dataset.id);
        photo_div.classList.remove("photo_card_selected");
        photo_div.classList.add("photo_card_unselected");
        const checkbox = photo_div.getElementsByClassName("photo_card_selector_checkbox")[0];
        if (checkbox)
        {
            checkbox.checked = false;
        }
    }
    photo_clipboard.previous_photo_select = null;
    setTimeout(() => photo_clipboard.save_clipboard(), 0);
}

// Tray management /////////////////////////////////////////////////////////////////////////////////

photo_clipboard.clipboard_tray_collapse =
function clipboard_tray_collapse()
{
    const tray_body = document.getElementById("clipboard_tray_body");
    tray_body.classList.add("hidden");
}

photo_clipboard.clipboard_tray_uncollapse =
function clipboard_tray_uncollapse()
{
    const tray_body = document.getElementById("clipboard_tray_body");
    tray_body.classList.remove("hidden");
    photo_clipboard.update_clipboard_tray();
}

photo_clipboard.clipboard_tray_collapse_toggle =
function clipboard_tray_collapse_toggle()
{
    /*
    Show or hide the clipboard.
    */
    const tray_body = document.getElementById("clipboard_tray_body");
    if (!tray_body)
    {
        return;
    }

    if (tray_body.classList.contains("hidden") && photo_clipboard.clipboard.size > 0)
    {
        photo_clipboard.clipboard_tray_uncollapse();
    }
    else
    {
        photo_clipboard.clipboard_tray_collapse();
    }
}

photo_clipboard.ingest_toolbox_items =
function ingest_toolbox_items()
{
    /*
    The page may provide divs with the class "my_clipboard_tray_toolbox", and
    we will migrate all the elements into the real clipboard tray toolbox.
    */
    const toolbox = document.getElementById("clipboard_tray_toolbox");
    const moreboxes = document.getElementsByClassName("my_clipboard_tray_toolbox");

    for (const morebox of moreboxes)
    {
        while (morebox.firstElementChild)
        {
            toolbox.appendChild(morebox.firstElementChild);
        }
        morebox.parentElement.removeChild(morebox);
    }
}

photo_clipboard.on_tray_delete_button =
function on_tray_delete_button(event)
{
    /*
    Remove the clicked row from the clipboard.
    */
    const clipboard_line = event.target.parentElement;
    const photo_id = clipboard_line.dataset.id;
    photo_clipboard.clipboard.delete(photo_id);
    photo_clipboard.save_clipboard();
}

photo_clipboard.update_clipboard_tray =
function update_clipboard_tray()
{
    /*
    Rebuild the rows if the tray is open.
    */
    const clipboard_tray = document.getElementById("clipboard_tray");
    if (clipboard_tray === null)
    {
        return;
    }

    if (photo_clipboard.clipboard.size == 0)
    {
        photo_clipboard.clipboard_tray_collapse();
    }

    const tray_lines = document.getElementById("clipboard_tray_lines");
    if (!clipboard_tray.classList.contains("hidden"))
    {
        common.delete_all_children(tray_lines);
        const photo_ids = Array.from(photo_clipboard.clipboard);
        photo_ids.sort();
        for (const photo_id of photo_ids)
        {
            const clipboard_line = document.createElement("div");
            clipboard_line.classList.add("clipboard_tray_line");
            clipboard_line.dataset.id = photo_id;

            const clipboard_line_delete_button = document.createElement("button");
            clipboard_line_delete_button.classList.add("remove_tag_button_perm");
            clipboard_line_delete_button.classList.add("red_button");
            clipboard_line_delete_button.onclick = photo_clipboard.on_tray_delete_button;

            const clipboard_line_link = document.createElement("a");
            clipboard_line_link.target = "_blank";
            clipboard_line_link.href = "/photo/" + photo_id;
            clipboard_line_link.innerText = photo_id;

            clipboard_line.appendChild(clipboard_line_delete_button);
            clipboard_line.appendChild(clipboard_line_link);
            tray_lines.appendChild(clipboard_line);
        }
    }
}

// Page and event management ///////////////////////////////////////////////////////////////////////

photo_clipboard.open_full_clipboard_tab =
function open_full_clipboard_tab()
{
    window.open("/clipboard");
}

photo_clipboard.update_clipboard_count =
function update_clipboard_count()
{
    const elements = document.getElementsByClassName("clipboard_count");
    for (const element of elements)
    {
        element.innerText = photo_clipboard.clipboard.size;
    }
}

photo_clipboard.on_storage_event =
function on_storage_event()
{
    /*
    Receive storage events from other tabs and update our state to match.
    */
    photo_clipboard.load_clipboard();
    photo_clipboard.update_pagestate();
}

photo_clipboard.update_pagestate =
function update_pagestate()
{
    /*
    Update all relevant DOM elements to match internal state.
    */
    photo_clipboard.update_clipboard_count();
    photo_clipboard.update_clipboard_tray();
    photo_clipboard.apply_check_all();
}

photo_clipboard.on_pageload =
function on_pageload()
{
    window.addEventListener("storage", photo_clipboard.on_storage_event, false);
    hotkeys.register_hotkey("ctrl a", photo_clipboard.select_all_photos, "Select all photos.");
    hotkeys.register_hotkey("ctrl d", photo_clipboard.unselect_all_photos, "Deselect all photos.");
    hotkeys.register_hotkey("c", photo_clipboard.clipboard_tray_collapse_toggle, "Toggle clipboard tray.");
    hotkeys.register_hotkey("shift c", photo_clipboard.open_full_clipboard_tab, "Open full clipboard page.");
    photo_clipboard.ingest_toolbox_items();
    photo_clipboard.load_clipboard();
    photo_clipboard.update_pagestate();
}
document.addEventListener("DOMContentLoaded", photo_clipboard.on_pageload);
<|endoftext|>


<commit_before>"use strict";

var CONF = {
    baseUrl: 'lib/ion-tree-list',
    digestTtl: 35
};

function addDepthToTree(obj, depth, collapsed) {
    for (var key in obj) {
        if (typeof(obj[key]) == 'object') {
            obj[key].depth = depth;
            obj[key].collapsed = collapsed;
            addDepthToTree(obj[key], key === 'tree' ? ++ depth : depth, collapsed)
        }
    }
    return obj
}

function toggleCollapse(obj) {
    for (var key in obj) {
        if (typeof(obj[key]) == 'object') {
            obj[key].collapsed = !obj[key].collapsed;
            toggleCollapse(obj[key])
        }
    }
    return obj
}

angular.module('ion-tree-list', [], function($rootScopeProvider){
    $rootScopeProvider.digestTtl(CONF.digestTtl)
})
.directive('ionTreeList', function () {
    return {
        restrict: 'E',
        scope: {
            items: '=',
            collapsed: '='
        },
        templateUrl: CONF.baseUrl + '/ion-tree-list.tmpl.html',
        controller: function($scope){
            $scope.baseUrl = CONF.baseUrl;
            $scope.items = addDepthToTree($scope.items, 1, $scope.collapsed);
            $scope.toggleCollapse = toggleCollapse
        }
    }
});<commit_msg>Added watch to look for 'collapse' changes<commit_after>"use strict";

var CONF = {
    baseUrl: 'lib/ion-tree-list',
    digestTtl: 35
};

function addDepthToTree(obj, depth, collapsed) {
    for (var key in obj) {
        if (typeof(obj[key]) == 'object') {
            obj[key].depth = depth;
            obj[key].collapsed = collapsed;
            addDepthToTree(obj[key], key === 'tree' ? ++ depth : depth, collapsed)
        }
    }
    return obj
}

function toggleCollapse(obj) {
    for (var key in obj) {
        if (typeof(obj[key]) == 'object') {
            obj[key].collapsed = !obj[key].collapsed;
            toggleCollapse(obj[key])
        }
    }
    return obj
}

angular.module('ion-tree-list', [], function($rootScopeProvider){
    $rootScopeProvider.digestTtl(CONF.digestTtl)
})
.directive('ionTreeList', function () {
    return {
        restrict: 'E',
        scope: {
            items: '=',
            collapsed: '='
        },
        templateUrl: CONF.baseUrl + '/ion-tree-list.tmpl.html',
        controller: function($scope){
            $scope.baseUrl = CONF.baseUrl;
            $scope.items = addDepthToTree($scope.items, 1, $scope.collapsed);
            $scope.toggleCollapse = toggleCollapse;

            $scope.$watch('collapsed', function(){
                $scope.toggleCollapse($scope.items);
            })
        }
    }
});
<|endoftext|>


<commit_before>function Test( settings ) {
	extend( this, settings );
	this.assert = new Assert( this );
	this.assertions = [];
	this.testNumber = ++Test.count;
}

Test.count = 0;

Test.prototype = {
	setup: function() {
		if (

			// Emit moduleStart when we're switching from one module to another
			this.module !== config.previousModule ||

				// They could be equal (both undefined) but if the previousModule property doesn't
				// yet exist it means this is the first test in a suite that isn't wrapped in a
				// module, in which case we'll just emit a moduleStart event for 'undefined'.
				// Without this, reporters can get testStart before moduleStart  which is a problem.
				!hasOwn.call( config, "previousModule" )
		) {
			if ( hasOwn.call( config, "previousModule" ) ) {
				runLoggingCallbacks( "moduleDone", {
					name: config.previousModule,
					failed: config.moduleStats.bad,
					passed: config.moduleStats.all - config.moduleStats.bad,
					total: config.moduleStats.all
				});
			}
			config.previousModule = this.module;
			config.moduleStats = { all: 0, bad: 0 };
			runLoggingCallbacks( "moduleStart", {
				name: this.module
			});
		}

		config.current = this;

		this.testEnvironment = extend({
			setup: function() {},
			teardown: function() {}
		}, this.moduleTestEnvironment );

		this.started = now();
		runLoggingCallbacks( "testStart", {
			name: this.testName,
			module: this.module,
			testNumber: this.testNumber
		});

		/*jshint camelcase:false */

		/**
		 * Expose the current test environment.
		 *
		 * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead.
		 */
		QUnit.current_testEnvironment = this.testEnvironment;

		/*jshint camelcase:true */

		if ( !config.pollution ) {
			saveGlobal();
		}
		if ( config.notrycatch ) {
			this.testEnvironment.setup.call( this.testEnvironment, this.assert );
			return;
		}
		try {
			this.testEnvironment.setup.call( this.testEnvironment, this.assert );
		} catch ( e ) {
			this.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
		}
	},
	run: function() {
		config.current = this;

		if ( this.async ) {
			QUnit.stop();
		}

		this.callbackStarted = now();

		if ( config.notrycatch ) {
			this.callback.call( this.testEnvironment, this.assert );
			this.callbackRuntime = now() - this.callbackStarted;
			return;
		}

		try {
			this.callback.call( this.testEnvironment, this.assert );
			this.callbackRuntime = now() - this.callbackStarted;
		} catch ( e ) {
			this.callbackRuntime = now() - this.callbackStarted;

			this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );

			// else next test will carry the responsibility
			saveGlobal();

			// Restart the tests if they're blocking
			if ( config.blocking ) {
				QUnit.start();
			}
		}
	},
	teardown: function() {
		config.current = this;
		if ( config.notrycatch ) {
			if ( typeof this.callbackRuntime === "undefined" ) {
				this.callbackRuntime = now() - this.callbackStarted;
			}
			this.testEnvironment.teardown.call( this.testEnvironment, this.assert );
			return;
		} else {
			try {
				this.testEnvironment.teardown.call( this.testEnvironment, this.assert );
			} catch ( e ) {
				this.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
			}
		}
		checkPollution();
	},
	finish: function() {
		config.current = this;
		if ( config.requireExpects && this.expected === null ) {
			this.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack );
		} else if ( this.expected !== null && this.expected !== this.assertions.length ) {
			this.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack );
		} else if ( this.expected === null && !this.assertions.length ) {
			this.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack );
		}

		var i,
			bad = 0;

		this.runtime = now() - this.started;
		config.stats.all += this.assertions.length;
		config.moduleStats.all += this.assertions.length;

		for ( i = 0; i < this.assertions.length; i++ ) {
			if ( !this.assertions[ i ].result ) {
				bad++;
				config.stats.bad++;
				config.moduleStats.bad++;
			}
		}

		runLoggingCallbacks( "testDone", {
			name: this.testName,
			module: this.module,
			failed: bad,
			passed: this.assertions.length - bad,
			total: this.assertions.length,
			runtime: this.runtime,

			// HTML Reporter use
			assertions: this.assertions,
			testNumber: this.testNumber,

			// DEPRECATED: this property will be removed in 2.0.0, use runtime instead
			duration: this.runtime
		});

		config.current = undefined;
	},

	queue: function() {
		var bad,
			test = this;

		function run() {
			// each of these can by async
			synchronize(function() {
				test.setup();
			});
			synchronize(function() {
				test.run();
			});
			synchronize(function() {
				test.teardown();
			});
			synchronize(function() {
				test.finish();
			});
		}

		// `bad` initialized at top of scope
		// defer when previous test run passed, if storage is available
		bad = QUnit.config.reorder && defined.sessionStorage &&
				+sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName );

		if ( bad ) {
			run();
		} else {
			synchronize( run, true );
		}
	},

	push: function( result, actual, expected, message ) {
		if ( !this instanceof Test ) {
			throw new Error( "assertion outside test context, was " + sourceFromStacktrace() );
		}

		var source,
			details = {
				module: this.module,
				name: this.testName,
				result: result,
				message: message,
				actual: actual,
				expected: expected,
				testNumber: this.testNumber
			};

		if ( !result ) {
			source = sourceFromStacktrace();

			if ( source ) {
				details.source = source;
			}
		}

		runLoggingCallbacks( "log", details );

		this.assertions.push({
			result: !!result,
			message: message
		});
	},

	pushFailure: function( message, source, actual ) {
		if ( !this instanceof Test ) {
			throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace( 2 ) );
		}

		var details = {
				module: this.module,
				name: this.testName,
				result: false,
				message: message || "error",
				actual: actual || null,
				testNumber: this.testNumber
			};

		if ( source ) {
			details.source = source;
		}

		runLoggingCallbacks( "log", details );

		this.assertions.push({
			result: false,
			message: message
		});
	}
};

// Deprecated
QUnit.push = function() {

	// Gets current test obj
	var currentTest = QUnit.config.current.assert.test;

	return currentTest.push.apply( currentTest, arguments );
};

QUnit.pushFailure = function() {

	// Gets current test obj
	var currentTest = QUnit.config.current.assert.test;

	return currentTest.pushFailure.apply( currentTest, arguments );
};
<commit_msg>Test: Remove deprecated QUnit.current_testEnvironment<commit_after>function Test( settings ) {
	extend( this, settings );
	this.assert = new Assert( this );
	this.assertions = [];
	this.testNumber = ++Test.count;
}

Test.count = 0;

Test.prototype = {
	setup: function() {
		if (

			// Emit moduleStart when we're switching from one module to another
			this.module !== config.previousModule ||

				// They could be equal (both undefined) but if the previousModule property doesn't
				// yet exist it means this is the first test in a suite that isn't wrapped in a
				// module, in which case we'll just emit a moduleStart event for 'undefined'.
				// Without this, reporters can get testStart before moduleStart  which is a problem.
				!hasOwn.call( config, "previousModule" )
		) {
			if ( hasOwn.call( config, "previousModule" ) ) {
				runLoggingCallbacks( "moduleDone", {
					name: config.previousModule,
					failed: config.moduleStats.bad,
					passed: config.moduleStats.all - config.moduleStats.bad,
					total: config.moduleStats.all
				});
			}
			config.previousModule = this.module;
			config.moduleStats = { all: 0, bad: 0 };
			runLoggingCallbacks( "moduleStart", {
				name: this.module
			});
		}

		config.current = this;

		this.testEnvironment = extend({
			setup: function() {},
			teardown: function() {}
		}, this.moduleTestEnvironment );

		this.started = now();
		runLoggingCallbacks( "testStart", {
			name: this.testName,
			module: this.module,
			testNumber: this.testNumber
		});

		if ( !config.pollution ) {
			saveGlobal();
		}
		if ( config.notrycatch ) {
			this.testEnvironment.setup.call( this.testEnvironment, this.assert );
			return;
		}
		try {
			this.testEnvironment.setup.call( this.testEnvironment, this.assert );
		} catch ( e ) {
			this.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
		}
	},
	run: function() {
		config.current = this;

		if ( this.async ) {
			QUnit.stop();
		}

		this.callbackStarted = now();

		if ( config.notrycatch ) {
			this.callback.call( this.testEnvironment, this.assert );
			this.callbackRuntime = now() - this.callbackStarted;
			return;
		}

		try {
			this.callback.call( this.testEnvironment, this.assert );
			this.callbackRuntime = now() - this.callbackStarted;
		} catch ( e ) {
			this.callbackRuntime = now() - this.callbackStarted;

			this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );

			// else next test will carry the responsibility
			saveGlobal();

			// Restart the tests if they're blocking
			if ( config.blocking ) {
				QUnit.start();
			}
		}
	},
	teardown: function() {
		config.current = this;
		if ( config.notrycatch ) {
			if ( typeof this.callbackRuntime === "undefined" ) {
				this.callbackRuntime = now() - this.callbackStarted;
			}
			this.testEnvironment.teardown.call( this.testEnvironment, this.assert );
			return;
		} else {
			try {
				this.testEnvironment.teardown.call( this.testEnvironment, this.assert );
			} catch ( e ) {
				this.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
			}
		}
		checkPollution();
	},
	finish: function() {
		config.current = this;
		if ( config.requireExpects && this.expected === null ) {
			this.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack );
		} else if ( this.expected !== null && this.expected !== this.assertions.length ) {
			this.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack );
		} else if ( this.expected === null && !this.assertions.length ) {
			this.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack );
		}

		var i,
			bad = 0;

		this.runtime = now() - this.started;
		config.stats.all += this.assertions.length;
		config.moduleStats.all += this.assertions.length;

		for ( i = 0; i < this.assertions.length; i++ ) {
			if ( !this.assertions[ i ].result ) {
				bad++;
				config.stats.bad++;
				config.moduleStats.bad++;
			}
		}

		runLoggingCallbacks( "testDone", {
			name: this.testName,
			module: this.module,
			failed: bad,
			passed: this.assertions.length - bad,
			total: this.assertions.length,
			runtime: this.runtime,

			// HTML Reporter use
			assertions: this.assertions,
			testNumber: this.testNumber,

			// DEPRECATED: this property will be removed in 2.0.0, use runtime instead
			duration: this.runtime
		});

		config.current = undefined;
	},

	queue: function() {
		var bad,
			test = this;

		function run() {
			// each of these can by async
			synchronize(function() {
				test.setup();
			});
			synchronize(function() {
				test.run();
			});
			synchronize(function() {
				test.teardown();
			});
			synchronize(function() {
				test.finish();
			});
		}

		// `bad` initialized at top of scope
		// defer when previous test run passed, if storage is available
		bad = QUnit.config.reorder && defined.sessionStorage &&
				+sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName );

		if ( bad ) {
			run();
		} else {
			synchronize( run, true );
		}
	},

	push: function( result, actual, expected, message ) {
		if ( !this instanceof Test ) {
			throw new Error( "assertion outside test context, was " + sourceFromStacktrace() );
		}

		var source,
			details = {
				module: this.module,
				name: this.testName,
				result: result,
				message: message,
				actual: actual,
				expected: expected,
				testNumber: this.testNumber
			};

		if ( !result ) {
			source = sourceFromStacktrace();

			if ( source ) {
				details.source = source;
			}
		}

		runLoggingCallbacks( "log", details );

		this.assertions.push({
			result: !!result,
			message: message
		});
	},

	pushFailure: function( message, source, actual ) {
		if ( !this instanceof Test ) {
			throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace( 2 ) );
		}

		var details = {
				module: this.module,
				name: this.testName,
				result: false,
				message: message || "error",
				actual: actual || null,
				testNumber: this.testNumber
			};

		if ( source ) {
			details.source = source;
		}

		runLoggingCallbacks( "log", details );

		this.assertions.push({
			result: false,
			message: message
		});
	}
};

// Deprecated
QUnit.push = function() {

	// Gets current test obj
	var currentTest = QUnit.config.current.assert.test;

	return currentTest.push.apply( currentTest, arguments );
};

QUnit.pushFailure = function() {

	// Gets current test obj
	var currentTest = QUnit.config.current.assert.test;

	return currentTest.pushFailure.apply( currentTest, arguments );
};
<|endoftext|>


<commit_before>/* *****************************************************************************
 * Caleydo - Visualization for Molecular Biology - http://caleydo.org
 * Copyright (c) The Caleydo Team. All rights reserved.
 * Licensed under the new BSD license, available at http://caleydo.org/license
 **************************************************************************** */

const {libraryAliases, libraryExternals, modules, entries, ignores, type} = require('./.yo-rc.json')['generator-phovea'];
const resolve = require('path').resolve;
const pkg = require('./package.json');
const webpack = require('webpack');
const fs = require('fs');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const buildInfo = require('./buildInfo.js');

const now = new Date();
const prefix = (n) => n < 10 ? ('0' + n) : n.toString();
const buildId = `${now.getUTCFullYear()}${prefix(now.getUTCMonth())}${prefix(now.getUTCDate())}-${prefix(now.getUTCHours())}${prefix(now.getUTCMinutes())}${prefix(now.getUTCSeconds())}`;
pkg.version = pkg.version.replace('SNAPSHOT', buildId);

const year = (new Date()).getFullYear();
const banner = '/*! ' + (pkg.title || pkg.name) + ' - v' + pkg.version + ' - ' + year + '\n' +
  (pkg.homepage ? '* ' + pkg.homepage + '\n' : '') +
  '* Copyright (c) ' + year + ' ' + pkg.author.name + ';' +
  ' Licensed ' + pkg.license + '*/\n';

//list of loaders and their mappings
const webpackloaders = [
  {test: /\.scss$/, loader: 'style-loader!css-loader!sass-loader'},
  {test: /\.tsx?$/, loader: 'awesome-typescript-loader'},
  {test: /\.json$/, loader: 'json-loader'},
  {
    test: /\.(png|jpg)$/,
    loader: 'url-loader',
    query: {
      limit: 10000, //inline <= 10kb
    }
  },
  {
    test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
    loader: 'url-loader',
    query: {
      limit: 10000, //inline <= 10kb
      mimetype: 'application/font-woff'
    }
  },
  {
    test: /\.svg(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
    loader: 'url-loader',
    query: {
      limit: 10000, //inline <= 10kb
      mimetype: 'image/svg+xml'
    }
  },
  {test: /\.(ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file-loader'}
];

/**
 * tests whether the given phovea module name is matching the requested file and if so convert it to an external lookup
 * depending on the loading type
 **/
function testPhoveaModule(moduleName, request) {
  if (!(new RegExp('^' + moduleName + '/src.*')).test(request)) {
    return false;
  }
  const subModule = request.match(/.*\/src\/?(.*)/)[1];
  //skip empty modules = root
  const path = subModule === '' ? [moduleName] : [moduleName, subModule];
  //phovea_<name> ... phovea.name
  const rootPath = /phovea_.*/.test(moduleName) ? ['phovea', moduleName.slice(7)].concat(path.slice(1)) : path;
  return {
    root: rootPath,
    commonjs2: path,
    commonjs: path,
    amd: request + (subModule === '' ? '/main' : '')
  };
}

function testPhoveaModules(modules) {
  return (context, request, callback) => {
    for (let i = 0; i < modules.length; ++i) {
      const r = testPhoveaModule(modules[i], request);
      if (r) {
        return callback(null, r);
      }
    }
    callback();
  };
}

// use workspace registry file if available
const isWorkspaceContext = fs.existsSync(resolve(__dirname, '..', 'phovea_registry.js'));
const registryFile = isWorkspaceContext ? '../phovea_registry.js' : './phovea_registry.js';
const actBuildInfoFile = `file-loader?name=buildInfo.json!${buildInfo.tmpFile()}`;

/**
 * inject the registry to be included
 * @param entry
 * @returns {*}
 */
function injectRegistry(entry) {
  //build also the registry
  if (typeof entry === 'string') {
    return [registryFile, actBuildInfoFile].concat(entry);
  } else {
    const transformed = {};
    Object.keys(entry).forEach((eentry) => {
      transformed[eentry] = [registryFile, actBuildInfoFile].concat(entry[eentry]);
    });
    return transformed;
  }

}
/**
 * generate a webpack configuration
 */
function generateWebpack(options) {
  let base = {
    entry: injectRegistry(options.entries),
    output: {
      path: resolve(__dirname, 'build'),
      filename: (options.name || (pkg.name + (options.bundle ? '_bundle' : ''))) + (options.min && !options.nosuffix ? '.min' : '') + '.js',
      publicPath: '' //no public path = relative
    },
    resolve: {
      // Add `.ts` and `.tsx` as a resolvable extension.
      extensions: ['.webpack.js', '.web.js', '.ts', '.tsx', '.js'],
      alias: Object.assign({}, options.libs || {}),
      //fallback to the directory above if they are siblings just in the workspace context
      modules: isWorkspaceContext ? [
        resolve(__dirname, '../'),
        'node_modules'
      ] : ['node_modules']
    },
    plugins: [
      new webpack.BannerPlugin({
        banner: banner,
        raw: true
      }),
      //define magic constants that are replaced
      new webpack.DefinePlugin({
        __VERSION__: JSON.stringify(pkg.version),
        __LICENSE__: JSON.stringify(pkg.license),
        __BUILD_ID__: buildId,
        __DEBUG__: options.isDev || options.isTest,
        __TEST__: options.isTest,
        __PRODUCTION__: options.isProduction
      }),
      new webpack.optimize.MinChunkSizePlugin({
        minChunkSize: 10000 //at least 10.000 characters
      }),
      new webpack.optimize.AggressiveMergingPlugin()
      //rest depends on type
    ],
    externals: [],
    module: {
      loaders: webpackloaders.slice()
    },
    devServer: {
      proxy: {
        '/api/*': {
          target: 'http://localhost:9000',
          secure: false
        },
        '/login': {
          target: 'http://localhost:9000',
          secure: false
        },
        '/logout': {
          target: 'http://localhost:9000',
          secure: false
        },
        '/loggedinas': {
          target: 'http://localhost:9000',
          secure: false
        }
      },
      contentBase: resolve(__dirname, 'build')
    }
  };

  if (options.library) {
    let libName = /phovea_.*/.test(pkg.name) ? ['phovea', pkg.name.slice(7)] : pkg.name;
    //generate a library, i.e. output the last entry element
    //create library name
    if (options.moduleBundle) {
      libName = 'phovea';
    }
    base.output.library = libName;
    base.output.libraryTarget = 'umd';
    base.output.umdNamedDefine = false; //anonymous require module
  }

  if (!options.bundle) {
    //if we don't bundle don't include external libraries and other phovea modules
    base.externals.push(...(options.externals || Object.keys(options.libs || {})));

    //ignore all phovea modules
    if (options.modules) {
      base.externals.push(testPhoveaModules(options.modules));
    }

    //ignore extra modules
    (options.ignore || []).forEach(function (d) {
      base.module.loaders.push({test: new RegExp(d), loader: 'null-loader'}); //use null loader
    });
    //ingore phovea module registry calls
    (options.modules || []).forEach(function (m) {
      base.module.loaders.push({test: new RegExp('.*[\\\\/]' + m + '[\\\\/]phovea_registry.js'), loader: 'null-loader'}); //use null loader
    });
  }
  if (!options.bundle || options.isApp) {
    //extract the included css file to own file
    let p = new ExtractTextPlugin({
      filename: (options.isApp || options.moduleBundle ? 'style' : pkg.name)  + (options.min && !options.nosuffix ? '.min' : '') + '.css',
      allChunks: true // there seems to be a bug in dynamically loaded chunk styles are not loaded, workaround: extract all styles from all chunks
    });
    base.plugins.push(p);
    base.module.loaders[0] = {
      test: /\.scss$/,
      loader: p.extract(['css-loader', 'sass-loader'])
    };
  }
  if (options.isApp) {
    // create manifest
    // base.plugins.push(new webpack.optimize.AppCachePlugin());
  }
  if (options.commons) {
    //build a commons plugin
    base.plugins.push(new webpack.optimize.CommonsChunkPlugin({
      // The order of this array matters
      names: ['common'],
      minChunks: 2
    }));
  }
  if (options.min) {
    //use a minifier
    base.plugins.push(
      new webpack.LoaderOptionsPlugin({
        minimize: true,
        debug: false
      }),
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false
        },
        output: {
          comments: false
        },
        sourceMap: false
      }));
  } else {
    //generate source maps
    base.devtool = 'source-map';
  }
  return base;
}

function generateWebpackConfig(env) {
  const isTest = env === 'test';
  const isProduction = env === 'prod';
  const isDev = !isProduction && !isTest;

  const base = {
    entries: entries,
    libs: libraryAliases,
    externals: libraryExternals,
    modules: modules,
    ignore: ignores,
    isProduction: isProduction,
    isDev: isDev,
    isTest: isTest
  };

  if (isTest) {
    return generateWebpack(Object.assign({}, base, {
      bundle: true
    }));
  }

  if (type.startsWith('app')) {
    base.isApp = true;
    base.bundle = true; //bundle everything together
    base.name = '[name]'; //multiple entries case
    base.commons = true; //extract commons module
  } else if (type === 'bundle') {
    base.library = true; //expose as library
    base.moduleBundle = true; //expose as library 'phovea'
    base.name = pkg.name; //to avoid adding _bundle
    base.bundle = true;
  } else { //type === 'lib'
    base.library = true;
  }

  //single generation
  if (isDev) {
    return generateWebpack(base);
  } else if (type === 'app') { //isProduction app
    return generateWebpack(Object.assign({}, base, {
        min: true,
        nosuffix: true
      }));
  } else { //isProduction
    return [
      //plain
      generateWebpack(base),
      //minified
      generateWebpack(Object.assign({}, base, {
        min: true
      }))
    ];
  }
}

module.exports = generateWebpackConfig;
module.exports.generateWebpack = generateWebpack;
<commit_msg>add missing __APP_CONTEXT_VARIABLE<commit_after>/* *****************************************************************************
 * Caleydo - Visualization for Molecular Biology - http://caleydo.org
 * Copyright (c) The Caleydo Team. All rights reserved.
 * Licensed under the new BSD license, available at http://caleydo.org/license
 **************************************************************************** */

const {libraryAliases, libraryExternals, modules, entries, ignores, type} = require('./.yo-rc.json')['generator-phovea'];
const resolve = require('path').resolve;
const pkg = require('./package.json');
const webpack = require('webpack');
const fs = require('fs');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const buildInfo = require('./buildInfo.js');

const now = new Date();
const prefix = (n) => n < 10 ? ('0' + n) : n.toString();
const buildId = `${now.getUTCFullYear()}${prefix(now.getUTCMonth())}${prefix(now.getUTCDate())}-${prefix(now.getUTCHours())}${prefix(now.getUTCMinutes())}${prefix(now.getUTCSeconds())}`;
pkg.version = pkg.version.replace('SNAPSHOT', buildId);

const year = (new Date()).getFullYear();
const banner = '/*! ' + (pkg.title || pkg.name) + ' - v' + pkg.version + ' - ' + year + '\n' +
  (pkg.homepage ? '* ' + pkg.homepage + '\n' : '') +
  '* Copyright (c) ' + year + ' ' + pkg.author.name + ';' +
  ' Licensed ' + pkg.license + '*/\n';

//list of loaders and their mappings
const webpackloaders = [
  {test: /\.scss$/, loader: 'style-loader!css-loader!sass-loader'},
  {test: /\.tsx?$/, loader: 'awesome-typescript-loader'},
  {test: /\.json$/, loader: 'json-loader'},
  {
    test: /\.(png|jpg)$/,
    loader: 'url-loader',
    query: {
      limit: 10000, //inline <= 10kb
    }
  },
  {
    test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
    loader: 'url-loader',
    query: {
      limit: 10000, //inline <= 10kb
      mimetype: 'application/font-woff'
    }
  },
  {
    test: /\.svg(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
    loader: 'url-loader',
    query: {
      limit: 10000, //inline <= 10kb
      mimetype: 'image/svg+xml'
    }
  },
  {test: /\.(ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file-loader'}
];

/**
 * tests whether the given phovea module name is matching the requested file and if so convert it to an external lookup
 * depending on the loading type
 **/
function testPhoveaModule(moduleName, request) {
  if (!(new RegExp('^' + moduleName + '/src.*')).test(request)) {
    return false;
  }
  const subModule = request.match(/.*\/src\/?(.*)/)[1];
  //skip empty modules = root
  const path = subModule === '' ? [moduleName] : [moduleName, subModule];
  //phovea_<name> ... phovea.name
  const rootPath = /phovea_.*/.test(moduleName) ? ['phovea', moduleName.slice(7)].concat(path.slice(1)) : path;
  return {
    root: rootPath,
    commonjs2: path,
    commonjs: path,
    amd: request + (subModule === '' ? '/main' : '')
  };
}

function testPhoveaModules(modules) {
  return (context, request, callback) => {
    for (let i = 0; i < modules.length; ++i) {
      const r = testPhoveaModule(modules[i], request);
      if (r) {
        return callback(null, r);
      }
    }
    callback();
  };
}

// use workspace registry file if available
const isWorkspaceContext = fs.existsSync(resolve(__dirname, '..', 'phovea_registry.js'));
const registryFile = isWorkspaceContext ? '../phovea_registry.js' : './phovea_registry.js';
const actBuildInfoFile = `file-loader?name=buildInfo.json!${buildInfo.tmpFile()}`;

/**
 * inject the registry to be included
 * @param entry
 * @returns {*}
 */
function injectRegistry(entry) {
  //build also the registry
  if (typeof entry === 'string') {
    return [registryFile, actBuildInfoFile].concat(entry);
  } else {
    const transformed = {};
    Object.keys(entry).forEach((eentry) => {
      transformed[eentry] = [registryFile, actBuildInfoFile].concat(entry[eentry]);
    });
    return transformed;
  }

}
/**
 * generate a webpack configuration
 */
function generateWebpack(options) {
  let base = {
    entry: injectRegistry(options.entries),
    output: {
      path: resolve(__dirname, 'build'),
      filename: (options.name || (pkg.name + (options.bundle ? '_bundle' : ''))) + (options.min && !options.nosuffix ? '.min' : '') + '.js',
      publicPath: '' //no public path = relative
    },
    resolve: {
      // Add `.ts` and `.tsx` as a resolvable extension.
      extensions: ['.webpack.js', '.web.js', '.ts', '.tsx', '.js'],
      alias: Object.assign({}, options.libs || {}),
      //fallback to the directory above if they are siblings just in the workspace context
      modules: isWorkspaceContext ? [
        resolve(__dirname, '../'),
        'node_modules'
      ] : ['node_modules']
    },
    plugins: [
      new webpack.BannerPlugin({
        banner: banner,
        raw: true
      }),
      //define magic constants that are replaced
      new webpack.DefinePlugin({
        __VERSION__: JSON.stringify(pkg.version),
        __LICENSE__: JSON.stringify(pkg.license),
        __BUILD_ID__: buildId,
        __DEBUG__: options.isDev || options.isTest,
        __TEST__: options.isTest,
        __PRODUCTION__: options.isProduction,
        __APP_CONTEXT__: JSON.stringify('/')
      }),
      new webpack.optimize.MinChunkSizePlugin({
        minChunkSize: 10000 //at least 10.000 characters
      }),
      new webpack.optimize.AggressiveMergingPlugin()
      //rest depends on type
    ],
    externals: [],
    module: {
      loaders: webpackloaders.slice()
    },
    devServer: {
      proxy: {
        '/api/*': {
          target: 'http://localhost:9000',
          secure: false
        },
        '/login': {
          target: 'http://localhost:9000',
          secure: false
        },
        '/logout': {
          target: 'http://localhost:9000',
          secure: false
        },
        '/loggedinas': {
          target: 'http://localhost:9000',
          secure: false
        }
      },
      contentBase: resolve(__dirname, 'build')
    }
  };

  if (options.library) {
    let libName = /phovea_.*/.test(pkg.name) ? ['phovea', pkg.name.slice(7)] : pkg.name;
    //generate a library, i.e. output the last entry element
    //create library name
    if (options.moduleBundle) {
      libName = 'phovea';
    }
    base.output.library = libName;
    base.output.libraryTarget = 'umd';
    base.output.umdNamedDefine = false; //anonymous require module
  }

  if (!options.bundle) {
    //if we don't bundle don't include external libraries and other phovea modules
    base.externals.push(...(options.externals || Object.keys(options.libs || {})));

    //ignore all phovea modules
    if (options.modules) {
      base.externals.push(testPhoveaModules(options.modules));
    }

    //ignore extra modules
    (options.ignore || []).forEach(function (d) {
      base.module.loaders.push({test: new RegExp(d), loader: 'null-loader'}); //use null loader
    });
    //ingore phovea module registry calls
    (options.modules || []).forEach(function (m) {
      base.module.loaders.push({test: new RegExp('.*[\\\\/]' + m + '[\\\\/]phovea_registry.js'), loader: 'null-loader'}); //use null loader
    });
  }
  if (!options.bundle || options.isApp) {
    //extract the included css file to own file
    let p = new ExtractTextPlugin({
      filename: (options.isApp || options.moduleBundle ? 'style' : pkg.name)  + (options.min && !options.nosuffix ? '.min' : '') + '.css',
      allChunks: true // there seems to be a bug in dynamically loaded chunk styles are not loaded, workaround: extract all styles from all chunks
    });
    base.plugins.push(p);
    base.module.loaders[0] = {
      test: /\.scss$/,
      loader: p.extract(['css-loader', 'sass-loader'])
    };
  }
  if (options.isApp) {
    // create manifest
    // base.plugins.push(new webpack.optimize.AppCachePlugin());
  }
  if (options.commons) {
    //build a commons plugin
    base.plugins.push(new webpack.optimize.CommonsChunkPlugin({
      // The order of this array matters
      names: ['common'],
      minChunks: 2
    }));
  }
  if (options.min) {
    //use a minifier
    base.plugins.push(
      new webpack.LoaderOptionsPlugin({
        minimize: true,
        debug: false
      }),
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false
        },
        output: {
          comments: false
        },
        sourceMap: false
      }));
  } else {
    //generate source maps
    base.devtool = 'source-map';
  }
  return base;
}

function generateWebpackConfig(env) {
  const isTest = env === 'test';
  const isProduction = env === 'prod';
  const isDev = !isProduction && !isTest;

  const base = {
    entries: entries,
    libs: libraryAliases,
    externals: libraryExternals,
    modules: modules,
    ignore: ignores,
    isProduction: isProduction,
    isDev: isDev,
    isTest: isTest
  };

  if (isTest) {
    return generateWebpack(Object.assign({}, base, {
      bundle: true
    }));
  }

  if (type.startsWith('app')) {
    base.isApp = true;
    base.bundle = true; //bundle everything together
    base.name = '[name]'; //multiple entries case
    base.commons = true; //extract commons module
  } else if (type === 'bundle') {
    base.library = true; //expose as library
    base.moduleBundle = true; //expose as library 'phovea'
    base.name = pkg.name; //to avoid adding _bundle
    base.bundle = true;
  } else { //type === 'lib'
    base.library = true;
  }

  //single generation
  if (isDev) {
    return generateWebpack(base);
  } else if (type === 'app') { //isProduction app
    return generateWebpack(Object.assign({}, base, {
        min: true,
        nosuffix: true
      }));
  } else { //isProduction
    return [
      //plain
      generateWebpack(base),
      //minified
      generateWebpack(Object.assign({}, base, {
        min: true
      }))
    ];
  }
}

module.exports = generateWebpackConfig;
module.exports.generateWebpack = generateWebpack;
<|endoftext|>


<commit_before>// @flow

function abstract() {
  throw new Error('Abstract method not implemented');
}

export { abstract };<commit_msg>src: add missing EOL as per linting rules<commit_after>// @flow

function abstract() {
  throw new Error('Abstract method not implemented');
}

export { abstract };
<|endoftext|>


<commit_before>import fetch from 'isomorphic-fetch';
import querystring from 'querystring';

const fetchOptions = {
	method: 'GET',
	mode: 'cors',
	credentials: 'omit'
};

export function api(apiOptions, params = {}) {
	const qs = Object.assign(
		{
			format: 'json',
			action: 'query',
			redirects: ''
		},
		params
	);
	// Remove undefined properties
	Object.keys(qs).forEach(key => {
		if (qs[key] === undefined) {
			delete qs[key];
		}
	});
	if (apiOptions.origin) {
		qs.origin = apiOptions.origin;
	}
	const url = `${apiOptions.apiUrl}?${querystring.stringify(qs)}`;
	return fetch(
		url,
		Object.assign({ headers: apiOptions.headers }, fetchOptions)
	)
		.then(res => res.json())
		.then(res => {
			if (res.error) {
				throw new Error(res.error.info);
			}
			return res;
		});
}

export function pagination(apiOptions, params, parseResults) {
	return api(apiOptions, params).then(res => {
		let resolution = {};
		resolution.results = parseResults(res);
		resolution.query = params.srsearch;
		if (res['continue']) {
			const continueType = Object.keys(res['continue']).filter(
				key => key !== 'continue'
			)[0];
			const continueKey = res['continue'][continueType];
			params[continueType] = continueKey;
			resolution.next = () => pagination(apiOptions, params, parseResults);
		}
		return resolution;
	});
}

export function aggregatePagination(pagination, previousResults = []) {
	return pagination.then(res => {
		const results = [...previousResults, ...res.results];
		if (res.next) {
			return aggregatePagination(res.next(), results);
		} else {
			return results;
		}
	});
}

const pageLimit = 500;

export function aggregate(apiOptions, params, list, key, prefix, results = []) {
	params.list = list;
	params[prefix + 'limit'] = pageLimit;
	return api(apiOptions, params).then(res => {
		const nextResults = [...results, ...res.query[list].map(e => e[key])];
		const continueWith = res['query-continue'] || res.continue;
		if (continueWith) {
			const nextFromKey =
				(continueWith[list] && continueWith[list][prefix + 'from']) ||
				continueWith[prefix + 'continue'];
			params[prefix + 'continue'] = nextFromKey;
			params[prefix + 'from'] = nextFromKey;
			return aggregate(apiOptions, params, list, key, prefix, nextResults);
		}
		return nextResults;
	});
}

const headingPattern = /(=+)\s?([\w\s-]+)\s?=+/g;

function getHeadings(text) {
	let match;
	const matches = [];
	while ((match = headingPattern.exec(text)) !== null) {
		matches.push({
			level: match[1].trim().length,
			text: match[2].trim(),
			start: match.index,
			end: match.index + match[0].length
		});
	}
	return matches;
}

export function parseContent(source) {
	const headings = getHeadings(source);

	const minLevel = Math.min(...headings.map(({ level }) => level));

	const sections = headings.map((heading, index) => {
		const next = headings[index + 1];
		const content = source
			.substring(heading.end, next ? next.start : undefined)
			.trim();
		return {
			title: heading.text,
			level: heading.level - minLevel,
			id: index,
			content,
			items: []
		};
	});

	const lastParentLevel = (index, level) => {
		if (level === 0) return null;
		for (let i = index - 1; i >= 0; i--) {
			if (sections[i].level < level) {
				return sections[i].id;
			}
		}
		return null;
	};

	// Set parents
	sections.forEach((section, index) => {
		section.parent = lastParentLevel(index, section.level);
	});

	const root = {
		items: []
	};

	const findSection = id => sections.find(s => id === s.id);

	// Organize
	sections.forEach(section => {
		if (section.parent === null) {
			root.items.push(section);
		} else {
			findSection(section.parent).items.push(section);
		}
	});

	// Clean up
	sections.forEach(section => {
		delete section.id;
		delete section.parent;
		delete section.level;
		if (!section.items.length) {
			delete section.items;
		}
	});

	return root.items;
}
<commit_msg>fixes article section headers regex (thanks to Wiktor Stribiżew from SO)<commit_after>import fetch from 'isomorphic-fetch';
import querystring from 'querystring';

const fetchOptions = {
	method: 'GET',
	mode: 'cors',
	credentials: 'omit'
};

export function api(apiOptions, params = {}) {
	const qs = Object.assign(
		{
			format: 'json',
			action: 'query',
			redirects: ''
		},
		params
	);
	// Remove undefined properties
	Object.keys(qs).forEach(key => {
		if (qs[key] === undefined) {
			delete qs[key];
		}
	});
	if (apiOptions.origin) {
		qs.origin = apiOptions.origin;
	}
	const url = `${apiOptions.apiUrl}?${querystring.stringify(qs)}`;
	return fetch(
		url,
		Object.assign({ headers: apiOptions.headers }, fetchOptions)
	)
		.then(res => res.json())
		.then(res => {
			if (res.error) {
				throw new Error(res.error.info);
			}
			return res;
		});
}

export function pagination(apiOptions, params, parseResults) {
	return api(apiOptions, params).then(res => {
		let resolution = {};
		resolution.results = parseResults(res);
		resolution.query = params.srsearch;
		if (res['continue']) {
			const continueType = Object.keys(res['continue']).filter(
				key => key !== 'continue'
			)[0];
			const continueKey = res['continue'][continueType];
			params[continueType] = continueKey;
			resolution.next = () => pagination(apiOptions, params, parseResults);
		}
		return resolution;
	});
}

export function aggregatePagination(pagination, previousResults = []) {
	return pagination.then(res => {
		const results = [...previousResults, ...res.results];
		if (res.next) {
			return aggregatePagination(res.next(), results);
		} else {
			return results;
		}
	});
}

const pageLimit = 500;

export function aggregate(apiOptions, params, list, key, prefix, results = []) {
	params.list = list;
	params[prefix + 'limit'] = pageLimit;
	return api(apiOptions, params).then(res => {
		const nextResults = [...results, ...res.query[list].map(e => e[key])];
		const continueWith = res['query-continue'] || res.continue;
		if (continueWith) {
			const nextFromKey =
				(continueWith[list] && continueWith[list][prefix + 'from']) ||
				continueWith[prefix + 'continue'];
			params[prefix + 'continue'] = nextFromKey;
			params[prefix + 'from'] = nextFromKey;
			return aggregate(apiOptions, params, list, key, prefix, nextResults);
		}
		return nextResults;
	});
}

const headingPattern = /(==+)((?!\n)\s?)(((?!==|\n)[^])+)((?!\n)\s?)(==+)/g;

function getHeadings(text) {
	let match;
	const matches = [];
	while ((match = headingPattern.exec(text)) !== null) {
		matches.push({
			level: match[1].trim().length,
			text: match[2].trim(),
			start: match.index,
			end: match.index + match[0].length
		});
	}
	return matches;
}

export function parseContent(source) {
	const headings = getHeadings(source);

	const minLevel = Math.min(...headings.map(({ level }) => level));

	const sections = headings.map((heading, index) => {
		const next = headings[index + 1];
		const content = source
			.substring(heading.end, next ? next.start : undefined)
			.trim();
		return {
			title: heading.text,
			level: heading.level - minLevel,
			id: index,
			content,
			items: []
		};
	});

	const lastParentLevel = (index, level) => {
		if (level === 0) return null;
		for (let i = index - 1; i >= 0; i--) {
			if (sections[i].level < level) {
				return sections[i].id;
			}
		}
		return null;
	};

	// Set parents
	sections.forEach((section, index) => {
		section.parent = lastParentLevel(index, section.level);
	});

	const root = {
		items: []
	};

	const findSection = id => sections.find(s => id === s.id);

	// Organize
	sections.forEach(section => {
		if (section.parent === null) {
			root.items.push(section);
		} else {
			findSection(section.parent).items.push(section);
		}
	});

	// Clean up
	sections.forEach(section => {
		delete section.id;
		delete section.parent;
		delete section.level;
		if (!section.items.length) {
			delete section.items;
		}
	});

	return root.items;
}
<|endoftext|>


<commit_before>'use strict'

const fs = require('fs')
const homedir = require('homedir')
const colors = require('colors/safe')

function createNofanDir () {
  return new Promise((resolve, reject) => {
    fs.mkdir(`${homedir()}/.nofan/`, () => {
      resolve()
    })
  })
}

function createJsonFile (filename, content) {
  return new Promise((resolve, reject) => {
    const filePath = `${homedir()}/.nofan/${filename}.json`
    fs.writeFile(filePath, JSON.stringify(content, null, 2), 'utf8', (e) => {
      if (e) reject(colors.red(`create file '${filePath}' failed`))
      else resolve()
    })
  })
}

function readJsonFile (filename) {
  return new Promise((resolve, reject) => {
    const filePath = `${homedir()}/.nofan/${filename}.json`
    fs.open(filePath, 'r', (e, fd) => {
      if (e) {
        if (e.code === 'ENOENT') {
          reject(colors.red(`file '${filePath}' does not exist`))
        }
        reject(colors.red(`read file '${filePath}' failed`))
      } else resolve(require(filePath))
    })
  })
}

async function getConfig () {
  try {
    return await readJsonFile('config')
  } catch (err) {
    console.log(err)
  }
}

async function getAccount () {
  try {
    return await readJsonFile('account')
  } catch (err) {
    return {}
  }
}

async function setConfig (config) {
  try {
    await createJsonFile('config', config)
  } catch (err) {
    console.log(err)
  }
}

async function setAccount (account) {
  try {
    await createJsonFile('account', account)
  } catch (err) {
    console.log(err)
  }
}

module.exports = {
  createNofanDir: createNofanDir,
  getConfig: getConfig,
  getAccount: getAccount,
  setConfig: setConfig,
  setAccount: setAccount
}
<commit_msg>remove try-catch<commit_after>'use strict'

const fs = require('fs')
const homedir = require('homedir')
const colors = require('colors/safe')

function createNofanDir () {
  return new Promise((resolve, reject) => {
    fs.mkdir(`${homedir()}/.nofan/`, () => {
      resolve()
    })
  })
}

function createJsonFile (filename, content) {
  return new Promise((resolve, reject) => {
    const filePath = `${homedir()}/.nofan/${filename}.json`
    fs.writeFile(filePath, JSON.stringify(content, null, 2), 'utf8', (e) => {
      if (e) reject(colors.red(`create file '${filePath}' failed`))
      else resolve()
    })
  })
}

function readJsonFile (filename) {
  return new Promise((resolve, reject) => {
    const filePath = `${homedir()}/.nofan/${filename}.json`
    fs.open(filePath, 'r', (e, fd) => {
      if (e) {
        if (e.code === 'ENOENT') {
          reject(colors.red(`file '${filePath}' does not exist`))
        }
        reject(colors.red(`read file '${filePath}' failed`))
      } else resolve(require(filePath))
    })
  })
}

function getConfig () {
  return readJsonFile('config')
}

async function getAccount () {
  try {
    return await readJsonFile('account')
  } catch (err) {
    return {}
  }
}

function setConfig (config) {
  createJsonFile('config', config)
}

function setAccount (account) {
  createJsonFile('account', account)
}

module.exports = {
  createNofanDir: createNofanDir,
  getConfig: getConfig,
  getAccount: getAccount,
  setConfig: setConfig,
  setAccount: setAccount
}
<|endoftext|>