<commit_before>define(function (require, exports) {
	const ko = require('knockout');
	const httpService = require('services/http');
	const sharedState = require('atlas-state');
	const config = require('appConfig');
	const authApi = require('services/AuthAPI');
	const vocabularyService = require('services/Vocabulary');
	const commonUtils = require('utils/CommonUtils');
	function getIncludedConceptSetDrawCallback({ searchConceptsColumns }) {
		return async function (settings) {
			if (settings.aoData) {
				const api = this.api();
				const rows = this.api().rows({page: 'current'});
				const data = rows.data();
				// The callback is called even when the table is not really populated with data.
				// In such case we would call API with set of NULLs which both doesn't make sense and breaks some DBs.
				// Therefore, we first check if there is real data to send to API.
				if (data[0] && data[0].CONCEPT_ID) {
					await loadAndApplyAncestors(data);
					const columnIndex = searchConceptsColumns.findIndex(v => v.data === 'ANCESTORS');
					api.cells(null, columnIndex).invalidate();
					rows.nodes().each((element, index) => {
						const rowData = data[index];
						commonUtils.contextSensitiveLinkColor(element, rowData);
						const context = ko.contextFor(element);
						ko.cleanNode(element);
						ko.applyBindings(context, element);
					})
				}
			}
		}
	}

	function getAncestorsModalHandler({ sharedState, ancestors, ancestorsModalIsShown }) {
		return function(conceptId) {
			ancestors(sharedState.includedConcepts()
				.find(v => v.CONCEPT_ID === conceptId)
				.ANCESTORS
				.map(v => ({concept: v})));
			if (!_.isEmpty(ancestors())) {
				ancestorsModalIsShown(true);
			}
		}
	}

	function getAncestorsRenderFunction() {
		return (s,p,d) => {
			const tooltip = d.ANCESTORS.map(d => d.CONCEPT_NAME).join('<br>');
			return `<a data-bind="click: d => $parents[1].showAncestorsModal(d.CONCEPT_ID), tooltip: \`${tooltip}\`">${d.ANCESTORS.length}</a>`
		};
	}

	function enhanceConceptSet(conceptSetItem) {
		return {
			...conceptSetItem,
			isExcluded: ko.observable(conceptSetItem.isExcluded),
			includeDescendants: ko.observable(conceptSetItem.includeDescendants),
			includeMapped: ko.observable(conceptSetItem.includeMapped),
		};
	}

	function loadConceptSet(id) {
		return httpService.doGet(config.api.url + 'conceptset/' + id).then(({ data }) => data);
	}

	function loadConceptSetExpression(conceptSetId) {
		return httpService.doGet(config.api.url + 'conceptset/' + conceptSetId + '/expression').then(({ data }) => data);
	}

	function lookupIdentifiers(identifiers) {
		return httpService.doPost(sharedState.vocabularyUrl() + 'lookup/identifiers', JSON.stringify(identifiers));
	}

	function getInclusionCount(expression) {
		const data = { items: ko.toJS(expression), };
		return httpService.doPost(sharedState.vocabularyUrl() + 'included-concepts/count', data);
	}

	function getGenerationInfo(conceptSetId) {
		return httpService.doGet(config.webAPIRoot + 'conceptset/' + + (conceptSetId || '-1') + '/generationinfo')
			.then(({ data }) => data)
			.catch(authApi.handleAccessDenied);
	}

    function deleteConceptSet(conceptSetId) {
		return httpService.doDelete(config.webAPIRoot + 'conceptset/' + (conceptSetId || '-1'))
			.catch(authApi.handleAccessDenied);
    }

  function exists(name, id) {
		return httpService.doGet(`${config.webAPIRoot}conceptset/${id}/exists?name=${name}`)
			.then(({ data }) => data);
  }

  function saveConceptSet(conceptSet) {
		let promise = new Promise(r => r());
		const url = `${config.api.url}conceptset/${conceptSet.id ? conceptSet.id : ''}`;
		if (conceptSet.id) {
			promise = httpService.doPut(url, conceptSet);
		} else {
			promise = httpService.doPost(url, conceptSet);
		}
		promise.catch(authApi.handleAccessDenied);

		return promise;
	}

	function saveConceptSetItems(id, conceptSetItems) {
		return httpService.doPut(config.api.url + 'conceptset/' + id + '/items', conceptSetItems)
			.catch(authApi.handleAccessDenied);
	}

	function getConceptSet(conceptSetId) {
		return httpService.doGet(config.webAPIRoot + 'conceptset/' + (conceptSetId || '-1'))
			.catch(authApi.handleAccessDenied);
	}

	function getCopyName(id) {
		return httpService.doGet(config.webAPIRoot + 'conceptset/' + (id || "") + "/copy-name")
			.then(({ data }) => data);
	}

	async function loadSourcecodes() {
		sharedState.loadingSourcecodes(true);

		// load mapped
		let identifiers = [];
		let concepts = sharedState.includedConcepts();
		for (var i = 0; i < concepts.length; i++) {
			identifiers.push(concepts[i].CONCEPT_ID);
		}
		try {
			const { data } = await httpService.doPost(`${sharedState.vocabularyUrl()}lookup/mapped`, identifiers);
			sharedState.includedSourcecodes(data);
			return data;
		} catch (err) {
			console.error(err);
		} finally {
			sharedState.loadingSourcecodes(false);
		}
	}

	async function onCurrentConceptSetModeChanged(newMode) {
		switch (newMode) {
			case 'included-conceptsets':
			case 'included':
				await loadIncluded();
				break;
			case 'included-sourcecodes':
				await loadIncluded();
				if (sharedState.includedSourcecodes().length === 0) {
					await loadSourcecodes();
				}
				break;
		}
	}

	async function loadAncestors(ancestors, descendants) {
		const data = { ancestors, descendants };
		return httpService.doPost(sharedState.vocabularyUrl() + 'lookup/identifiers/ancestors', data);
	};

	function loadAndApplyAncestors(data) {
		const selectedConceptIds = sharedState.selectedConcepts().filter(v => !v.isExcluded()).map(v => v.concept.CONCEPT_ID);
		const ids = [];
		$.each(data, idx => {
			const element = data[idx];
			if (_.isEmpty(element.ANCESTORS) && sharedState.selectedConceptsIndex[element.CONCEPT_ID] !== 1) {
				ids.push(element.CONCEPT_ID);
			}
		});
		return new Promise((resolve, reject) => {
			if (!_.isEmpty(selectedConceptIds) && !_.isEmpty(ids)) {
				loadAncestors(selectedConceptIds, ids).then(({ data: ancestors }) => {
					const map = sharedState.includedConceptsMap();
					$.each(data, idx => {
						const line = data[idx];
						const ancArray = ancestors[line.CONCEPT_ID];
						if (!_.isEmpty(ancArray) && _.isEmpty(line.ANCESTORS)) {
							line.ANCESTORS = ancArray.map(conceptId => map[conceptId]);
						}
					});
					resolve();
				});
			} else {
				resolve();
			}
		});
	};

	async function loadIncluded(identifiers) {
		const data = identifiers || sharedState.conceptSetInclusionIdentifiers();
		try {
			sharedState.loadingIncluded(true);
			const response = await httpService.doPost(`${sharedState.vocabularyUrl()}lookup/identifiers`, data);
			await vocabularyService.loadDensity(response.data);
			sharedState.includedConcepts((response.data || []).map(v => ({...v, ANCESTORS: []})));
			const map = response.data.reduce((result, item) => {
				result[item.CONCEPT_ID] = item;
				return result;
			}, {});
			sharedState.includedConceptsMap(map);
			await loadAndApplyAncestors(sharedState.includedConcepts());

		} catch (err) {
			console.error(err);
		} finally {
			sharedState.loadingIncluded(false);
		}
	}

	function setConceptSet(conceptset, expressionItems) {
		var conceptSetItemsToAdd = [];
		expressionItems.forEach((conceptSet) => {
			const conceptSetItem = enhanceConceptSet(conceptSet);

			sharedState.selectedConceptsIndex[conceptSetItem.concept.CONCEPT_ID] = 1;
			conceptSetItemsToAdd.push(conceptSetItem);
		});

		sharedState.selectedConcepts(conceptSetItemsToAdd);
		sharedState.ConceptSet.current({
			name: ko.observable(conceptset.name),
			id: conceptset.id
		});
	}

	function setConceptSetExpressionExportItems() {
		var highlightedJson = commonUtils.syntaxHighlight(sharedState.conceptSetExpression());
		sharedState.currentConceptSetExpressionJson(highlightedJson);
		var conceptIdentifierList = [];
		for (var i = 0; i < sharedState.selectedConcepts().length; i++) {
			conceptIdentifierList.push(sharedState.selectedConcepts()[i].concept.CONCEPT_ID);
		}
		sharedState.currentConceptIdentifierList(conceptIdentifierList.join(','));
	}

	// for the current selected concepts:
	// update the export panel
	// resolve the included concepts and update the include concept set identifier list
	function resolveConceptSetExpression(resolveAgainstServer = true) {
		sharedState.includedConcepts.removeAll();
		sharedState.includedSourcecodes.removeAll();
		sharedState.conceptSetInclusionIdentifiers.removeAll();
		sharedState.currentConceptIdentifierList(null);
		sharedState.currentIncludedConceptIdentifierList(null);
		setConceptSetExpressionExportItems(sharedState.conceptSetExpression());
		return resolveAgainstServer ? resolveConceptSetExpressionSimple(sharedState.conceptSetExpression()) : null;
	}

	function resolveConceptSetExpressionSimple(expression, success) {
		const callback = typeof success === 'function'
			? success
			: ({ data }) => {
				let info = data;
				if (!Array.isArray(info)) {
					throw new Error();
				}
				sharedState.conceptSetInclusionIdentifiers(info);
				sharedState.currentIncludedConceptIdentifierList(info.join(','));
			};
		sharedState.resolvingConceptSetExpression(true);
		const resolvingPromise = httpService.doPost(sharedState.vocabularyUrl() + 'resolveConceptSetExpression', expression)
			.then(callback)
			.then(() => sharedState.resolvingConceptSetExpression(false))

		return resolvingPromise;
	}

	function clearConceptSet() {
		sharedState.ConceptSet.current(null);
		sharedState.clearSelectedConcepts();
		resolveConceptSetExpression(false);
		sharedState.ConceptSet.dirtyFlag().reset();
	}

	const api = {
		getIncludedConceptSetDrawCallback: getIncludedConceptSetDrawCallback,
		getAncestorsModalHandler: getAncestorsModalHandler,
		getAncestorsRenderFunction: getAncestorsRenderFunction,
		enhanceConceptSet,
		loadConceptSet,
		loadConceptSetExpression,
		lookupIdentifiers,
		getInclusionCount,
		getCopyName: getCopyName,
		getConceptSet: getConceptSet,
		getGenerationInfo: getGenerationInfo,
		deleteConceptSet: deleteConceptSet,
		exists: exists,
		saveConceptSet,
		saveConceptSetItems,
		loadSourcecodes,
		onCurrentConceptSetModeChanged,
		loadIncluded,
		setConceptSet,
		setConceptSetExpressionExportItems,
		resolveConceptSetExpression,
		resolveConceptSetExpressionSimple,
		clearConceptSet,
	};

	return api;
});<commit_msg>Fixed tooltip for conceptset ancestors modal link (#2027)<commit_after>define(function (require, exports) {
	const ko = require('knockout');
	const httpService = require('services/http');
	const sharedState = require('atlas-state');
	const config = require('appConfig');
	const authApi = require('services/AuthAPI');
	const vocabularyService = require('services/Vocabulary');
	const commonUtils = require('utils/CommonUtils');
	function getIncludedConceptSetDrawCallback({ searchConceptsColumns }) {
		return async function (settings) {
			if (settings.aoData) {
				const api = this.api();
				const rows = this.api().rows({page: 'current'});
				const data = rows.data();
				// The callback is called even when the table is not really populated with data.
				// In such case we would call API with set of NULLs which both doesn't make sense and breaks some DBs.
				// Therefore, we first check if there is real data to send to API.
				if (data[0] && data[0].CONCEPT_ID) {
					await loadAndApplyAncestors(data);
					const columnIndex = searchConceptsColumns.findIndex(v => v.data === 'ANCESTORS');
					api.cells(null, columnIndex).invalidate();
					rows.nodes().each((element, index) => {
						const rowData = data[index];
						commonUtils.contextSensitiveLinkColor(element, rowData);
						const context = ko.contextFor(element);
						ko.cleanNode(element);
						ko.applyBindings(context, element);
					})
				}
			}
		}
	}

	function getAncestorsModalHandler({ sharedState, ancestors, ancestorsModalIsShown }) {
		return function(conceptId) {
			ancestors(sharedState.includedConcepts()
				.find(v => v.CONCEPT_ID === conceptId)
				.ANCESTORS
				.map(v => ({concept: v})));
			if (!_.isEmpty(ancestors())) {
				ancestorsModalIsShown(true);
			}
		}
	}

	function getAncestorsRenderFunction() {
		return (s,p,d) => {
			const tooltip = d.ANCESTORS.map(d => d.CONCEPT_NAME).join('\n');
			return `<a data-bind="click: d => $parents[1].showAncestorsModal(d.CONCEPT_ID), tooltip: '${tooltip}'">${d.ANCESTORS.length}</a>`
		};
	}

	function enhanceConceptSet(conceptSetItem) {
		return {
			...conceptSetItem,
			isExcluded: ko.observable(conceptSetItem.isExcluded),
			includeDescendants: ko.observable(conceptSetItem.includeDescendants),
			includeMapped: ko.observable(conceptSetItem.includeMapped),
		};
	}

	function loadConceptSet(id) {
		return httpService.doGet(config.api.url + 'conceptset/' + id).then(({ data }) => data);
	}

	function loadConceptSetExpression(conceptSetId) {
		return httpService.doGet(config.api.url + 'conceptset/' + conceptSetId + '/expression').then(({ data }) => data);
	}

	function lookupIdentifiers(identifiers) {
		return httpService.doPost(sharedState.vocabularyUrl() + 'lookup/identifiers', JSON.stringify(identifiers));
	}

	function getInclusionCount(expression) {
		const data = { items: ko.toJS(expression), };
		return httpService.doPost(sharedState.vocabularyUrl() + 'included-concepts/count', data);
	}

	function getGenerationInfo(conceptSetId) {
		return httpService.doGet(config.webAPIRoot + 'conceptset/' + + (conceptSetId || '-1') + '/generationinfo')
			.then(({ data }) => data)
			.catch(authApi.handleAccessDenied);
	}

    function deleteConceptSet(conceptSetId) {
		return httpService.doDelete(config.webAPIRoot + 'conceptset/' + (conceptSetId || '-1'))
			.catch(authApi.handleAccessDenied);
    }

  function exists(name, id) {
		return httpService.doGet(`${config.webAPIRoot}conceptset/${id}/exists?name=${name}`)
			.then(({ data }) => data);
  }

  function saveConceptSet(conceptSet) {
		let promise = new Promise(r => r());
		const url = `${config.api.url}conceptset/${conceptSet.id ? conceptSet.id : ''}`;
		if (conceptSet.id) {
			promise = httpService.doPut(url, conceptSet);
		} else {
			promise = httpService.doPost(url, conceptSet);
		}
		promise.catch(authApi.handleAccessDenied);

		return promise;
	}

	function saveConceptSetItems(id, conceptSetItems) {
		return httpService.doPut(config.api.url + 'conceptset/' + id + '/items', conceptSetItems)
			.catch(authApi.handleAccessDenied);
	}

	function getConceptSet(conceptSetId) {
		return httpService.doGet(config.webAPIRoot + 'conceptset/' + (conceptSetId || '-1'))
			.catch(authApi.handleAccessDenied);
	}

	function getCopyName(id) {
		return httpService.doGet(config.webAPIRoot + 'conceptset/' + (id || "") + "/copy-name")
			.then(({ data }) => data);
	}

	async function loadSourcecodes() {
		sharedState.loadingSourcecodes(true);

		// load mapped
		let identifiers = [];
		let concepts = sharedState.includedConcepts();
		for (var i = 0; i < concepts.length; i++) {
			identifiers.push(concepts[i].CONCEPT_ID);
		}
		try {
			const { data } = await httpService.doPost(`${sharedState.vocabularyUrl()}lookup/mapped`, identifiers);
			sharedState.includedSourcecodes(data);
			return data;
		} catch (err) {
			console.error(err);
		} finally {
			sharedState.loadingSourcecodes(false);
		}
	}

	async function onCurrentConceptSetModeChanged(newMode) {
		switch (newMode) {
			case 'included-conceptsets':
			case 'included':
				await loadIncluded();
				break;
			case 'included-sourcecodes':
				await loadIncluded();
				if (sharedState.includedSourcecodes().length === 0) {
					await loadSourcecodes();
				}
				break;
		}
	}

	async function loadAncestors(ancestors, descendants) {
		const data = { ancestors, descendants };
		return httpService.doPost(sharedState.vocabularyUrl() + 'lookup/identifiers/ancestors', data);
	};

	function loadAndApplyAncestors(data) {
		const selectedConceptIds = sharedState.selectedConcepts().filter(v => !v.isExcluded()).map(v => v.concept.CONCEPT_ID);
		const ids = [];
		$.each(data, idx => {
			const element = data[idx];
			if (_.isEmpty(element.ANCESTORS) && sharedState.selectedConceptsIndex[element.CONCEPT_ID] !== 1) {
				ids.push(element.CONCEPT_ID);
			}
		});
		return new Promise((resolve, reject) => {
			if (!_.isEmpty(selectedConceptIds) && !_.isEmpty(ids)) {
				loadAncestors(selectedConceptIds, ids).then(({ data: ancestors }) => {
					const map = sharedState.includedConceptsMap();
					$.each(data, idx => {
						const line = data[idx];
						const ancArray = ancestors[line.CONCEPT_ID];
						if (!_.isEmpty(ancArray) && _.isEmpty(line.ANCESTORS)) {
							line.ANCESTORS = ancArray.map(conceptId => map[conceptId]);
						}
					});
					resolve();
				});
			} else {
				resolve();
			}
		});
	};

	async function loadIncluded(identifiers) {
		const data = identifiers || sharedState.conceptSetInclusionIdentifiers();
		try {
			sharedState.loadingIncluded(true);
			const response = await httpService.doPost(`${sharedState.vocabularyUrl()}lookup/identifiers`, data);
			await vocabularyService.loadDensity(response.data);
			sharedState.includedConcepts((response.data || []).map(v => ({...v, ANCESTORS: []})));
			const map = response.data.reduce((result, item) => {
				result[item.CONCEPT_ID] = item;
				return result;
			}, {});
			sharedState.includedConceptsMap(map);
			await loadAndApplyAncestors(sharedState.includedConcepts());

		} catch (err) {
			console.error(err);
		} finally {
			sharedState.loadingIncluded(false);
		}
	}

	function setConceptSet(conceptset, expressionItems) {
		var conceptSetItemsToAdd = [];
		expressionItems.forEach((conceptSet) => {
			const conceptSetItem = enhanceConceptSet(conceptSet);

			sharedState.selectedConceptsIndex[conceptSetItem.concept.CONCEPT_ID] = 1;
			conceptSetItemsToAdd.push(conceptSetItem);
		});

		sharedState.selectedConcepts(conceptSetItemsToAdd);
		sharedState.ConceptSet.current({
			name: ko.observable(conceptset.name),
			id: conceptset.id
		});
	}

	function setConceptSetExpressionExportItems() {
		var highlightedJson = commonUtils.syntaxHighlight(sharedState.conceptSetExpression());
		sharedState.currentConceptSetExpressionJson(highlightedJson);
		var conceptIdentifierList = [];
		for (var i = 0; i < sharedState.selectedConcepts().length; i++) {
			conceptIdentifierList.push(sharedState.selectedConcepts()[i].concept.CONCEPT_ID);
		}
		sharedState.currentConceptIdentifierList(conceptIdentifierList.join(','));
	}

	// for the current selected concepts:
	// update the export panel
	// resolve the included concepts and update the include concept set identifier list
	function resolveConceptSetExpression(resolveAgainstServer = true) {
		sharedState.includedConcepts.removeAll();
		sharedState.includedSourcecodes.removeAll();
		sharedState.conceptSetInclusionIdentifiers.removeAll();
		sharedState.currentConceptIdentifierList(null);
		sharedState.currentIncludedConceptIdentifierList(null);
		setConceptSetExpressionExportItems(sharedState.conceptSetExpression());
		return resolveAgainstServer ? resolveConceptSetExpressionSimple(sharedState.conceptSetExpression()) : null;
	}

	function resolveConceptSetExpressionSimple(expression, success) {
		const callback = typeof success === 'function'
			? success
			: ({ data }) => {
				let info = data;
				if (!Array.isArray(info)) {
					throw new Error();
				}
				sharedState.conceptSetInclusionIdentifiers(info);
				sharedState.currentIncludedConceptIdentifierList(info.join(','));
			};
		sharedState.resolvingConceptSetExpression(true);
		const resolvingPromise = httpService.doPost(sharedState.vocabularyUrl() + 'resolveConceptSetExpression', expression)
			.then(callback)
			.then(() => sharedState.resolvingConceptSetExpression(false))

		return resolvingPromise;
	}

	function clearConceptSet() {
		sharedState.ConceptSet.current(null);
		sharedState.clearSelectedConcepts();
		resolveConceptSetExpression(false);
		sharedState.ConceptSet.dirtyFlag().reset();
	}

	const api = {
		getIncludedConceptSetDrawCallback: getIncludedConceptSetDrawCallback,
		getAncestorsModalHandler: getAncestorsModalHandler,
		getAncestorsRenderFunction: getAncestorsRenderFunction,
		enhanceConceptSet,
		loadConceptSet,
		loadConceptSetExpression,
		lookupIdentifiers,
		getInclusionCount,
		getCopyName: getCopyName,
		getConceptSet: getConceptSet,
		getGenerationInfo: getGenerationInfo,
		deleteConceptSet: deleteConceptSet,
		exists: exists,
		saveConceptSet,
		saveConceptSetItems,
		loadSourcecodes,
		onCurrentConceptSetModeChanged,
		loadIncluded,
		setConceptSet,
		setConceptSetExpressionExportItems,
		resolveConceptSetExpression,
		resolveConceptSetExpressionSimple,
		clearConceptSet,
	};

	return api;
});<|endoftext|>


