<commit_before>"use strict";

/**************************
 * Import important stuff *
 **************************/

const R = require("ramda");
const From = require("./From");
const MessageMap = require("../MessageMap");
const { sleepOneMinute } = require("../sleep");
const helpers = require("./helpers");

/***********
 * Helpers *
 ***********/

/**
 * Makes an endware function be handled by all bridges it applies to. Curried
 *
 * @param {Function} func	The message handler to wrap
 * @param {Context} ctx	The Telegraf context
 *
 * @returns {undefined}
 *
 * @private
 */
const createMessageHandler = R.curry((func, ctx) => {
	// Wait for the Discord bot to become ready
	ctx.TediCross.dcBot.ready.then(() =>
		R.forEach(bridge => func(ctx, bridge))(ctx.tediCross.bridges)
	);
});

/*************************
 * The endware functions *
 *************************/

/**
 * Replies to a message with info about the chat
 *
 * @param {Object} ctx	The Telegraf context
 * @param {Object} ctx.tediCross	The TediCross object on the context
 * @param {Object} ctx.tediCross.message	The message to reply to
 * @param {Object} ctx.tediCross.message.chat	The object of the chat the message is from
 * @param {Integer} ctx.tediCross.message.chat.id	ID of the chat the message is from
 *
 * @returns {undefined}
 */
const chatinfo = ctx => {
	// Reply with the info
	ctx.reply(`chatID: ${ctx.tediCross.message.chat.id}`)
		// Wait some time
		.then(sleepOneMinute)
		// Delete the info and the command
		.then(message => Promise.all([
			// Delete the info
			helpers.deleteMessage(ctx, message),
			// Delete the command
			ctx.deleteMessage()
		]))
		.catch(helpers.ignoreAlreadyDeletedError);
};

/**
 * Handles users joining chats
 *
 * @param {Object} ctx	The Telegraf context
 * @param {Object} ctx.tediCross.message	The Telegram message received
 * @param {Object} ctx.tediCross.message	The Telegram message received
 * @param {Object} ctx.tediCross.message.new_chat_members	List of the users who joined the chat
 * @param {Object} ctx.TediCross	The global TediCross context of the message
 *
 * @returns {undefined}
 */
const newChatMembers = createMessageHandler((ctx, bridge) =>
	// Notify Discord about each user
	R.forEach(user => {
		// Make the text to send
		const from = From.createFromObjFromUser(user);
		const text = `**${from.firstName} (${R.defaultTo("No username", from.username)})** joined the Telegram side of the chat`;

		// Pass it on
		helpers.getDiscordChannel(ctx, bridge)
			.send(text);
	})(ctx.tediCross.message.new_chat_members)
);

/**
 * Handles users leaving chats
 *
 * @param {Object} ctx	The Telegraf context
 * @param {Object} ctx.tediCross	The TediCross context of the message
 * @param {Object} ctx.tediCross.message	The Telegram message received
 * @param {Object} ctx.tediCross.message.left_chat_member	The user object of the user who left
 * @param {Object} ctx.TediCross	The global TediCross context of the message
 *
 * @returns {undefined}
 */
const leftChatMember = createMessageHandler((ctx, bridge) => {
	// Make the text to send
	const from = From.createFromObjFromUser(ctx.tediCross.message.left_chat_member);
	const text = `**${from.firstName} (${R.defaultTo("No username", from.username)})** left the Telegram side of the chat`;

	// Pass it on
	helpers.getDiscordChannel(ctx, bridge)
		.send(text);
});

/**
 * Relays a message from Telegram to Discord
 *
 * @param {Object} ctx	The Telegraf context
 * @param {Object} ctx.tediCross	The TediCross context of the message
 * @param {Object} ctx.TediCross	The global TediCross context of the message
 *
 * @returns {undefined}
 */
const relayMessage = ctx =>
	R.forEach(async prepared => {
		// Get the channel to send to
		const channel = helpers.getDiscordChannel(ctx, prepared.bridge);

		// Discord doesn't handle messages longer than 2000 characters. Split it up into chunks that big
		const messageText = prepared.header + "\n" + prepared.text;
		const chunks = R.splitEvery(2000, messageText);

		// Send them in serial, with the attachment first, if there is one
		let dcMessage = await channel.send(R.head(chunks), { file: prepared.file });
		if (R.length(chunks) > 1) {
			dcMessage = await R.reduce((p, chunk) => p.then(() => channel.send(chunk)), Promise.resolve(), R.tail(chunks));
		}

		// Make the mapping so future edits can work XXX Only the last chunk is considered
		ctx.TediCross.messageMap.insert(MessageMap.TELEGRAM_TO_DISCORD, prepared.bridge, ctx.tediCross.messageId, dcMessage.id);
	})(ctx.tediCross.prepared);

/**
 * Handles message edits
 *
 * @param {Object} ctx	The Telegraf context
 *
 * @returns {undefined}
 */
const handleEdits = createMessageHandler(async (ctx, bridge) => {
	try {
		const tgMessage = ctx.tediCross.message;

		// Find the ID of this message on Discord
		const [dcMessageId] = ctx.TediCross.messageMap.getCorresponding(MessageMap.TELEGRAM_TO_DISCORD, bridge, tgMessage.message_id);

		// Get the messages from Discord
		const dcMessage = await helpers.getDiscordChannel(ctx, bridge)
			.fetchMessage(dcMessageId);

		R.forEach(async prepared => {
			// Discord doesn't handle messages longer than 2000 characters. Take only the first 2000
			const messageText = R.slice(0, 2000, prepared.header + "\n" + prepared.text);

			// Send them in serial, with the attachment first, if there is one
			await dcMessage.edit(messageText, { attachment: prepared.attachment });
		})(ctx.tediCross.prepared);
	} catch (err) {
		// Log it
		ctx.TediCross.logger.error(`[${bridge.name}] Could not edit Discord message:`, err);
	}
});

/***************
 * Export them *
 ***************/

module.exports = {
	chatinfo,
	newChatMembers,
	leftChatMember,
	relayMessage,
	handleEdits
};
<commit_msg>Telegram side now waits for the Discord bot to become ready<commit_after>"use strict";

/**************************
 * Import important stuff *
 **************************/

const R = require("ramda");
const From = require("./From");
const MessageMap = require("../MessageMap");
const { sleepOneMinute } = require("../sleep");
const helpers = require("./helpers");

/***********
 * Helpers *
 ***********/

/**
 * Makes an endware function be handled by all bridges it applies to. Curried
 *
 * @param {Function} func	The message handler to wrap
 * @param {Context} ctx	The Telegraf context
 *
 * @returns {undefined}
 *
 * @private
 */
const createMessageHandler = R.curry((func, ctx) => {
	// Wait for the Discord bot to become ready
	ctx.TediCross.dcBot.ready.then(() =>
		R.forEach(bridge => func(ctx, bridge))(ctx.tediCross.bridges)
	);
});

/*************************
 * The endware functions *
 *************************/

/**
 * Replies to a message with info about the chat
 *
 * @param {Object} ctx	The Telegraf context
 * @param {Object} ctx.tediCross	The TediCross object on the context
 * @param {Object} ctx.tediCross.message	The message to reply to
 * @param {Object} ctx.tediCross.message.chat	The object of the chat the message is from
 * @param {Integer} ctx.tediCross.message.chat.id	ID of the chat the message is from
 *
 * @returns {undefined}
 */
const chatinfo = ctx => {
	// Reply with the info
	ctx.reply(`chatID: ${ctx.tediCross.message.chat.id}`)
		// Wait some time
		.then(sleepOneMinute)
		// Delete the info and the command
		.then(message => Promise.all([
			// Delete the info
			helpers.deleteMessage(ctx, message),
			// Delete the command
			ctx.deleteMessage()
		]))
		.catch(helpers.ignoreAlreadyDeletedError);
};

/**
 * Handles users joining chats
 *
 * @param {Object} ctx	The Telegraf context
 * @param {Object} ctx.tediCross.message	The Telegram message received
 * @param {Object} ctx.tediCross.message	The Telegram message received
 * @param {Object} ctx.tediCross.message.new_chat_members	List of the users who joined the chat
 * @param {Object} ctx.TediCross	The global TediCross context of the message
 *
 * @returns {undefined}
 */
const newChatMembers = createMessageHandler((ctx, bridge) =>
	// Notify Discord about each user
	R.forEach(user => {
		// Make the text to send
		const from = From.createFromObjFromUser(user);
		const text = `**${from.firstName} (${R.defaultTo("No username", from.username)})** joined the Telegram side of the chat`;

		// Pass it on
		ctx.TediCross.dcBot.ready.then(() =>
			helpers.getDiscordChannel(ctx, bridge)
				.send(text)
		);
	})(ctx.tediCross.message.new_chat_members)
);

/**
 * Handles users leaving chats
 *
 * @param {Object} ctx	The Telegraf context
 * @param {Object} ctx.tediCross	The TediCross context of the message
 * @param {Object} ctx.tediCross.message	The Telegram message received
 * @param {Object} ctx.tediCross.message.left_chat_member	The user object of the user who left
 * @param {Object} ctx.TediCross	The global TediCross context of the message
 *
 * @returns {undefined}
 */
const leftChatMember = createMessageHandler((ctx, bridge) => {
	// Make the text to send
	const from = From.createFromObjFromUser(ctx.tediCross.message.left_chat_member);
	const text = `**${from.firstName} (${R.defaultTo("No username", from.username)})** left the Telegram side of the chat`;

	// Pass it on
	ctx.TediCross.dcBot.ready.then(() =>
		helpers.getDiscordChannel(ctx, bridge)
			.send(text)
		);
});

/**
 * Relays a message from Telegram to Discord
 *
 * @param {Object} ctx	The Telegraf context
 * @param {Object} ctx.tediCross	The TediCross context of the message
 * @param {Object} ctx.TediCross	The global TediCross context of the message
 *
 * @returns {undefined}
 */
