import { setupRerender } from 'preact/test-utils';
import { createElement, render, Component, options } from 'preact';
import {
	setupScratch,
	teardown,
	getMixedArray,
	mixedArrayHTML,
	serializeHtml,
	supportsDataList,
	sortAttributes,
	spyOnElementAttributes,
	createEvent
} from '../_util/helpers';
import { clearLog, getLog, logCall } from '../_util/logCall';
import { useState } from 'preact/hooks';

/** @jsx createElement */

function getAttributes(node) {
	let attrs = {};
	for (let i = node.attributes.length; i--; ) {
		attrs[node.attributes[i].name] = node.attributes[i].value;
	}
	return attrs;
}

const isIE11 = /Trident\//.test(navigator.userAgent);

describe('render()', () => {
	let scratch, rerender;

	let resetAppendChild;
	let resetInsertBefore;
	let resetRemoveChild;
	let resetRemove;

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

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

	before(() => {
		resetAppendChild = logCall(Element.prototype, 'appendChild');
		resetInsertBefore = logCall(Element.prototype, 'insertBefore');
		resetRemoveChild = logCall(Element.prototype, 'removeChild');
		resetRemove = logCall(Element.prototype, 'remove');
	});

	after(() => {
		resetAppendChild();
		resetInsertBefore();
		resetRemoveChild();
		resetRemove();
	});

	it('should rerender when value from "" to 0', () => {
		render('', scratch);
		expect(scratch.innerHTML).to.equal('');

		render(0, scratch);
		expect(scratch.innerHTML).to.equal('0');
	});

	it('should render an empty text node given an empty string', () => {
		render('', scratch);
		let c = scratch.childNodes;
		expect(c).to.have.length(1);
		expect(c[0].data).to.equal('');
		expect(c[0].nodeName).to.equal('#text');
	});

	it('should allow node type change with content', () => {
		render(Bad, scratch);
		render(
Good
, scratch);
		expect(scratch.innerHTML).to.eql(`<div>Good</div>`);
	});

	it('should not render when detecting JSON-injection', () => {
		const vnode = JSON.parse('{"type":"span","children":"Malicious"}');
		render(vnode, scratch);
		expect(scratch.firstChild).to.be.null;
	});

	it('should create empty nodes (<* />)', () => {
		render(
, scratch);
		expect(scratch.childNodes).to.have.length(1);
		expect(scratch.childNodes[0].nodeName).to.equal('DIV');

		scratch.parentNode.removeChild(scratch);
		scratch = document.createElement('div');
		(document.body || document.documentElement).appendChild(scratch);

		render(, scratch);
		expect(scratch.childNodes).to.have.length(1);
		expect(scratch.childNodes[0].nodeName).to.equal('SPAN');
	});

	it('should not throw error in IE11 with type date', () => {
		expect(() => render(<input type="date" />, scratch)).to.not.throw();
	});

	it('should support custom tag names', () => {
		render(<foo />, scratch);
		expect(scratch.childNodes).to.have.length(1);
		expect(scratch.firstChild).to.have.property('nodeName', 'FOO');

		scratch.parentNode.removeChild(scratch);
		scratch = document.createElement('div');
		(document.body || document.documentElement).appendChild(scratch);

		render(<x-bar />, scratch);
		expect(scratch.childNodes).to.have.length(1);
		expect(scratch.firstChild).to.have.property('nodeName', 'X-BAR');
	});

	it('should support the form attribute', () => {
		render(

				<form id="myform" />
				<button form="myform">test</button>
				<input form="myform" />

,
			scratch
		);
		const div = scratch.childNodes[0];
		const form = div.childNodes[0];
		const button = div.childNodes[1];
		const input = div.childNodes[2];

		// IE11 doesn't support the form attribute
		if (!isIE11) {
			expect(button).to.have.property('form', form);
			expect(input).to.have.property('form', form);
		}
	});

	it('should allow VNode reuse', () => {
		let reused =
Hello World!
;
		render(

				{reused}

				{reused}

,
			scratch
		);
		expect(serializeHtml(scratch)).to.eql(
			`<div><div class="reuse">Hello World!</div><hr><div class="reuse">Hello World!</div></div>`
		);

		render(

				{reused}

,
			scratch
		);
		expect(serializeHtml(scratch)).to.eql(
			`<div><hr><div class="reuse">Hello World!</div></div>`
		);
	});

	it('should merge new elements when called multiple times', () => {
		render(
, scratch);
		expect(scratch.childNodes).to.have.length(1);
		expect(scratch.firstChild).to.have.property('nodeName', 'DIV');
		expect(scratch.innerHTML).to.equal('

');

		render(, scratch);
		expect(scratch.childNodes).to.have.length(1);
		expect(scratch.firstChild).to.have.property('nodeName', 'SPAN');
		expect(scratch.innerHTML).to.equal('');

		render(Hello!, scratch);
		expect(scratch.childNodes).to.have.length(1);
		expect(scratch.firstChild).to.have.property('nodeName', 'SPAN');
		expect(scratch.innerHTML).to.equal('Hello!');
	});

	it('should nest empty nodes', () => {
		render(

				<foo />
				<x-bar />

,
			scratch
		);

		expect(scratch.childNodes).to.have.length(1);
		expect(scratch.childNodes[0].nodeName).to.equal('DIV');

		let c = scratch.childNodes[0].childNodes;
		expect(c).to.have.length(3);
		expect(c[0].nodeName).to.equal('SPAN');
		expect(c[1].nodeName).to.equal('FOO');
		expect(c[2].nodeName).to.equal('X-BAR');
	});

	it('should not render falsy values', () => {
		render(

				{null},{undefined},{false},{0},{NaN}

,
			scratch
		);

		expect(scratch.firstChild).to.have.property('innerHTML', ',,,0,NaN');
	});

	it('should not render null', () => {
		render(null, scratch);
		expect(scratch.innerHTML).to.equal('');
		expect(scratch.childNodes).to.have.length(0);
	});

	it('should not render undefined', () => {
		render(undefined, scratch);
		expect(scratch.innerHTML).to.equal('');
		expect(scratch.childNodes).to.have.length(0);
	});

	it('should not render boolean true', () => {
		render(true, scratch);
		expect(scratch.innerHTML).to.equal('');
		expect(scratch.childNodes).to.have.length(0);
	});

	it('should not render boolean false', () => {
		render(false, scratch);
		expect(scratch.innerHTML).to.equal('');
		expect(scratch.childNodes).to.have.length(0);
	});

	it('should not render children when using function children', () => {
		render(
{() => {}}
, scratch);
		expect(scratch.innerHTML).to.equal('

');
	});

	it('should render NaN as text content', () => {
		render(NaN, scratch);
		expect(scratch.innerHTML).to.equal('NaN');
	});

	it('should render numbers (0) as text content', () => {
		render(0, scratch);
		expect(scratch.innerHTML).to.equal('0');
	});

	it('should render numbers (42) as text content', () => {
		render(42, scratch);
		expect(scratch.innerHTML).to.equal('42');
	});

	it('should render strings as text content', () => {
		render('Testing, huh! How is it going?', scratch);
		expect(scratch.innerHTML).to.equal('Testing, huh! How is it going?');
	});

	it('should render arrays of mixed elements', () => {
		render(getMixedArray(), scratch);
		expect(scratch.innerHTML).to.equal(mixedArrayHTML);
	});

	it('should clear falsy attributes', () => {
		render(

,
			scratch
		);

		render(

,
			scratch
		);

		expect(
			getAttributes(scratch.firstChild),
			'from previous truthy values'
		).to.eql({
			a0: '0',
			anan: 'NaN'
		});
	});

	it('should not render falsy attributes on hydrate', () => {
		render(

,
			scratch
		);

		expect(getAttributes(scratch.firstChild), 'initial render').to.eql({
			a0: '0',
			anan: 'NaN'
		});
	});

	it('should clear falsy input values', () => {
		// Note: this test just demonstrates the default browser behavior
		render(

				<input value={0} />
				<input value={false} />
				<input value={null} />
				<input value={undefined} />

,
			scratch
		);

		let root = scratch.firstChild;
		expect(root.children[0]).to.have.property('value', '0');
		expect(root.children[1]).to.have.property('value', 'false');
		expect(root.children[2]).to.have.property('value', '');
		expect(root.children[3]).to.have.property('value', '');
	});

	it('should set value inside the specified range', () => {
		render(
			<input type="range" value={0.5} min="0" max="1" step="0.05" />,
			scratch
		);
		expect(scratch.firstChild.value).to.equal('0.5');
	});

	// IE or IE Edge will throw when attribute values don't conform to the
	// spec. That's the correct behaviour, but bad for this test...
	if (!/(Edge|MSIE|Trident)/.test(navigator.userAgent)) {
		it('should not clear falsy DOM properties', () => {
			function test(val) {
				render(

						<input value={val} />

,
					scratch
				);
			}

			test('2');
			test(false);
			expect(scratch.innerHTML).to.equal(
				'
<input>

',
				'for false'
			);

			test('3');
			test(null);
			expect(scratch.innerHTML).to.equal(
				'
<input>

',
				'for null'
			);

			test('4');
			test(undefined);
			expect(scratch.innerHTML).to.equal(
				'
<input>

',
				'for undefined'
			);
		});
	}

	// Test for preactjs/preact#651
	it('should set enumerable boolean attribute', () => {
		render(<input spellcheck={false} />, scratch);
		expect(scratch.firstChild.spellcheck).to.equal(false);
	});

	it('should render download attribute', () => {
		render(, scratch);
		expect(scratch.firstChild.getAttribute('download')).to.equal('');

		render(, scratch);
		expect(scratch.firstChild.getAttribute('download')).to.equal(null);
	});

	it('should not set tagName', () => {
		expect(() => render(<input tagName="div" />, scratch)).not.to.throw();
	});

	it('should apply string attributes', () => {
		render(
, scratch);
		expect(serializeHtml(scratch)).to.equal(
			'

'
		);
	});

	it('should not serialize function props as attributes', () => {
		render(
, scratch);

		let div = scratch.childNodes[0];
		expect(div.attributes.length).to.equal(0);
	});

	it('should serialize object props as attributes', () => {
		render(

,
			scratch
		);

		let div = scratch.childNodes[0];
		expect(div.attributes.length).to.equal(2);

		// Normalize attribute order because it's different in various browsers
		let normalized = {};
		for (let i = 0; i
 {
		render(
, scratch);
		expect(scratch.childNodes[0]).to.have.property('className', 'foo');
	});

	it('should alias className to class', () => {
		render(
, scratch);
		expect(scratch.childNodes[0]).to.have.property('className', 'bar');
	});

	it('should support false aria-* attributes', () => {
		render(
, scratch);
		expect(scratch.firstChild.getAttribute('aria-checked')).to.equal('false');
	});

	it('should set checked attribute on custom elements without checked property', () => {
		render(<o-checkbox checked />, scratch);
		expect(scratch.innerHTML).to.equal(
			'<o-checkbox checked="true"></o-checkbox>'
		);
	});

	it('should set value attribute on custom elements without value property', () => {
		render(<o-input value="test" />, scratch);
		expect(scratch.innerHTML).to.equal('<o-input value="test"></o-input>');
	});

	it('should mask value on password input elements', () => {
		render(<input value="xyz" type="password" />, scratch);
		expect(scratch.innerHTML).to.equal('<input type="password">');
	});

	it('should unset href if null || undefined', () => {
		render(

				href="#"
				href="undefined"
				href="null"
				href="''"

,
			scratch
		);

		const links = scratch.querySelectorAll('a');
		expect(links[0].hasAttribute('href')).to.equal(true);
		expect(links[1].hasAttribute('href')).to.equal(false);
		expect(links[2].hasAttribute('href')).to.equal(false);
		expect(links[3].hasAttribute('href')).to.equal(true);
	});

	describe('dangerouslySetInnerHTML', () => {
		it('should support dangerouslySetInnerHTML', () => {
			let html = 'foo & bar';
			// eslint-disable-next-line react/no-danger
			render(
, scratch);

			expect(scratch.firstChild, 'set').to.have.property('innerHTML', html);
			expect(scratch.innerHTML).to.equal('
' + html + '
');

			render(

					ab

,
				scratch
			);

			expect(scratch, 'unset').to.have.property(
				'innerHTML',
				`<div>a<strong>b</strong></div>`
			);

			// eslint-disable-next-line react/no-danger
			render(
, scratch);
			expect(scratch.innerHTML, 're-set').to.equal('
' + html + '
');
		});

		it('should apply proper mutation for VNodes with dangerouslySetInnerHTML attr', () => {
			let thing;
			class Thing extends Component {
				constructor(props, context) {
					super(props, context);
					this.state = { html: this.props.html };
					thing = this;
				}
				render(props, { html }) {
					// eslint-disable-next-line react/no-danger
					return html ? (

					) : (

					);
				}
			}

			render(<Thing html="test" />, scratch);
			expect(scratch.innerHTML).to.equal('
test
');

			thing.setState({ html: false });
			rerender();
			expect(scratch.innerHTML).to.equal('

');

			thing.setState({ html: '<foo><bar>test</bar></foo>' });
			rerender();
			expect(scratch.innerHTML).to.equal(
				'
<foo><bar>test</bar></foo>
'
			);
		});

		it('should not hydrate with dangerouslySetInnerHTML', () => {
			let html = 'foo & bar';
			scratch.innerHTML = `<div>${html}</div>`;
			// eslint-disable-next-line react/no-danger
			render(
, scratch);

			expect(scratch.firstChild).to.have.property('innerHTML', html);
			expect(scratch.innerHTML).to.equal(`<div>${html}</div>`);
		});

		it('should avoid reapplying innerHTML when __html property of dangerouslySetInnerHTML attr remains unchanged', () => {
			class Thing extends Component {
				render() {
					// eslint-disable-next-line react/no-danger
					return (
						<div dangerouslySetInnerHTML={{ __html: 'same' }} />
					);
				}
			}

			let thing;
			render(<Thing ref={r => (thing = r)} />, scratch);

			let firstInnerHTMLChild = scratch.firstChild.firstChild;

			// Re-render
			thing.forceUpdate();

			expect(firstInnerHTMLChild).to.equalNode(scratch.firstChild.firstChild);
		});

		it('should unmount dangerouslySetInnerHTML', () => {
			let set;

			const TextDiv = () => (

some text

			);

			class App extends Component {
				constructor(props) {
					super(props);
					set = this.setState.bind(this);
					this.state = { show: true };
				}

				render() {
					return this.state.show && <TextDiv />;
				}
			}

			render(<App />, scratch);
			expect(scratch.innerHTML).to.equal('

');

			set({ show: false });
			rerender();
			expect(scratch.innerHTML).to.equal('');
		});
	});

	it('should reconcile mutated DOM attributes', () => {
		let check = p => render(<input type="checkbox" checked={p} />, scratch),
			value = () => scratch.lastChild.checked,
			setValue = p => (scratch.lastChild.checked = p);
		check(true);
		expect(value()).to.equal(true);
		check(false);
		expect(value()).to.equal(false);
		check(true);
		expect(value()).to.equal(true);
		setValue(true);
		check(false);
		expect(value()).to.equal(false);
		setValue(false);
		check(true);
		expect(value()).to.equal(true);
	});

	it('should reorder child pairs', () => {
		render(

				a
				b

,
			scratch
		);

		let a = scratch.firstChild.firstChild;
		let b = scratch.firstChild.lastChild;

		expect(a).to.have.property('nodeName', 'A');
		expect(b).to.have.property('nodeName', 'B');

		render(

				b
				a

,
			scratch
		);

		expect(scratch.firstChild.firstChild).to.equalNode(b);
		expect(scratch.firstChild.lastChild).to.equalNode(a);
	});

	// Discussion: https://github.com/preactjs/preact/issues/287
	// <datalist> is not supported in Safari, even though the element
	// constructor is present
	if (supportsDataList()) {
		it('should allow <input list /> to pass through as an attribute', () => {
			render(

					<input type="range" min="0" max="100" list="steplist" />
					<datalist id="steplist">
						<option>0</option>
						<option>50</option>
						<option>100</option>
					</datalist>

,
				scratch
			);

			let html = scratch.firstElementChild.firstElementChild.outerHTML;
			expect(sortAttributes(html)).to.equal(
				sortAttributes('<input type="range" min="0" max="100" list="steplist">')
			);
		});
	}

	// Issue #2284
	it('should not throw when setting size to an invalid value', () => {
		// These values are usually used to reset the `size` attribute to its
		// initial state.
		expect(() => render(<input size={undefined} />, scratch)).to.not.throw();
		expect(() => render(<input size={null} />, scratch)).to.not.throw();
		expect(() => render(<input size={0} />, scratch)).to.not.throw();
	});

	it('should not execute append operation when child is at last', () => {
		// See preactjs/preact#717 for discussion about the issue this addresses

		let todoText = 'new todo that I should complete';
		let input;
		let setText;
		let addTodo;

		const ENTER = 13;

		class TodoList extends Component {
			constructor(props) {
				super(props);
				this.state = { todos: [], text: '' };
				setText = this.setText = this.setText.bind(this);
				addTodo = this.addTodo = this.addTodo.bind(this);
			}
			setText(e) {
				this.setState({ text: e.target.value });
			}
			addTodo(e) {
				if (e.keyCode === ENTER) {
					let { todos, text } = this.state;
					todos = todos.concat({ text });
					this.setState({ todos, text: '' });
				}
			}
			render() {
				const { todos, text } = this.state;
				return (

						{todos.map(todo => [
							{todo.text},

								{' '}
								[ Delete ]
							,

						])}
						<input value={text} onInput={this.setText} ref={i => (input = i)} />

				);
			}
		}

		render(<TodoList />, scratch);

		// Simulate user typing
		input.focus();
		input.value = todoText;
		setText({
			target: input
		});

		// Commit the user typing setState call
		rerender();

		// Simulate user pressing enter
		addTodo({
			keyCode: ENTER
		});

		// Before Preact rerenders, focus should be on the input
		expect(document.activeElement).to.equalNode(input);

		rerender();

		// After Preact rerenders, focus should remain on the input
		expect(document.activeElement).to.equalNode(input);
		expect(scratch.innerHTML).to.contain(`<span>${todoText}</span>`);
	});

	it('should keep value of uncontrolled inputs', () => {
		render(<input value={undefined} />, scratch);
		scratch.firstChild.value = 'foo';
		render(<input value={undefined} />, scratch);
		expect(scratch.firstChild.value).to.equal('foo');
	});

	it('should keep value of uncontrolled checkboxes', () => {
		render(<input type="checkbox" checked={undefined} />, scratch);
		scratch.firstChild.checked = true;
		render(<input type="checkbox" checked={undefined} />, scratch);
		expect(scratch.firstChild.checked).to.equal(true);
	});

	// #2756
	it('should set progress value to 0', () => {
		render(<progress value={0} max="100" />, scratch);
		expect(scratch.firstChild.value).to.equal(0);
		expect(scratch.firstChild.getAttribute('value')).to.equal('0');
	});

	it('should always diff `checked` and `value` properties against the DOM', () => {
		// See https://github.com/preactjs/preact/issues/1324

		let inputs;
		let text;
		let checkbox;

		class Inputs extends Component {
			render() {
				return (

						<input value={'Hello'} ref={el => (text = el)} />
						<input type="checkbox" checked ref={el => (checkbox = el)} />

				);
			}
		}

		render(<Inputs ref={x => (inputs = x)} />, scratch);

		expect(text.value).to.equal('Hello');
		expect(checkbox.checked).to.equal(true);

		text.value = 'World';
		checkbox.checked = false;

		inputs.forceUpdate();
		rerender();

		expect(text.value).to.equal('Hello');
		expect(checkbox.checked).to.equal(true);
	});

	it('should always diff `contenteditable` `innerHTML` against the DOM', () => {
		// This tests that we do not cause any cursor jumps in contenteditable fields
		// See https://github.com/preactjs/preact/issues/2691

		function Editable() {
			const [value, setValue] = useState('Hello');

			return (

 setValue(e.currentTarget.innerHTML)}
				/>
			);
		}

		render(<Editable />, scratch);

		let editable = scratch.querySelector('[contenteditable]');

		// modify the innerHTML and set the caret to character 2 to simulate a user typing
		editable.innerHTML = 'World';

		const range = document.createRange();
		range.selectNodeContents(editable);
		range.setStart(editable.childNodes[0], 2);
		range.collapse(true);
		const sel = window.getSelection();
		sel.removeAllRanges();
		sel.addRange(range);

		// ensure we didn't mess up setting the cursor to position 2
		expect(window.getSelection().getRangeAt(0).startOffset).to.equal(2);

		// dispatch the input event to tell preact to re-render
		editable.dispatchEvent(createEvent('input'));
		rerender();

		// ensure innerHTML is still correct (was not an issue before) and
		// more importantly the caret is still at character 2
		editable = scratch.querySelector('[contenteditable]');
		expect(editable.innerHTML).to.equal('World');
		expect(window.getSelection().getRangeAt(0).startOffset).to.equal(2);
	});

	it('should not re-render when a component returns undefined', () => {
		let Dialog = () => undefined;
		let updateState;
		class App extends Component {
			constructor(props) {
				super(props);
				this.state = { name: '' };
				updateState = () => this.setState({ name: ', friend' });
			}

			render(props, { name }) {
				return (

						<Dialog />

Hi{name}

				);
			}
		}

		render(<App />, scratch);
		clearLog();

		updateState();
		rerender();

		// We don't log text updates
		expect(getLog()).to.deep.equal([]);
	});

	it('should not lead to stale DOM nodes', () => {
		let i = 0;
		let updateApp;
		class App extends Component {
			render() {
				updateApp = () => this.forceUpdate();
				return <Parent />;
			}
		}

		let updateParent;
		function Parent() {
			updateParent = () => this.forceUpdate();
			i++;
			return <Child i={i} />;
		}

		function Child({ i }) {
			return i < 3 ? null :
foo
;
		}

		render(<App />, scratch);

		updateApp();
		rerender();
		updateParent();
		rerender();
		updateApp();
		rerender();

		// Without a fix it would render: `<div>foo</div><div></div>`
		expect(scratch.innerHTML).to.equal('
foo
');
	});

	// see preact/#1327
	it('should not reuse unkeyed components', () => {
		class X extends Component {
			constructor() {
				super();
				this.state = { i: 0 };
			}

			update() {
				this.setState(prev => ({ i: prev.i + 1 }));
			}

			componentWillUnmount() {
				clearTimeout(this.id);
			}

			render() {
				return
{this.state.i}
;
			}
		}

		let ref;
		let updateApp;
		class App extends Component {
			constructor() {
				super();
				this.state = { i: 0 };
				updateApp = () => this.setState(prev => ({ i: prev.i ^ 1 }));
			}

			render() {
				return (

						{this.state.i === 0 && <X />}
						<X ref={node => (ref = node)} />

				);
			}
		}

		render(<App />, scratch);
		expect(scratch.textContent).to.equal('00');

		ref.update();
		updateApp();
		rerender();
		expect(scratch.textContent).to.equal('1');

		updateApp();
		rerender();

		expect(scratch.textContent).to.equal('01');
	});

	it('should not cause infinite loop with referentially equal props', () => {
		let i = 0;
		let prevDiff = options._diff;
		options._diff = () => {
			if (++i > 10) {
				options._diff = prevDiff;
				throw new Error('Infinite loop');
			}
		};

		function App({ children, ...rest }) {
			return (

{children}

			);
		}

		render(<App>10</App>, scratch);
		expect(scratch.textContent).to.equal('10');
		options._diff = prevDiff;
	});

	it('should not call options.debounceRendering unnecessarily', () => {
		let comp;

		class A extends Component {
			constructor(props) {
				super(props);
				this.state = { updates: 0 };
				comp = this;
			}

			render() {
				return
{this.state.updates}
;
			}
		}

		render(, scratch);
		expect(scratch.innerHTML).to.equal('
0
');

		const sandbox = sinon.createSandbox();
		try {
			sandbox.spy(options, 'debounceRendering');

			comp.setState({ updates: 1 }, () => {
				comp.setState({ updates: 2 });
			});
			rerender();
			expect(scratch.innerHTML).to.equal('
2
');

			expect(options.debounceRendering).to.have.been.calledOnce;
		} finally {
			sandbox.restore();
		}
	});

	it('should remove attributes on pre-existing DOM', () => {
		const div = document.createElement('div');
		div.setAttribute('class', 'red');
		const span = document.createElement('span');
		const text = document.createTextNode('Hi');

		span.appendChild(text);
		div.appendChild(span);
		scratch.appendChild(div);

		const App = () => (

				Bye

		);

		render(<App />, scratch);
		expect(serializeHtml(scratch)).to.equal('
Bye
');
	});

	it('should remove class attributes', () => {
		const App = props => (

				Bye

		);

		render(<App class="hi" />, scratch);
		expect(scratch.innerHTML).to.equal(
			'
Bye
'
		);

		render(<App />, scratch);
		expect(serializeHtml(scratch)).to.equal('
Bye
');
	});

	it('should not read DOM attributes on render without existing DOM', () => {
		const attributesSpy = spyOnElementAttributes();
		render(

Page 1

,
			scratch
		);
		expect(scratch.innerHTML).to.equal(
			'

Page 1

'
		);

		// IE11 doesn't allow modifying Element.prototype functions properly.
		// Custom spies will never be called.
		if (!isIE11) {
			expect(attributesSpy.get).to.not.have.been.called;
		}

		render(

Page 2

,
			scratch
		);
		expect(scratch.innerHTML).to.equal(
			'

Page 2

'
		);

		// IE11 doesn't allow modifying Element.prototype functions properly.
		// Custom spies will never be called.
		if (!isIE11) {
			expect(attributesSpy.get).to.not.have.been.called;
		}
	});

	// #2926
	it('should not throw when changing contentEditable to undefined or null', () => {
		render(
foo
, scratch);

		expect(() =>
			render(
foo
, scratch)
		).to.not.throw();
		expect(scratch.firstChild.contentEditable).to.equal('inherit');

		expect(() =>
			render(
foo
, scratch)
		).to.not.throw();
		expect(scratch.firstChild.contentEditable).to.equal('inherit');
	});

	// #2926 Part 2
	it('should allow setting contentEditable to false', () => {
		render(

				editable

not editable

,
			scratch
		);

		expect(scratch.firstChild.contentEditable).to.equal('true');
		expect(scratch.querySelector('p').contentEditable).to.equal('false');
	});
});


"dev": true,
"requires": {
"chokidar": "^3.4.3",
"karma-sourcemap-loader": "^0.3.8"
},
"dependencies": {
"karma-sourcemap-loader": {
"version": "0.3.8",

keep keep replace keep keep keep keep keep

#people-app {
	.avatar {
		display: inline-block;
		overflow: hidden;
		width: var(--avatar-size, 32px);
		height: var(--avatar-size, 32px);
		background-color: var(--avatar-color, var(--app-primary));
		border-radius: 50%;
		font-size: calc(var(--avatar-size, 32px) * 0.5);
		line-height: var(--avatar-size, 32px);
		object-fit: cover;
		text-align: center;
		text-transform: uppercase;
		white-space: nowrap;
	}
}


keep keep keep keep replace keep keep keep keep

preact

Fast 3kb React-compatible Virtual DOM library.

microbundle build --raw -f iife src/cjs.js -o dist/preact.min.js

microbundle build --raw --cwd test-utils

microbundle build --raw --cwd compat --globals 'preact/hooks=preactHooks'

microbundle build --raw --cwd jsx-runtime

node ./config/node-13-exports.js && node ./config/compat-entries.js

microbundle watch --raw --format cjs --cwd hooks

microbundle watch --raw --format cjs --cwd compat --globals 'preact/hooks=preactHooks'

run-p test:mocha test:karma:minify test:ts

tsc -p test/ts/ && mocha --require "@babel/register" test/ts/**/*-test.js

mocha --recursive --require "@babel/register" test/shared test/node

cross-env COVERAGE=true BABEL_NO_MODULES=true karma start karma.conf.js --single-run

cross-env COVERAGE=true MINIFY=true BABEL_NO_MODULES=true karma start karma.conf.js --single-run

cross-env BABEL_NO_MODULES=true karma start karma.conf.js --no-single-run

cross-env COVERAGE=false BABEL_NO_MODULES=true karma start karma.conf.js --grep=hooks/test/browser/**.js --no-single-run

cross-env PERFORMANCE=false COVERAGE=false BABEL_NO_MODULES=true karma start karma.conf.js --grep=test-utils/test/shared/**.js --no-single-run

cross-env PERFORMANCE=true COVERAGE=false BABEL_NO_MODULES=true karma start karma.conf.js --grep=test/benchmarks/**.js --single-run

npm run test:karma:bench -- no-single-run

eslint src test debug compat hooks test-utils

preact
react
ui
user interface
virtual dom
vdom
components
dom diff
front-end
framework

The Preact Authors (https://github.com/preactjs/preact/contributors)


import { unstable_batchedUpdates } from 'preact/compat';

describe('unstable_batchedUpdates', () => {
	it('should call the callback', () => {
		const spy = sinon.spy();
		unstable_batchedUpdates(spy);
		expect(spy).to.be.calledOnce;
	});

	it('should call callback with only one arg', () => {
		const spy = sinon.spy();
		unstable_batchedUpdates(spy, 'foo', 'bar');
		expect(spy).to.be.calledWithExactly('foo');
	});
});


#people-app {
	position: relative;
	overflow: hidden;
	min-height: 100vh;
	animation: popup 300ms cubic-bezier(0.3, 0.7, 0.3, 1) forwards;
	background: var(--app-background);
	--menu-width: 260px;
	--menu-item-height: 50px;

	@media (min-width: 1280px) {
		max-width: 1280px;
		min-height: calc(100vh - 64px);
		margin: 32px auto;
		border-radius: 10px;
	}

	> nav {
		position: absolute;
		display: flow-root;
		width: var(--menu-width);
		height: 100%;
		background-color: var(--app-background-secondary);
		overflow-x: hidden;
		overflow-y: auto;
	}

	> nav h4 {
		padding-left: 16px;
		font-weight: normal;
		text-transform: uppercase;
	}

	> nav ul {
		position: relative;
	}

	> nav li {
		position: absolute;
		width: 100%;
		animation: zoom 200ms forwards;
		opacity: 0;
		transition: top 200ms;
	}

	> nav li > a {
		position: relative;
		display: flex;
		overflow: hidden;
		flex-flow: row;
		align-items: center;
		margin-left: 16px;
		border-right: 2px solid transparent;
		border-bottom-left-radius: 48px;
		border-top-left-radius: 48px;
		text-transform: capitalize;
		transition: border 500ms;
	}

	> nav li > a:hover {
		background-color: var(--app-highlight);
	}

	> nav li > a::after {
		position: absolute;
		top: 0;
		right: -2px;
		bottom: 0;
		left: 0;
		background-image: radial-gradient(
			circle,
			var(--app-ripple) 1%,
			transparent 1%
		);
		background-position: center;
		background-repeat: no-repeat;
		background-size: 10000%;
		content: '';
		opacity: 0;
		transition: opacity 700ms, background 300ms;
	}

	> nav li > a:active::after {
		background-size: 100%;
		opacity: 0.5;
		transition: none;
	}

	> nav li > a.active {
		border-color: var(--app-primary);
		background-color: var(--app-highlight);
	}

	> nav li > a > * {
		margin: 8px;
	}

	#people-main {
		padding-left: var(--menu-width);
	}
}


import { options } from 'preact';
import { initDevTools } from './devtools';

initDevTools();

/**
 * Display a custom label for a custom hook for the devtools panel
 * @type {<T>(value: T, name: string) => T}
 */
export function addHookName(value, name) {
	if (options._addHookName) {
		options._addHookName(name);
	}
	return value;
}


// Size the code mirror editors height to the editor content

/*
 * Copyright 2012 Adobe Systems Incorporated. All Rights Reserved.
 */

/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */
/*global define: false, $: false, CodeMirror: false */

define(function (require, exports, module) {
    'use strict';

    // Load dependent modules
    var DocumentManager     = require("document/DocumentManager"),
        HTMLUtils           = require("language/HTMLUtils"),
        CSSUtils            = require("language/CSSUtils"),
        EditorManager       = require("editor/EditorManager"),
        InlineTextEditor    = require("editor/InlineTextEditor").InlineTextEditor;

    /**
     * @constructor
     * @extends {InlineWidget}
     */
    function CSSInlineEditor(rules) {
        InlineTextEditor.call(this);
        this._rules = rules;
        this._selectedRuleIndex = -1;
    }
    CSSInlineEditor.prototype = new InlineTextEditor();
    CSSInlineEditor.prototype.constructor = CSSInlineEditor;
    CSSInlineEditor.prototype.parentClass = InlineTextEditor.prototype;
    CSSInlineEditor.prototype.$editorsDiv = null;
    CSSInlineEditor.prototype.$relatedContainer = null;
    CSSInlineEditor.prototype._updateRelatedContainerProxy = null;

    /**
     * @override
     * @param {!Editor} hostEditor  Outer Editor instance that inline editor will sit within.
     *
     */
    CSSInlineEditor.prototype.load = function (hostEditor) {
        this.parentClass.load.call(this, hostEditor);

        // Container to hold all editors
        var self = this,
            $ruleItem,
            $location;

        // Create DOM to hold editors and related list
        this.$editorsDiv = $(document.createElement('div')).addClass("inlineEditorHolder");

        // Outer container for border-left and scrolling
        this.$relatedContainer = $(document.createElement("div")).addClass("relatedContainer");

        // List "selection" highlight
        this.$selectedMarker = $(document.createElement("div")).appendTo(this.$relatedContainer).addClass("selection");

        // Inner container
        var $related = $(document.createElement("div")).appendTo(this.$relatedContainer).addClass("related");

        // Rule list
        var $ruleList = $(document.createElement("ul")).appendTo($related);

        // create rule list
        this._ruleItems = [];
        this._rules.forEach(function (rule, i) {
            $ruleItem = $(document.createElement("li")).appendTo($ruleList);
            $ruleItem.text(rule.selector + " ");
            $ruleItem.click(function () {
                self.setSelectedRule(i);
            });

            $location = $(document.createElement("span")).appendTo($ruleItem);
            $location.addClass("location");
            $location.text(rule.document.file.name + ":" + (rule.lineStart + 1));

            self._ruleItems.push($ruleItem);
        });

        // select the first rule
        self.setSelectedRule(0);

        // attach to main container
        this.$htmlContent.append(this.$editorsDiv).append(this.$relatedContainer);

        // initialize position based on the main #editorHolder
        setTimeout(function () {
            self._updateRelatedContainer();
        }, 0);

        this._updateRelatedContainerProxy = $.proxy(this._updateRelatedContainer, this);

        // Changes to the host editor should update the relatedContainer
        $(this.hostEditor).on("change.CSSInlineEditor", this._updateRelatedContainerProxy);

        // Since overflow-y is hidden on the CM scrollerElement, the scroll event is never fired.
        // Instead, we add a hook to CM's onScroll to reposition the relatedContainer.
        this.hostEditor._codeMirror.setOption("onScroll", this._updateRelatedContainerProxy);

        return (new $.Deferred()).resolve();
    };

    /**
     *
     *
     */
    CSSInlineEditor.prototype.setSelectedRule = function (index) {
        var newIndex = Math.min(Math.max(0, index), this._rules.length - 1);

        this._selectedRuleIndex = newIndex;
        var $ruleItem = this._ruleItems[this._selectedRuleIndex];

        this._ruleItems[this._selectedRuleIndex].toggleClass("selected", true);

        // Remove previous editors
        this.editors.forEach(function (editor) {
            editor.destroy(); //release ref on Document
        });
        this.editors = [];
        this.$editorsDiv.children().remove();
        $(this.editors[0]).off("change.CSSInlineEditor");

        // Keyboard shortcuts
        var extraKeys = {
            "Alt-Up" : $.proxy(this.previousRule, this),
            "Alt-Down" : $.proxy(this.nextRule, this)
        };

        // Add new editor
        var rule = this.getSelectedRule();
        this.createInlineEditorFromText(rule.document, rule.lineStart, rule.lineEnd, this.$editorsDiv.get(0), extraKeys);

        // Changes in size to the inline editor should update the relatedContainer
        $(this.editors[0]).on("change.CSSInlineEditor", this._updateRelatedContainerProxy);

        this.sizeInlineWidgetToContents(true);
        this._updateRelatedContainer();

        // scroll the selection to the ruleItem, use setTimeout to wait for DOM updates
        var self = this;
        setTimeout(function () {
            var itemTop = $ruleItem.position().top;
            self.$selectedMarker.css("top", itemTop);
            self.$selectedMarker.height($ruleItem.height());

            // FUTURE (jasonsj): figure out if rule list should scroll
            itemTop -=  $ruleItem.parent().css("paddingTop").replace("px", "");
            self.$relatedContainer.scrollTop(itemTop);
        }, 0);
    };

    /**
     * Called any time inline was closed, whether manually (via closeThisInline()) or automatically
     */
    CSSInlineEditor.prototype.onClosed = function () {
        this.parentClass.onClosed.call(this); // super.onClosed()

        // remove resize handlers for relatedContainer
        $(this.hostEditor).off("change.CSSInlineEditor");
        $(this.editors[0]).off("change.CSSInlineEditor");
        this.hostEditor._codeMirror.setOption("onScroll", null);
    };

    /**
     *
     *
     */
    CSSInlineEditor.prototype._updateRelatedContainer = function () {
        var borderThickness = (this.$htmlContent.outerHeight() - this.$htmlContent.innerHeight()) / 2;
        this.$relatedContainer.css("top", this.$htmlContent.offset().top + borderThickness);
        this.$relatedContainer.height(this.$htmlContent.height());
    };

    /**
     *
     *
     */
    CSSInlineEditor.prototype.getRules = function () {
        return this._rules;
    };

    /**
     *
     *
     */
    CSSInlineEditor.prototype.getSelectedRule = function () {
        return this._rules[this._selectedRuleIndex];
    };

    /**
     * Display the next css rule in the rule list
     */
    CSSInlineEditor.prototype.nextRule = function () {
        this.setSelectedRule(this._selectedRuleIndex + 1);
    };

    /**
     *  Display the previous css rule in the rule list
     */
    CSSInlineEditor.prototype.previousRule = function () {
        this.setSelectedRule(this._selectedRuleIndex - 1);
    };

    /**
     * Sizes the inline widget height to be the maximum between the rule list height and the editor height
     * @overide
     */
    CSSInlineEditor.prototype.sizeInlineWidgetToContents = function (force) {
        this.parentClass.sizeInlineWidgetToContents.call(this, force);
        var widgetHeight = Math.max(this.$relatedContainer.height(), this.$editorsDiv.height());

        this.hostEditor.setInlineWidgetHeight(this.inlineId, widgetHeight, true);
    };

    /**
     * Given a position in an HTML editor, returns the relevant selector for the attribute/tag
     * surrounding that position, or "" if none is found.
     * @param {!Editor} editor
     * @private
     */
    function _getSelectorName(editor, pos) {
        var tagInfo = HTMLUtils.getTagInfo(editor, pos),
            selectorName = "";

        if (tagInfo.position.tokenType === HTMLUtils.TAG_NAME) {
            // Type selector
            selectorName = tagInfo.tagName;
        } else if (tagInfo.position.tokenType === HTMLUtils.ATTR_NAME ||
                   tagInfo.position.tokenType === HTMLUtils.ATTR_VALUE) {
            if (tagInfo.attr.name === "class") {
                // Class selector. We only look for the class name
                // that includes the insertion point. For example, if
                // the attribute is:
                //   class="error-dialog modal hide"
                // and the insertion point is inside "modal", we want ".modal"
                var attributeValue = tagInfo.attr.value;
                var startIndex = attributeValue.substr(0, tagInfo.position.offset).lastIndexOf(" ");
                var endIndex = attributeValue.indexOf(" ", tagInfo.position.offset);
                selectorName = "." +
                    attributeValue.substring(
                        startIndex === -1 ? 0 : startIndex + 1,
                        endIndex === -1 ? attributeValue.length : endIndex
                    );

                // If the insertion point is surrounded by space, selectorName is "."
                // Check for that here
                if (selectorName === ".") {
                    selectorName = "";
                }
            } else if (tagInfo.attr.name === "id") {
                // ID selector
                selectorName = "#" + tagInfo.attr.value;
            }
        }

        return selectorName;
    }

    /**
     * This function is registered with EditManager as an inline editor provider. It creates a CSSInlineEditor
     * when cursor is on an HTML tag name, class attribute, or id attribute, find associated
     * CSS rules and show (one/all of them) in an inline editor.
     *
     * @param {!Editor} editor
     * @param {!{line:Number, ch:Number}} pos
     * @return {$.Promise} a promise that will be resolved with an InlineWidget
     *      or null if we're not going to provide anything.
     */
    function htmlToCSSProvider(hostEditor, pos) {
        // Only provide a CSS editor when cursor is in HTML content
        if (hostEditor._codeMirror.getOption("mode") !== "htmlmixed") {
            return null;
        }
        var htmlmixedState = hostEditor._codeMirror.getTokenAt(pos).state;
        if (htmlmixedState.mode !== "html") {
            return null;
        }

        // Only provide CSS editor if the selection is an insertion point
        var sel = hostEditor.getSelection(false);
        if (sel.start.line !== sel.end.line || sel.start.ch !== sel.end.ch) {
            return null;
        }

        var selectorName = _getSelectorName(hostEditor, pos);
        if (selectorName === "") {
            return null;
        }

        var result = new $.Deferred();

        CSSUtils.findMatchingRules(selectorName)
            .done(function (rules) {
                if (rules && rules.length > 0) {
                    var cssInlineEditor = new CSSInlineEditor(rules);

                    cssInlineEditor.load(hostEditor).done(function () {
                        result.resolve(cssInlineEditor);
                    }).fail(function () {
                        result.reject();
                    });
                } else {
                    // No matching rules were found.
                    result.reject();
                }
            })
            .fail(function () {
                console.log("Error in findMatchingRules()");
                result.reject();
            });

        return result.promise();
    }

    EditorManager.registerInlineEditProvider(htmlToCSSProvider);

    exports.CSSInlineEditor = CSSInlineEditor;

});


/* Patterns.less
 * Repeatable UI elements outside the base styles provided from the scaffolding
 * ---------------------------------------------------------------------------- */

// TOPBAR
// ------

// Topbar for Branding and Nav
.topbar {
  height: 40px;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10000;
  overflow: visible;

  // Links get text shadow
  a {
    color: @grayLight;
    text-shadow: 0 -1px 0 rgba(0,0,0,.25);
  }

  // Hover and active states
  // h3 for backwards compatibility
  h3 a:hover,
  .brand:hover,
  ul .active > a {
    background-color: #333;
    background-color: rgba(255,255,255,.05);
    color: @white;
    text-decoration: none;
  }

  // Website name
  // h3 left for backwards compatibility
  h3 {
    position: relative;
  }
  h3 a,
  .brand {
    float: left;
    display: block;
    padding: 8px 20px 12px;
    margin-left: -20px; // negative indent to left-align the text down the page
    color: @white;
    font-size: 20px;
    font-weight: 200;
    line-height: 1;
  }

  // Plain text in topbar
  p {
    margin: 0;
    line-height: 40px;
    a:hover {
      background-color: transparent;
      color: @white;
    }
  }

  // Search Form
  form {
    float: left;
    margin: 5px 0 0 0;
    position: relative;
    .opacity(100);
  }
  // Todo: remove from v2.0 when ready, added for legacy
  form.pull-right {
    float: right;
  }
  input {
    background-color: #444;
    background-color: rgba(255,255,255,.3);
    #font > .sans-serif(13px, normal, 1);
    padding: 4px 9px;
    color: @white;
    color: rgba(255,255,255,.75);
    border: 1px solid #111;
    .border-radius(4px);
    @shadow: inset 0 1px 2px rgba(0,0,0,.1), 0 1px 0px rgba(255,255,255,.25);
    .box-shadow(@shadow);
    .transition(none);

    // Placeholder text gets special styles; can't be bundled together though for some reason
    &:-moz-placeholder {
      color: @grayLighter;
    }
    &::-webkit-input-placeholder {
      color: @grayLighter;
    }
    // Hover states
    &:hover {
      background-color: @grayLight;
      background-color: rgba(255,255,255,.5);
      color: @white;
    }
    // Focus states (we use .focused since IE8 and down doesn't support :focus)
    &:focus,
    &.focused {
      outline: 0;
      background-color: @white;
      color: @grayDark;
      text-shadow: 0 1px 0 @white;
      border: 0;
      padding: 5px 10px;
      .box-shadow(0 0 3px rgba(0,0,0,.15));
    }
  }
}

// gradient is applied to it's own element because overflow visible is not honored by ie when filter is present
// For backwards compatibility, include .topbar .fill
.topbar-inner,
.topbar .fill {
  background-color: #222;
  #gradient > .vertical(#333, #222);
  @shadow: 0 1px 3px rgba(0,0,0,.25), inset 0 -1px 0 rgba(0,0,0,.1);
  .box-shadow(@shadow);
}

// NAVIGATION
// ----------

// Topbar Nav
// ul.nav for all topbar based navigation to avoid inheritance issues and over-specificity
// For backwards compatibility, leave in .topbar div > ul
.topbar div > ul,
.nav {
  display: block;
  float: left;
  margin: 0 10px 0 0;
  position: relative;
  left: 0;
  > li {
    display: block;
    float: left;
  }
  a {
    display: block;
    float: none;
    padding: 10px 10px 11px;
    line-height: 19px;
    text-decoration: none;
    &:hover {
      color: @white;
      text-decoration: none;
    }
  }
  .active > a {
    background-color: #222;
    background-color: rgba(0,0,0,.5);
  }

  // Secondary (floated right) nav in topbar
  &.secondary-nav {
    float: right;
    margin-left: 10px;
    margin-right: 0;
    // backwards compatibility
    .menu-dropdown,
    .dropdown-menu {
      right: 0;
      border: 0;
    }
  }
  // Dropdowns within the .nav
  // a.menu:hover and li.open .menu for backwards compatibility
  a.menu:hover,
  li.open .menu,
  .dropdown-toggle:hover,
  .dropdown.open .dropdown-toggle {
    background: #444;
    background: rgba(255,255,255,.05);
  }
  // .menu-dropdown for backwards compatibility
  .menu-dropdown,
  .dropdown-menu {
    background-color: #333;
    // a.menu for backwards compatibility
    a.menu,
    .dropdown-toggle {
      color: @white;
      &.open {
        background: #444;
        background: rgba(255,255,255,.05);
      }
    }
    li a {
      color: #999;
      text-shadow: 0 1px 0 rgba(0,0,0,.5);
      &:hover {
        #gradient > .vertical(#292929,#191919);
        color: @white;
      }
    }
    .active a {
      color: @white;
    }
    .divider {
      background-color: #222;
      border-color: #444;
    }
  }
}

// For backwards compatibility with new dropdowns, redeclare dropdown link padding
.topbar ul .menu-dropdown li a,
.topbar ul .dropdown-menu li a {
  padding: 4px 15px;
}

// Dropdown Menus
// Use the .menu class on any <li> element within the topbar or ul.tabs and you'll get some superfancy dropdowns
// li.menu for backwards compatibility
li.menu,
.dropdown {
  position: relative;
}
// The link that is clicked to toggle the dropdown
// a.menu for backwards compatibility
a.menu:after,
.dropdown-toggle:after {
  width: 0;
  height: 0;
  display: inline-block;
  content: "↓";
  text-indent: -99999px;
  vertical-align: top;
  margin-top: 8px;
  margin-left: 4px;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 4px solid @white;
  .opacity(50);
}
// The dropdown menu (ul)
// .menu-dropdown for backwards compatibility
.menu-dropdown,
.dropdown-menu {
  background-color: @white;
  float: left;
  display: none; // None by default, but block on "open" of the menu
  position: absolute;
  top: 40px;
  z-index: 900;
  min-width: 160px;
  max-width: 220px;
  _width: 160px;
  margin-left: 0; // override default ul styles
  margin-right: 0;
  padding: 6px 0;
  zoom: 1; // do we need this?
  border-color: #999;
  border-color: rgba(0,0,0,.2);
  border-style: solid;
  border-width: 0 1px 1px;
  .border-radius(0 0 6px 6px);
  .box-shadow(0 2px 4px rgba(0,0,0,.2));
  .background-clip(padding-box);

  // Unfloat any li's to make them stack
  li {
    float: none;
    display: block;
    background-color: none;
  }
  // Dividers (basically an hr) within the dropdown
  .divider {
    height: 1px;
    margin: 5px 0;
    overflow: hidden;
    background-color: #eee;
    border-bottom: 1px solid @white;
  }
}

.topbar .dropdown-menu,
.dropdown-menu {
  // Links within the dropdown menu
  a {
    display: block;
    padding: 4px 15px;
    clear: both;
    font-weight: normal;
    line-height: 18px;
    color: @gray;
    text-shadow: 0 1px 0 @white;
    // Hover state
    &:hover,
    &.hover {
      #gradient > .vertical(#eeeeee, #dddddd);
      color: @grayDark;
      text-decoration: none;
      @shadow: inset 0 1px 0 rgba(0,0,0,.025), inset 0 -1px rgba(0,0,0,.025);
      .box-shadow(@shadow);
    }
  }
}

// Open state for the dropdown
// .open for backwards compatibility
.open,
.dropdown.open {
  // .menu for backwards compatibility
  .menu,
  .dropdown-toggle {
    color: @white;
    background: #ccc;
    background: rgba(0,0,0,.3);
  }
  // .menu-dropdown for backwards compatibility
  .menu-dropdown,
  .dropdown-menu {
    display: block;
  }
}

// TABS AND PILLS
// --------------

// Common styles
.tabs,
.pills {
  margin: 0 0 @baseline;
  padding: 0;
  list-style: none;
  .clearfix();
  > li {
    float: left;
    > a {
      display: block;
    }
  }
}

// Tabs
.tabs {
  border-color: #ddd;
  border-style: solid;
  border-width: 0 0 1px;
  > li {
    position: relative; // For the dropdowns mostly
    margin-bottom: -1px;
    > a {
      padding: 0 15px;
      margin-right: 2px;
      line-height: (@baseline * 2) - 2;
      border: 1px solid transparent;
      .border-radius(4px 4px 0 0);
      &:hover {
        text-decoration: none;
        background-color: #eee;
        border-color: #eee #eee #ddd;
      }
    }
  }
  // Active state, and it's :hover to override normal :hover
  .active > a,
  .active > a:hover {
    color: @gray;
    background-color: @white;
    border: 1px solid #ddd;
    border-bottom-color: transparent;
    cursor: default;
  }
}

// Dropdowns in tabs
.tabs {
  // first one for backwards compatibility
  .menu-dropdown,
  .dropdown-menu {
    top: 35px;
    border-width: 1px;
    .border-radius(0 6px 6px 6px);
  }
  // first one for backwards compatibility
  a.menu:after,
  .dropdown-toggle:after {
    border-top-color: #999;
    margin-top: 15px;
    margin-left: 5px;
  }
  // first one for backwards compatibility
  li.open.menu .menu,
  .open.dropdown .dropdown-toggle {
    border-color: #999;
  }
  // first one for backwards compatibility
  li.open a.menu:after,
  .dropdown.open .dropdown-toggle:after {
    border-top-color: #555;
  }
}

// Pills
.pills {
  a {
    margin: 5px 3px 5px 0;
    padding: 0 15px;
    line-height: 30px;
    text-shadow: 0 1px 1px @white;
    .border-radius(15px);
    &:hover {
      color: @white;
      text-decoration: none;
      text-shadow: 0 1px 1px rgba(0,0,0,.25);
      background-color: @linkColorHover;
    }
  }
  .active a {
    color: @white;
    text-shadow: 0 1px 1px rgba(0,0,0,.25);
    background-color: @linkColor;
  }
}

// Stacked pills
.pills-vertical > li {
  float: none;
}

// Tabbable areas
.tab-content,
.pill-content {
}
.tab-content > .tab-pane,
.pill-content > .pill-pane {
  display: none;
}
.tab-content > .active,
.pill-content > .active {
  display: block;
}

// BREADCRUMBS
// -----------

.breadcrumb {
  padding: 7px 14px;
  margin: 0 0 @baseline;
  #gradient > .vertical(#ffffff, #f5f5f5);
  border: 1px solid #ddd;
  .border-radius(3px);
  .box-shadow(inset 0 1px 0 @white);
  li {
    display: inline;
    text-shadow: 0 1px 0 @white;
  }
  .divider {
    padding: 0 5px;
    color: @grayLight;
  }
  .active a {
    color: @grayDark;
  }
}

// PAGE HEADERS
// ------------

.hero-unit {
  background-color: #f5f5f5;
  margin-bottom: 30px;
  padding: 60px;
  .border-radius(6px);
  h1 {
    margin-bottom: 0;
    font-size: 60px;
    line-height: 1;
    letter-spacing: -1px;
  }
  p {
    font-size: 18px;
    font-weight: 200;
    line-height: @baseline * 1.5;
  }
}
footer {
  margin-top: @baseline - 1;
  padding-top: @baseline - 1;
  border-top: 1px solid #eee;
}

// PAGE HEADERS
// ------------

.page-header {
  margin-bottom: @baseline - 1;
  border-bottom: 1px solid #ddd;
  .box-shadow(0 1px 0 rgba(255,255,255,.5));
  h1 {
    margin-bottom: (@baseline / 2) - 1px;
  }
}

// BUTTON STYLES
// -------------

// Shared colors for buttons and alerts
.btn,
.alert-message {
  // Set text color
  &.danger,
  &.danger:hover,
  &.error,
  &.error:hover,
  &.success,
  &.success:hover,
  &.info,
  &.info:hover {
    color: @white
  }
  // Sets the close button to the middle of message
  .close{
    font-family: Arial, sans-serif;
    line-height: 18px;
  }
  // Danger and error appear as red
  &.danger,
  &.error {
    .gradientBar(#ee5f5b, #c43c35);
  }
  // Success appears as green
  &.success {
    .gradientBar(#62c462, #57a957);
  }
  // Info appears as a neutral blue
  &.info {
    .gradientBar(#5bc0de, #339bb9);
  }
}

// Base .btn styles
.btn {
  // Button Base
  cursor: pointer;
  display: inline-block;
  #gradient > .vertical-three-colors(#ffffff, #ffffff, 25%, darken(#ffffff, 10%)); // Don't use .gradientbar() here since it does a three-color gradient
  padding: 5px 14px 6px;
  text-shadow: 0 1px 1px rgba(255,255,255,.75);
  color: #333;
  font-size: @basefont;
  line-height: normal;
  border: 1px solid #ccc;
  border-bottom-color: #bbb;
  .border-radius(4px);
  @shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
  .box-shadow(@shadow);

  &:hover {
    background-position: 0 -15px;
    color: #333;
    text-decoration: none;
  }

  // Focus state for keyboard and accessibility
  &:focus {
    outline: 1px dotted #666;
  }

  // Primary Button Type
  &.primary {
    color: @white;
    .gradientBar(@blue, @blueDark)
  }

   // Transitions
  .transition(.1s linear all);

  // Active and Disabled states
  &.active,
  :active {
    @shadow: inset 0 2px 4px rgba(0,0,0,.25), 0 1px 2px rgba(0,0,0,.05);
    .box-shadow(@shadow);
  }
  &.disabled {
    cursor: default;
    background-image: none;
    .reset-filter();
    .opacity(65);
    .box-shadow(none);
  }
  &[disabled] {
    // disabled pseudo can't be included with .disabled
    // def because IE8 and below will drop it ;_;
    cursor: default;
    background-image: none;
    .reset-filter();
    .opacity(65);
    .box-shadow(none);
  }

  // Button Sizes
  &.large {
    font-size: @basefont + 2px;
    line-height: normal;
    padding: 9px 14px 9px;
    .border-radius(6px);
  }
  &.small {
    padding: 7px 9px 7px;
    font-size: @basefont - 2px;
  }
}
// Super jank hack for removing border-radius from IE9 so we can keep filter gradients on alerts and buttons
:root .alert-message,
:root .btn {
  border-radius: 0 \0;
}

// Help Firefox not be a jerk about adding extra padding to buttons
button.btn,
input[type=submit].btn {
  &::-moz-focus-inner {
      padding: 0;
      border: 0;
  }
}

// CLOSE ICONS
// -----------
.close {
  float: right;
  color: @black;
  font-size: 20px;
  font-weight: bold;
  line-height: @baseline * .75;
  text-shadow: 0 1px 0 rgba(255,255,255,1);
  .opacity(25);
  &:hover {
    color: @black;
    text-decoration: none;
    .opacity(40);
  }
}

// ERROR STYLES
// ------------

// Base alert styles
.alert-message {
  position: relative;
  padding: 7px 15px;
  margin-bottom: @baseline;
  color: @grayDark;
  .gradientBar(#fceec1, #eedc94); // warning by default
  text-shadow: 0 1px 0 rgba(255,255,255,.5);
  border-width: 1px;
  border-style: solid;
  .border-radius(4px);
  .box-shadow(inset 0 1px 0 rgba(255,255,255,.25));

  // Adjust close icon
  .close {
    margin-top: 1px;
    *margin-top: 0; // For IE7
  }

  // Make links same color as text and stand out more
  a {
    font-weight: bold;
    color: @grayDark;
  }
  &.danger p a,
  &.error p a,
  &.success p a,
  &.info p a {
    color: @white;
  }

  // Remove extra margin from content
  h5 {
    line-height: @baseline;
  }
  p {
    margin-bottom: 0;
  }
  div {
    margin-top: 5px;
    margin-bottom: 2px;
    line-height: 28px;
  }
  .btn {
    // Provide actions with buttons
    .box-shadow(0 1px 0 rgba(255,255,255,.25));
  }

  &.block-message {
    background-image: none;
    background-color: lighten(#fceec1, 5%);
    .reset-filter();
    padding: 14px;
    border-color: #fceec1;
    .box-shadow(none);
    ul, p {
      margin-right: 30px;
    }
    ul {
      margin-bottom: 0;
    }
    li {
      color: @grayDark;
    }
    .alert-actions {
      margin-top: 5px;
    }
    &.error,
    &.success,
    &.info {
      color: @grayDark;
      text-shadow: 0 1px 0 rgba(255,255,255,.5);
    }
    &.error {
      background-color: lighten(#f56a66, 25%);
      border-color: lighten(#f56a66, 20%);
    }
    &.success {
      background-color: lighten(#62c462, 30%);
      border-color: lighten(#62c462, 25%);
    }
    &.info {
      background-color: lighten(#6bd0ee, 25%);
      border-color: lighten(#6bd0ee, 20%);
    }
    // Change link color back
    &.danger p a,
    &.error p a,
    &.success p a,
    &.info p a {
      color: @grayDark;
    }

  }
}

// PAGINATION
// ----------

.pagination {
  height: @baseline * 2;
  margin: @baseline 0;
  ul {
    float: left;
    margin: 0;
    border: 1px solid #ddd;
    border: 1px solid rgba(0,0,0,.15);
    .border-radius(3px);
    .box-shadow(0 1px 2px rgba(0,0,0,.05));
  }
  li {
    display: inline;
  }
  a {
    float: left;
    padding: 0 14px;
    line-height: (@baseline * 2) - 2;
    border-right: 1px solid;
    border-right-color: #ddd;
    border-right-color: rgba(0,0,0,.15);
    *border-right-color: #ddd; /* IE6-7 */
    text-decoration: none;
  }
  a:hover,
  .active a {
    background-color: lighten(@blue, 45%);
  }
  .disabled a,
  .disabled a:hover {
    background-color: transparent;
    color: @grayLight;
  }
  .next a {
    border: 0;
  }
}

// WELLS
// -----

.well {
  background-color: #f5f5f5;
  margin-bottom: 20px;
  padding: 19px;
  min-height: 20px;
  border: 1px solid #eee;
  border: 1px solid rgba(0,0,0,.05);
  .border-radius(4px);
  .box-shadow(inset 0 1px 1px rgba(0,0,0,.05));
  blockquote {
    border-color: #ddd;
    border-color: rgba(0,0,0,.15);
  }
}

// MODALS
// ------

.modal-backdrop {
  background-color: @black;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 10000;
  // Fade for backdrop
  &.fade { opacity: 0; }
}

.modal-backdrop,
.modal-backdrop.fade.in {
  .opacity(80);
}

.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  z-index: 11000;
  width: 560px;
  margin: -250px 0 0 -280px;
  background-color: @white;
  border: 1px solid #999;
  border: 1px solid rgba(0,0,0,.3);
  *border: 1px solid #999; /* IE6-7 */
  .border-radius(6px);
  .box-shadow(0 3px 7px rgba(0,0,0,0.3));
  .background-clip(padding-box);
  .close { margin-top: 7px; }
  &.fade {
    .transition(e('opacity .3s linear, top .3s ease-out'));
    top: -25%;
  }
  &.fade.in { top: 50%; }
}
.modal-header {
  border-bottom: 1px solid #eee;
  padding: 5px 15px;
}
.modal-body {
  padding: 15px;
}
.modal-body form {
  margin-bottom: 0;
}
.modal-footer {
  background-color: #f5f5f5;
  padding: 14px 15px 15px;
  border-top: 1px solid #ddd;
  .border-radius(0 0 6px 6px);
  .box-shadow(inset 0 1px 0 @white);
  .clearfix();
  margin-bottom: 0;
  .btn {
    float: right;
    margin-left: 5px;
  }
}

// Fix the stacking of these components when in modals
.modal .popover,
.modal .twipsy {
  z-index: 12000;
}

// POPOVER ARROWS
// --------------

#popoverArrow {
  .above(@arrowWidth: 5px) {
    bottom: 0;
    left: 50%;
    margin-left: -@arrowWidth;
    border-left: @arrowWidth solid transparent;
    border-right: @arrowWidth solid transparent;
    border-top: @arrowWidth solid @black;
  }
  .left(@arrowWidth: 5px) {
    top: 50%;
    right: 0;
    margin-top: -@arrowWidth;
    border-top: @arrowWidth solid transparent;
    border-bottom: @arrowWidth solid transparent;
    border-left: @arrowWidth solid @black;
  }
  .below(@arrowWidth: 5px) {
    top: 0;
    left: 50%;
    margin-left: -@arrowWidth;
    border-left: @arrowWidth solid transparent;
    border-right: @arrowWidth solid transparent;
    border-bottom: @arrowWidth solid @black;
  }
  .right(@arrowWidth: 5px) {
    top: 50%;
    left: 0;
    margin-top: -@arrowWidth;
    border-top: @arrowWidth solid transparent;
    border-bottom: @arrowWidth solid transparent;
    border-right: @arrowWidth solid @black;
  }
}

// TWIPSY
// ------

.twipsy {
  display: block;
  position: absolute;
  visibility: visible;
  padding: 5px;
  font-size: 11px;
  z-index: 1000;
  .opacity(80);
  &.fade.in {
    .opacity(80);
  }
  &.above .twipsy-arrow   { #popoverArrow > .above(); }
  &.left .twipsy-arrow    { #popoverArrow > .left(); }
  &.below .twipsy-arrow   { #popoverArrow > .below(); }
  &.right .twipsy-arrow   { #popoverArrow > .right(); }
}
.twipsy-inner {
  padding: 3px 8px;
  background-color: @black;
  color: white;
  text-align: center;
  max-width: 200px;
  text-decoration: none;
  .border-radius(4px);
}
.twipsy-arrow {
  position: absolute;
  width: 0;
  height: 0;
}

// POPOVERS
// --------

.popover {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1000;
  padding: 5px;
  display: none;
  &.above .arrow { #popoverArrow > .above(); }
  &.right .arrow { #popoverArrow > .right(); }
  &.below .arrow { #popoverArrow > .below(); }
  &.left .arrow  { #popoverArrow > .left(); }
  .arrow {
    position: absolute;
    width: 0;
    height: 0;
  }
  .inner {
    background: @black;
    background: rgba(0,0,0,.8);
    padding: 3px;
    overflow: hidden;
    width: 280px;
    .border-radius(6px);
    .box-shadow(0 3px 7px rgba(0,0,0,0.3));
  }
  .title {
    background-color: #f5f5f5;
    padding: 9px 15px;
    line-height: 1;
    .border-radius(3px 3px 0 0);
    border-bottom:1px solid #eee;
  }
  .content {
    background-color: @white;
    padding: 14px;
    .border-radius(0 0 3px 3px);
    .background-clip(padding-box);
    p, ul, ol {
      margin-bottom: 0;
    }
  }
}

// PATTERN ANIMATIONS
// ------------------

.fade {
  .transition(opacity .15s linear);
  opacity: 0;
  &.in {
    opacity: 1;
  }
}

// LABELS
// ------

.label {
  padding: 1px 3px 2px;
  font-size: @basefont * .75;
  font-weight: bold;
  color: @white;
  text-transform: uppercase;
  white-space: nowrap;
  background-color: @grayLight;
  .border-radius(3px);
  &.important { background-color: #c43c35; }
  &.warning   { background-color: @orange; }
  &.success   { background-color: @green; }
  &.notice    { background-color: lighten(@blue, 25%); }
}

// MEDIA GRIDS
// -----------

.media-grid {
  margin-left: -@gridGutterWidth;
  margin-bottom: 0;
  .clearfix();
  li {
    display: inline;
  }
  a {
    float: left;
    padding: 4px;
    margin: 0 0 @baseline @gridGutterWidth;
    border: 1px solid #ddd;
    .border-radius(4px);
    .box-shadow(0 1px 1px rgba(0,0,0,.075));
    img {
      display: block;
    }
    &:hover {
      border-color: @linkColor;
      .box-shadow(0 1px 4px rgba(0,105,214,.25));
    }
  }
}


/*
 * Copyright 2011 Adobe Systems Incorporated. All Rights Reserved.
 */

/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */
/*global define: false, describe: false, it: false, expect: false, beforeEach: false, afterEach: false, waitsFor: false, runs: false, $: false, CodeMirror: false */

define(function (require, exports, module) {
    'use strict';

    var Editor          = require("editor/Editor").Editor,
        SpecRunnerUtils = require("./SpecRunnerUtils.js"),
        EditorUtils     = require("editor/EditorUtils");

    describe("Editor", function () {
        var content = 'Brackets is going to be awesome!\n';

        var myDocument, myEditor;
        beforeEach(function () {
            // create dummy Document for the Editor
            myDocument = SpecRunnerUtils.createMockDocument(content);

            // create Editor instance (containing a CodeMirror instance)
            $("body").append("<div id='editor'/>");
            myEditor = new Editor(myDocument, true, "", $("#editor").get(0), {});
        });

        afterEach(function () {
            myEditor.destroy();
            myEditor = null;
            $("#editor").remove();
            myDocument = null;
        });

        describe("Editor wrapper", function () {

            it("should initialize with content", function () {
                // verify editor content
                expect(myEditor._getText()).toEqual(content);
            });

            // FUTURE: this should really be in a Document unit test, but there's no "official"
            // way to create the model for a Document without manually creating an Editor, so we're
            // testing this here for now until we get a real central model.
            it("should trigger a synchronous Document change event when an edit is performed", function () {
                var changeFired = false;
                function changeHandler(event, doc, changeList) {
                    $(myDocument).off("change", changeHandler);
                    changeFired = true;
                    expect(doc).toBe(myDocument);
                    expect(changeList.from).toEqual({line: 0, ch: 0});
                    expect(changeList.to).toEqual({line: 1, ch: 0});
                    expect(changeList.text).toEqual(["new content"]);
                    expect(changeList.next).toBe(undefined);
                }
                $(myDocument).on("change", changeHandler);
                myEditor._setText("new content");
                expect(changeFired).toBe(true);
            });

        });

        describe("File extension to mode mapping", function () {

            it("should switch to the HTML mode for files ending in .html", function () {
                // verify editor content
                var mode = EditorUtils.getModeFromFileExtension("file:///only/testing/the/path.html");
                expect(mode).toEqual("htmlmixed");
            });

            it("should switch modes even if the url has a query string", function () {
                // verify editor content
                var mode = EditorUtils.getModeFromFileExtension("http://only.org/testing/the/path.css?v=2");
                expect(mode).toEqual("css");
            });

            it("should accecpt just a file name too", function () {
                // verify editor content
                var mode = EditorUtils.getModeFromFileExtension("path.js");
                expect(mode).toEqual("javascript");
            });

            it("should default to plaintext for unknown file extensions", function () {
                // verify editor content
                var mode = EditorUtils.getModeFromFileExtension("test.foo");
                expect(mode).toEqual("");
            });
        });

        describe("Focus", function () {

            it("should not have focus until explicitly set", function () {
                expect(myEditor.hasFocus()).toBe(false);
            });

            it("should be able to detect when it has focus", function () {
                myEditor.focus();
                expect(myEditor.hasFocus()).toBe(true);
            });
        });
    });
});


/*
 * Copyright 2012 Adobe Systems Incorporated. All Rights Reserved.
 * @author Jonathan Diehl <jdiehl@adobe.com>
 */

/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */
/*global define, $ */

/**
 * NetworkAgent tracks all resources loaded by the remote debugger. Use
 * `wasURLRequested(url)` to query whether a resource was loaded.
 */
define(function NetworkAgent(require, exports, module) {
    'use strict';

    var Inspector = require("LiveDevelopment/Inspector/Inspector");

    var _urlRequested; // url -> request info

    /** Return the URL without the query string
     * @param {string} URL
     */
    function _urlWithoutQueryString(url) {
        var index = url.search(/[#\?]/);
        if (index >= 0) {
            url = url.substr(0, index);
        }
        return url;
    }

    /** Return the resource information for a given URL
     * @param {string} url
     */
    function wasURLRequested(url) {
        return _urlRequested[url];
    }

    // WebInspector Event: Network.requestWillBeSent
    function _onRequestWillBeSent(res) {
        // res = {requestId, frameId, loaderId, documentURL, request, timestamp, initiator, stackTrace, redirectResponse}
        var url = _urlWithoutQueryString(res.request.url);
        _urlRequested[url] = true;
    }

    /** Initialize the agent */
    function load() {
        _urlRequested = {};
        Inspector.Network.enable();
        Inspector.on("Network.requestWillBeSent", _onRequestWillBeSent);
    }

    /** Unload the agent */
    function unload() {
        Inspector.off("Network.requestWillBeSent", _onRequestWillBeSent);
    }

    // Export public functions
    exports.wasURLRequested = wasURLRequested;
    exports.load = load;
    exports.unload = unload;
});


var widgetHeight = Math.max(this.$relatedContainer.height(), this.$editorsDiv.height());


/*
 * Copyright 2012 Adobe Systems Incorporated. All Rights Reserved.
 * @author Jonathan Diehl <jdiehl@adobe.com>
 */

/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */
/*global define, $ */

/**
 * RemoteFunctions define the functions to be executed in the browser. This
 * modules should define a single function that returns an object of all
 * exported functions.
 */
function RemoteFunctions() {
    'use strict';

    var _body = document.getElementsByTagName("body")[0]; // the document body
    var _sourceHighlight; // the highlighted element in the source
    var _highlight = []; // the highlighted elements
    var _dialog; // the active dialog
    var _target; // the active target element of a dialog
    var _editor; // the editor
    var _editText; // the original editor text

    // prevent an event
    function _preventEventWhenMeta(event) {
        if (event.metaKey) {
            event.preventDefault();
            event.stopPropagation();
        }
    }

    // compute the screen offset of an element
    function _screenOffset(element, key) {
        var value = 0;
        while (element) {
            value += element[key];
            element = element.offsetParent;
        }
        return value;
    }

    // hide dialog
    function _hideDialog() {
        if (_dialog) {
            var dialog = _dialog;
            _dialog = undefined;
            _body.removeChild(dialog);
        }
    }

    // show dialog
    function _createDialog(element, drawBackground) {
        _hideDialog();

        // compute the position on screen
        var x = _screenOffset(element, "offsetLeft");
        var y = _screenOffset(element, "offsetTop") + element.offsetHeight;
        var width = element.offsetWidth;

        // create the container
        _dialog = document.createElement("div");
        _dialog.style.setProperty("z-index", 2147483647);
        _dialog.style.setProperty("position", "absolute");
        _dialog.style.setProperty("left", x + "px");
        _dialog.style.setProperty("top", y + "px");
        _dialog.style.setProperty("font-size", "11pt");
        if (drawBackground) {
            _dialog.style.setProperty("background", "#fff");
            _dialog.style.setProperty("border", "1px solid #888");
            _dialog.style.setProperty("-webkit-box-shadow", "2px 2px 6px 0px #ccc");
            _dialog.style.setProperty("border-radius", "6px");
            _dialog.style.setProperty("padding", "6px");
        }

        return _dialog;
    }

    // row background color
    function _backgroundColorForType(type, highlight) {
        switch (type) {
        case "html":
            return highlight ? "#eec" : "#ffe";
        case "css":
            return highlight ? "#cee" : "#eff";
        case "js":
            return highlight ? "#ccf" : "#eef";
        }
    }

    // set an event on a element
    function _trigger(element, name, value, autoRemove) {
        var key = "data-ld-" + name;
        if (value !== undefined && value !== null) {
            element.setAttribute(key, value);
            if (autoRemove) {
                setTimeout(element.removeAttribute.bind(element, key));
            }
        } else {
            element.removeAttribute(key);
        }
    }

    // low level node highlighting
    function _highlightNode(info) {
        if (!(info instanceof Array)) {
            info = [info, "#ecc"];
        }
        var prevColor = info[0].style.getPropertyValue("background-color");
        info[0].style.setProperty("background-color", info[1]);
        return [info[0], prevColor];
    }

    // editor key press
    function _onEditKeyUp(event) {
        switch (event.which) {
        case 13:
            // return
            _editor.blur();
            break;
        case 27:
            // esc
            _trigger(_target, "edit", _editText, true);
            _editor.blur();
            break;
        case 18:
            // nothing
            break;
        default:
            _trigger(_target, "edit", _editor.value);
        }
    }

    // open the editor
    function _showEdit(text) {
        _editText = text;
        var dialog = _createDialog(_target);
        _editor = document.createElement("input");
        _editor.setAttribute("value", text);
        _editor.setAttribute("width", _target.attributes.width);
        _editor.addEventListener("keyup", _onEditKeyUp);
        _editor.addEventListener("blur", _hideDialog);
        dialog.appendChild(_editor);
        _body.appendChild(dialog);
        _editor.focus();
    }

    /** Event Handlers ***********************************************************/

    // remove highlight on release of the meta key
    function _onKeyUp(event) {
        if (!event.metaKey && _sourceHighlight) {
            _trigger(_sourceHighlight[0], "highlight", 0, true);
            _highlightNode(_sourceHighlight);
            _sourceHighlight = undefined;
        }
    }

    // trigger highlight on mouse move
    function _onMouse(event) {
        if (!event.metaKey || event.type !== "mousemove" || !event.toElement) {
            if (_sourceHighlight) {
                _trigger(_sourceHighlight[0], "highlight", 0, true);
                _highlightNode(_sourceHighlight);
                _sourceHighlight = undefined;
            }
            return;
        }
        if (_sourceHighlight) {
            if (_sourceHighlight[0] === event.toElement) {
                return;
            }
            _trigger(_sourceHighlight[0], "highlight");
            _highlightNode(_sourceHighlight);
        }
        _trigger(event.toElement, "highlight", 1);
        _sourceHighlight = _highlightNode(event.toElement);
    }

    // trigger goto on alt-click
    function _onClick(event) {
        _hideDialog();
        if (event.metaKey && event.toElement) {
            event.preventDefault();
            event.stopPropagation();
            _target = event.toElement;
            _trigger(_target, "showgoto", 1, true);
        } else if (event.altKey && event.toElement && event.toElement.childNodes.length === 1) {
            event.preventDefault();
            event.stopPropagation();
            _target = event.toElement;
            _showEdit(_target.childNodes[0].nodeValue);
        }
    }

    // Goto dialog highlighting
    function _onGotoMouse(row, type, event) {
        if (event.type === "mouseover") {
            row.style.setProperty("background", _backgroundColorForType(type, true));
        } else {
            row.style.setProperty("background", _backgroundColorForType(type));
        }
    }

    // Goto dialog click
    function _onGotoClick(url, event) {
        _hideDialog();
        _trigger(_target, "goto", url, true);
    }

    /** Public Commands **********************************************************/

    // show goto
    function showGoto(targets) {
        var dialog = _createDialog(_target, true);
        var i, target, row, file;
        for (i in targets) {
            target = targets[i];
            row = document.createElement("div");
            row.style.setProperty("padding", "6px 12px");
            row.style.setProperty("display", "block");
            row.style.setProperty("border-bottom", "1px solid #ccc");
            row.style.setProperty("cursor", "pointer");
            row.style.setProperty("background", _backgroundColorForType(target.type));
            row.innerHTML = target.name;
            row.addEventListener("click", _onGotoClick.bind(undefined, target.url));
            row.addEventListener("mouseover", _onGotoMouse.bind(undefined, row, target.type));
            row.addEventListener("mouseout", _onGotoMouse.bind(undefined, row, target.type));
            if (target.file) {
                file = document.createElement("i");
                file.style.setProperty("float", "right");
                file.style.setProperty("margin-left", "12px");
                file.innerHTML = " " + target.file;
                row.appendChild(file);
            }
            dialog.appendChild(row);
        }
        _body.appendChild(dialog);
    }

    // remove active highlights
    function hideHighlight() {
        if (_highlight.length === 0) {
            return;
        }
        var i;
        for (i in _highlight) {
            _highlightNode(_highlight[i]);
        }
        _highlight = [];
    }

    // highlight a node
    function highlight(node) {
        _highlight.push(_highlightNode(node));
    }

    // highlight a rule
    function highlightRule(rule) {
        hideHighlight();
        var i, nodes = document.querySelectorAll(rule);
        for (i = 0; i < nodes.length; i++) {
            highlight(nodes[i]);
        }
    }

    // insert a node
    function insertNode(payload, parent, index) {
        var node;
        if (payload.value) {
            node = document.createTextNode(payload.value);
        } else {
            node = document.createElement(payload.nodeName);
        }
        var sibling;
        if (index !== undefined) {
            sibling = parent.childNodes[index];
        }
        if (sibling) {
            parent.insertBefore(node, sibling);
        } else {
            parent.appendChild(node);
        }
        return node;
    }

    // log something (for testing)
    function log() {
        var i;
        for (i in arguments) {
            console.info(arguments[i]);
        }
    }

    // install event listeners

    /* FUTURE
    document.addEventListener("keyup", _onKeyUp);
    document.addEventListener("mousemove", _onMouse);
    document.addEventListener("mouseout", _onMouse);
    document.addEventListener("mousedown", _preventEventWhenMeta, true);
    document.addEventListener("mouseup", _preventEventWhenMeta, true);
    document.addEventListener("click", _onClick, true);
    */

    return {
        "showGoto": showGoto,
        "hideHighlight": hideHighlight,
        "highlight": highlight,
        "highlightRule": highlightRule,
        "insertNode": insertNode,
        "log": log
    };
}


/*
 * Copyright 2011 Adobe Systems Incorporated. All Rights Reserved.
 */

/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */
/*global define: false, $: true  */

/**
 * WorkingSetView generates the UI for the list of the files user is editing based on the model provided by EditorManager.
 * The UI allows the user to see what files are open/dirty and allows them to close files and specify the current editor.
 */
define(function (require, exports, module) {
    'use strict';

    // Load dependent modules
    var DocumentManager       = require("document/DocumentManager"),
        CommandManager        = require("command/CommandManager"),
        Commands              = require("command/Commands"),
        EditorManager         = require("editor/EditorManager"),
        FileViewController    = require("project/FileViewController"),
        NativeFileSystem      = require("file/NativeFileSystem").NativeFileSystem,
        ViewUtils             = require("utils/ViewUtils");

    /** Each list item in the working set stores a references to the related document in the list item's data.
     *  Use listItem.data(_FILE_KEY) to get the document reference
     */
    var _FILE_KEY = "file";

    function _hideShowOpenFileHeader() {
        if (DocumentManager.getWorkingSet().length === 0) {
            $("#open-files-header").hide();
            $("#open-files-container").hide();
            $("#open-files-divider").hide();
        } else {
            $("#open-files-header").show();
            $("#open-files-container").show();
            $("#open-files-divider").show();
        }

        ViewUtils.updateChildrenToParentScrollwidth($("#open-files-container"));
    }

    /**
     * Updates the appearance of the list element based on the parameters provided
     * @private
     * @param {!HTMLLIElement} listElement
     * @param {bool} isDirty
     * @param {bool} canClose
     */
    function _updateFileStatusIcon(listElement, isDirty, canClose) {
        var fileStatusIcon = listElement.find(".file-status-icon");
        var showIcon = isDirty || canClose;

        // remove icon if its not needed
        if (!showIcon && fileStatusIcon.length !== 0) {
            fileStatusIcon.remove();
            fileStatusIcon = null;

        // create icon if its needed and doesn't exist
        } else if (showIcon && fileStatusIcon.length === 0) {

            fileStatusIcon = $("

")
                .prependTo(listElement)
                .click(function () {
                    // Clicking the "X" button is equivalent to File > Close; it doesn't merely
                    // remove a file from the working set
                    var file = listElement.data(_FILE_KEY);
                    CommandManager.execute(Commands.FILE_CLOSE, {file: file});
                });
        }

        // Set icon's class
        if (fileStatusIcon) {
            // cast to Boolean needed because toggleClass() distinguishes true/false from truthy/falsy
            fileStatusIcon.toggleClass("dirty", Boolean(isDirty));
            fileStatusIcon.toggleClass("canClose", Boolean(canClose));
        }
    }

    /**
     * Updates the appearance of the list element based on the parameters provided.
     * @private
     * @param {!HTMLLIElement} listElement
     * @param {?Document} selectedDoc
     */
    function _updateListItemSelection(listItem, selectedDoc) {
        var shouldBeSelected = (selectedDoc && $(listItem).data(_FILE_KEY).fullPath === selectedDoc.file.fullPath);

        // cast to Boolean needed because toggleClass() distinguishes true/false from truthy/falsy
        $(listItem).toggleClass("selected", Boolean(shouldBeSelected));
    }

    function isOpenAndDirty(file) {
        var docIfOpen = DocumentManager.getOpenDocumentForPath(file.fullPath);
        return (docIfOpen && docIfOpen.isDirty);
    }

    /**
     * Builds the UI for a new list item and inserts in into the end of the list
     * @private
     * @param {FileEntry} file
     * @return {HTMLLIElement} newListItem
     */
    function _createNewListItem(file) {
        var curDoc = DocumentManager.getCurrentDocument();

        // Create new list item with a link
        var link = $("").text(file.name);
        var newItem = $("

")
            .append(link)
            .data(_FILE_KEY, file);

        $("#open-files-container > ul").append(newItem);

        // working set item might never have been opened; if so, then it's definitely not dirty

        // Update the listItem's apperance
        _updateFileStatusIcon(newItem, isOpenAndDirty(file), false);
        _updateListItemSelection(newItem, curDoc);

        newItem.click(function () {
            FileViewController.openAndSelectDocument(file.fullPath, FileViewController.WORKING_SET_VIEW);
        });

        newItem.hover(
            function () {
                _updateFileStatusIcon($(this), isOpenAndDirty(file), true);
            },
            function () {
                _updateFileStatusIcon($(this), isOpenAndDirty(file), false);
            }
        );
    }

    /**
     * Deletes all the list items in the view and rebuilds them from the working set model
     * @private
     */
    function _rebuildWorkingSet() {
        $("#open-files-container > ul").empty();

        DocumentManager.getWorkingSet().forEach(function (file) {
            _createNewListItem(file);
        });

        _hideShowOpenFileHeader();
    }

    /**
     * @private
     */
    function _updateListSelection() {
        var doc;
        if (FileViewController.getFileSelectionFocus() === FileViewController.WORKING_SET_VIEW) {
            doc = DocumentManager.getCurrentDocument();
        } else {
            doc = null;
        }

        // Iterate through working set list and update the selection on each
        var items = $("#open-files-container > ul").children().each(function () {
            _updateListItemSelection(this, doc);
        });
    }

    /**
     * @private
     */
    function _handleFileAdded(file) {
        _createNewListItem(file);
        _hideShowOpenFileHeader();
    }

    /**
     * @private
     */
    function _handleDocumentSelectionChange() {
        _updateListSelection();
    }

    /**
     * Finds the listItem item assocated with the file. Returns null if not found.
     * @private
     * @param {!FileEntry} file
     * @return {HTMLLIItem}
     */
    function _findListItemFromFile(file) {
        var result = null;

        if (file) {
            var items = $("#open-files-container > ul").children();
            items.each(function () {
                var listItem = $(this);
                if (listItem.data(_FILE_KEY).fullPath === file.fullPath) {
                    result = listItem;
                    return false;
                    // breaks each
                }
            });
        }

        return result;
    }

    /**
     * @private
     * @param {FileEntry} file
     */
    function _handleFileRemoved(file) {
        var listItem = _findListItemFromFile(file);
        if (listItem) {
            listItem.remove();
        }

        _hideShowOpenFileHeader();
    }

    /**
     * @private
     * @param {Document} doc
     */
    function _handleDirtyFlagChanged(doc) {
        var listItem = _findListItemFromFile(doc.file);
        if (listItem) {
            var canClose = $(listItem).find("canClose").length === 1;
            _updateFileStatusIcon(listItem, doc.isDirty, canClose);
        }

    }

    // Initialize: register listeners
    $(DocumentManager).on("workingSetAdd", function (event, addedFile) {
        //console.log("Working set ++ " + addedFile);
        //console.log("  set: " + DocumentManager.getWorkingSet().join());
        _handleFileAdded(addedFile);
    });

    $(DocumentManager).on("workingSetRemove", function (event, removedFile) {
        //console.log("Working set -- " + removedFile);
        //console.log("  set: " + DocumentManager.getWorkingSet().join());
        _handleFileRemoved(removedFile);
    });

    $(DocumentManager).on("dirtyFlagChange", function (event, doc) {
        //console.log("Dirty flag change: " + doc);
        _handleDirtyFlagChanged(doc);
    });

    $(FileViewController).on("documentSelectionFocusChange", function (event, eventTarget) {
        _handleDocumentSelectionChange();
    });

    _hideShowOpenFileHeader();

});