<commit_before>'use strict'

/*  ------------------------------------------------------------------------ */

global.log = require ('ololog') // for easier debugging

/*  ------------------------------------------------------------------------ */

const { Exchange, keys, values, unique, index, aggregate } = require ('../../../ccxt')
const { strictEqual: equal, deepEqual } = require ('assert')

/*  ------------------------------------------------------------------------ */

describe ('ccxt base code', () => {

/*  ------------------------------------------------------------------------ */

    require ('./functions/test.generic')
    require ('./functions/test.time')
    require ('./functions/test.type')
    // Some test files are transplitted into other languages and must be kept very simple so we put `it` call externally to them
    // Some mocha reporters will swallow `assert` failures if they are outside of `it` clause so we must call `it`
    it ('decimalToPrecision() works', () => {
        require ('./functions/test.number')
    })
    it ('date&time functions work', () => {
        require('./functions/test.datetime')
    })

/*  ------------------------------------------------------------------------ */

    it ('calculateFee() works', () => {

        const price  = 100.00
        const amount = 10.00
        const taker  = 0.0025
        const maker  = 0.0010
        const fees   = { taker, maker }
        const market = {
            'id':     'foobar',
            'symbol': 'FOO/BAR',
            'base':   'FOO',
            'quote':  'BAR',
            'taker':   taker,
            'maker':   maker,
            'precision': {
                'amount': 8,
                'price': 8,
            },
        }

        const exchange = new Exchange ({
            'id': 'mock',
            'markets': {
                'FOO/BAR': market,
            },
        })

        Object.keys (fees).forEach (takerOrMaker => {

            const result = exchange.calculateFee (market['symbol'], 'limit', 'sell', amount, price, takerOrMaker, {})

            deepEqual (result, {
                'type': takerOrMaker,
                'currency': 'BAR',
                'rate': fees[takerOrMaker],
                'cost': fees[takerOrMaker] * amount * price,
            })
        })
    })

/*  ------------------------------------------------------------------------ */

    it ('getCurrencyUsedOnOpenOrders() works', () => {

        const calls = []
        const rateLimit = 100
        const exchange = new Exchange ({
            'orders': [
                { status: 'open',   symbol: 'ETH/BTC', side: 'sell', price: 200.0, amount: 21.0, remaining: 20.0 },
                { status: 'open',   symbol: 'ETH/BTC', side: 'buy',  price: 200.0, amount: 22.0, remaining: 20.0 },
                { status: 'open',   symbol: 'ETH/BTC', side: 'sell', price: 200.0, amount: 23.0, remaining: 20.0 },
                { status: 'closed', symbol: 'BTC/USD', side: 'sell', price: 10.0, amount: 11.0, remaining: 10.0 },
                { status: 'open',   symbol: 'BTC/USD', side: 'buy',  price: 10.0, amount: 12.0, remaining: 10.0 },
                { status: 'open',   symbol: 'BTC/USD', side: 'sell', price: 10.0, amount: 13.0, remaining: 10.0 },
            ],
            'markets': {
                'ETH/BTC': { id: 'ETH/BTC', symbol: 'ETH/BTC', base: 'ETH', quote: 'BTC' },
                'BTC/USD': { id: 'BTC/USD', symbol: 'BTC/USD', base: 'BTC', quote: 'USD' },
            },
        })

        equal (exchange.getCurrencyUsedOnOpenOrders ('LTC'), 0)
        equal (exchange.getCurrencyUsedOnOpenOrders ('ETH'), 40)
        equal (exchange.getCurrencyUsedOnOpenOrders ('USD'), 100)
        equal (exchange.getCurrencyUsedOnOpenOrders ('BTC'), 4010)

    })

/*  ------------------------------------------------------------------------ */

    it.skip ('exchange config extension works', () => {

        cost = { min: 0.001, max: 1000 }
        precision = { amount: 3 }
        const exchange = new binance ({
            'markets': {
                'ETH/BTC': { limits: { cost }, precision },
            },
        })

        deepEqual (exchange.markets['ETH/BTC'].limits.cost, cost)
        deepEqual (exchange.markets['ETH/BTC'].precision, { price: 6, amount: 3 })
        deepEqual (exchange.markets['ETH/BTC'].symbol, 'ETH/BTC')
    })

/*  ------------------------------------------------------------------------ */

    it ('aggregate() works', () => {

        const bids = [
            [ 789.1, 123.0 ],
            [ 789.100, 123.0 ],
            [ 123.0, 456.0 ],
            [ 789.0, 123.0 ],
            [ 789.10, 123.0 ],
        ]

        const asks = [
            [ 123.0, 456.0 ],
            [ 789.0, 123.0 ],
            [ 789.10, 123.0 ],
        ]

        deepEqual (aggregate (bids.sort ()), [
            [ 123.0, 456.0 ],
            [ 789.0, 123.0 ],
            [ 789.1, 369.0 ],
        ])

        deepEqual (aggregate (asks.sort ()), [
            [ 123.0, 456.0 ],
            [ 789.0, 123.0 ],
            [ 789.10, 123.0 ],
        ])

        deepEqual (aggregate ([]), [])
    })

/*  ------------------------------------------------------------------------ */

    it ('parseBalance() works', () => {

        const exchange = new Exchange ({
            'markets': {
                'ETH/BTC': { 'id': 'ETH/BTC', 'symbol': 'ETH/BTC', 'base': 'ETH', 'quote': 'BTC', }
            }
        })

        const input = {
            'ETH': { 'free': 10, 'used': 10, 'total': 20 },
            'ZEC': { 'free': 0, 'used': 0, 'total': 0 },
        }

        const expected = {
            'ETH': { 'free': 10, 'used': 10, 'total': 20 },
            'ZEC': { 'free': 0, 'used': 0, 'total': 0 },
            'free': {
                'ETH': 10,
                'ZEC': 0,
            },
            'used': {
                'ETH': 10,
                'ZEC': 0,
            },
            'total': {
                'ETH': 20,
                'ZEC': 0,
            },
        }

        const actual = exchange.parseBalance (input)

        deepEqual (actual, expected)
    })

/*  ------------------------------------------------------------------------ */

    it ('camelCase/camel_case property conversion works', () => {

        const exchange = new Exchange ({ 'id': 'mock' })

        const propsSeenBefore = index (
                                    ["isNode", "empty", "keys", "values", "extend", "clone", "index", "ordered", "unique", "keysort", "indexBy", "groupBy", "filterBy", "sortBy", "flatten", "pluck", "omit", "sum", "deepExtend", "uuid", "unCamelCase", "capitalize", "isNumber", "isArray", "isObject", "isString", "isStringCoercible", "isDictionary", "hasProps", "prop", "asFloat", "asInteger", "safeFloat", "safeInteger", "safeValue", "safeString", "decimal", "toFixed", "truncate", "truncateToString", "precisionFromString", "stringToBinary", "stringToBase64", "utf16ToBase64", "base64ToBinary", "base64ToString", "binaryToString", "binaryConcat", "urlencode", "rawencode", "urlencodeBase64", "hash", "hmac", "jwt", "time", "setTimeout_safe", "sleep", "TimedOut", "timeout", "throttle", "json", "unjson", "aggregate", "is_node", "index_by", "group_by", "filter_by", "sort_by", "deep_extend", "un_camel_case", "is_number", "is_array", "is_object", "is_string", "is_string_coercible", "is_dictionary", "has_props", "as_float", "as_integer", "safe_float", "safe_integer", "safe_value", "safe_string", "to_fixed", "truncate_to_string", "precision_from_string", "string_to_binary", "string_to_base64", "utf16To_base64", "base64To_binary", "base64To_string", "binary_to_string", "binary_concat", "urlencode_base64", "set_timeout_safe", "Timed_out", "encode", "decode", "userAgents", "headers", "proxy", "origin", "iso8601", "parse8601", "milliseconds", "microseconds", "seconds", "id", "enableRateLimit", "rateLimit", "parseJsonResponse", "substituteCommonCurrencyCodes", "parseBalanceFromOpenOrders", "verbose", "debug", "journal", "userAgent", "twofa", "timeframes", "hasPublicAPI", "hasPrivateAPI", "hasCORS", "hasDeposit", "hasFetchBalance", "hasFetchClosedOrders", "hasFetchCurrencies", "hasFetchMyTrades", "hasFetchOHLCV", "hasFetchOpenOrders", "hasFetchOrder", "hasFetchOrderBook", "hasFetchOrders", "hasFetchTicker", "hasFetchTickers", "hasFetchBidsAsks", "hasFetchTrades", "hasWithdraw", "hasCreateOrder", "hasCancelOrder", "apiKey", "secret", "uid", "login", "password", "requiredCredentials", "exceptions", "balance", "orderbooks", "tickers", "fees", "orders", "trades", "currencies", "last_http_response", "last_json_response", "arrayConcat", "market_id", "market_ids", "array_concat", "implode_params", "extract_params", "fetch_balance", "fetch_free_balance", "fetch_used_balance","fetch_total_balance", "fetch_l2_order_book", "fetch_order_book", "fetch_bids_asks", "fetch_tickers", "fetch_ticker", "fetch_trades", "fetch_order", "fetch_orders", "fetch_open_orders", "fetch_closed_orders", "fetch_order_status", "fetch_markets", "load_markets", "set_markets", "parse_balance", "parse_bid_ask", "parse_bids_asks", "parse_order_book", "parse_trades", "parse_orders", "parse_ohlcv", "parse_ohlcvs", "edit_limit_buy_order", "edit_limit_sell_order", "edit_limit_order", "edit_order", "create_limit_buy_order", "create_limit_sell_order", "create_market_buy_order", "create_market_sell_order", "create_order", "calculate_fee", "common_currency_code", "price_to_precision", "amount_to_precision", "amount_to_string", "fee_to_precision", "cost_to_precision", "constructor", "getMarket", "describe", "defaults", "nonce", "encodeURIComponent", "checkRequiredCredentials", "initRestRateLimiter", "defineRestApi", "fetch", "fetch2", "request", "handleErrors", "defaultErrorHandler", "handleRestErrors", "handleRestResponse", "setMarkets", "loadMarkets", "fetchBidsAsks", "fetchTickers", "fetchOrder", "fetchOrders", "fetchOpenOrders", "fetchClosedOrders", "fetchMyTrades", "fetchCurrencies", "fetchMarkets", "fetchOrderStatus", "account", "commonCurrencyCode", "currency", "market", "marketId", "marketIds", "symbol", "extractParams", "implodeParams", "url", "parseBidAsk", "parseBidsAsks", "fetchL2OrderBook", "parseOrderBook", "getCurrencyUsedOnOpenOrders", "parseBalance", "fetchPartialBalance", "fetchFreeBalance", "fetchUsedBalance", "fetchTotalBalance", "filterBySinceLimit", "parseTrades", "parseOrders", "filterOrdersBySymbol", "parseOHLCV", "parseOHLCVs", "editLimitBuyOrder", "editLimitSellOrder", "editLimitOrder", "editOrder", "createLimitBuyOrder", "createLimitSellOrder", "createMarketBuyOrder", "createMarketSellOrder", "costToPrecision", "priceToPrecision", "amountToPrecision", "amountToString", "amountToLots", "feeToPrecision", "calculateFee", "Ymd", "YmdHMS"]
                                )

        const props = index (Object.getOwnPropertyNames (exchange).concat (
                             Object.getOwnPropertyNames (exchange.constructor.prototype)))

        for (const k of Array.from (propsSeenBefore))
            if (this[k] && !props.has (k))
                throw new Error (`missing prop: ${k}`)

        for (const k of Array.from (props))
            if (!propsSeenBefore.has (k))
                log.magenta.noLocate (`+ ${k}`)

    })

/*  ------------------------------------------------------------------------ */

    it ('camelCase/camel_case property conversion works #2', () => {

        class Derived extends Exchange {}
        const derived = new Derived ()
        equal (typeof derived.load_markets, 'function')
    })

/*  ------------------------------------------------------------------------ */

    it ('legacy .hasSomething maps to has.something automatically', () => {

        const exchange = new Exchange ({
                                id: 'mock',
                                has: {
                                    CORS: true,
                                    publicAPI: false,
                                    fetchDepositAddress: 'emulated'
                                }
                            })

        equal (exchange.hasCORS, true)
        equal (exchange.hasPublicAPI, false)
        equal (exchange.hasFetchDepositAddress, true)
    })
})