const relayMessage = ctx =>
	R.forEach(async prepared => {
		// Discord doesn't handle messages longer than 2000 characters. Split it up into chunks that big
		const messageText = prepared.header + "\n" + prepared.text;
		const chunks = R.splitEvery(2000, messageText);

		// Wait for the Discord bot to become ready
		await ctx.TediCross.dcBot.ready;

		// Get the channel to send to
		const channel = helpers.getDiscordChannel(ctx, prepared.bridge);

		// Send them in serial, with the attachment first, if there is one
		let dcMessage = await channel.send(R.head(chunks), { file: prepared.file });
		if (R.length(chunks) > 1) {
			dcMessage = await R.reduce((p, chunk) => p.then(() => channel.send(chunk)), Promise.resolve(), R.tail(chunks));
		}

		// Make the mapping so future edits can work XXX Only the last chunk is considered
		ctx.TediCross.messageMap.insert(MessageMap.TELEGRAM_TO_DISCORD, prepared.bridge, ctx.tediCross.messageId, dcMessage.id);
	})(ctx.tediCross.prepared);

/**
 * Handles message edits
 *
 * @param {Object} ctx	The Telegraf context
 *
 * @returns {undefined}
 */
const handleEdits = createMessageHandler(async (ctx, bridge) => {
	try {
		const tgMessage = ctx.tediCross.message;

		// Find the ID of this message on Discord
		const [dcMessageId] = ctx.TediCross.messageMap.getCorresponding(MessageMap.TELEGRAM_TO_DISCORD, bridge, tgMessage.message_id);

		// Wait for the Discord bot to become ready
		await ctx.TediCross.dcBot.ready;

		// Get the messages from Discord
		const dcMessage = await helpers.getDiscordChannel(ctx, bridge)
			.fetchMessage(dcMessageId);

		R.forEach(async prepared => {
			// Discord doesn't handle messages longer than 2000 characters. Take only the first 2000
			const messageText = R.slice(0, 2000, prepared.header + "\n" + prepared.text);

			// Send them in serial, with the attachment first, if there is one
			await dcMessage.edit(messageText, { attachment: prepared.attachment });
		})(ctx.tediCross.prepared);
	} catch (err) {
		// Log it
		ctx.TediCross.logger.error(`[${bridge.name}] Could not edit Discord message:`, err);
	}
});

/***************
 * Export them *
 ***************/

module.exports = {
	chatinfo,
	newChatMembers,
	leftChatMember,
	relayMessage,
	handleEdits
};
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// @flow

// We want to test these components in isolation and tightly control the actions
// dispatched and avoid any side-effects.  That's why we mock this module and
// return dummy thunk actions that return a Promise.
jest.mock('../../actions/receive-profile', () => ({
  // These mocks will get their implementation in the `setup` function.
  // Otherwise the implementation is wiped before the test starts.
  // See https://github.com/facebook/jest/issues/7573 for more info.
  retrieveProfileFromAddon: jest.fn(),
  retrieveProfileFromStore: jest.fn(),
}));

// We mock <ProfileViewer> because it's complex and we should really test it
// elsewhere. Using a custom name makes it possible to test that  _this_
// component is used.
jest.mock('../../components/app/ProfileViewer', () => 'profile-viewer');
// We mock <Home> as well because it brings too much noise in snapshots and it's
// overly tested in another test file.
jest.mock('../../components/app/Home', () => 'home');

import * as React from 'react';
import { Provider } from 'react-redux';
import { render, cleanup } from 'react-testing-library';

import { ProfileViewWhenReady } from '../../components/app/Root';
import { updateUrlState } from '../../actions/app';
// Because this module is mocked but we want the real actions in the test, we
// use `jest.requireActual` here.
const {
  fatalError,
  temporaryError,
  viewProfile,
  waitingForProfileFromAddon,
  waitingForProfileFromStore,
} = jest.requireActual('../../actions/receive-profile');
// These functions are mocks
import {
  retrieveProfileFromStore,
  retrieveProfileFromAddon,
} from '../../actions/receive-profile';
import { stateFromLocation } from '../../app-logic/url-handling';
import { TemporaryError } from '../../utils/errors';

import { blankStore } from '../fixtures/stores';
import { getProfileFromTextSamples } from '../fixtures/profiles/make-profile';

afterEach(cleanup);
describe('app/ProfileViewWhenReady', function() {
  it('renders an initial home', function() {
    const { container } = setup();

    expect(container.firstChild).toMatchSnapshot();
  });

  it('renders the addon loading page, and the profile view after capturing', function() {
    const { container, dispatch, navigateToAddonLoadingPage } = setup();

    navigateToAddonLoadingPage();
    dispatch(waitingForProfileFromAddon());
    expect(container.firstChild).toMatchSnapshot();
    expect(retrieveProfileFromAddon).toBeCalled();

    const { profile } = getProfileFromTextSamples(`A`);
    dispatch(viewProfile(profile));
    expect(container.firstChild).toMatchSnapshot();
  });

  it('does not try to retrieve a profile when moving from from-addon to public', function() {
    const {
      container,
      dispatch,
      navigateToAddonLoadingPage,
      navigateToStoreLoadingPage,
    } = setup();

    navigateToAddonLoadingPage();
    dispatch(waitingForProfileFromAddon());
    const { profile } = getProfileFromTextSamples(`A`);
    dispatch(viewProfile(profile));

    navigateToStoreLoadingPage();
    expect(container.firstChild).toMatchSnapshot();
    expect(retrieveProfileFromStore).not.toBeCalled();
  });

  it('renders the profile view', function() {
    const { container, dispatch, navigateToStoreLoadingPage } = setup();

    navigateToStoreLoadingPage();
    dispatch(waitingForProfileFromStore());
    expect(container.firstChild).toMatchSnapshot();
    expect(retrieveProfileFromStore).toBeCalledWith('ThisIsAFakeHash');

    const { profile } = getProfileFromTextSamples(`A`);
    dispatch(viewProfile(profile));
    expect(container.firstChild).toMatchSnapshot();
  });

  it('renders temporary errors', function() {
    const { container, dispatch, navigateToStoreLoadingPage } = setup();

    navigateToStoreLoadingPage();
    dispatch(
      temporaryError(
        new TemporaryError('This is a temporary error, wait a bit more.', {
          count: 3,
          total: 10,
        })
      )
    );
    expect(container.firstChild).toMatchSnapshot();
  });

  it('renders an home when the user pressed back after an error', function() {
    const {
      container,
      dispatch,
      navigateToStoreLoadingPage,
      navigateBackToHome,
    } = setup();

    navigateToStoreLoadingPage();
    dispatch(fatalError(new Error('Error while loading profile')));
    expect(container.firstChild).toMatchSnapshot();
    expect(console.error).toHaveBeenCalled();

    navigateBackToHome();
    expect(container.firstChild).toMatchSnapshot();
  });
});

function setup() {
  // Let's silence the error output to the console
  jest.spyOn(console, 'error').mockImplementation(() => {});
  // $FlowFixMe Flow doesn't know retrieveProfileFromAddon is a jest mock.
  retrieveProfileFromAddon.mockImplementation(() => async () => {});
  // $FlowFixMe Flow doesn't know retrieveProfileFromStore is a jest mock.
  retrieveProfileFromStore.mockImplementation(() => async () => {});

  const store = blankStore();
  const renderResult = render(
    <Provider store={store}>
      <ProfileViewWhenReady />
    </Provider>
  );

  function navigateToStoreLoadingPage() {
    const newUrlState = stateFromLocation({
      pathname: '/public/ThisIsAFakeHash/calltree',
      search: '',
      hash: '',
    });
    store.dispatch(updateUrlState(newUrlState));
  }

  function navigateToAddonLoadingPage() {
    const newUrlState = stateFromLocation({
      pathname: '/from-addon/',
      search: '',
      hash: '',
    });
    store.dispatch(updateUrlState(newUrlState));
  }

  function navigateBackToHome() {
    const newUrlState = stateFromLocation({
      pathname: '/',
      hash: '',
      search: '',
    });
    store.dispatch(updateUrlState(newUrlState));
  }

  return {
    ...renderResult,
    dispatch: store.dispatch,
    navigateToStoreLoadingPage,
    navigateToAddonLoadingPage,
    navigateBackToHome,
  };
}
<commit_msg> -> any<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// @flow

// We want to test these components in isolation and tightly control the actions
// dispatched and avoid any side-effects.  That's why we mock this module and
// return dummy thunk actions that return a Promise.
jest.mock('../../actions/receive-profile', () => ({
  // These mocks will get their implementation in the `setup` function.
  // Otherwise the implementation is wiped before the test starts.
  // See https://github.com/facebook/jest/issues/7573 for more info.
  retrieveProfileFromAddon: jest.fn(),
  retrieveProfileFromStore: jest.fn(),
}));

// We mock <ProfileViewer> because it's complex and we should really test it
// elsewhere. Using a custom name makes it possible to test that  _this_
// component is used.
jest.mock('../../components/app/ProfileViewer', () => 'profile-viewer');
// We mock <Home> as well because it brings too much noise in snapshots and it's
// overly tested in another test file.
jest.mock('../../components/app/Home', () => 'home');

import * as React from 'react';
import { Provider } from 'react-redux';
import { render, cleanup } from 'react-testing-library';

import { ProfileViewWhenReady } from '../../components/app/Root';
import { updateUrlState } from '../../actions/app';
// Because this module is mocked but we want the real actions in the test, we
// use `jest.requireActual` here.
const {
  fatalError,
  temporaryError,
  viewProfile,
  waitingForProfileFromAddon,
  waitingForProfileFromStore,
} = jest.requireActual('../../actions/receive-profile');
// These functions are mocks
import {
  retrieveProfileFromStore,
  retrieveProfileFromAddon,
} from '../../actions/receive-profile';
import { stateFromLocation } from '../../app-logic/url-handling';
import { TemporaryError } from '../../utils/errors';

import { blankStore } from '../fixtures/stores';
import { getProfileFromTextSamples } from '../fixtures/profiles/make-profile';

afterEach(cleanup);
describe('app/ProfileViewWhenReady', function() {
  it('renders an initial home', function() {
    const { container } = setup();

    expect(container.firstChild).toMatchSnapshot();
  });

  it('renders the addon loading page, and the profile view after capturing', function() {
    const { container, dispatch, navigateToAddonLoadingPage } = setup();

    navigateToAddonLoadingPage();
    dispatch(waitingForProfileFromAddon());
    expect(container.firstChild).toMatchSnapshot();
    expect(retrieveProfileFromAddon).toBeCalled();

    const { profile } = getProfileFromTextSamples(`A`);
    dispatch(viewProfile(profile));
    expect(container.firstChild).toMatchSnapshot();
  });

  it('does not try to retrieve a profile when moving from from-addon to public', function() {
    const {
      container,
      dispatch,
      navigateToAddonLoadingPage,
      navigateToStoreLoadingPage,
    } = setup();

    navigateToAddonLoadingPage();
    dispatch(waitingForProfileFromAddon());
    const { profile } = getProfileFromTextSamples(`A`);
    dispatch(viewProfile(profile));

    navigateToStoreLoadingPage();
    expect(container.firstChild).toMatchSnapshot();
    expect(retrieveProfileFromStore).not.toBeCalled();
  });

  it('renders the profile view', function() {
    const { container, dispatch, navigateToStoreLoadingPage } = setup();

    navigateToStoreLoadingPage();
    dispatch(waitingForProfileFromStore());
    expect(container.firstChild).toMatchSnapshot();
    expect(retrieveProfileFromStore).toBeCalledWith('ThisIsAFakeHash');

    const { profile } = getProfileFromTextSamples(`A`);
    dispatch(viewProfile(profile));
    expect(container.firstChild).toMatchSnapshot();
  });

  it('renders temporary errors', function() {
    const { container, dispatch, navigateToStoreLoadingPage } = setup();

    navigateToStoreLoadingPage();
    dispatch(
      temporaryError(
        new TemporaryError('This is a temporary error, wait a bit more.', {
          count: 3,
          total: 10,
        })
      )
    );
    expect(container.firstChild).toMatchSnapshot();
  });

  it('renders an home when the user pressed back after an error', function() {
    const {
      container,
      dispatch,
      navigateToStoreLoadingPage,
      navigateBackToHome,
    } = setup();

    navigateToStoreLoadingPage();
    dispatch(fatalError(new Error('Error while loading profile')));
    expect(container.firstChild).toMatchSnapshot();
    expect(console.error).toHaveBeenCalled();

    navigateBackToHome();
    expect(container.firstChild).toMatchSnapshot();
  });
});

function setup() {
  // Let's silence the error output to the console
  jest.spyOn(console, 'error').mockImplementation(() => {});
  // Flow doesn't know retrieveProfileFromAddon is a jest mock.
  (retrieveProfileFromAddon: any).mockImplementation(() => async () => {});
  // Flow doesn't know retrieveProfileFromStore is a jest mock.
  (retrieveProfileFromStore: any).mockImplementation(() => async () => {});

  const store = blankStore();
  const renderResult = render(
    <Provider store={store}>
      <ProfileViewWhenReady />
    </Provider>
  );

  function navigateToStoreLoadingPage() {
    const newUrlState = stateFromLocation({
      pathname: '/public/ThisIsAFakeHash/calltree',
      search: '',
      hash: '',
    });
    store.dispatch(updateUrlState(newUrlState));
  }

  function navigateToAddonLoadingPage() {
    const newUrlState = stateFromLocation({
      pathname: '/from-addon/',
      search: '',
      hash: '',
    });
    store.dispatch(updateUrlState(newUrlState));
  }

  function navigateBackToHome() {
    const newUrlState = stateFromLocation({
      pathname: '/',
      hash: '',
      search: '',
    });
    store.dispatch(updateUrlState(newUrlState));
  }

  return {
    ...renderResult,
    dispatch: store.dispatch,
    navigateToStoreLoadingPage,
    navigateToAddonLoadingPage,
    navigateBackToHome,
  };
}
<|endoftext|>


<commit_before>require("include/include.js");

var Struct = demand("ephox.scullion.Struct");
var Events = demand("ephox.porkbun.Events");
var Binder = demand("ephox.porkbun.Binder");

function testBinder() {
  var events = Events.create({
    myEvent: Struct.immutable('one', 'two')
  });

  var binder = Binder.create();

  var called = false;

  binder.bind(events.registry.myEvent, function(event) {
    called = true;
  });

  jssert.assertThrows(function () {
    binder.bind(events.registry.myEvent, function(event) {
      called = true;
    });
  }, 'Bind error: event type has already been bound');

  events.trigger.myEvent('a', 'b');
  jssert.assertEq(true, called);

  called = false;

  binder.unbind(events.registry.myEvent);

  events.trigger.myEvent('a', 'b');
  jssert.assertEq(false, called);

  jssert.assertThrows(function () {
    binder.unbind(events.registry.myEvent);
  }, 'Unbind error: unknown event type');
}<commit_msg>Update tests<commit_after>require("include/include.js");

var Struct = demand("ephox.scullion.Struct");
var Events = demand("ephox.porkbun.Events");
var Binder = demand("ephox.porkbun.Binder");

function testBinder() {
  var events = Events.create({
    myEvent: Struct.immutable('one', 'two')
  });

  var binder = Binder.create();

  var called = false;

  binder.bind(events.registry.myEvent, function(event) {
    called = true;
  });

  jssert.assertThrows(function () {
    binder.bind(events.registry.myEvent, function(event) {
      called = true;
    });
  }, 'Bind error: event registration has already been bound');

  events.trigger.myEvent('a', 'b');
  jssert.assertEq(true, called);

  called = false;

  binder.unbind(events.registry.myEvent);

  events.trigger.myEvent('a', 'b');
  jssert.assertEq(false, called);

  jssert.assertThrows(function () {
    binder.unbind(events.registry.myEvent);
  }, 'Unbind error: unknown event registration');
}<|endoftext|>


<commit_before>/**
 * @license
 * Visual Blocks Editor
 *
 * Copyright 2012 Google Inc.
 * https://developers.google.com/blockly/
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Text input field.
 * @author fraser@google.com (Neil Fraser)
 */
'use strict';

goog.provide('Blockly.FieldTextInput');

goog.require('Blockly.Field');
goog.require('Blockly.Msg');
goog.require('Blockly.utils');

goog.require('goog.math.Coordinate');
goog.require('goog.userAgent');

/**
 * Class for an editable text field.
 * @param {string} text The initial content of the field.
 * @param {Function=} opt_validator An optional function that is called
 *     to validate any constraints on what the user entered.  Takes the new
 *     text as an argument and returns either the accepted text, a replacement
 *     text, or null to abort the change.
 * @extends {Blockly.Field}
 * @constructor
 */
Blockly.FieldTextInput = function(text, opt_validator) {
  Blockly.FieldTextInput.superClass_.constructor.call(this, text,
      opt_validator);
};
goog.inherits(Blockly.FieldTextInput, Blockly.Field);

/**
 * Construct a FieldTextInput from a JSON arg object,
 * dereferencing any string table references.
 * @param {!Object} options A JSON object with options (text, class, and
 *                          spellcheck).
 * @returns {!Blockly.FieldTextInput} The new field instance.
 * @package
 * @nocollapse
 */
Blockly.FieldTextInput.fromJson = function(options) {
  var text = Blockly.utils.replaceMessageReferences(options['text']);
  var field = new Blockly.FieldTextInput(text, options['class']);
  if (typeof options['spellcheck'] === 'boolean') {
    field.setSpellcheck(options['spellcheck']);
  }
  return field;
};

/**
 * Point size of text.  Should match blocklyText's font-size in CSS.
 */
Blockly.FieldTextInput.FONTSIZE = 11;

/**
 * The HTML input element for the user to type, or null if no FieldTextInput
 * editor is currently open.
 * @type {HTMLInputElement}
 * @protected
 */
Blockly.FieldTextInput.htmlInput_ = null;

/**
 * Mouse cursor style when over the hotspot that initiates the editor.
 */
Blockly.FieldTextInput.prototype.CURSOR = 'text';

/**
 * Allow browser to spellcheck this field.
 * @private
 */
Blockly.FieldTextInput.prototype.spellcheck_ = true;

/**
 * Close the input widget if this input is being deleted.
 */
Blockly.FieldTextInput.prototype.dispose = function() {
  Blockly.WidgetDiv.hideIfOwner(this);
  Blockly.FieldTextInput.superClass_.dispose.call(this);
};

/**
 * Set the value of this field.
 * @param {?string} newValue New value.
 * @override
 */
Blockly.FieldTextInput.prototype.setValue = function(newValue) {
  if (newValue !== null) { // No change if null.
    if (this.sourceBlock_) {
      var validated = this.callValidator(newValue);
      // If the new value is invalid, validation returns null.
      // In this case we still want to display the illegal result.
      if (validated !== null) {
        newValue = validated;
      }
    }
    Blockly.Field.prototype.setValue.call(this, newValue);
  }
};

/**
 * Set the text in this field and fire a change event.
 * @param {*} newText New text.
 */
Blockly.FieldTextInput.prototype.setText = function(newText) {
  if (newText === null) {
    // No change if null.
    return;
  }
  newText = String(newText);
  if (newText === this.text_) {
    // No change.
    return;
  }
  if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
    Blockly.Events.fire(new Blockly.Events.BlockChange(
        this.sourceBlock_, 'field', this.name, this.text_, newText));
  }
  Blockly.Field.prototype.setText.call(this, newText);
};

/**
 * Set whether this field is spellchecked by the browser.
 * @param {boolean} check True if checked.
 */
Blockly.FieldTextInput.prototype.setSpellcheck = function(check) {
  this.spellcheck_ = check;
};