/*  ------------------------------------------------------------------------ */
<commit_msg>typos in comments<commit_after>'use strict'

/*  ------------------------------------------------------------------------ */

global.log = require ('ololog') // for easier debugging

/*  ------------------------------------------------------------------------ */

const { Exchange, keys, values, unique, index, aggregate } = require ('../../../ccxt')
const { strictEqual: equal, deepEqual } = require ('assert')

/*  ------------------------------------------------------------------------ */

describe ('ccxt base code', () => {

/*  ------------------------------------------------------------------------ */

    require ('./functions/test.generic')
    require ('./functions/test.time')
    require ('./functions/test.type')
    // Some test files are transpiled to other languages and must be kept very simple so we put `it` call externally to them
    // Some mocha reporters will swallow `assert` failures if they are outside of `it` clause so we must call `it`
    it ('decimalToPrecision() works', () => {
        require ('./functions/test.number')
    })
    it ('date&time functions work', () => {
        require('./functions/test.datetime')
    })

/*  ------------------------------------------------------------------------ */

    it ('calculateFee() works', () => {

        const price  = 100.00
        const amount = 10.00
        const taker  = 0.0025
        const maker  = 0.0010
        const fees   = { taker, maker }
        const market = {
            'id':     'foobar',
            'symbol': 'FOO/BAR',
            'base':   'FOO',
            'quote':  'BAR',
            'taker':   taker,
            'maker':   maker,
            'precision': {
                'amount': 8,
                'price': 8,
            },
        }

        const exchange = new Exchange ({
            'id': 'mock',
            'markets': {
                'FOO/BAR': market,
            },
        })

        Object.keys (fees).forEach (takerOrMaker => {

            const result = exchange.calculateFee (market['symbol'], 'limit', 'sell', amount, price, takerOrMaker, {})

            deepEqual (result, {
                'type': takerOrMaker,
                'currency': 'BAR',
                'rate': fees[takerOrMaker],
                'cost': fees[takerOrMaker] * amount * price,
            })
        })
    })

/*  ------------------------------------------------------------------------ */

    it ('getCurrencyUsedOnOpenOrders() works', () => {

        const calls = []
        const rateLimit = 100
        const exchange = new Exchange ({
            'orders': [
                { status: 'open',   symbol: 'ETH/BTC', side: 'sell', price: 200.0, amount: 21.0, remaining: 20.0 },
                { status: 'open',   symbol: 'ETH/BTC', side: 'buy',  price: 200.0, amount: 22.0, remaining: 20.0 },
                { status: 'open',   symbol: 'ETH/BTC', side: 'sell', price: 200.0, amount: 23.0, remaining: 20.0 },
                { status: 'closed', symbol: 'BTC/USD', side: 'sell', price: 10.0, amount: 11.0, remaining: 10.0 },
                { status: 'open',   symbol: 'BTC/USD', side: 'buy',  price: 10.0, amount: 12.0, remaining: 10.0 },
                { status: 'open',   symbol: 'BTC/USD', side: 'sell', price: 10.0, amount: 13.0, remaining: 10.0 },
            ],
            'markets': {
                'ETH/BTC': { id: 'ETH/BTC', symbol: 'ETH/BTC', base: 'ETH', quote: 'BTC' },
                'BTC/USD': { id: 'BTC/USD', symbol: 'BTC/USD', base: 'BTC', quote: 'USD' },
            },
        })

        equal (exchange.getCurrencyUsedOnOpenOrders ('LTC'), 0)
        equal (exchange.getCurrencyUsedOnOpenOrders ('ETH'), 40)
        equal (exchange.getCurrencyUsedOnOpenOrders ('USD'), 100)
        equal (exchange.getCurrencyUsedOnOpenOrders ('BTC'), 4010)

    })

/*  ------------------------------------------------------------------------ */

    it.skip ('exchange config extension works', () => {

        cost = { min: 0.001, max: 1000 }
        precision = { amount: 3 }
        const exchange = new binance ({
            'markets': {
                'ETH/BTC': { limits: { cost }, precision },
            },
        })

        deepEqual (exchange.markets['ETH/BTC'].limits.cost, cost)
        deepEqual (exchange.markets['ETH/BTC'].precision, { price: 6, amount: 3 })
        deepEqual (exchange.markets['ETH/BTC'].symbol, 'ETH/BTC')
    })

/*  ------------------------------------------------------------------------ */

    it ('aggregate() works', () => {

        const bids = [
            [ 789.1, 123.0 ],
            [ 789.100, 123.0 ],
            [ 123.0, 456.0 ],
            [ 789.0, 123.0 ],
            [ 789.10, 123.0 ],
        ]

        const asks = [
            [ 123.0, 456.0 ],
            [ 789.0, 123.0 ],
            [ 789.10, 123.0 ],
        ]

        deepEqual (aggregate (bids.sort ()), [
            [ 123.0, 456.0 ],
            [ 789.0, 123.0 ],
            [ 789.1, 369.0 ],
        ])

        deepEqual (aggregate (asks.sort ()), [
            [ 123.0, 456.0 ],
            [ 789.0, 123.0 ],
            [ 789.10, 123.0 ],
        ])

        deepEqual (aggregate ([]), [])
    })

/*  ------------------------------------------------------------------------ */

    it ('parseBalance() works', () => {

        const exchange = new Exchange ({
            'markets': {
                'ETH/BTC': { 'id': 'ETH/BTC', 'symbol': 'ETH/BTC', 'base': 'ETH', 'quote': 'BTC', }
            }
        })

        const input = {
            'ETH': { 'free': 10, 'used': 10, 'total': 20 },
            'ZEC': { 'free': 0, 'used': 0, 'total': 0 },
        }

        const expected = {
            'ETH': { 'free': 10, 'used': 10, 'total': 20 },
            'ZEC': { 'free': 0, 'used': 0, 'total': 0 },
            'free': {
                'ETH': 10,
                'ZEC': 0,
            },
            'used': {
                'ETH': 10,
                'ZEC': 0,
            },
            'total': {
                'ETH': 20,
                'ZEC': 0,
            },
        }

        const actual = exchange.parseBalance (input)

        deepEqual (actual, expected)
    })

/*  ------------------------------------------------------------------------ */

    it ('camelCase/camel_case property conversion works', () => {

        const exchange = new Exchange ({ 'id': 'mock' })

        const propsSeenBefore = index (
                                    ["isNode", "empty", "keys", "values", "extend", "clone", "index", "ordered", "unique", "keysort", "indexBy", "groupBy", "filterBy", "sortBy", "flatten", "pluck", "omit", "sum", "deepExtend", "uuid", "unCamelCase", "capitalize", "isNumber", "isArray", "isObject", "isString", "isStringCoercible", "isDictionary", "hasProps", "prop", "asFloat", "asInteger", "safeFloat", "safeInteger", "safeValue", "safeString", "decimal", "toFixed", "truncate", "truncateToString", "precisionFromString", "stringToBinary", "stringToBase64", "utf16ToBase64", "base64ToBinary", "base64ToString", "binaryToString", "binaryConcat", "urlencode", "rawencode", "urlencodeBase64", "hash", "hmac", "jwt", "time", "setTimeout_safe", "sleep", "TimedOut", "timeout", "throttle", "json", "unjson", "aggregate", "is_node", "index_by", "group_by", "filter_by", "sort_by", "deep_extend", "un_camel_case", "is_number", "is_array", "is_object", "is_string", "is_string_coercible", "is_dictionary", "has_props", "as_float", "as_integer", "safe_float", "safe_integer", "safe_value", "safe_string", "to_fixed", "truncate_to_string", "precision_from_string", "string_to_binary", "string_to_base64", "utf16To_base64", "base64To_binary", "base64To_string", "binary_to_string", "binary_concat", "urlencode_base64", "set_timeout_safe", "Timed_out", "encode", "decode", "userAgents", "headers", "proxy", "origin", "iso8601", "parse8601", "milliseconds", "microseconds", "seconds", "id", "enableRateLimit", "rateLimit", "parseJsonResponse", "substituteCommonCurrencyCodes", "parseBalanceFromOpenOrders", "verbose", "debug", "journal", "userAgent", "twofa", "timeframes", "hasPublicAPI", "hasPrivateAPI", "hasCORS", "hasDeposit", "hasFetchBalance", "hasFetchClosedOrders", "hasFetchCurrencies", "hasFetchMyTrades", "hasFetchOHLCV", "hasFetchOpenOrders", "hasFetchOrder", "hasFetchOrderBook", "hasFetchOrders", "hasFetchTicker", "hasFetchTickers", "hasFetchBidsAsks", "hasFetchTrades", "hasWithdraw", "hasCreateOrder", "hasCancelOrder", "apiKey", "secret", "uid", "login", "password", "requiredCredentials", "exceptions", "balance", "orderbooks", "tickers", "fees", "orders", "trades", "currencies", "last_http_response", "last_json_response", "arrayConcat", "market_id", "market_ids", "array_concat", "implode_params", "extract_params", "fetch_balance", "fetch_free_balance", "fetch_used_balance","fetch_total_balance", "fetch_l2_order_book", "fetch_order_book", "fetch_bids_asks", "fetch_tickers", "fetch_ticker", "fetch_trades", "fetch_order", "fetch_orders", "fetch_open_orders", "fetch_closed_orders", "fetch_order_status", "fetch_markets", "load_markets", "set_markets", "parse_balance", "parse_bid_ask", "parse_bids_asks", "parse_order_book", "parse_trades", "parse_orders", "parse_ohlcv", "parse_ohlcvs", "edit_limit_buy_order", "edit_limit_sell_order", "edit_limit_order", "edit_order", "create_limit_buy_order", "create_limit_sell_order", "create_market_buy_order", "create_market_sell_order", "create_order", "calculate_fee", "common_currency_code", "price_to_precision", "amount_to_precision", "amount_to_string", "fee_to_precision", "cost_to_precision", "constructor", "getMarket", "describe", "defaults", "nonce", "encodeURIComponent", "checkRequiredCredentials", "initRestRateLimiter", "defineRestApi", "fetch", "fetch2", "request", "handleErrors", "defaultErrorHandler", "handleRestErrors", "handleRestResponse", "setMarkets", "loadMarkets", "fetchBidsAsks", "fetchTickers", "fetchOrder", "fetchOrders", "fetchOpenOrders", "fetchClosedOrders", "fetchMyTrades", "fetchCurrencies", "fetchMarkets", "fetchOrderStatus", "account", "commonCurrencyCode", "currency", "market", "marketId", "marketIds", "symbol", "extractParams", "implodeParams", "url", "parseBidAsk", "parseBidsAsks", "fetchL2OrderBook", "parseOrderBook", "getCurrencyUsedOnOpenOrders", "parseBalance", "fetchPartialBalance", "fetchFreeBalance", "fetchUsedBalance", "fetchTotalBalance", "filterBySinceLimit", "parseTrades", "parseOrders", "filterOrdersBySymbol", "parseOHLCV", "parseOHLCVs", "editLimitBuyOrder", "editLimitSellOrder", "editLimitOrder", "editOrder", "createLimitBuyOrder", "createLimitSellOrder", "createMarketBuyOrder", "createMarketSellOrder", "costToPrecision", "priceToPrecision", "amountToPrecision", "amountToString", "amountToLots", "feeToPrecision", "calculateFee", "Ymd", "YmdHMS"]
                                )

        const props = index (Object.getOwnPropertyNames (exchange).concat (
                             Object.getOwnPropertyNames (exchange.constructor.prototype)))

        for (const k of Array.from (propsSeenBefore))
            if (this[k] && !props.has (k))
                throw new Error (`missing prop: ${k}`)

        for (const k of Array.from (props))
            if (!propsSeenBefore.has (k))
                log.magenta.noLocate (`+ ${k}`)

    })

/*  ------------------------------------------------------------------------ */

    it ('camelCase/camel_case property conversion works #2', () => {

        class Derived extends Exchange {}
        const derived = new Derived ()
        equal (typeof derived.load_markets, 'function')
    })

/*  ------------------------------------------------------------------------ */

    it ('legacy .hasSomething maps to has.something automatically', () => {

        const exchange = new Exchange ({
                                id: 'mock',
                                has: {
                                    CORS: true,
                                    publicAPI: false,
                                    fetchDepositAddress: 'emulated'
                                }
                            })

        equal (exchange.hasCORS, true)
        equal (exchange.hasPublicAPI, false)
        equal (exchange.hasFetchDepositAddress, true)
    })
})

/*  ------------------------------------------------------------------------ */
<|endoftext|>


<commit_before>// Copyright 2021, University of Colorado Boulder

/**
 * TODO: doc
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */

import Property from '../../../axon/js/Property.js';
import TinyProperty from '../../../axon/js/TinyProperty.js';
import Bounds2 from '../../../dot/js/Bounds2.js';
import Enumeration from '../../../phet-core/js/Enumeration.js';
import Orientation from '../../../phet-core/js/Orientation.js';
import arrayRemove from '../../../phet-core/js/arrayRemove.js';
import merge from '../../../phet-core/js/merge.js';
import mutate from '../../../phet-core/js/mutate.js';
import Node from '../nodes/Node.js';
import scenery from '../scenery.js';
import Constraint from './Constraint.js';
import FlowCell from './FlowCell.js';
import FlowConfigurable from './FlowConfigurable.js';

const FLOW_CONSTRAINT_OPTION_KEYS = [
  'orientation',
  'spacing',
  'lineSpacing',
  'justify',
  'wrap',
  'excludeInvisible'
].concat( FlowConfigurable.FLOW_CONFIGURABLE_OPTION_KEYS );

// TODO: Have LayoutBox use this when we're ready
class FlowConstraint extends FlowConfigurable( Constraint ) {
  /**
   * @param {Node} rootNode
   * @param {Object} [options]
   */
  constructor( rootNode, options ) {
    assert && assert( rootNode instanceof Node );

    options = merge( {
      // As options, so we could hook into a Node's preferred/minimum sizes if desired
      preferredWidthProperty: new TinyProperty( null ),
      preferredHeightProperty: new TinyProperty( null ),
      minimumWidthProperty: new TinyProperty( null ),
      minimumHeightProperty: new TinyProperty( null )
    }, options );

    super( rootNode );

    // @private {Array.<FlowCell>}
    this.cells = [];

    // @private {Orientation}
    this._orientation = Orientation.HORIZONTAL;

    // @private {FlowConstraint.Justify}
    this._justify = FlowConstraint.Justify.SPACE_BETWEEN; // TODO: decide on a good default here

    // @private {boolean}
    this._wrap = false;

    // @private {number}
    this._spacing = 0;
    this._lineSpacing = 0;

    // @private {boolean}
    this._excludeInvisible = true;

    // @public {Property.<Bounds2>} - Reports out the used layout bounds (may be larger than actual bounds, since it
    // will include margins, etc.)
    this.layoutBoundsProperty = new Property( Bounds2.NOTHING );

    this.preferredWidthProperty = options.preferredWidthProperty;
    this.preferredHeightProperty = options.preferredHeightProperty;
    this.minimumWidthProperty = options.minimumWidthProperty;
    this.minimumHeightProperty = options.minimumHeightProperty;

    this.setConfigToBaseDefault();
    this.mutateConfigurable( options );
    mutate( this, FLOW_CONSTRAINT_OPTION_KEYS, options );

    // Key configuration changes to relayout
    this.changedEmitter.addListener( this._updateLayoutListener );

    // TODO: Add disposal capabilities?
    this.preferredWidthProperty.lazyLink( this._updateLayoutListener );
    this.preferredHeightProperty.lazyLink( this._updateLayoutListener );
  }