/**
 * Show the inline free-text editor on top of the text.
 * @param {boolean=} opt_quietInput True if editor should be created without
 *     focus.  Defaults to false.
 * @protected
 */
Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput) {
  this.workspace_ = this.sourceBlock_.workspace;
  var quietInput = opt_quietInput || false;
  if (!quietInput && (goog.userAgent.MOBILE || goog.userAgent.ANDROID ||
                      goog.userAgent.IPAD)) {
    this.showPromptEditor_();
  } else {
    this.showInlineEditor_(quietInput);
  }
};

/**
 * Create and show a text input editor that is a prompt (usually a popup).
 * Mobile browsers have issues with in-line textareas (focus and keyboards).
 * @private
 */
Blockly.FieldTextInput.prototype.showPromptEditor_ = function() {
  var fieldText = this;
  Blockly.prompt(Blockly.Msg['CHANGE_VALUE_TITLE'], this.text_,
      function(newValue) {
        if (fieldText.sourceBlock_) {
          newValue = fieldText.callValidator(newValue);
        }
        fieldText.setValue(newValue);
      });
};

/**
 * Create and show a text input editor that sits directly over the text input.
 * @param {boolean} quietInput True if editor should be created without
 *     focus.
 * @private
 */
Blockly.FieldTextInput.prototype.showInlineEditor_ = function(quietInput) {
  Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, this.widgetDispose_());
  var div = Blockly.WidgetDiv.DIV;
  // Create the input.
  var htmlInput = document.createElement('input');
  htmlInput.className = 'blocklyHtmlInput';
  htmlInput.setAttribute('spellcheck', this.spellcheck_);
  var fontSize =
      (Blockly.FieldTextInput.FONTSIZE * this.workspace_.scale) + 'pt';
  div.style.fontSize = fontSize;
  htmlInput.style.fontSize = fontSize;

  Blockly.FieldTextInput.htmlInput_ = htmlInput;
  div.appendChild(htmlInput);

  htmlInput.value = htmlInput.defaultValue = this.text_;
  htmlInput.oldValue_ = null;
  this.validate_();
  this.resizeEditor_();
  if (!quietInput) {
    htmlInput.focus();
    htmlInput.select();
  }

  this.bindEvents_(htmlInput);
};

/**
 * Bind handlers for user input on this field and size changes on the workspace.
 * @param {!HTMLInputElement} htmlInput The htmlInput created in showEditor, to
 *     which event handlers will be bound.
 * @private
 */
Blockly.FieldTextInput.prototype.bindEvents_ = function(htmlInput) {
  // Bind to keydown -- trap Enter without IME and Esc to hide.
  htmlInput.onKeyDownWrapper_ =
      Blockly.bindEventWithChecks_(
          htmlInput, 'keydown', this, this.onHtmlInputKeyDown_);
  // Bind to keyup -- trap Enter; resize after every keystroke.
  htmlInput.onKeyUpWrapper_ =
      Blockly.bindEventWithChecks_(
          htmlInput, 'keyup', this, this.onHtmlInputChange_);
  // Bind to keyPress -- repeatedly resize when holding down a key.
  htmlInput.onKeyPressWrapper_ =
      Blockly.bindEventWithChecks_(
          htmlInput, 'keypress', this, this.onHtmlInputChange_);
  htmlInput.onWorkspaceChangeWrapper_ = this.resizeEditor_.bind(this);
  this.workspace_.addChangeListener(htmlInput.onWorkspaceChangeWrapper_);
};

/**
 * Unbind handlers for user input and workspace size changes.
 * @param {!HTMLInputElement} htmlInput The html for this text input.
 * @private
 */
Blockly.FieldTextInput.prototype.unbindEvents_ = function(htmlInput) {
  Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_);
  Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_);
  Blockly.unbindEvent_(htmlInput.onKeyPressWrapper_);
  this.workspace_.removeChangeListener(
      htmlInput.onWorkspaceChangeWrapper_);
};

/**
 * Handle key down to the editor.
 * @param {!Event} e Keyboard event.
 * @private
 */
Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) {
  var htmlInput = Blockly.FieldTextInput.htmlInput_;
  var tabKey = 9, enterKey = 13, escKey = 27;
  if (e.keyCode == enterKey) {
    Blockly.WidgetDiv.hide();
  } else if (e.keyCode == escKey) {
    htmlInput.value = htmlInput.defaultValue;
    Blockly.WidgetDiv.hide();
  } else if (e.keyCode == tabKey) {
    Blockly.WidgetDiv.hide();
    this.sourceBlock_.tab(this, !e.shiftKey);
    e.preventDefault();
  }
};

/**
 * Handle a change to the editor.
 * @param {!Event} _e Keyboard event.
 * @private
 */
Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(_e) {
  var htmlInput = Blockly.FieldTextInput.htmlInput_;
  // Update source block.
  var text = htmlInput.value;
  if (text !== htmlInput.oldValue_) {
    htmlInput.oldValue_ = text;
    Blockly.Events.setGroup(true);
    this.setValue(text);
    Blockly.Events.setGroup(false);
    this.validate_();
  } else if (goog.userAgent.WEBKIT) {
    // Cursor key.  Render the source block to show the caret moving.
    // Chrome only (version 26, OS X).
    this.sourceBlock_.render();
  }
  this.resizeEditor_();
  Blockly.svgResize(this.sourceBlock_.workspace);
};

/**
 * Check to see if the contents of the editor validates.
 * Style the editor accordingly.
 * @protected
 */
Blockly.FieldTextInput.prototype.validate_ = function() {
  var valid = true;
  if (!Blockly.FieldTextInput.htmlInput_) {
    throw Error('htmlInput not defined');
  }
  var htmlInput = Blockly.FieldTextInput.htmlInput_;
  if (this.sourceBlock_) {
    valid = this.callValidator(htmlInput.value);
  }
  if (valid === null) {
    Blockly.utils.addClass(htmlInput, 'blocklyInvalidInput');
  } else {
    Blockly.utils.removeClass(htmlInput, 'blocklyInvalidInput');
  }
};

/**
 * Resize the editor and the underlying block to fit the text.
 * @protected
 */
Blockly.FieldTextInput.prototype.resizeEditor_ = function() {
  var div = Blockly.WidgetDiv.DIV;
  var bBox = this.getScaledBBox_();
  div.style.width = bBox.right - bBox.left + 'px';
  div.style.height = bBox.bottom - bBox.top + 'px';

  // In RTL mode block fields and LTR input fields the left edge moves,
  // whereas the right edge is fixed.  Reposition the editor.
  var x = this.sourceBlock_.RTL ? bBox.right - div.offsetWidth : bBox.left;
  var xy = new goog.math.Coordinate(x, bBox.top);

  // Shift by a few pixels to line up exactly.
  xy.y += 1;
  if (goog.userAgent.GECKO && Blockly.WidgetDiv.DIV.style.top) {
    // Firefox mis-reports the location of the border by a pixel
    // once the WidgetDiv is moved into position.
    xy.x -= 1;
    xy.y -= 1;
  }
  if (goog.userAgent.WEBKIT) {
    xy.y -= 3;
  }
  div.style.left = xy.x + 'px';
  div.style.top = xy.y + 'px';
};

/**
 * Close the editor, save the results, and dispose of the editable
 * text field's elements.
 * @return {!Function} Closure to call on destruction of the WidgetDiv.
 * @private
 */
Blockly.FieldTextInput.prototype.widgetDispose_ = function() {
  var thisField = this;
  return function() {
    var htmlInput = Blockly.FieldTextInput.htmlInput_;
    // Save the edit (if it validates).
    thisField.maybeSaveEdit_();

    thisField.unbindEvents_(htmlInput);
    Blockly.FieldTextInput.htmlInput_ = null;
    Blockly.Events.setGroup(false);

    // Delete style properties.
    var style = Blockly.WidgetDiv.DIV.style;
    style.width = 'auto';
    style.height = 'auto';
    style.fontSize = '';
  };
};

/**
 * Attempt to save the text field changes when the user input loses focus.
 * If the value is not valid, revert to the default value.
 * @private
 */
Blockly.FieldTextInput.prototype.maybeSaveEdit_ = function() {
  var htmlInput = Blockly.FieldTextInput.htmlInput_;
  // Save the edit (if it validates).
  var text = htmlInput.value;
  if (this.sourceBlock_) {
    var text1 = this.callValidator(text);
    if (text1 === null) {
      // Invalid edit.
      text = htmlInput.defaultValue;
    } else {
      // Validation function has changed the text.
      text = text1;
      if (this.onFinishEditing_) {
        this.onFinishEditing_(text);
      }
    }
  }
  this.setText(text);
  this.sourceBlock_.rendered && this.sourceBlock_.render();
};

/**
 * Ensure that only a number may be entered.
 * @param {string} text The user's text.
 * @return {?string} A string representing a valid number, or null if invalid.
 */
Blockly.FieldTextInput.numberValidator = function(text) {
  console.warn('Blockly.FieldTextInput.numberValidator is deprecated. ' +
               'Use Blockly.FieldNumber instead.');
  if (text === null) {
    return null;
  }
  text = String(text);
  // TODO: Handle cases like 'ten', '1.203,14', etc.
  // 'O' is sometimes mistaken for '0' by inexperienced users.
  text = text.replace(/O/ig, '0');
  // Strip out thousands separators.
  text = text.replace(/,/g, '');
  var n = parseFloat(text || 0);
  return isNaN(n) ? null : String(n);
};

/**
 * Ensure that only a nonnegative integer may be entered.
 * @param {string} text The user's text.
 * @return {?string} A string representing a valid int, or null if invalid.
 */
Blockly.FieldTextInput.nonnegativeIntegerValidator = function(text) {
  var n = Blockly.FieldTextInput.numberValidator(text);
  if (n) {
    n = String(Math.max(0, Math.floor(n)));
  }
  return n;
};