  /**
   * @protected
   * @override
   */
  layout() {
    super.layout();

    // The orientation along the laid-out lines
    const orientation = this.orientation;

    // The perpendicular orientation, where alignment is handled
    const oppositeOrientation = this.orientation.opposite;

    const cells = this.cells.filter( cell => {
      // TODO: Also don't lay out disconnected nodes!!!!
      return cell.node.bounds.isValid() && ( !this._excludeInvisible || cell.node.visible );
    } );

    if ( !cells.length ) {
      return;
    }

    // {number|null} Determine our preferred sizes (they can be null, in which case)
    const preferredSize = orientation === Orientation.HORIZONTAL ? this.preferredWidthProperty.value : this.preferredHeightProperty.value;
    const preferredOppositeSize = orientation === Orientation.HORIZONTAL ? this.preferredHeightProperty.value : this.preferredWidthProperty.value;

    // What is the largest of the minimum sizes of cells (e.g. if we're wrapping, this would be our minimum size)
    const maxMinimumCellSize = _.max( cells.map( cell => cell.getMinimumSize( orientation, this ) ) );

    assert && assert( maxMinimumCellSize <= preferredSize || Number.POSITIVE_INFINITY, 'Will not be able to fit in this preferred size' );

    // Wrapping all of the cells into lines
    const lines = [];
    if ( this.wrap ) {
      let currentLine = [];
      let availableSpace = preferredSize || Number.POSITIVE_INFINITY;

      while ( cells.length ) {
        const cell = cells.shift();
        const cellSpace = cell.getMinimumSize( orientation, this );

        if ( currentLine.length === 0 ) {
          currentLine.push( cell );
          availableSpace -= cellSpace;
        }
        else if ( this.spacing + cellSpace <= availableSpace + 1e-7 ) {
          currentLine.push( cell );
          availableSpace -= this.spacing + cellSpace;
        }
        else {
          lines.push( currentLine );
          availableSpace = preferredSize || Number.POSITIVE_INFINITY;

          currentLine = [ cell ];
          availableSpace -= cellSpace;
        }
      }

      if ( currentLine.length ) {
        lines.push( currentLine );
      }
    }
    else {
      lines.push( cells );
    }

    // {number} - Given our wrapped lines, what is our minimum size we could take up?
    const minimumCurrentSize = _.max( lines.map( line => {
      return ( line.length - 1 ) * this.spacing + _.sum( line.map( cell => cell.getMinimumSize( orientation, this ) ) );
    } ) );
    const minimumCurrentOppositeSize = _.sum( lines.map( line => {
      return _.max( line.map( cell => cell.getMinimumSize( oppositeOrientation, this ) ) );
    } ) ) + ( lines.length - 1 ) * this.lineSpacing;

    // Used for determining our "minimum" size for preferred sizes... if wrapping is enabled, we can be smaller than
    // current minimums
    const minimumAllowableSize = this.wrap ? maxMinimumCellSize : minimumCurrentSize;

    // Tell others about our new "minimum" sizes
    this.minimumWidthProperty.value = orientation === Orientation.HORIZONTAL ? minimumAllowableSize : minimumCurrentOppositeSize;
    this.minimumHeightProperty.value = orientation === Orientation.HORIZONTAL ? minimumCurrentOppositeSize : minimumAllowableSize;

    // {number} - Increase things if our preferred size is larger than our minimums (we'll figure out how to compensate
    // for the extra space below).
    const size = Math.max( minimumCurrentSize, preferredSize || 0 );
    const oppositeSize = Math.max( minimumCurrentOppositeSize, preferredOppositeSize || 0 );

    // We're taking up these layout bounds (nodes could use them for localBounds)
    // TODO: How to handle this with align:origin!!!
    this.layoutBoundsProperty.value = new Bounds2( 0, 0, orientation === Orientation.HORIZONTAL ? size : oppositeSize, orientation === Orientation.HORIZONTAL ? oppositeSize : size );

    // TODO: align-content flexbox equivalent

    // Primary-direction layout
    lines.forEach( line => {
      const minimumContent = _.sum( line.map( cell => cell.getMinimumSize( orientation, this ) ) );
      const spacingAmount = this.spacing * ( line.length - 1 );
      let spaceRemaining = size - minimumContent - spacingAmount;

      // Initial pending sizes
      line.forEach( cell => {
        cell._pendingSize = cell.getMinimumSize( orientation, this );
      } );

      // Grow potential sizes if possible
      // TODO: This looks unfun to read... check this with a fresh mind
      let growableCells;
      while ( spaceRemaining > 1e7 && ( growableCells = line.filter( cell => {
        if ( cell.grow !== null ) {
          if ( cell.grow === 0 ) {
            return false;
          }
        }
        else if ( this.grow === 0 ) {
          return false;
        }
        return cell._pendingSize < cell.getMaximumSize( orientation, this ) - 1e-7;
      } ) ).length ) {
        const amountEachToGrow = Math.min(
          _.min( growableCells.map( cell => cell.getMaximumSize( orientation, this ) - cell._pendingSize ) ),
          spaceRemaining / growableCells.length
        );

        assert && assert( amountEachToGrow > 1e-11 );

        growableCells.forEach( cell => {
          cell._pendingSize += amountEachToGrow;
        } );
        spaceRemaining -= amountEachToGrow * growableCells.length;
      }

      // Update preferred dimension based on the pending size
      line.forEach( cell => cell.attemptedPreferredSize( orientation, this, cell._pendingSize ) );

      // TODO: optimize, OMG, but is this generally a good idea?
      // TODO: Only I would write this code in this mental state?
      const spacingFunction = {
        [ FlowConstraint.Justify.START ]: index => 0,
        [ FlowConstraint.Justify.END ]: index => index === 0 ? spaceRemaining : 0,
        [ FlowConstraint.Justify.CENTER ]: index => index === 0 ? spaceRemaining / 2 : 0,
        [ FlowConstraint.Justify.SPACE_BETWEEN ]: index => index !== 0 ? ( spaceRemaining / ( line.length - 1 ) ) : 0,
        [ FlowConstraint.Justify.SPACE_AROUND ]: index => ( index !== 0 ? 2 : 1 ) * spaceRemaining / ( 2 * line.length ),
        [ FlowConstraint.Justify.SPACE_EVENLY ]: index => spaceRemaining / ( line.length + 1 )
      }[ this._justify ];

      let position = 0;

      line.forEach( ( cell, index ) => {
        position += spacingFunction( index );
        if ( index > 0 ) {
          position += this.spacing;
        }
        // TODO: handle coordinate transforms properly
        cell.positionStart( orientation, this, position );
        position += cell._pendingSize;
      } );
    } );

    // Secondary-direction layout
    let secondaryPosition = 0;
    lines.forEach( line => {
      const maximumSize = _.max( line.map( cell => cell.getMinimumSize( oppositeOrientation, this ) ) );

      line.forEach( cell => {
        const align = cell.withDefault( 'align', this );
        const size = cell.getMinimumSize( oppositeOrientation, this );

        if ( align === FlowConfigurable.Align.STRETCH ) {
          cell.attemptedPreferredSize( oppositeOrientation, this, maximumSize );
          cell.positionStart( oppositeOrientation, this, secondaryPosition );
        }
        else {
          cell.attemptedPreferredSize( oppositeOrientation, this, size );

          if ( align === FlowConfigurable.Align.ORIGIN ) {
            // TODO: handle layout bounds
            cell.positionOrigin( oppositeOrientation, this, secondaryPosition );
          }
          else {
            // TODO: optimize
            const padRatio = {
              [ FlowConfigurable.Align.START ]: 0,
              [ FlowConfigurable.Align.CENTER ]: 0.5,
              [ FlowConfigurable.Align.END ]: 1
            }[ align ];
            cell.positionStart( oppositeOrientation, this, secondaryPosition + ( maximumSize - size ) * padRatio );
          }
        }
      } );

      secondaryPosition += maximumSize + this.lineSpacing;
    } );
  }

  /**
   * @public
   *
   * @returns {Orientation}
   */
  get orientation() {
    return this._orientation;
  }

  /**
   * @public
   *
   * @param {Orientation|string} value
   */
  set orientation( value ) {
    if ( value === 'horizontal' ) {
      value = Orientation.HORIZONTAL;
    }
    if ( value === 'vertical' ) {
      value = Orientation.VERTICAL;
    }

    assert && assert( Orientation.includes( value ) );

    if ( this._orientation !== value ) {
      this._orientation = value;

      this.updateLayoutAutomatically();
    }
  }

  /**
   * @public
   *
   * @returns {FlowConstraint.Justify}
   */
  get justify() {
    return this._justify;
  }

  /**
   * @public
   *
   * @param {FlowConstraint.Justify|string} value
   */
  set justify( value ) {
    if ( typeof value === 'string' ) {
      value = justifyMap[ value ];
    }

    assert && assert( FlowConstraint.Justify.includes( value ) );

    if ( this._justify !== value ) {
      this._justify = value;

      this.updateLayoutAutomatically();
    }
  }

  /**
   * @public
   *
   * @returns {number}
   */
  get wrap() {
    return this._wrap;
  }

  /**
   * @public
   *
   * @param {number} value
   */
  set wrap( value ) {
    assert && assert( typeof value === 'boolean' );

    if ( this._wrap !== value ) {
      this._wrap = value;

      this.updateLayoutAutomatically();
    }
  }

  /**
   * @public
   *
   * @returns {number}
   */
  get spacing() {
    return this._spacing;
  }

  /**
   * @public
   *
   * @param {number} value
   */
  set spacing( value ) {
    assert && assert( typeof value === 'number' && isFinite( value ) && value >= 0 );

    if ( this._spacing !== value ) {
      this._spacing = value;

      this.updateLayoutAutomatically();
    }
  }

  /**
   * @public
   *
   * @returns {number}
   */
  get lineSpacing() {
    return this._lineSpacing;
  }

  /**
   * @public
   *
   * @param {number} value
   */
  set lineSpacing( value ) {
    assert && assert( typeof value === 'number' && isFinite( value ) && value >= 0 );

    if ( this._lineSpacing !== value ) {
      this._lineSpacing = value;

      this.updateLayoutAutomatically();
    }
  }

  /**
   * @public
   *
   * @returns {string}
   */
  get excludeInvisible() {
    return this._excludeInvisible;
  }

  /**
   * @public
   *
   * @param {Orientation|string} value
   */
  set excludeInvisible( value ) {
    assert && assert( typeof value === 'boolean' );

    if ( this._excludeInvisible !== value ) {
      this._excludeInvisible = value;

      this.updateLayoutAutomatically();
    }
  }

  /**
   * @public
   *
   * @param {number} index
   * @param {FlowCell} cell
   */
  insertCell( index, cell ) {
    assert && assert( typeof index === 'number' );
    assert && assert( index >= 0 );
    assert && assert( index <= this.cells.length );
    assert && assert( cell instanceof FlowCell );
    assert && assert( !_.includes( this.cells, cell ) );

    this.cells.splice( index, 0, cell );
    this.addNode( cell.node );
    cell.changedEmitter.addListener( this._updateLayoutListener );

    this.updateLayoutAutomatically();
  }

  /**
   * @public
   *
   * @param {FlowCell} cell
   */
  removeCell( cell ) {
    assert && assert( cell instanceof FlowCell );
    assert && assert( _.includes( this.cells, cell ) );

    // TODO: handle disposing here?
    arrayRemove( this.cells, cell );
    this.removeNode( cell.node );
    cell.changedEmitter.removeListener( this._updateLayoutListener );

    this.updateLayoutAutomatically();
  }

  /**
   * @public
   *
   * @param {Array.<FlowCell>} cells
   * @param {number} minChangeIndex
   * @param {number} maxChangeIndex
   */
  reorderCells( cells, minChangeIndex, maxChangeIndex ) {
    // TODO: assertions for this!!! So many things could go wrong here

    this.cells.splice( minChangeIndex, maxChangeIndex - minChangeIndex + 1, ...cells );

    this.updateLayoutAutomatically();
  }