Blockly.Field.register('field_input', Blockly.FieldTextInput);
<commit_msg>Added todo<commit_after>/**
 * @license
 * Visual Blocks Editor
 *
 * Copyright 2012 Google Inc.
 * https://developers.google.com/blockly/
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Text input field.
 * @author fraser@google.com (Neil Fraser)
 */
'use strict';

goog.provide('Blockly.FieldTextInput');

goog.require('Blockly.Field');
goog.require('Blockly.Msg');
goog.require('Blockly.utils');

goog.require('goog.math.Coordinate');
goog.require('goog.userAgent');

/**
 * Class for an editable text field.
 * @param {string} text The initial content of the field.
 * @param {Function=} opt_validator An optional function that is called
 *     to validate any constraints on what the user entered.  Takes the new
 *     text as an argument and returns either the accepted text, a replacement
 *     text, or null to abort the change.
 * @extends {Blockly.Field}
 * @constructor
 */
Blockly.FieldTextInput = function(text, opt_validator) {
  Blockly.FieldTextInput.superClass_.constructor.call(this, text,
      opt_validator);
};
goog.inherits(Blockly.FieldTextInput, Blockly.Field);

/**
 * Construct a FieldTextInput from a JSON arg object,
 * dereferencing any string table references.
 * @param {!Object} options A JSON object with options (text, class, and
 *                          spellcheck).
 * @returns {!Blockly.FieldTextInput} The new field instance.
 * @package
 * @nocollapse
 */
Blockly.FieldTextInput.fromJson = function(options) {
  var text = Blockly.utils.replaceMessageReferences(options['text']);
  var field = new Blockly.FieldTextInput(text, options['class']);
  if (typeof options['spellcheck'] === 'boolean') {
    field.setSpellcheck(options['spellcheck']);
  }
  return field;
};

/**
 * Point size of text.  Should match blocklyText's font-size in CSS.
 */
Blockly.FieldTextInput.FONTSIZE = 11;

/**
 * The HTML input element for the user to type, or null if no FieldTextInput
 * editor is currently open.
 * @type {HTMLInputElement}
 * @protected
 */
Blockly.FieldTextInput.htmlInput_ = null;

/**
 * Mouse cursor style when over the hotspot that initiates the editor.
 */
Blockly.FieldTextInput.prototype.CURSOR = 'text';

/**
 * Allow browser to spellcheck this field.
 * @private
 */
Blockly.FieldTextInput.prototype.spellcheck_ = true;

/**
 * Close the input widget if this input is being deleted.
 */
Blockly.FieldTextInput.prototype.dispose = function() {
  Blockly.WidgetDiv.hideIfOwner(this);
  Blockly.FieldTextInput.superClass_.dispose.call(this);
};

/**
 * Set the value of this field.
 * @param {?string} newValue New value.
 * @override
 */
Blockly.FieldTextInput.prototype.setValue = function(newValue) {
  if (newValue !== null) { // No change if null.
    if (this.sourceBlock_) {
      var validated = this.callValidator(newValue);
      // If the new value is invalid, validation returns null.
      // In this case we still want to display the illegal result.
      if (validated !== null) {
        newValue = validated;
      }
    }
    Blockly.Field.prototype.setValue.call(this, newValue);
  }
};

/**
 * Set the text in this field and fire a change event.
 * @param {*} newText New text.
 */
Blockly.FieldTextInput.prototype.setText = function(newText) {
  if (newText === null) {
    // No change if null.
    return;
  }
  newText = String(newText);
  if (newText === this.text_) {
    // No change.
    return;
  }
  if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
    Blockly.Events.fire(new Blockly.Events.BlockChange(
        this.sourceBlock_, 'field', this.name, this.text_, newText));
  }
  Blockly.Field.prototype.setText.call(this, newText);
};

/**
 * Set whether this field is spellchecked by the browser.
 * @param {boolean} check True if checked.
 */
Blockly.FieldTextInput.prototype.setSpellcheck = function(check) {
  this.spellcheck_ = check;
};

/**
 * Show the inline free-text editor on top of the text.
 * @param {boolean=} opt_quietInput True if editor should be created without
 *     focus.  Defaults to false.
 * @protected
 */
Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput) {
  this.workspace_ = this.sourceBlock_.workspace;
  var quietInput = opt_quietInput || false;
  if (!quietInput && (goog.userAgent.MOBILE || goog.userAgent.ANDROID ||
                      goog.userAgent.IPAD)) {
    this.showPromptEditor_();
  } else {
    this.showInlineEditor_(quietInput);
  }
};

/**
 * Create and show a text input editor that is a prompt (usually a popup).
 * Mobile browsers have issues with in-line textareas (focus and keyboards).
 * @private
 */
Blockly.FieldTextInput.prototype.showPromptEditor_ = function() {
  var fieldText = this;
  Blockly.prompt(Blockly.Msg['CHANGE_VALUE_TITLE'], this.text_,
      function(newValue) {
        if (fieldText.sourceBlock_) {
          newValue = fieldText.callValidator(newValue);
        }
        fieldText.setValue(newValue);
      });
};

/**
 * Create and show a text input editor that sits directly over the text input.
 * @param {boolean} quietInput True if editor should be created without
 *     focus.
 * @private
 */
Blockly.FieldTextInput.prototype.showInlineEditor_ = function(quietInput) {
  Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, this.widgetDispose_());
  var div = Blockly.WidgetDiv.DIV;
  // Create the input.
  var htmlInput = document.createElement('input');
  htmlInput.className = 'blocklyHtmlInput';
  htmlInput.setAttribute('spellcheck', this.spellcheck_);
  var fontSize =
      (Blockly.FieldTextInput.FONTSIZE * this.workspace_.scale) + 'pt';
  div.style.fontSize = fontSize;
  htmlInput.style.fontSize = fontSize;

  Blockly.FieldTextInput.htmlInput_ = htmlInput;
  div.appendChild(htmlInput);

  htmlInput.value = htmlInput.defaultValue = this.text_;
  htmlInput.oldValue_ = null;
  this.validate_();
  this.resizeEditor_();
  if (!quietInput) {
    htmlInput.focus();
    htmlInput.select();
  }

  this.bindEvents_(htmlInput);
};

/**
 * Bind handlers for user input on this field and size changes on the workspace.
 * @param {!HTMLInputElement} htmlInput The htmlInput created in showEditor, to
 *     which event handlers will be bound.
 * @private
 */
Blockly.FieldTextInput.prototype.bindEvents_ = function(htmlInput) {
  // Bind to keydown -- trap Enter without IME and Esc to hide.
  htmlInput.onKeyDownWrapper_ =
      Blockly.bindEventWithChecks_(
          htmlInput, 'keydown', this, this.onHtmlInputKeyDown_);
  // Bind to keyup -- trap Enter; resize after every keystroke.
  htmlInput.onKeyUpWrapper_ =
      Blockly.bindEventWithChecks_(
          htmlInput, 'keyup', this, this.onHtmlInputChange_);
  // Bind to keyPress -- repeatedly resize when holding down a key.
  htmlInput.onKeyPressWrapper_ =
      Blockly.bindEventWithChecks_(
          htmlInput, 'keypress', this, this.onHtmlInputChange_);
  htmlInput.onWorkspaceChangeWrapper_ = this.resizeEditor_.bind(this);
  this.workspace_.addChangeListener(htmlInput.onWorkspaceChangeWrapper_);
};

/**
 * Unbind handlers for user input and workspace size changes.
 * @param {!HTMLInputElement} htmlInput The html for this text input.
 * @private
 */
Blockly.FieldTextInput.prototype.unbindEvents_ = function(htmlInput) {
  Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_);
  Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_);
  Blockly.unbindEvent_(htmlInput.onKeyPressWrapper_);
  this.workspace_.removeChangeListener(
      htmlInput.onWorkspaceChangeWrapper_);
};

/**
 * Handle key down to the editor.
 * @param {!Event} e Keyboard event.
 * @private
 */
Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) {
  var htmlInput = Blockly.FieldTextInput.htmlInput_;
  var tabKey = 9, enterKey = 13, escKey = 27;
  if (e.keyCode == enterKey) {
    Blockly.WidgetDiv.hide();
  } else if (e.keyCode == escKey) {
    htmlInput.value = htmlInput.defaultValue;
    Blockly.WidgetDiv.hide();
  } else if (e.keyCode == tabKey) {
    Blockly.WidgetDiv.hide();
    this.sourceBlock_.tab(this, !e.shiftKey);
    e.preventDefault();
  }
};

/**
 * Handle a change to the editor.
 * @param {!Event} _e Keyboard event.
 * @private
 */
Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(_e) {
  var htmlInput = Blockly.FieldTextInput.htmlInput_;
  // Update source block.
  var text = htmlInput.value;
  if (text !== htmlInput.oldValue_) {
    htmlInput.oldValue_ = text;

    // TODO(#2169): Once issue is fixed the setGroup functionality could be
    //              moved up to the Field setValue method. This would create a
    //              broader fix for all field types.
    Blockly.Events.setGroup(true);
    this.setValue(text);
    Blockly.Events.setGroup(false);
    this.validate_();
  } else if (goog.userAgent.WEBKIT) {
    // Cursor key.  Render the source block to show the caret moving.
    // Chrome only (version 26, OS X).
    this.sourceBlock_.render();
  }
  this.resizeEditor_();
  Blockly.svgResize(this.sourceBlock_.workspace);
};

/**
 * Check to see if the contents of the editor validates.
 * Style the editor accordingly.
 * @protected
 */
Blockly.FieldTextInput.prototype.validate_ = function() {
  var valid = true;
  if (!Blockly.FieldTextInput.htmlInput_) {
    throw Error('htmlInput not defined');
  }
  var htmlInput = Blockly.FieldTextInput.htmlInput_;
  if (this.sourceBlock_) {
    valid = this.callValidator(htmlInput.value);
  }
  if (valid === null) {
    Blockly.utils.addClass(htmlInput, 'blocklyInvalidInput');
  } else {
    Blockly.utils.removeClass(htmlInput, 'blocklyInvalidInput');
  }
};