  /**
   * @public
   *
   * @param {Node} rootNode
   * @param {Object} [options]
   * @returns {FlowConstraint}
   */
  static create( rootNode, options ) {
    return new FlowConstraint( rootNode, options );
  }
}

FlowConstraint.Justify = Enumeration.byKeys( [
  'START',
  'END',
  'CENTER',
  'SPACE_BETWEEN',
  'SPACE_AROUND',
  'SPACE_EVENLY'
] );
const justifyMap = {
  'start': FlowConstraint.Justify.START,
  'top': FlowConstraint.Justify.START,
  'left': FlowConstraint.Justify.START,

  'end': FlowConstraint.Justify.END,
  'bottom': FlowConstraint.Justify.END,
  'right': FlowConstraint.Justify.END,

  'center': FlowConstraint.Justify.CENTER,
  'spaceBetween': FlowConstraint.Justify.SPACE_BETWEEN,
  'spaceAround': FlowConstraint.Justify.SPACE_AROUND,
  'spaceEvenly': FlowConstraint.Justify.SPACE_EVENLY
};

// @public {Array.<string>}
FlowConstraint.FLOW_CONSTRAINT_OPTION_KEYS = FLOW_CONSTRAINT_OPTION_KEYS;

scenery.register( 'FlowConstraint', FlowConstraint );
export default FlowConstraint;<commit_msg>Grow bugfix for https://github.com/phetsims/sun/issues/48<commit_after>// Copyright 2021, University of Colorado Boulder

/**
 * TODO: doc
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */

import Property from '../../../axon/js/Property.js';
import TinyProperty from '../../../axon/js/TinyProperty.js';
import Bounds2 from '../../../dot/js/Bounds2.js';
import Enumeration from '../../../phet-core/js/Enumeration.js';
import Orientation from '../../../phet-core/js/Orientation.js';
import arrayRemove from '../../../phet-core/js/arrayRemove.js';
import merge from '../../../phet-core/js/merge.js';
import mutate from '../../../phet-core/js/mutate.js';
import Node from '../nodes/Node.js';
import scenery from '../scenery.js';
import Constraint from './Constraint.js';
import FlowCell from './FlowCell.js';
import FlowConfigurable from './FlowConfigurable.js';

const FLOW_CONSTRAINT_OPTION_KEYS = [
  'orientation',
  'spacing',
  'lineSpacing',
  'justify',
  'wrap',
  'excludeInvisible'
].concat( FlowConfigurable.FLOW_CONFIGURABLE_OPTION_KEYS );

// TODO: Have LayoutBox use this when we're ready
class FlowConstraint extends FlowConfigurable( Constraint ) {
  /**
   * @param {Node} rootNode
   * @param {Object} [options]
   */
  constructor( rootNode, options ) {
    assert && assert( rootNode instanceof Node );

    options = merge( {
      // As options, so we could hook into a Node's preferred/minimum sizes if desired
      preferredWidthProperty: new TinyProperty( null ),
      preferredHeightProperty: new TinyProperty( null ),
      minimumWidthProperty: new TinyProperty( null ),
      minimumHeightProperty: new TinyProperty( null )
    }, options );

    super( rootNode );

    // @private {Array.<FlowCell>}
    this.cells = [];

    // @private {Orientation}
    this._orientation = Orientation.HORIZONTAL;

    // @private {FlowConstraint.Justify}
    this._justify = FlowConstraint.Justify.SPACE_BETWEEN; // TODO: decide on a good default here

    // @private {boolean}
    this._wrap = false;

    // @private {number}
    this._spacing = 0;
    this._lineSpacing = 0;

    // @private {boolean}
    this._excludeInvisible = true;

    // @public {Property.<Bounds2>} - Reports out the used layout bounds (may be larger than actual bounds, since it
    // will include margins, etc.)
    this.layoutBoundsProperty = new Property( Bounds2.NOTHING );

    this.preferredWidthProperty = options.preferredWidthProperty;
    this.preferredHeightProperty = options.preferredHeightProperty;
    this.minimumWidthProperty = options.minimumWidthProperty;
    this.minimumHeightProperty = options.minimumHeightProperty;

    this.setConfigToBaseDefault();
    this.mutateConfigurable( options );
    mutate( this, FLOW_CONSTRAINT_OPTION_KEYS, options );

    // Key configuration changes to relayout
    this.changedEmitter.addListener( this._updateLayoutListener );

    // TODO: Add disposal capabilities?
    this.preferredWidthProperty.lazyLink( this._updateLayoutListener );
    this.preferredHeightProperty.lazyLink( this._updateLayoutListener );
  }

  /**
   * @protected
   * @override
   */
  layout() {
    super.layout();

    // The orientation along the laid-out lines
    const orientation = this.orientation;

    // The perpendicular orientation, where alignment is handled
    const oppositeOrientation = this.orientation.opposite;

    const cells = this.cells.filter( cell => {
      // TODO: Also don't lay out disconnected nodes!!!!
      return cell.node.bounds.isValid() && ( !this._excludeInvisible || cell.node.visible );
    } );

    if ( !cells.length ) {
      return;
    }

    // {number|null} Determine our preferred sizes (they can be null, in which case)
    const preferredSize = orientation === Orientation.HORIZONTAL ? this.preferredWidthProperty.value : this.preferredHeightProperty.value;
    const preferredOppositeSize = orientation === Orientation.HORIZONTAL ? this.preferredHeightProperty.value : this.preferredWidthProperty.value;

    // What is the largest of the minimum sizes of cells (e.g. if we're wrapping, this would be our minimum size)
    const maxMinimumCellSize = _.max( cells.map( cell => cell.getMinimumSize( orientation, this ) ) );

    assert && assert( maxMinimumCellSize <= preferredSize || Number.POSITIVE_INFINITY, 'Will not be able to fit in this preferred size' );

    // Wrapping all of the cells into lines
    const lines = [];
    if ( this.wrap ) {
      let currentLine = [];
      let availableSpace = preferredSize || Number.POSITIVE_INFINITY;

      while ( cells.length ) {
        const cell = cells.shift();
        const cellSpace = cell.getMinimumSize( orientation, this );

        if ( currentLine.length === 0 ) {
          currentLine.push( cell );
          availableSpace -= cellSpace;
        }
        else if ( this.spacing + cellSpace <= availableSpace + 1e-7 ) {
          currentLine.push( cell );
          availableSpace -= this.spacing + cellSpace;
        }
        else {
          lines.push( currentLine );
          availableSpace = preferredSize || Number.POSITIVE_INFINITY;

          currentLine = [ cell ];
          availableSpace -= cellSpace;
        }
      }

      if ( currentLine.length ) {
        lines.push( currentLine );
      }
    }
    else {
      lines.push( cells );
    }

    // {number} - Given our wrapped lines, what is our minimum size we could take up?
    const minimumCurrentSize = _.max( lines.map( line => {
      return ( line.length - 1 ) * this.spacing + _.sum( line.map( cell => cell.getMinimumSize( orientation, this ) ) );
    } ) );
    const minimumCurrentOppositeSize = _.sum( lines.map( line => {
      return _.max( line.map( cell => cell.getMinimumSize( oppositeOrientation, this ) ) );
    } ) ) + ( lines.length - 1 ) * this.lineSpacing;

    // Used for determining our "minimum" size for preferred sizes... if wrapping is enabled, we can be smaller than
    // current minimums
    const minimumAllowableSize = this.wrap ? maxMinimumCellSize : minimumCurrentSize;

    // Tell others about our new "minimum" sizes
    this.minimumWidthProperty.value = orientation === Orientation.HORIZONTAL ? minimumAllowableSize : minimumCurrentOppositeSize;
    this.minimumHeightProperty.value = orientation === Orientation.HORIZONTAL ? minimumCurrentOppositeSize : minimumAllowableSize;

    // {number} - Increase things if our preferred size is larger than our minimums (we'll figure out how to compensate
    // for the extra space below).
    const size = Math.max( minimumCurrentSize, preferredSize || 0 );
    const oppositeSize = Math.max( minimumCurrentOppositeSize, preferredOppositeSize || 0 );

    // We're taking up these layout bounds (nodes could use them for localBounds)
    // TODO: How to handle this with align:origin!!!
    this.layoutBoundsProperty.value = new Bounds2( 0, 0, orientation === Orientation.HORIZONTAL ? size : oppositeSize, orientation === Orientation.HORIZONTAL ? oppositeSize : size );

    // TODO: align-content flexbox equivalent

    // Primary-direction layout
    lines.forEach( line => {
      const minimumContent = _.sum( line.map( cell => cell.getMinimumSize( orientation, this ) ) );
      const spacingAmount = this.spacing * ( line.length - 1 );
      let spaceRemaining = size - minimumContent - spacingAmount;

      // Initial pending sizes
      line.forEach( cell => {
        cell._pendingSize = cell.getMinimumSize( orientation, this );
      } );

      // Grow potential sizes if possible
      // TODO: This looks unfun to read... check this with a fresh mind
      let growableCells;
      while ( spaceRemaining > 1e-7 && ( growableCells = line.filter( cell => {
        const grow = cell.withDefault( 'grow', this );
        if ( grow === 0 ) {
          return false;
        }
        return cell._pendingSize < cell.getMaximumSize( orientation, this ) - 1e-7;
      } ) ).length ) {
        const amountEachToGrow = Math.min(
          _.min( growableCells.map( cell => cell.getMaximumSize( orientation, this ) - cell._pendingSize ) ),
          spaceRemaining / growableCells.length
        );

        assert && assert( amountEachToGrow > 1e-11 );

        growableCells.forEach( cell => {
          cell._pendingSize += amountEachToGrow;
        } );
        spaceRemaining -= amountEachToGrow * growableCells.length;
      }

      // Update preferred dimension based on the pending size
      line.forEach( cell => cell.attemptedPreferredSize( orientation, this, cell._pendingSize ) );

      // TODO: optimize, OMG, but is this generally a good idea?
      // TODO: Only I would write this code in this mental state?
      const spacingFunction = {
        [ FlowConstraint.Justify.START ]: index => 0,
        [ FlowConstraint.Justify.END ]: index => index === 0 ? spaceRemaining : 0,
        [ FlowConstraint.Justify.CENTER ]: index => index === 0 ? spaceRemaining / 2 : 0,
        [ FlowConstraint.Justify.SPACE_BETWEEN ]: index => index !== 0 ? ( spaceRemaining / ( line.length - 1 ) ) : 0,
        [ FlowConstraint.Justify.SPACE_AROUND ]: index => ( index !== 0 ? 2 : 1 ) * spaceRemaining / ( 2 * line.length ),
        [ FlowConstraint.Justify.SPACE_EVENLY ]: index => spaceRemaining / ( line.length + 1 )
      }[ this._justify ];

      let position = 0;

      line.forEach( ( cell, index ) => {
        position += spacingFunction( index );
        if ( index > 0 ) {
          position += this.spacing;
        }
        // TODO: handle coordinate transforms properly
        cell.positionStart( orientation, this, position );
        position += cell._pendingSize;
      } );
    } );

    // Secondary-direction layout
    let secondaryPosition = 0;
    lines.forEach( line => {
      const maximumSize = _.max( line.map( cell => cell.getMinimumSize( oppositeOrientation, this ) ) );

      line.forEach( cell => {
        const align = cell.withDefault( 'align', this );
        const size = cell.getMinimumSize( oppositeOrientation, this );

        if ( align === FlowConfigurable.Align.STRETCH ) {
          cell.attemptedPreferredSize( oppositeOrientation, this, maximumSize );
          cell.positionStart( oppositeOrientation, this, secondaryPosition );
        }
        else {
          cell.attemptedPreferredSize( oppositeOrientation, this, size );

          if ( align === FlowConfigurable.Align.ORIGIN ) {
            // TODO: handle layout bounds
            cell.positionOrigin( oppositeOrientation, this, secondaryPosition );
          }
          else {
            // TODO: optimize
            const padRatio = {
              [ FlowConfigurable.Align.START ]: 0,
              [ FlowConfigurable.Align.CENTER ]: 0.5,
              [ FlowConfigurable.Align.END ]: 1
            }[ align ];
            cell.positionStart( oppositeOrientation, this, secondaryPosition + ( maximumSize - size ) * padRatio );
          }
        }
      } );

      secondaryPosition += maximumSize + this.lineSpacing;
    } );
  }

  /**
   * @public
   *
   * @returns {Orientation}
   */
  get orientation() {
    return this._orientation;
  }

  /**
   * @public
   *
   * @param {Orientation|string} value
   */
  set orientation( value ) {
    if ( value === 'horizontal' ) {
      value = Orientation.HORIZONTAL;
    }
    if ( value === 'vertical' ) {
      value = Orientation.VERTICAL;
    }

    assert && assert( Orientation.includes( value ) );

    if ( this._orientation !== value ) {
      this._orientation = value;

      this.updateLayoutAutomatically();
    }
  }

  /**
   * @public
   *
   * @returns {FlowConstraint.Justify}
   */
  get justify() {
    return this._justify;
  }

  /**
   * @public
   *
   * @param {FlowConstraint.Justify|string} value
   */
  set justify( value ) {
    if ( typeof value === 'string' ) {
      value = justifyMap[ value ];
    }

    assert && assert( FlowConstraint.Justify.includes( value ) );

    if ( this._justify !== value ) {
      this._justify = value;

      this.updateLayoutAutomatically();
    }
  }

  /**
   * @public
   *
   * @returns {number}
   */
  get wrap() {
    return this._wrap;
  }

  /**
   * @public
   *
   * @param {number} value
   */
  set wrap( value ) {
    assert && assert( typeof value === 'boolean' );

    if ( this._wrap !== value ) {
      this._wrap = value;

      this.updateLayoutAutomatically();
    }
  }

  /**
   * @public
   *
   * @returns {number}
   */
  get spacing() {
    return this._spacing;
  }

  /**
   * @public
   *
   * @param {number} value
   */
  set spacing( value ) {
    assert && assert( typeof value === 'number' && isFinite( value ) && value >= 0 );

    if ( this._spacing !== value ) {
      this._spacing = value;

      this.updateLayoutAutomatically();
    }
  }

  /**
   * @public
   *
   * @returns {number}
   */
  get lineSpacing() {
    return this._lineSpacing;
  }

  /**
   * @public
   *
   * @param {number} value
   */
  set lineSpacing( value ) {
    assert && assert( typeof value === 'number' && isFinite( value ) && value >= 0 );

    if ( this._lineSpacing !== value ) {
      this._lineSpacing = value;

      this.updateLayoutAutomatically();
    }
  }

  /**
   * @public
   *
   * @returns {string}
   */
  get excludeInvisible() {
    return this._excludeInvisible;
  }

  /**
   * @public
   *
   * @param {Orientation|string} value
   */
  set excludeInvisible( value ) {
    assert && assert( typeof value === 'boolean' );

    if ( this._excludeInvisible !== value ) {
      this._excludeInvisible = value;

      this.updateLayoutAutomatically();
    }
  }

  /**
   * @public
   *
   * @param {number} index
   * @param {FlowCell} cell
   */
  insertCell( index, cell ) {
    assert && assert( typeof index === 'number' );
    assert && assert( index >= 0 );
    assert && assert( index <= this.cells.length );
    assert && assert( cell instanceof FlowCell );
    assert && assert( !_.includes( this.cells, cell ) );

    this.cells.splice( index, 0, cell );
    this.addNode( cell.node );
    cell.changedEmitter.addListener( this._updateLayoutListener );

    this.updateLayoutAutomatically();
  }

  /**
   * @public
   *
   * @param {FlowCell} cell
   */
  removeCell( cell ) {
    assert && assert( cell instanceof FlowCell );
    assert && assert( _.includes( this.cells, cell ) );

    // TODO: handle disposing here?
    arrayRemove( this.cells, cell );
    this.removeNode( cell.node );
    cell.changedEmitter.removeListener( this._updateLayoutListener );

    this.updateLayoutAutomatically();
  }

  /**
   * @public
   *
   * @param {Array.<FlowCell>} cells
   * @param {number} minChangeIndex
   * @param {number} maxChangeIndex
   */
  reorderCells( cells, minChangeIndex, maxChangeIndex ) {
    // TODO: assertions for this!!! So many things could go wrong here

    this.cells.splice( minChangeIndex, maxChangeIndex - minChangeIndex + 1, ...cells );

    this.updateLayoutAutomatically();
  }

  /**
   * @public
   *
   * @param {Node} rootNode
   * @param {Object} [options]
   * @returns {FlowConstraint}
   */
  static create( rootNode, options ) {
    return new FlowConstraint( rootNode, options );
  }
}

FlowConstraint.Justify = Enumeration.byKeys( [
  'START',
  'END',
  'CENTER',
  'SPACE_BETWEEN',
  'SPACE_AROUND',
  'SPACE_EVENLY'
] );
const justifyMap = {
  'start': FlowConstraint.Justify.START,
  'top': FlowConstraint.Justify.START,
  'left': FlowConstraint.Justify.START,

  'end': FlowConstraint.Justify.END,
  'bottom': FlowConstraint.Justify.END,
  'right': FlowConstraint.Justify.END,

  'center': FlowConstraint.Justify.CENTER,
  'spaceBetween': FlowConstraint.Justify.SPACE_BETWEEN,
  'spaceAround': FlowConstraint.Justify.SPACE_AROUND,
  'spaceEvenly': FlowConstraint.Justify.SPACE_EVENLY
};

// @public {Array.<string>}
FlowConstraint.FLOW_CONSTRAINT_OPTION_KEYS = FLOW_CONSTRAINT_OPTION_KEYS;

scenery.register( 'FlowConstraint', FlowConstraint );
export default FlowConstraint;<|endoftext|>


<commit_before>// $Id$
// Farbtastic 1.1

jQuery.fn.farbtastic = function (callback) {
  $.farbtastic(this, callback);
  return this;
};

jQuery.farbtastic = function (container, callback) {
  var container = $(container).get(0);
  return container.farbtastic || (container.farbtastic = new jQuery._farbtastic(container, callback));
}

jQuery._farbtastic = function (container, callback) {
  // Store farbtastic object
  var fb = this;

  // Insert markup
  $(container).html('

');
  var e = $('.farbtastic', container);
  fb.wheel = $('.wheel', container).get(0);
  // Dimensions
  fb.radius = 84;
  fb.square = 100;
  fb.width = 194;

  // Fix background PNGs in IE6
  if (navigator.appVersion.match(/MSIE [0-6]\./)) {
    $('*', e).each(function () {
      if (this.currentStyle.backgroundImage != 'none') {
        var image = this.currentStyle.backgroundImage;
        image = this.currentStyle.backgroundImage.substring(5, image.length - 2);
        $(this).css({
          'backgroundImage': 'none',
          'filter': "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='" + image + "')"
        });
      }
    });
  }

  /**
   * Link to the given element(s) or callback.
   */
  fb.linkTo = function (callback) {
    // Unbind previous nodes
    if (typeof fb.callback == 'object') {
      $(fb.callback).unbind('keyup', fb.updateValue);
    }

    // Bind callback or elements
    if (typeof callback == 'function') {
      fb.callback = callback;
    }
    else if (typeof callback == 'object' || typeof callback == 'string') {
      fb.callback = $(callback);
      fb.callback.bind('keyup', fb.updateValue);
      if (fb.callback.get(0).value) {
        fb.setColor(fb.callback.get(0).value);
      }
    }
    return this;
  }
  fb.updateValue = function (event) {
    if (this.value && this.value != fb.color) {
      fb.setColor(this.value);
    }
  }

  /**
   * Change color with HTML syntax #123456
   */
  fb.setColor = function (color) {
    var unpack = fb.unpack(color);
    if (fb.color != color && unpack) {
      fb.color = color;
      fb.rgb = unpack;
      fb.hsl = fb.RGBToHSL(fb.rgb);
      fb.updateDisplay();
    }
    return this;
  }

  /**
   * Change color with HSL triplet [0..1, 0..1, 0..1]
   */
  fb.setHSL = function (hsl) {
    fb.hsl = hsl;
    fb.rgb = fb.HSLToRGB(hsl);
    fb.color = fb.pack(fb.rgb);
    fb.updateDisplay();
    return this;
  }

  /////////////////////////////////////////////////////

  /**
   * Retrieve the coordinates of the given event relative to the center
   * of the widget.
   */
  fb.widgetCoords = function (event) {
    var x, y;
    var el = event.target || event.srcElement;
    var reference = fb.wheel;

    if (typeof event.offsetX != 'undefined') {
      // Use offset coordinates and find common offsetParent
      var pos = { x: event.offsetX, y: event.offsetY };

      // Send the coordinates upwards through the offsetParent chain.
      var e = el;
      while (e) {
        e.mouseX = pos.x;
        e.mouseY = pos.y;
        pos.x += e.offsetLeft;
        pos.y += e.offsetTop;
        e = e.offsetParent;
      }

      // Look for the coordinates starting from the wheel widget.
      var e = reference;
      var offset = { x: 0, y: 0 }
      while (e) {
        if (typeof e.mouseX != 'undefined') {
          x = e.mouseX - offset.x;
          y = e.mouseY - offset.y;
          break;
        }
        offset.x += e.offsetLeft;
        offset.y += e.offsetTop;
        e = e.offsetParent;
      }

      // Reset stored coordinates
      e = el;
      while (e) {
        e.mouseX = undefined;
        e.mouseY = undefined;
        e = e.offsetParent;
      }
    }
    else {
      // Use absolute coordinates
      var pos = fb.absolutePosition(reference);
      x = (event.pageX || 0*(event.clientX + $('html').get(0).scrollLeft)) - pos.x;
      y = (event.pageY || 0*(event.clientY + $('html').get(0).scrollTop)) - pos.y;
    }
    // Subtract distance to middle
    return { x: x - fb.width / 2, y: y - fb.width / 2 };
  }

  /**
   * Mousedown handler
   */
  fb.mousedown = function (event) {
    // Capture mouse
    if (!document.dragging) {
      $(document).bind('mousemove', fb.mousemove).bind('mouseup', fb.mouseup);
      document.dragging = true;
    }

    // Check which area is being dragged
    var pos = fb.widgetCoords(event);
    fb.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) * 2 > fb.square;

    // Process
    fb.mousemove(event);
    return false;
  }

  /**
   * Mousemove handler
   */
  fb.mousemove = function (event) {
    // Get coordinates relative to color picker center
    var pos = fb.widgetCoords(event);

    // Set new HSL parameters
    if (fb.circleDrag) {
      var hue = Math.atan2(pos.x, -pos.y) / 6.28;
      if (hue < 0) hue += 1;
      fb.setHSL([hue, fb.hsl[1], fb.hsl[2]]);
    }
    else {
      var sat = Math.max(0, Math.min(1, -(pos.x / fb.square) + .5));
      var lum = Math.max(0, Math.min(1, -(pos.y / fb.square) + .5));
      fb.setHSL([fb.hsl[0], sat, lum]);
    }
    return false;
  }

  /**
   * Mouseup handler
   */
  fb.mouseup = function () {
    // Uncapture mouse
    $(document).unbind('mousemove', fb.mousemove);
    $(document).unbind('mouseup', fb.mouseup);
    document.dragging = false;
  }

  /**
   * Update the markers and styles
   */
  fb.updateDisplay = function () {
    // Markers
    var angle = fb.hsl[0] * 6.28;
    $('.h-marker', e).css({
      left: Math.round(Math.sin(angle) * fb.radius + fb.width / 2) + 'px',
      top: Math.round(-Math.cos(angle) * fb.radius + fb.width / 2) + 'px'
    });

    $('.sl-marker', e).css({
      left: Math.round(fb.square * (.5 - fb.hsl[1]) + fb.width / 2) + 'px',
      top: Math.round(fb.square * (.5 - fb.hsl[2]) + fb.width / 2) + 'px'
    });

    // Saturation/Luminance gradient
    $('.color', e).css('backgroundColor', fb.pack(fb.HSLToRGB([fb.hsl[0], 1, 0.5])));

    // Linked elements or callback
    if (typeof fb.callback == 'object') {
      // Set background/foreground color
      $(fb.callback).css({
        backgroundColor: fb.color,
        color: fb.hsl[2] > 0.5 ? '#000' : '#fff'
      });

      // Change linked value
      $(fb.callback).each(function() {
        if (this.value && this.value != fb.color) {
          this.value = fb.color;
        }
      });
    }
    else if (typeof fb.callback == 'function') {
      fb.callback.call(fb, fb.color);
    }
  }

  /**
   * Get absolute position of element
   */
  fb.absolutePosition = function (el) {
    var r = { x: el.offsetLeft, y: el.offsetTop };
    // Resolve relative to offsetParent
    if (el.offsetParent) {
      var tmp = fb.absolutePosition(el.offsetParent);
      r.x += tmp.x;
      r.y += tmp.y;
    }
    return r;
  };

  /* Various color utility functions */
  fb.pack = function (rgb) {
    var r = Math.round(rgb[0] * 255);
    var g = Math.round(rgb[1] * 255);
    var b = Math.round(rgb[2] * 255);
    return '#' + (r < 16 ? '0' : '') + r.toString(16) +
           (g < 16 ? '0' : '') + g.toString(16) +
           (b < 16 ? '0' : '') + b.toString(16);
  }

  fb.unpack = function (color) {
    if (color.length == 7) {
      return [parseInt('0x' + color.substring(1, 3)) / 255,
        parseInt('0x' + color.substring(3, 5)) / 255,
        parseInt('0x' + color.substring(5, 7)) / 255];
    }
    else if (color.length == 4) {
      return [parseInt('0x' + color.substring(1, 2)) / 15,
        parseInt('0x' + color.substring(2, 3)) / 15,
        parseInt('0x' + color.substring(3, 4)) / 15];
    }
  }

  fb.HSLToRGB = function (hsl) {
    var m1, m2, r, g, b;
    var h = hsl[0], s = hsl[1], l = hsl[2];
    m2 = (l <= 0.5) ? l * (s + 1) : l + s - l*s;
    m1 = l * 2 - m2;
    return [this.hueToRGB(m1, m2, h+0.33333),
        this.hueToRGB(m1, m2, h),
        this.hueToRGB(m1, m2, h-0.33333)];
  }

  fb.hueToRGB = function (m1, m2, h) {
    h = (h < 0) ? h + 1 : ((h > 1) ? h - 1 : h);
    if (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
    if (h * 2 < 1) return m2;
    if (h * 3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * 6;
    return m1;
  }

  fb.RGBToHSL = function (rgb) {
    var min, max, delta, h, s, l;
    var r = rgb[0], g = rgb[1], b = rgb[2];
    min = Math.min(r, Math.min(g, b));
    max = Math.max(r, Math.max(g, b));
    delta = max - min;
    l = (min + max) / 2;
    s = 0;
    if (l > 0 && l < 1) {
      s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l));
    }
    h = 0;
    if (delta > 0) {
      if (max == r && max != g) h += (g - b) / delta;
      if (max == g && max != b) h += (2 + (b - r) / delta);
      if (max == b && max != r) h += (4 + (r - g) / delta);
      h /= 6;
    }
    return [h, s, l];
  }

  // Install mousedown handler (the others are set on the document on-demand)
  $('*', e).mousedown(fb.mousedown);

    // Init color
  fb.setColor('#000000');

  // Set linked elements/callback
  if (callback) {
    fb.linkTo(callback);
  }
}<commit_msg>Update to Farbtastic 1.2.<commit_after>// $Id$
// Farbtastic 1.2