/**
 * Resize the editor and the underlying block to fit the text.
 * @protected
 */
Blockly.FieldTextInput.prototype.resizeEditor_ = function() {
  var div = Blockly.WidgetDiv.DIV;
  var bBox = this.getScaledBBox_();
  div.style.width = bBox.right - bBox.left + 'px';
  div.style.height = bBox.bottom - bBox.top + 'px';

  // In RTL mode block fields and LTR input fields the left edge moves,
  // whereas the right edge is fixed.  Reposition the editor.
  var x = this.sourceBlock_.RTL ? bBox.right - div.offsetWidth : bBox.left;
  var xy = new goog.math.Coordinate(x, bBox.top);

  // Shift by a few pixels to line up exactly.
  xy.y += 1;
  if (goog.userAgent.GECKO && Blockly.WidgetDiv.DIV.style.top) {
    // Firefox mis-reports the location of the border by a pixel
    // once the WidgetDiv is moved into position.
    xy.x -= 1;
    xy.y -= 1;
  }
  if (goog.userAgent.WEBKIT) {
    xy.y -= 3;
  }
  div.style.left = xy.x + 'px';
  div.style.top = xy.y + 'px';
};

/**
 * Close the editor, save the results, and dispose of the editable
 * text field's elements.
 * @return {!Function} Closure to call on destruction of the WidgetDiv.
 * @private
 */
Blockly.FieldTextInput.prototype.widgetDispose_ = function() {
  var thisField = this;
  return function() {
    var htmlInput = Blockly.FieldTextInput.htmlInput_;
    // Save the edit (if it validates).
    thisField.maybeSaveEdit_();

    thisField.unbindEvents_(htmlInput);
    Blockly.FieldTextInput.htmlInput_ = null;
    Blockly.Events.setGroup(false);

    // Delete style properties.
    var style = Blockly.WidgetDiv.DIV.style;
    style.width = 'auto';
    style.height = 'auto';
    style.fontSize = '';
  };
};

/**
 * Attempt to save the text field changes when the user input loses focus.
 * If the value is not valid, revert to the default value.
 * @private
 */
Blockly.FieldTextInput.prototype.maybeSaveEdit_ = function() {
  var htmlInput = Blockly.FieldTextInput.htmlInput_;
  // Save the edit (if it validates).
  var text = htmlInput.value;
  if (this.sourceBlock_) {
    var text1 = this.callValidator(text);
    if (text1 === null) {
      // Invalid edit.
      text = htmlInput.defaultValue;
    } else {
      // Validation function has changed the text.
      text = text1;
      if (this.onFinishEditing_) {
        this.onFinishEditing_(text);
      }
    }
  }
  this.setText(text);
  this.sourceBlock_.rendered && this.sourceBlock_.render();
};

/**
 * Ensure that only a number may be entered.
 * @param {string} text The user's text.
 * @return {?string} A string representing a valid number, or null if invalid.
 */
Blockly.FieldTextInput.numberValidator = function(text) {
  console.warn('Blockly.FieldTextInput.numberValidator is deprecated. ' +
               'Use Blockly.FieldNumber instead.');
  if (text === null) {
    return null;
  }
  text = String(text);
  // TODO: Handle cases like 'ten', '1.203,14', etc.
  // 'O' is sometimes mistaken for '0' by inexperienced users.
  text = text.replace(/O/ig, '0');
  // Strip out thousands separators.
  text = text.replace(/,/g, '');
  var n = parseFloat(text || 0);
  return isNaN(n) ? null : String(n);
};

/**
 * Ensure that only a nonnegative integer may be entered.
 * @param {string} text The user's text.
 * @return {?string} A string representing a valid int, or null if invalid.
 */
Blockly.FieldTextInput.nonnegativeIntegerValidator = function(text) {
  var n = Blockly.FieldTextInput.numberValidator(text);
  if (n) {
    n = String(Math.max(0, Math.floor(n)));
  }
  return n;
};

Blockly.Field.register('field_input', Blockly.FieldTextInput);
<|endoftext|>


<commit_before>/**
Copyright © 2015 Luis Sieira Garcia

This file is part of Planète.

    Planète is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Planète is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with along with planète.  If not, see <http://www.gnu.org/licenses/>.  If not, see <http://www.gnu.org/licenses/>
**/
'use strict';

var angularInjections = angularInjections || [];

var app = angular.module('planete', [].concat(angularInjections))
.config(function($routeProvider, $httpProvider) {
  $httpProvider.interceptors.push('authInterceptor');
})
.factory('authInterceptor', function($q, $log, $rootScope) {
  return {
    response: function(response) {
      if (response.status === 401) {
        $log.debug("Response 401");
      }
      return response || $q.when(response);
    },
    responseError: function(rejection) {
      if (rejection.status === 401) {
        $log.debug("Response Error 401",rejection);
        $rootScope.$broadcast('unauthorized');
      }
      return $q.reject(rejection);
    }
  };
})
.controller('planeteController', function($rootScope, $scope, $ocLazyLoad, $http) {
  $scope.login = function() {
    return $ocLazyLoad.load({
      cache: false,
      rerun: true,
      files: ['modules/auth/js/app.js']
    });
  };

  $scope.admin = function() {
    $http({method: 'GET', url: '/admin'});
  }

  $rootScope.$on('unauthorized', function() {
    $scope.login();
  });
});
<commit_msg>unauthorized broadcast listener attached on main app run method<commit_after>/**
Copyright © 2015 Luis Sieira Garcia

This file is part of Planète.

    Planète is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Planète is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with along with planète.  If not, see <http://www.gnu.org/licenses/>.  If not, see <http://www.gnu.org/licenses/>
**/
'use strict';

var angularInjections = angularInjections || [];

var app = angular.module('planete', [].concat(angularInjections))
.config(function($httpProvider) {
  $httpProvider.interceptors.push('authInterceptor');
})
.run(function($rootScope, $ocLazyLoad) {
  var popLogin = $rootScope.popLogin = function () {
    return $ocLazyLoad.load({
      cache: false,
      rerun: true,
      files: ['modules/auth/js/app.js']
    });
  };

  $rootScope.$on('unauthorized', function() {
    popLogin();
  });
})
.factory('authInterceptor', function($rootScope, $q, $log) {
  return {
    response: function(response) {
      if (response.status === 401) {
        $log.debug("Response 401");
        $rootScope.$broadcast('unauthorized');
      }
      return response || $q.when(response);
    },
    responseError: function(rejection) {
      if (rejection.status === 401) {
        $log.debug("Response Error 401",rejection);
        $rootScope.$broadcast('unauthorized');
      }
      return $q.reject(rejection);
    }
  };
})
.controller('planeteController', function($rootScope, $scope, $http) {
  $scope.login = $rootScope.popLogin;

  $scope.admin = function() {
    $http({method: 'GET', url: '/admin'});
  }
});
<|endoftext|>


<commit_before>/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var Drawing = require('../../components/drawing');

var constants = require('./constants');