jQuery.fn.farbtastic = function (callback) {
  $.farbtastic(this, callback);
  return this;
};

jQuery.farbtastic = function (container, callback) {
  var container = $(container).get(0);
  return container.farbtastic || (container.farbtastic = new jQuery._farbtastic(container, callback));
}

jQuery._farbtastic = function (container, callback) {
  // Store farbtastic object
  var fb = this;

  // Insert markup
  $(container).html('

');
  var e = $('.farbtastic', container);
  fb.wheel = $('.wheel', container).get(0);
  // Dimensions
  fb.radius = 84;
  fb.square = 100;
  fb.width = 194;

  // Fix background PNGs in IE6
  if (navigator.appVersion.match(/MSIE [0-6]\./)) {
    $('*', e).each(function () {
      if (this.currentStyle.backgroundImage != 'none') {
        var image = this.currentStyle.backgroundImage;
        image = this.currentStyle.backgroundImage.substring(5, image.length - 2);
        $(this).css({
          'backgroundImage': 'none',
          'filter': "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='" + image + "')"
        });
      }
    });
  }

  /**
   * Link to the given element(s) or callback.
   */
  fb.linkTo = function (callback) {
    // Unbind previous nodes
    if (typeof fb.callback == 'object') {
      $(fb.callback).unbind('keyup', fb.updateValue);
    }

    // Reset color
    fb.color = null;

    // Bind callback or elements
    if (typeof callback == 'function') {
      fb.callback = callback;
    }
    else if (typeof callback == 'object' || typeof callback == 'string') {
      fb.callback = $(callback);
      fb.callback.bind('keyup', fb.updateValue);
      if (fb.callback.get(0).value) {
        fb.setColor(fb.callback.get(0).value);
      }
    }
    return this;
  }
  fb.updateValue = function (event) {
    if (this.value && this.value != fb.color) {
      fb.setColor(this.value);
    }
  }

  /**
   * Change color with HTML syntax #123456
   */
  fb.setColor = function (color) {
    var unpack = fb.unpack(color);
    if (fb.color != color && unpack) {
      fb.color = color;
      fb.rgb = unpack;
      fb.hsl = fb.RGBToHSL(fb.rgb);
      fb.updateDisplay();
    }
    return this;
  }

  /**
   * Change color with HSL triplet [0..1, 0..1, 0..1]
   */
  fb.setHSL = function (hsl) {
    fb.hsl = hsl;
    fb.rgb = fb.HSLToRGB(hsl);
    fb.color = fb.pack(fb.rgb);
    fb.updateDisplay();
    return this;
  }

  /////////////////////////////////////////////////////

  /**
   * Retrieve the coordinates of the given event relative to the center
   * of the widget.
   */
  fb.widgetCoords = function (event) {
    var x, y;
    var el = event.target || event.srcElement;
    var reference = fb.wheel;

    if (typeof event.offsetX != 'undefined') {
      // Use offset coordinates and find common offsetParent
      var pos = { x: event.offsetX, y: event.offsetY };

      // Send the coordinates upwards through the offsetParent chain.
      var e = el;
      while (e) {
        e.mouseX = pos.x;
        e.mouseY = pos.y;
        pos.x += e.offsetLeft;
        pos.y += e.offsetTop;
        e = e.offsetParent;
      }

      // Look for the coordinates starting from the wheel widget.
      var e = reference;
      var offset = { x: 0, y: 0 }
      while (e) {
        if (typeof e.mouseX != 'undefined') {
          x = e.mouseX - offset.x;
          y = e.mouseY - offset.y;
          break;
        }
        offset.x += e.offsetLeft;
        offset.y += e.offsetTop;
        e = e.offsetParent;
      }

      // Reset stored coordinates
      e = el;
      while (e) {
        e.mouseX = undefined;
        e.mouseY = undefined;
        e = e.offsetParent;
      }
    }
    else {
      // Use absolute coordinates
      var pos = fb.absolutePosition(reference);
      x = (event.pageX || 0*(event.clientX + $('html').get(0).scrollLeft)) - pos.x;
      y = (event.pageY || 0*(event.clientY + $('html').get(0).scrollTop)) - pos.y;
    }
    // Subtract distance to middle
    return { x: x - fb.width / 2, y: y - fb.width / 2 };
  }

  /**
   * Mousedown handler
   */
  fb.mousedown = function (event) {
    // Capture mouse
    if (!document.dragging) {
      $(document).bind('mousemove', fb.mousemove).bind('mouseup', fb.mouseup);
      document.dragging = true;
    }

    // Check which area is being dragged
    var pos = fb.widgetCoords(event);
    fb.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) * 2 > fb.square;

    // Process
    fb.mousemove(event);
    return false;
  }

  /**
   * Mousemove handler
   */
  fb.mousemove = function (event) {
    // Get coordinates relative to color picker center
    var pos = fb.widgetCoords(event);

    // Set new HSL parameters
    if (fb.circleDrag) {
      var hue = Math.atan2(pos.x, -pos.y) / 6.28;
      if (hue < 0) hue += 1;
      fb.setHSL([hue, fb.hsl[1], fb.hsl[2]]);
    }
    else {
      var sat = Math.max(0, Math.min(1, -(pos.x / fb.square) + .5));
      var lum = Math.max(0, Math.min(1, -(pos.y / fb.square) + .5));
      fb.setHSL([fb.hsl[0], sat, lum]);
    }
    return false;
  }

  /**
   * Mouseup handler
   */
  fb.mouseup = function () {
    // Uncapture mouse
    $(document).unbind('mousemove', fb.mousemove);
    $(document).unbind('mouseup', fb.mouseup);
    document.dragging = false;
  }

  /**
   * Update the markers and styles
   */
  fb.updateDisplay = function () {
    // Markers
    var angle = fb.hsl[0] * 6.28;
    $('.h-marker', e).css({
      left: Math.round(Math.sin(angle) * fb.radius + fb.width / 2) + 'px',
      top: Math.round(-Math.cos(angle) * fb.radius + fb.width / 2) + 'px'
    });

    $('.sl-marker', e).css({
      left: Math.round(fb.square * (.5 - fb.hsl[1]) + fb.width / 2) + 'px',
      top: Math.round(fb.square * (.5 - fb.hsl[2]) + fb.width / 2) + 'px'
    });

    // Saturation/Luminance gradient
    $('.color', e).css('backgroundColor', fb.pack(fb.HSLToRGB([fb.hsl[0], 1, 0.5])));

    // Linked elements or callback
    if (typeof fb.callback == 'object') {
      // Set background/foreground color
      $(fb.callback).css({
        backgroundColor: fb.color,
        color: fb.hsl[2] > 0.5 ? '#000' : '#fff'
      });

      // Change linked value
      $(fb.callback).each(function() {
        if (this.value && this.value != fb.color) {
          this.value = fb.color;
        }
      });
    }
    else if (typeof fb.callback == 'function') {
      fb.callback.call(fb, fb.color);
    }
  }

  /**
   * Get absolute position of element
   */
  fb.absolutePosition = function (el) {
    var r = { x: el.offsetLeft, y: el.offsetTop };
    // Resolve relative to offsetParent
    if (el.offsetParent) {
      var tmp = fb.absolutePosition(el.offsetParent);
      r.x += tmp.x;
      r.y += tmp.y;
    }
    return r;
  };

  /* Various color utility functions */
  fb.pack = function (rgb) {
    var r = Math.round(rgb[0] * 255);
    var g = Math.round(rgb[1] * 255);
    var b = Math.round(rgb[2] * 255);
    return '#' + (r < 16 ? '0' : '') + r.toString(16) +
           (g < 16 ? '0' : '') + g.toString(16) +
           (b < 16 ? '0' : '') + b.toString(16);
  }

  fb.unpack = function (color) {
    if (color.length == 7) {
      return [parseInt('0x' + color.substring(1, 3)) / 255,
        parseInt('0x' + color.substring(3, 5)) / 255,
        parseInt('0x' + color.substring(5, 7)) / 255];
    }
    else if (color.length == 4) {
      return [parseInt('0x' + color.substring(1, 2)) / 15,
        parseInt('0x' + color.substring(2, 3)) / 15,
        parseInt('0x' + color.substring(3, 4)) / 15];
    }
  }

  fb.HSLToRGB = function (hsl) {
    var m1, m2, r, g, b;
    var h = hsl[0], s = hsl[1], l = hsl[2];
    m2 = (l <= 0.5) ? l * (s + 1) : l + s - l*s;
    m1 = l * 2 - m2;
    return [this.hueToRGB(m1, m2, h+0.33333),
        this.hueToRGB(m1, m2, h),
        this.hueToRGB(m1, m2, h-0.33333)];
  }

  fb.hueToRGB = function (m1, m2, h) {
    h = (h < 0) ? h + 1 : ((h > 1) ? h - 1 : h);
    if (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
    if (h * 2 < 1) return m2;
    if (h * 3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * 6;
    return m1;
  }

  fb.RGBToHSL = function (rgb) {
    var min, max, delta, h, s, l;
    var r = rgb[0], g = rgb[1], b = rgb[2];
    min = Math.min(r, Math.min(g, b));
    max = Math.max(r, Math.max(g, b));
    delta = max - min;
    l = (min + max) / 2;
    s = 0;
    if (l > 0 && l < 1) {
      s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l));
    }
    h = 0;
    if (delta > 0) {
      if (max == r && max != g) h += (g - b) / delta;
      if (max == g && max != b) h += (2 + (b - r) / delta);
      if (max == b && max != r) h += (4 + (r - g) / delta);
      h /= 6;
    }
    return [h, s, l];
  }

  // Install mousedown handler (the others are set on the document on-demand)
  $('*', e).mousedown(fb.mousedown);

    // Init color
  fb.setColor('#000000');

  // Set linked elements/callback
  if (callback) {
    fb.linkTo(callback);
  }
}<|endoftext|>