module.exports = {
    x: {
        valType: 'data_array',
        description: 'Sets the x coordinates.'
    },
    x0: {
        valType: 'any',
        dflt: 0,
        role: 'info',
        description: [
            'Alternate to `x`.',
            'Builds a linear space of x coordinates.',
            'Use with `dx`',
            'where `x0` is the starting coordinate and `dx` the step.'
        ].join(' ')
    },
    dx: {
        valType: 'number',
        dflt: 1,
        role: 'info',
        description: [
            'Sets the x coordinate step.',
            'See `x0` for more info.'
        ].join(' ')
    },
    y: {
        valType: 'data_array',
        description: 'Sets the y coordinates.'
    },
    y0: {
        valType: 'any',
        dflt: 0,
        role: 'info',
        description: [
            'Alternate to `y`.',
            'Builds a linear space of y coordinates.',
            'Use with `dy`',
            'where `y0` is the starting coordinate and `dy` the step.'
        ].join(' ')
    },
    dy: {
        valType: 'number',
        dflt: 1,
        role: 'info',
        description: [
            'Sets the y coordinate step.',
            'See `y0` for more info.'
        ].join(' ')
    },
    text: {
        valType: 'string',
        role: 'info',
        dflt: '',
        arrayOk: true,
        description: [
            'Sets text elements associated with each (x,y) pair.',
            'If a single string, the same string appears over',
            'all the data points.',
            'If an array of string, the items are mapped in order to the',
            'this trace\'s (x,y) coordinates.'
        ].join(' ')
    },
    mode: {
        valType: 'flaglist',
        flags: ['lines', 'markers', 'text'],
        extras: ['none'],
        role: 'info',
        description: [
            'Determines the drawing mode for this scatter trace.',
            'If the provided `mode` includes *text* then the `text` elements',
            'appear at the coordinates. Otherwise, the `text` elements',
            'appear on hover.',
            'If there are less than ' + constants.PTS_LINESONLY + ' points,',
            'then the default is *lines+markers*. Otherwise, *lines*.'
        ].join(' ')
    },
    line: {
        color: {
            valType: 'color',
            role: 'style',
            description: 'Sets the line color.'
        },
        width: {
            valType: 'number',
            min: 0,
            dflt: 2,
            role: 'style',
            description: 'Sets the line width (in px).'
        },
        shape: {
            valType: 'enumerated',
            values: ['linear', 'spline', 'hv', 'vh', 'hvh', 'vhv'],
            dflt: 'linear',
            role: 'style',
            description: [
                'Determines the line shape.',
                'With *spline* the lines are drawn using spline interpolation.',
                'The other available values correspond to step-wise line shapes.'
            ].join(' ')
        },
        smoothing: {
            valType: 'number',
            min: 0,
            max: 1.3,
            dflt: 1,
            role: 'style',
            description: [
                'Has an effect only if `shape` is set to *spline*',
                'Sets the amount of smoothing.',
                '*0* corresponds to no smoothing (equivalent to a *linear* shape).'
            ].join(' ')
        },
        dash: {
            valType: 'string',
            // string type usually doesn't take values... this one should really be
            // a special type or at least a special coercion function, from the GUI
            // you only get these values but elsewhere the user can supply a list of
            // dash lengths in px, and it will be honored
            values: ['solid', 'dot', 'dash', 'longdash', 'dashdot', 'longdashdot'],
            dflt: 'solid',
            role: 'style',
            description: [
                'Sets the style of the lines. Set to a dash string type',
                'or a dash length in px.'
            ].join(' ')
        }
    },
    connectgaps: {
        valType: 'boolean',
        dflt: false,
        role: 'info',
        description: [
            'Determines whether or not gaps',
            '(i.e. {nan} or missing values)',
            'in the provided data arrays are connected.'
        ].join(' ')
    },
    fill: {
        valType: 'enumerated',
        values: ['none', 'tozeroy', 'tozerox', 'tonexty', 'tonextx', 'toself', 'tonext'],
        dflt: 'none',
        role: 'style',
        description: [
            'Sets the area to fill with a solid color.',
            'Use with `fillcolor` if not *none*.',
            '*tozerox* and *tozeroy* fill to x=0 and y=0 respectively.',
            '*tonextx* and *tonexty* fill between the endpoints of this',
            'trace and the endpoints of the trace before it, connecting those',
            'endpoints with straight lines (to make a stacked area graph);',
            'if there is no trace before it, they behave like *tozerox* and',
            '*tozeroy*.',
            '*toself* connects the endpoints of the trace (or each segment',
            'of the trace if it has gaps) into a closed shape.',
            '*tonext* fills the space between two traces if one completely',
            'encloses the other (eg consecutive contour lines), and behaves like',
            '*toself* if there is no trace before it. *tonext* should not be',
            'used if one trace does not enclose the other.'
        ].join(' ')
    },
    fillcolor: {
        valType: 'color',
        role: 'style',
        description: [
            'Sets the fill color.',
            'Defaults to a half-transparent variant of the line color,',
            'marker color, or marker line color, whichever is available.'
        ].join(' ')
    },
    marker: {
        symbol: {
            valType: 'enumerated',
            values: Drawing.symbolList,
            dflt: 'circle',
            arrayOk: true,
            role: 'style',
            description: [
                'Sets the marker symbol type.',
                'Adding 100 is equivalent to appending *-open* to a symbol name.',
                'Adding 200 is equivalent to appending *-dot* to a symbol name.',
                'Adding 300 is equivalent to appending *-open-dot*',
                'or *dot-open* to a symbol name.'
            ].join(' ')
        },
        opacity: {
            valType: 'number',
            min: 0,
            max: 1,
            arrayOk: true,
            role: 'style',
            description: 'Sets the marker opacity.'
        },
        size: {
            valType: 'number',
            min: 0,
            dflt: 6,
            arrayOk: true,
            role: 'style',
            description: 'Sets the marker size (in px).'
        },
        color: {
            valType: 'color',
            arrayOk: true,
            role: 'style',
            description: [
                'Sets the marker color. It accepts either a specific color',
                'or an array of values that are mapped to the colorscale',
                'relative to the max and min values of the array or relative to',
                '`cmin` and `cmax` if set.'
            ].join(' ')
        },
        maxdisplayed: {
            valType: 'number',
            min: 0,
            dflt: 0,
            role: 'style',
            description: [
                'Sets a maximum number of points to be drawn on the graph.',
                '*0* corresponds to no limit.'
            ].join(' ')
        },
        sizeref: {
            valType: 'number',
            dflt: 1,
            role: 'style',
            description: [
                'Has an effect only if `marker.size` is set to a numerical array.',
                'Sets the scale factor used to determine the rendered size of',
                'marker points. Use with `sizemin` and `sizemode`.'
            ].join(' ')
        },
        sizemin: {
            valType: 'number',
            min: 0,
            dflt: 0,
            role: 'style',
            description: [
                'Has an effect only if `marker.size` is set to a numerical array.',
                'Sets the minimum size (in px) of the rendered marker points.'
            ].join(' ')
        },
        sizemode: {
            valType: 'enumerated',
            values: ['diameter', 'area'],
            dflt: 'diameter',
            role: 'info',
            description: [
                'Has an effect only if `marker.size` is set to a numerical array.',
                'Sets the rule for which the data in `size` is converted',
                'to pixels.'
            ].join(' ')
        },
        colorscale: {
            valType: 'colorscale',
            role: 'style',
            description: [
                'Sets the colorscale and only has an effect',
                'if `marker.color` is set to a numerical array.',
                'The colorscale must be an array containing',
                'arrays mapping a normalized value to an',
                'rgb, rgba, hex, hsl, hsv, or named color string.',
                'At minimum, a mapping for the lowest (0) and highest (1)',
                'values are required. For example,',
                '`[[0, \'rgb(0,0,255)\', [1, \'rgb(255,0,0)\']]`.',
                'To control the bounds of the colorscale in color space,',
                'use `marker.cmin` and `marker.cmax`.'
            ].join(' ')
        },
        cauto: {
            valType: 'boolean',
            dflt: true,
            role: 'style',
            description: [
                'Has an effect only if `marker.color` is set to a numerical array.',
                'Determines the whether or not the color domain is computed',
                'automatically.'
            ].join(' ')
        },
        cmax: {
            valType: 'number',
            dflt: null,
            role: 'info',
            description: [
                'Has an effect only if `marker.color` is set to a numerical array.',
                'Sets the upper bound of the color domain.',
                'Value should be associated to the `marker.color` array index,',
                'and if set, `marker.cmin` must be set as well.'
            ].join(' ')
        },
        cmin: {
            valType: 'number',
            dflt: null,
            role: 'info',
            description: [
                'Has an effect only if `marker.color` is set to a numerical array.',
                'Sets the lower bound of the color domain.',
                'Value should be associated to the `marker.color` array index,',
                'and if set, `marker.cmax` must be set as well.'
            ].join(' ')
        },
        autocolorscale: {
            valType: 'boolean',
            dflt: true,
            role: 'style',
            description: [
                'Has an effect only if `marker.color` is set to a numerical array.',
                'Determines whether or not the colorscale is picked using',
                'values inside `marker.color`.'
            ].join(' ')
        },
        reversescale: {
            valType: 'boolean',
            role: 'style',
            dflt: false,
            description: [
                'Has an effect only if `marker.color` is set to a numerical array.',
                'Reverses the colorscale.'
            ].join(' ')
        },
        showscale: {
            valType: 'boolean',
            role: 'info',
            dflt: false,
            description: [
                'Has an effect only if `marker.color` is set to a numerical array.',
                'Determines whether or not a colorbar is displayed.'
            ].join(' ')
        },
        line: {
            color: {
                valType: 'color',
                arrayOk: true,
                role: 'style',
                description: [
                    'Sets the marker outline color. It accepts either a specific color',
                    'or an array of values that are mapped to the colorscale',
                    'relative to the max and min values of the array or relative to',
                    '`cmin` and `cmax` if set.'
                ].join(' ')
            },
            width: {
                valType: 'number',
                min: 0,
                arrayOk: true,
                role: 'style',
                description: 'Sets the width (in px) of the lines bounding the marker points.'
            },
            colorscale: {
                valType: 'colorscale',
                role: 'style',
                description: [
                    'Sets the colorscale and only has an effect',
                    'if `marker.line.color` is set to a numerical array.',
                    'The colorscale must be an array containing',
                    'arrays mapping a normalized value to an',
                    'rgb, rgba, hex, hsl, hsv, or named color string.',
                    'At minimum, a mapping for the lowest (0) and highest (1)',
                    'values are required. For example,',
                    '`[[0, \'rgb(0,0,255)\', [1, \'rgb(255,0,0)\']]`.',
                    'To control the bounds of the colorscale in color space,',
                    'use `marker.line.cmin` and `marker.line.cmax`.'
                ].join(' ')
            },
            cauto: {
                valType: 'boolean',
                dflt: true,
                role: 'style',
                description: [
                    'Has an effect only if `marker.line.color` is set to a numerical array.',
                    'Determines the whether or not the color domain is computed',
                    'with respect to the input data.'
                ].join(' ')
            },
            cmax: {
                valType: 'number',
                dflt: null,
                role: 'info',
                description: [
                    'Has an effect only if `marker.line.color` is set to a numerical array.',
                    'Sets the upper bound of the color domain.',
                    'Value should be associated to the `marker.line.color` array index,',
                    'and if set, `marker.line.cmin` must be set as well.'
                ].join(' ')
            },
            cmin: {
                valType: 'number',
                dflt: null,
                role: 'info',
                description: [
                    'Has an effect only if `marker.line.color` is set to a numerical array.',
                    'Sets the lower bound of the color domain.',
                    'Value should be associated to the `marker.line.color` array index,',
                    'and if set, `marker.line.cmax` must be set as well.'
                ].join(' ')
            },
            autocolorscale: {
                valType: 'boolean',
                dflt: true,
                role: 'style',
                description: [
                    'Has an effect only if `marker.line.color` is set to a numerical array.',
                    'Determines whether or not the colorscale is picked using',
                    'the sign of values inside `marker.line.color`.'
                ].join(' ')
            },
            reversescale: {
                valType: 'boolean',
                dflt: false,
                role: 'style',
                description: [
                    'Has an effect only if `marker.line.color` is set to a numerical array.',
                    'Reverses the colorscale.'
                ].join(' ')
            }
        }
    },
    textposition: {
        valType: 'enumerated',
        values: [
            'top left', 'top center', 'top right',
            'middle left', 'middle center', 'middle right',
            'bottom left', 'bottom center', 'bottom right'
        ],
        dflt: 'middle center',
        arrayOk: true,
        role: 'style',
        description: [
            'Sets the positions of the `text` elements',
            'with respects to the (x,y) coordinates.'
        ].join(' ')
    },
    textfont: {
        family: {
            valType: 'string',
            role: 'style',
            noBlank: true,
            strict: true,
            arrayOk: true
        },
        size: {
            valType: 'number',
            role: 'style',
            min: 1,
            arrayOk: true
        },
        color: {
            valType: 'color',
            role: 'style',
            arrayOk: true
        },
        description: 'Sets the text font.'
    },
    r: {
        valType: 'data_array',
        description: [
            'For polar chart only.',
            'Sets the radial coordinates.'
        ].join('')
    },
    t: {
        valType: 'data_array',
        description: [
            'For polar chart only.',
            'Sets the angular coordinates.'
        ].join('')
    },
    _nestedModules: {  // nested module coupling
        'error_y': 'ErrorBars',
        'error_x': 'ErrorBars',
        'marker.colorbar': 'Colorbar'
    }
};
<commit_msg>581a dry colors - scatter<commit_after>/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var colorAttributes = require('../../components/colorscale/color_attributes');

var Drawing = require('../../components/drawing');

var constants = require('./constants');

var extendDeep = require('../../lib/extend').extendDeep;

module.exports = {
    x: {
        valType: 'data_array',
        description: 'Sets the x coordinates.'
    },
    x0: {
        valType: 'any',
        dflt: 0,
        role: 'info',
        description: [
            'Alternate to `x`.',
            'Builds a linear space of x coordinates.',
            'Use with `dx`',
            'where `x0` is the starting coordinate and `dx` the step.'
        ].join(' ')
    },
    dx: {
        valType: 'number',
        dflt: 1,
        role: 'info',
        description: [
            'Sets the x coordinate step.',
            'See `x0` for more info.'
        ].join(' ')
    },
    y: {
        valType: 'data_array',
        description: 'Sets the y coordinates.'
    },
    y0: {
        valType: 'any',
        dflt: 0,
        role: 'info',
        description: [
            'Alternate to `y`.',
            'Builds a linear space of y coordinates.',
            'Use with `dy`',
            'where `y0` is the starting coordinate and `dy` the step.'
        ].join(' ')
    },
    dy: {
        valType: 'number',
        dflt: 1,
        role: 'info',
        description: [
            'Sets the y coordinate step.',
            'See `y0` for more info.'
        ].join(' ')
    },
    text: {
        valType: 'string',
        role: 'info',
        dflt: '',
        arrayOk: true,
        description: [
            'Sets text elements associated with each (x,y) pair.',
            'If a single string, the same string appears over',
            'all the data points.',
            'If an array of string, the items are mapped in order to the',
            'this trace\'s (x,y) coordinates.'
        ].join(' ')
    },
    mode: {
        valType: 'flaglist',
        flags: ['lines', 'markers', 'text'],
        extras: ['none'],
        role: 'info',
        description: [
            'Determines the drawing mode for this scatter trace.',
            'If the provided `mode` includes *text* then the `text` elements',
            'appear at the coordinates. Otherwise, the `text` elements',
            'appear on hover.',
            'If there are less than ' + constants.PTS_LINESONLY + ' points,',
            'then the default is *lines+markers*. Otherwise, *lines*.'
        ].join(' ')
    },
    line: {
        color: {
            valType: 'color',
            role: 'style',
            description: 'Sets the line color.'
        },
        width: {
            valType: 'number',
            min: 0,
            dflt: 2,
            role: 'style',
            description: 'Sets the line width (in px).'
        },
        shape: {
            valType: 'enumerated',
            values: ['linear', 'spline', 'hv', 'vh', 'hvh', 'vhv'],
            dflt: 'linear',
            role: 'style',
            description: [
                'Determines the line shape.',
                'With *spline* the lines are drawn using spline interpolation.',
                'The other available values correspond to step-wise line shapes.'
            ].join(' ')
        },
        smoothing: {
            valType: 'number',
            min: 0,
            max: 1.3,
            dflt: 1,
            role: 'style',
            description: [
                'Has an effect only if `shape` is set to *spline*',
                'Sets the amount of smoothing.',
                '*0* corresponds to no smoothing (equivalent to a *linear* shape).'
            ].join(' ')
        },
        dash: {
            valType: 'string',
            // string type usually doesn't take values... this one should really be
            // a special type or at least a special coercion function, from the GUI
            // you only get these values but elsewhere the user can supply a list of
            // dash lengths in px, and it will be honored
            values: ['solid', 'dot', 'dash', 'longdash', 'dashdot', 'longdashdot'],
            dflt: 'solid',
            role: 'style',
            description: [
                'Sets the style of the lines. Set to a dash string type',
                'or a dash length in px.'
            ].join(' ')
        }
    },
    connectgaps: {
        valType: 'boolean',
        dflt: false,
        role: 'info',
        description: [
            'Determines whether or not gaps',
            '(i.e. {nan} or missing values)',
            'in the provided data arrays are connected.'
        ].join(' ')
    },
    fill: {
        valType: 'enumerated',
        values: ['none', 'tozeroy', 'tozerox', 'tonexty', 'tonextx', 'toself', 'tonext'],
        dflt: 'none',
        role: 'style',
        description: [
            'Sets the area to fill with a solid color.',
            'Use with `fillcolor` if not *none*.',
            '*tozerox* and *tozeroy* fill to x=0 and y=0 respectively.',
            '*tonextx* and *tonexty* fill between the endpoints of this',
            'trace and the endpoints of the trace before it, connecting those',
            'endpoints with straight lines (to make a stacked area graph);',
            'if there is no trace before it, they behave like *tozerox* and',
            '*tozeroy*.',
            '*toself* connects the endpoints of the trace (or each segment',
            'of the trace if it has gaps) into a closed shape.',
            '*tonext* fills the space between two traces if one completely',
            'encloses the other (eg consecutive contour lines), and behaves like',
            '*toself* if there is no trace before it. *tonext* should not be',
            'used if one trace does not enclose the other.'
        ].join(' ')
    },
    fillcolor: {
        valType: 'color',
        role: 'style',
        description: [
            'Sets the fill color.',
            'Defaults to a half-transparent variant of the line color,',
            'marker color, or marker line color, whichever is available.'
        ].join(' ')
    },
    marker: extendDeep({}, colorAttributes('marker'), {
        symbol: {
            valType: 'enumerated',
            values: Drawing.symbolList,
            dflt: 'circle',
            arrayOk: true,
            role: 'style',
            description: [
                'Sets the marker symbol type.',
                'Adding 100 is equivalent to appending *-open* to a symbol name.',
                'Adding 200 is equivalent to appending *-dot* to a symbol name.',
                'Adding 300 is equivalent to appending *-open-dot*',
                'or *dot-open* to a symbol name.'
            ].join(' ')
        },
        opacity: {
            valType: 'number',
            min: 0,
            max: 1,
            arrayOk: true,
            role: 'style',
            description: 'Sets the marker opacity.'
        },
        size: {
            valType: 'number',
            min: 0,
            dflt: 6,
            arrayOk: true,
            role: 'style',
            description: 'Sets the marker size (in px).'
        },
        maxdisplayed: {
            valType: 'number',
            min: 0,
            dflt: 0,
            role: 'style',
            description: [
                'Sets a maximum number of points to be drawn on the graph.',
                '*0* corresponds to no limit.'
            ].join(' ')
        },
        sizeref: {
            valType: 'number',
            dflt: 1,
            role: 'style',
            description: [
                'Has an effect only if `marker.size` is set to a numerical array.',
                'Sets the scale factor used to determine the rendered size of',
                'marker points. Use with `sizemin` and `sizemode`.'
            ].join(' ')
        },
        sizemin: {
            valType: 'number',
            min: 0,
            dflt: 0,
            role: 'style',
            description: [
                'Has an effect only if `marker.size` is set to a numerical array.',
                'Sets the minimum size (in px) of the rendered marker points.'
            ].join(' ')
        },
        sizemode: {
            valType: 'enumerated',
            values: ['diameter', 'area'],
            dflt: 'diameter',
            role: 'info',
            description: [
                'Has an effect only if `marker.size` is set to a numerical array.',
                'Sets the rule for which the data in `size` is converted',
                'to pixels.'
            ].join(' ')
        },
        showscale: {
            valType: 'boolean',
            role: 'info',
            dflt: false,
            description: [
                'Has an effect only if `marker.color` is set to a numerical array.',
                'Determines whether or not a colorbar is displayed.'
            ].join(' ')
        },
        line: extendDeep({}, colorAttributes('marker.line'), {
            width: {
                valType: 'number',
                min: 0,
                arrayOk: true,
                role: 'style',
                description: 'Sets the width (in px) of the lines bounding the marker points.'
            }
        })
    }),
    textposition: {
        valType: 'enumerated',
        values: [
            'top left', 'top center', 'top right',
            'middle left', 'middle center', 'middle right',
            'bottom left', 'bottom center', 'bottom right'
        ],
        dflt: 'middle center',
        arrayOk: true,
        role: 'style',
        description: [
            'Sets the positions of the `text` elements',
            'with respects to the (x,y) coordinates.'
        ].join(' ')
    },
    textfont: {
        family: {
            valType: 'string',
            role: 'style',
            noBlank: true,
            strict: true,
            arrayOk: true
        },
        size: {
            valType: 'number',
            role: 'style',
            min: 1,
            arrayOk: true
        },
        color: {
            valType: 'color',
            role: 'style',
            arrayOk: true
        },
        description: 'Sets the text font.'
    },
    r: {
        valType: 'data_array',
        description: [
            'For polar chart only.',
            'Sets the radial coordinates.'
        ].join('')
    },
    t: {
        valType: 'data_array',
        description: [
            'For polar chart only.',
            'Sets the angular coordinates.'
        ].join('')
    },
    _nestedModules: {  // nested module coupling
        'error_y': 'ErrorBars',
        'error_x': 'ErrorBars',
        'marker.colorbar': 'Colorbar'
    }
};
<|endoftext|>