<commit_before>// @flow

import type {
  CalendarQuery,
  FetchEntryInfosResponse,
  RawEntryInfo,
} from 'lib/types/entry-types';
import type { HistoryRevisionInfo } from 'lib/types/history-types';
import type { Viewer } from '../session/viewer';
import {
  threadPermissions,
  type ThreadPermission,
} from 'lib/types/thread-types';
import { calendarThreadFilterTypes } from 'lib/types/filter-types';

import invariant from 'invariant';

import { permissionLookup } from 'lib/permissions/thread-permissions';
import { ServerError } from 'lib/utils/errors';
import {
  filteredThreadIDs,
  filterExists,
} from 'lib/selectors/calendar-filter-selectors';
import { rawEntryInfoWithinCalendarQuery } from 'lib/shared/entry-utils';

import {
  dbQuery,
  SQL,
  SQLStatement,
  mergeAndConditions,
  mergeOrConditions,
} from '../database';

async function fetchEntryInfo(
  viewer: Viewer,
  entryID: string,
): Promise<?RawEntryInfo> {
  const results = await fetchEntryInfosByID(viewer, [ entryID ]);
  if (results.length === 0) {
    return null;
  }
  return results[0];
}

const visPermissionExtractString = `$.${threadPermissions.VISIBLE}.value`;
async function fetchEntryInfosByID(
  viewer: Viewer,
  entryIDs: $ReadOnlyArray<string>,
): Promise<RawEntryInfo[]> {
  if (entryIDs.length === 0) {
    return [];
  }
  const viewerID = viewer.id;
  const query = SQL`
    SELECT DAY(d.date) AS day, MONTH(d.date) AS month, YEAR(d.date) AS year,
      e.id, e.text, e.creation_time AS creationTime, d.thread AS threadID,
      e.deleted, e.creator AS creatorID
    FROM entries e
    LEFT JOIN days d ON d.id = e.day
    LEFT JOIN memberships m ON m.thread = d.thread AND m.user = ${viewerID}
    WHERE e.id IN (${entryIDs}) AND
      JSON_EXTRACT(m.permissions, ${visPermissionExtractString}) IS TRUE
  `;
  const [ result ] = await dbQuery(query);
  return result.map(row => ({
    id: row.id.toString(),
    threadID: row.threadID.toString(),
    text: row.text,
    year: row.year,
    month: row.month,
    day: row.day,
    creationTime: row.creationTime,
    creatorID: row.creatorID.toString(),
    deleted: !!row.deleted,
  }));
}

function sqlConditionForCalendarQuery(
  calendarQuery: CalendarQuery,
): ?SQLStatement {
  const { filters, startDate, endDate } = calendarQuery;
  const conditions = [];

  conditions.push(SQL`d.date BETWEEN ${startDate} AND ${endDate}`);

  const filterToThreadIDs = filteredThreadIDs(filters);
  if (filterToThreadIDs && filterToThreadIDs.size > 0) {
    conditions.push(SQL`d.thread IN (${[...filterToThreadIDs]})`);
  } else if (filterToThreadIDs) {
    // Filter to empty set means the result is empty
    return null;
  } else {
    conditions.push(SQL`m.role != 0`);
  }

  if (filterExists(filters, calendarThreadFilterTypes.NOT_DELETED)) {
    conditions.push(SQL`e.deleted = 0`);
  }

  return mergeAndConditions(conditions);
}

async function fetchEntryInfos(
  viewer: Viewer,
  calendarQueries: $ReadOnlyArray<CalendarQuery>,
): Promise<FetchEntryInfosResponse> {
  const queryConditions = calendarQueries
    .map(sqlConditionForCalendarQuery)
    .filter(condition => condition);
  if (queryConditions.length === 0) {
    return { rawEntryInfos: [], userInfos: {} };
  }
  const queryCondition = mergeOrConditions(queryConditions);

  const viewerID = viewer.id;
  const query = SQL`
    SELECT DAY(d.date) AS day, MONTH(d.date) AS month, YEAR(d.date) AS year,
      e.id, e.text, e.creation_time AS creationTime, d.thread AS threadID,
      e.deleted, e.creator AS creatorID, u.username AS creator
    FROM entries e
    LEFT JOIN days d ON d.id = e.day
    LEFT JOIN memberships m ON m.thread = d.thread AND m.user = ${viewerID}
    LEFT JOIN users u ON u.id = e.creator
    WHERE JSON_EXTRACT(m.permissions, ${visPermissionExtractString}) IS TRUE AND
  `;
  query.append(queryCondition);
  query.append(SQL`ORDER BY e.creation_time DESC`);
  const [ result ] = await dbQuery(query);

  const rawEntryInfos = [];
  const userInfos = {};
  for (let row of result) {
    const creatorID = row.creatorID.toString();
    rawEntryInfos.push({
      id: row.id.toString(),
      threadID: row.threadID.toString(),
      text: row.text,
      year: row.year,
      month: row.month,
      day: row.day,
      creationTime: row.creationTime,
      creatorID,
      deleted: !!row.deleted,
    });
    if (row.creator) {
      userInfos[creatorID] = {
        id: creatorID,
        username: row.creator,
      };
    }
  }
  return { rawEntryInfos, userInfos };
}

async function checkThreadPermissionForEntry(
  viewer: Viewer,
  entryID: string,
  permission: ThreadPermission,
): Promise<bool> {
  const viewerID = viewer.id;
  const query = SQL`
    SELECT m.permissions, t.id
    FROM entries e
    LEFT JOIN days d ON d.id = e.day
    LEFT JOIN threads t ON t.id = d.thread
    LEFT JOIN memberships m ON m.thread = t.id AND m.user = ${viewerID}
    WHERE e.id = ${entryID}
  `;
  const [ result ] = await dbQuery(query);

  if (result.length === 0) {
    return false;
  }
  const row = result[0];
  if (row.id === null) {
    return false;
  }
  return permissionLookup(row.permissions, permission);
}

async function fetchEntryRevisionInfo(
  viewer: Viewer,
  entryID: string,
): Promise<$ReadOnlyArray<HistoryRevisionInfo>> {
  const hasPermission = await checkThreadPermissionForEntry(
    viewer,
    entryID,
    threadPermissions.VISIBLE,
  );
  if (!hasPermission) {
    throw new ServerError('invalid_credentials');
  }

  const query = SQL`
    SELECT r.id, u.username AS author, r.text, r.last_update AS lastUpdate,
      r.deleted, d.thread AS threadID, r.entry AS entryID
    FROM revisions r
    LEFT JOIN users u ON u.id = r.author
    LEFT JOIN entries e ON e.id = r.entry
    LEFT JOIN days d ON d.id = e.day
    WHERE r.entry = ${entryID}
    ORDER BY r.last_update DESC
  `;
  const [ result ] = await dbQuery(query);

  const revisions = [];
  for (let row of result) {
    revisions.push({
      id: row.id.toString(),
      author: row.author,
      text: row.text,
      lastUpdate: row.lastUpdate,
      deleted: !!row.deleted,
      threadID: row.threadID.toString(),
      entryID: row.entryID.toString(),
    });
  }
  return revisions;
}

// calendarQueries are the "difference" queries we get from subtracting the old
// CalendarQuery from the new one. See calendarQueryDifference.
// oldCalendarQuery is the old CalendarQuery. We make sure none of the returned
// RawEntryInfos match the old CalendarQuery, so that only the difference is
// returned.
async function fetchEntriesForSession(
  viewer: Viewer,
  calendarQueries: $ReadOnlyArray<CalendarQuery>,
  oldCalendarQuery: CalendarQuery,
): Promise<FetchEntryInfosResponse> {
  const { rawEntryInfos, userInfos } = await fetchEntryInfos(
    viewer,
    calendarQueries,
  );
  const filteredRawEntryInfos = rawEntryInfos.filter(
    rawEntryInfo => !rawEntryInfoWithinCalendarQuery(
      rawEntryInfo,
      oldCalendarQuery,
    ),
  );
  const userIDs = new Set(filteredRawEntryInfos.map(
    rawEntryInfo => rawEntryInfo.creatorID,
  ));
  const filteredUserInfos = {};
  for (let userID in userInfos) {
    if (!userIDs.has(userID)) {
      continue;
    }
    filteredUserInfos[userID] = userInfos[userID];
  }
  return { rawEntryInfos: filteredRawEntryInfos, userInfos: filteredUserInfos };
}

export {
  fetchEntryInfo,
  fetchEntryInfosByID,
  fetchEntryInfos,
  checkThreadPermissionForEntry,
  fetchEntryRevisionInfo,
  fetchEntriesForSession,
};
<commit_msg>[server] Use usersInRawEntryInfos in fetchEntriesForSession<commit_after>// @flow

import type {
  CalendarQuery,
  FetchEntryInfosResponse,
  RawEntryInfo,
} from 'lib/types/entry-types';
import type { HistoryRevisionInfo } from 'lib/types/history-types';
import type { Viewer } from '../session/viewer';
import {
  threadPermissions,
  type ThreadPermission,
} from 'lib/types/thread-types';
import { calendarThreadFilterTypes } from 'lib/types/filter-types';

import invariant from 'invariant';

import { permissionLookup } from 'lib/permissions/thread-permissions';
import { ServerError } from 'lib/utils/errors';
import {
  filteredThreadIDs,
  filterExists,
} from 'lib/selectors/calendar-filter-selectors';
import {
  rawEntryInfoWithinCalendarQuery,
  usersInRawEntryInfos,
} from 'lib/shared/entry-utils';

import {
  dbQuery,
  SQL,
  SQLStatement,
  mergeAndConditions,
  mergeOrConditions,
} from '../database';

async function fetchEntryInfo(
  viewer: Viewer,
  entryID: string,
): Promise<?RawEntryInfo> {
  const results = await fetchEntryInfosByID(viewer, [ entryID ]);
  if (results.length === 0) {
    return null;
  }
  return results[0];
}

const visPermissionExtractString = `$.${threadPermissions.VISIBLE}.value`;
async function fetchEntryInfosByID(
  viewer: Viewer,
  entryIDs: $ReadOnlyArray<string>,
): Promise<RawEntryInfo[]> {
  if (entryIDs.length === 0) {
    return [];
  }
  const viewerID = viewer.id;
  const query = SQL`
    SELECT DAY(d.date) AS day, MONTH(d.date) AS month, YEAR(d.date) AS year,
      e.id, e.text, e.creation_time AS creationTime, d.thread AS threadID,
      e.deleted, e.creator AS creatorID
    FROM entries e
    LEFT JOIN days d ON d.id = e.day
    LEFT JOIN memberships m ON m.thread = d.thread AND m.user = ${viewerID}
    WHERE e.id IN (${entryIDs}) AND
      JSON_EXTRACT(m.permissions, ${visPermissionExtractString}) IS TRUE
  `;
  const [ result ] = await dbQuery(query);
  return result.map(row => ({
    id: row.id.toString(),
    threadID: row.threadID.toString(),
    text: row.text,
    year: row.year,
    month: row.month,
    day: row.day,
    creationTime: row.creationTime,
    creatorID: row.creatorID.toString(),
    deleted: !!row.deleted,
  }));
}

function sqlConditionForCalendarQuery(
  calendarQuery: CalendarQuery,
): ?SQLStatement {
  const { filters, startDate, endDate } = calendarQuery;
  const conditions = [];

  conditions.push(SQL`d.date BETWEEN ${startDate} AND ${endDate}`);

  const filterToThreadIDs = filteredThreadIDs(filters);
  if (filterToThreadIDs && filterToThreadIDs.size > 0) {
    conditions.push(SQL`d.thread IN (${[...filterToThreadIDs]})`);
  } else if (filterToThreadIDs) {
    // Filter to empty set means the result is empty
    return null;
  } else {
    conditions.push(SQL`m.role != 0`);
  }

  if (filterExists(filters, calendarThreadFilterTypes.NOT_DELETED)) {
    conditions.push(SQL`e.deleted = 0`);
  }

  return mergeAndConditions(conditions);
}

async function fetchEntryInfos(
  viewer: Viewer,
  calendarQueries: $ReadOnlyArray<CalendarQuery>,
): Promise<FetchEntryInfosResponse> {
  const queryConditions = calendarQueries
    .map(sqlConditionForCalendarQuery)
    .filter(condition => condition);
  if (queryConditions.length === 0) {
    return { rawEntryInfos: [], userInfos: {} };
  }
  const queryCondition = mergeOrConditions(queryConditions);

  const viewerID = viewer.id;
  const query = SQL`
    SELECT DAY(d.date) AS day, MONTH(d.date) AS month, YEAR(d.date) AS year,
      e.id, e.text, e.creation_time AS creationTime, d.thread AS threadID,
      e.deleted, e.creator AS creatorID, u.username AS creator
    FROM entries e
    LEFT JOIN days d ON d.id = e.day
    LEFT JOIN memberships m ON m.thread = d.thread AND m.user = ${viewerID}
    LEFT JOIN users u ON u.id = e.creator
    WHERE JSON_EXTRACT(m.permissions, ${visPermissionExtractString}) IS TRUE AND
  `;
  query.append(queryCondition);
  query.append(SQL`ORDER BY e.creation_time DESC`);
  const [ result ] = await dbQuery(query);

  const rawEntryInfos = [];
  const userInfos = {};
  for (let row of result) {
    const creatorID = row.creatorID.toString();
    rawEntryInfos.push({
      id: row.id.toString(),
      threadID: row.threadID.toString(),
      text: row.text,
      year: row.year,
      month: row.month,
      day: row.day,
      creationTime: row.creationTime,
      creatorID,
      deleted: !!row.deleted,
    });
    if (row.creator) {
      userInfos[creatorID] = {
        id: creatorID,
        username: row.creator,
      };
    }
  }
  return { rawEntryInfos, userInfos };
}

async function checkThreadPermissionForEntry(
  viewer: Viewer,
  entryID: string,
  permission: ThreadPermission,
): Promise<bool> {
  const viewerID = viewer.id;
  const query = SQL`
    SELECT m.permissions, t.id
    FROM entries e
    LEFT JOIN days d ON d.id = e.day
    LEFT JOIN threads t ON t.id = d.thread
    LEFT JOIN memberships m ON m.thread = t.id AND m.user = ${viewerID}
    WHERE e.id = ${entryID}
  `;
  const [ result ] = await dbQuery(query);

  if (result.length === 0) {
    return false;
  }
  const row = result[0];
  if (row.id === null) {
    return false;
  }
  return permissionLookup(row.permissions, permission);
}

async function fetchEntryRevisionInfo(
  viewer: Viewer,
  entryID: string,
): Promise<$ReadOnlyArray<HistoryRevisionInfo>> {
  const hasPermission = await checkThreadPermissionForEntry(
    viewer,
    entryID,
    threadPermissions.VISIBLE,
  );
  if (!hasPermission) {
    throw new ServerError('invalid_credentials');
  }

  const query = SQL`
    SELECT r.id, u.username AS author, r.text, r.last_update AS lastUpdate,
      r.deleted, d.thread AS threadID, r.entry AS entryID
    FROM revisions r
    LEFT JOIN users u ON u.id = r.author
    LEFT JOIN entries e ON e.id = r.entry
    LEFT JOIN days d ON d.id = e.day
    WHERE r.entry = ${entryID}
    ORDER BY r.last_update DESC
  `;
  const [ result ] = await dbQuery(query);

  const revisions = [];
  for (let row of result) {
    revisions.push({
      id: row.id.toString(),
      author: row.author,
      text: row.text,
      lastUpdate: row.lastUpdate,
      deleted: !!row.deleted,
      threadID: row.threadID.toString(),
      entryID: row.entryID.toString(),
    });
  }
  return revisions;
}

// calendarQueries are the "difference" queries we get from subtracting the old
// CalendarQuery from the new one. See calendarQueryDifference.
// oldCalendarQuery is the old CalendarQuery. We make sure none of the returned
// RawEntryInfos match the old CalendarQuery, so that only the difference is
// returned.
async function fetchEntriesForSession(
  viewer: Viewer,
  calendarQueries: $ReadOnlyArray<CalendarQuery>,
  oldCalendarQuery: CalendarQuery,
): Promise<FetchEntryInfosResponse> {
  const { rawEntryInfos, userInfos } = await fetchEntryInfos(
    viewer,
    calendarQueries,
  );
  const filteredRawEntryInfos = rawEntryInfos.filter(
    rawEntryInfo => !rawEntryInfoWithinCalendarQuery(
      rawEntryInfo,
      oldCalendarQuery,
    ),
  );
  const userIDs = new Set(usersInRawEntryInfos(filteredRawEntryInfos));
  const filteredUserInfos = {};
  for (let userID in userInfos) {
    if (!userIDs.has(userID)) {
      continue;
    }
    filteredUserInfos[userID] = userInfos[userID];
  }
  return { rawEntryInfos: filteredRawEntryInfos, userInfos: filteredUserInfos };
}

export {
  fetchEntryInfo,
  fetchEntryInfosByID,
  fetchEntryInfos,
  checkThreadPermissionForEntry,
  fetchEntryRevisionInfo,
  fetchEntriesForSession,
};
<|endoftext|>


<commit_before>/* global context */
import { View } from "react-native";
import React from "react";
import { shallow } from "enzyme";
import {
  StandardSlice,
  standardRoles,
  LeadAndTwoSlice,
  leadAndTwoRoles,
  OpinionAndTwoSlice,
  opinionAndTwoRoles
} from "../slice";

export default () => {
  const ExampleChild = () => <View />;
  ExampleChild.displayName = "ExampleChild";

  const createItem = id => <ExampleChild id={id} />;

  context("Standard template", () => {
    it("renders a single child element", () => {
      const wrapper = shallow(
        <StandardSlice
          itemCount={1}
          renderItems={() => [createItem("standard-1")]}
        />
      );
      expect(wrapper).toMatchSnapshot(
        "1. Standard template renders a single child element"
      );
    });

    it("renders two child elements", () => {
      const wrapper = shallow(
        <StandardSlice
          itemCount={2}
          renderItems={() => [
            createItem("standard-1"),
            createItem("standard-2")
          ]}
        />
      );
      expect(wrapper).toMatchSnapshot(
        "2. Standard template renders two child elements"
      );
    });

    it("renders three child elements", () => {
      const wrapper = shallow(
        <StandardSlice
          itemCount={3}
          renderItems={() => [
            createItem("standard-1"),
            createItem("standard-2"),
            createItem("standard-3")
          ]}
        />
      );
      expect(wrapper).toMatchSnapshot(
        "3. Standard template renders three child elements"
      );
    });
  });

  context("Lead and two template", () => {
    it("renders a single child element", () => {
      const wrapper = shallow(
        <LeadAndTwoSlice lead={() => createItem("lead")} />
      );
      expect(wrapper).toMatchSnapshot(
        "4. Lead and two template renders a single child element"
      );
    });

    it("renders two child elements", () => {
      const wrapper = shallow(
        <LeadAndTwoSlice
          lead={() => createItem("lead")}
          renderSupports={() => [createItem("support-1")]}
        />
      );
      expect(wrapper).toMatchSnapshot(
        "5. Lead and two template renders two child elements"
      );
    });

    it("renders three child elements", () => {
      const wrapper = shallow(
        <LeadAndTwoSlice
          lead={() => createItem("lead")}
          renderSupports={() => [
            createItem("support-1"),
            createItem("support-2")
          ]}
        />
      );
      expect(wrapper).toMatchSnapshot(
        "6. Lead and two template renders three child elements"
      );
    });
  });

  context("Opinion and two template", () => {
    it("renders a single child element", () => {
      const wrapper = shallow(
        <OpinionAndTwoSlice opinion={() => createItem("opinion")} />
      );
      expect(wrapper).toMatchSnapshot(
        "7. Opinion and two template renders a single child element"
      );
    });

    it("renders two child elements", () => {
      const wrapper = shallow(
        <OpinionAndTwoSlice
          opinion={() => createItem("opinion")}
          renderSupports={() => [createItem("support-1")]}
        />
      );
      expect(wrapper).toMatchSnapshot(
        "8. Opinion and two template renders two child elements"
      );
    });

    it("renders three child elements", () => {
      const wrapper = shallow(
        <OpinionAndTwoSlice
          opinion={() => createItem("opinion")}
          renderSupports={() => [
            createItem("support-1"),
            createItem("support-2")
          ]}
        />
      );
      expect(wrapper).toMatchSnapshot(
        "9. Opinion and two template renders three child elements"
      );
    });
  });

  context("Roles", () => {
    it("should return the standard roles for tracking", () => {
      expect(standardRoles).toMatchSnapshot("10. Standard tracking roles");
    });

    it("should return the lead and two roles for tracking", () => {
      expect(leadAndTwoRoles).toMatchSnapshot(
        "11. Lead and two tracking roles"
      );
    });

    it("should return the opinion and two roles for tracking", () => {
      expect(opinionAndTwoRoles).toMatchSnapshot(
        "12. Opinion and two tracking roles"
      );
    });
  });
};
<commit_msg>test: rename Slice package tests (#833)<commit_after>/* global context */
import { View } from "react-native";
import React from "react";
import { shallow } from "enzyme";
import {
  StandardSlice,
  standardRoles,
  LeadAndTwoSlice,
  leadAndTwoRoles,
  OpinionAndTwoSlice,
  opinionAndTwoRoles
} from "../slice";

export default () => {
  const ExampleChild = () => <View />;
  ExampleChild.displayName = "ExampleChild";

  const createItem = id => <ExampleChild id={id} />;

  context("Standard template", () => {
    it("should render a single child element", () => {
      const wrapper = shallow(
        <StandardSlice
          itemCount={1}
          renderItems={() => [createItem("standard-1")]}
        />
      );
      expect(wrapper).toMatchSnapshot(
        "1. Standard template renders a single child element"
      );
    });

    it("should render two child elements", () => {
      const wrapper = shallow(
        <StandardSlice
          itemCount={2}
          renderItems={() => [
            createItem("standard-1"),
            createItem("standard-2")
          ]}
        />
      );
      expect(wrapper).toMatchSnapshot(
        "2. Standard template renders two child elements"
      );
    });

    it("should render three child elements", () => {
      const wrapper = shallow(
        <StandardSlice
          itemCount={3}
          renderItems={() => [
            createItem("standard-1"),
            createItem("standard-2"),
            createItem("standard-3")
          ]}
        />
      );
      expect(wrapper).toMatchSnapshot(
        "3. Standard template renders three child elements"
      );
    });
  });

  context("Lead and two template", () => {
    it("should render a single child element", () => {
      const wrapper = shallow(
        <LeadAndTwoSlice lead={() => createItem("lead")} />
      );
      expect(wrapper).toMatchSnapshot(
        "4. Lead and two template renders a single child element"
      );
    });

    it("should render two child elements", () => {
      const wrapper = shallow(
        <LeadAndTwoSlice
          lead={() => createItem("lead")}
          renderSupports={() => [createItem("support-1")]}
        />
      );
      expect(wrapper).toMatchSnapshot(
        "5. Lead and two template renders two child elements"
      );
    });

    it("should render three child elements", () => {
      const wrapper = shallow(
        <LeadAndTwoSlice
          lead={() => createItem("lead")}
          renderSupports={() => [
            createItem("support-1"),
            createItem("support-2")
          ]}
        />
      );
      expect(wrapper).toMatchSnapshot(
        "6. Lead and two template renders three child elements"
      );
    });
  });

  context("Opinion and two template", () => {
    it("should render a single child element", () => {
      const wrapper = shallow(
        <OpinionAndTwoSlice opinion={() => createItem("opinion")} />
      );
      expect(wrapper).toMatchSnapshot(
        "7. Opinion and two template renders a single child element"
      );
    });

    it("should render two child elements", () => {
      const wrapper = shallow(
        <OpinionAndTwoSlice
          opinion={() => createItem("opinion")}
          renderSupports={() => [createItem("support-1")]}
        />
      );
      expect(wrapper).toMatchSnapshot(
        "8. Opinion and two template renders two child elements"
      );
    });

    it("should render three child elements", () => {
      const wrapper = shallow(
        <OpinionAndTwoSlice
          opinion={() => createItem("opinion")}
          renderSupports={() => [
            createItem("support-1"),
            createItem("support-2")
          ]}
        />
      );
      expect(wrapper).toMatchSnapshot(
        "9. Opinion and two template renders three child elements"
      );
    });
  });

  context("Roles", () => {
    it("should return the standard roles for tracking", () => {
      expect(standardRoles).toMatchSnapshot("10. Standard tracking roles");
    });

    it("should return the lead and two roles for tracking", () => {
      expect(leadAndTwoRoles).toMatchSnapshot(
        "11. Lead and two tracking roles"
      );
    });

    it("should return the opinion and two roles for tracking", () => {
      expect(opinionAndTwoRoles).toMatchSnapshot(
        "12. Opinion and two tracking roles"
      );
    });
  });
};
<|endoftext|>


<commit_before>var _ = require("underscore");

var input1 = {
	due_date: '2016-02-01T00:00:00Z',
	from_document: '',
	allow_online_payment: 'false',
	paid: 'true',
	locked: 'false',
	customer_id: '2446859',
	modified: '2016-02-02T15:09:06.983',
	created: '2016-02-01T21:09:37.873',
	date: '2016-02-01T00:00:00Z',
	inclusive: 'false',
	discount_percentage: '0',
	tax_reference: '',
	discount: '0',
	amount_due: '0',
	printed: 'false',
	editable: 'true',
	has_attachments: 'false',
	has_notes: 'false',
	has_anticipated_date: 'false',
	'lines[0][SelectionId]': '6824935',
	'lines[0][TaxTypeId]': '965302',
	'lines[0][ID]': '74465675',
	'lines[0][LineType]': '0',
	'lines[0][Quantity]': '1',
	'lines[0][Unit]': '',
	'lines[0][TaxPercentage]': '0.14',
	'lines[0][DiscountPercentage]': '0',
	'lines[0][Discount]': '0',
	'lines[1][ID]': '74465676',
	'lines[1][LineType]': '0',
	'lines[1][Quantity]': '4',
	'lines[1][Unit]': '',
	'lines[1][TaxPercentage]': '0.14',
	'lines[1][DiscountPercentage]': '0',
	'lines[1][Discount]': '0',
	'lines[1][Comments]': '',
	location: '547d506b184d110c0601314c',
	id: '93490358',
}

var test1 = { due_date: '2016-02-01T00:00:00Z',
  from_document: '',
  allow_online_payment: 'false',
  paid: 'true',
  locked: 'false',
  customer_id: '2446859',
  modified: '2016-02-02T15:09:06.983',
  created: '2016-02-01T21:09:37.873',
  date: '2016-02-01T00:00:00Z',
  inclusive: 'false',
  discount_percentage: '0',
  tax_reference: '',
  discount: '0',
  amount_due: '0',
  printed: 'false',
  editable: 'true',
  has_attachments: 'false',
  has_notes: 'false',
  has_anticipated_date: 'false',
  lines:
   [ { SelectionId: '6824935',
       TaxTypeId: '965302',
       ID: '74465675',
       LineType: '0',
       Quantity: '1',
       Unit: '',
       TaxPercentage: '0.14',
       DiscountPercentage: '0',
       Discount: '0' },
     { ID: '74465676',
       LineType: '0',
       Quantity: '4',
       Unit: '',
       TaxPercentage: '0.14',
       DiscountPercentage: '0',
       Discount: '0',
       Comments: '' } ],
  location: '547d506b184d110c0601314c',
  id: '93490358' }

input2 = {
	'test[0][Yo]': "0Yo",
	'test[0][No]': '0No',
	'test[1][Yo]': "1Yo",
	'test[1][No]': '1No',
	'test[3][No][Yo][0][Lo]': '2NoYo0Lo',
	normalProperty: 123
}

test2 = {
	test: [
		{
			Yo: "0Yo",
			No: "0No"
		},
		{
			Yo: "1Yo",
			No: "1No"
		},
		{
			No: {
				Yo: [
					{ Lo: "2NoYo0Lo" }
				]
			}
		}
	],
	normalProperty: 123
}

input3 = {
	'test[0][Yo]': "0Yo",
	'test[0][No]': '0No',
	'test[1][Yo]': "1Yo",
	'test[1][No]': '1No',
	normalProperty: 123
}

test3 = {
	test: [
		{
			Yo: "0Yo",
			No: "0No"
		},
		{
			Yo: "1Yo",
			No: "1No"
		}
	],
	normalProperty: 123
}

input4 = {
	'test[Blah][Yack]': 'BlahYack',
	'test[Blah][Blah]': 'BlahBlah'
}

test4 = {
	test: {
		Blah: {
			Yack: 'BlahYack',
			Blah: 'BlahBlah'
		}
	}
}

var arrayMatch = /\[\d+\]/;
var objMatch = /\[[a-zA-Z][a-zA-Z\d]*\]/;
var allMatch = /^[a-zA-Z\d_\-]+|\[[a-zA-Z_\-][a-zA-Z_\-\d]*\]|\[\d+\]/g;

/*  Copyright (C) 2012-2014  Kurt Milam - http://xioup.com | Source: https://gist.github.com/1868955
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
**/

// Based conceptually on the _.extend() function in underscore.js ( see http://documentcloud.github.com/underscore/#extend for more details )

function deepExtend(obj) {
  var parentRE = /#{\s*?_\s*?}/,
  slice = Array.prototype.slice;

  _.each(slice.call(arguments, 1), function(source) {
    for (var prop in source) {
      if (_.isUndefined(obj[prop]) || _.isFunction(obj[prop]) || _.isNull(source[prop]) || _.isDate(source[prop])) {
        obj[prop] = source[prop];
      }
      else if (_.isString(source[prop]) && parentRE.test(source[prop])) {
        if (_.isString(obj[prop])) {
          obj[prop] = source[prop].replace(parentRE, obj[prop]);
        }
      }
      else if (_.isArray(obj[prop]) || _.isArray(source[prop])){
        if (!_.isArray(obj[prop]) || !_.isArray(source[prop])){
          throw new Error('Trying to combine an array with a non-array (' + prop + ')');
        } else {
          obj[prop] = _.reject(_.deepExtend(_.clone(obj[prop]), source[prop]), function (item) { return _.isNull(item);});
        }
      }
      else if (_.isObject(obj[prop]) || _.isObject(source[prop])){
        if (!_.isObject(obj[prop]) || !_.isObject(source[prop])){
          throw new Error('Trying to combine an object with a non-object (' + prop + ')');
        } else {
          obj[prop] = _.deepExtend(_.clone(obj[prop]), source[prop]);
        }
      } else {
        obj[prop] = source[prop];
      }
    }
  });
  return obj;
};

_.mixin({ 'deepExtend': deepExtend });

/**
 * Dependency: underscore.js ( http://documentcloud.github.com/underscore/ )
 *
 * Mix it in with underscore.js:
 * _.mixin({deepExtend: deepExtend});
 *
 * Call it like this:
 * var myObj = _.deepExtend(grandparent, child, grandchild, greatgrandchild)
 *
 * Notes:
 * Keep it DRY.
 * This function is especially useful if you're working with JSON config documents. It allows you to create a default
 * config document with the most common settings, then override those settings for specific cases. It accepts any
 * number of objects as arguments, giving you fine-grained control over your config document hierarchy.
 *
 * Special Features and Considerations:
 * - parentRE allows you to concatenate strings. example:
 *   var obj = _.deepExtend({url: "www.example.com"}, {url: "http://#{_}/path/to/file.html"});
 *   console.log(obj.url);
 *   output: "http://www.example.com/path/to/file.html"
 *
 * - parentRE also acts as a placeholder, which can be useful when you need to change one value in an array, while
 *   leaving the others untouched. example:
 *   var arr = _.deepExtend([100,    {id: 1234}, true,  "foo",  [250, 500]],
 *                          ["#{_}", "#{_}",     false, "#{_}", "#{_}"]);
 *   console.log(arr);
 *   output: [100, {id: 1234}, false, "foo", [250, 500]]
 *
 * - The previous example can also be written like this:
 *   var arr = _.deepExtend([100,    {id:1234},   true,  "foo",  [250, 500]],
 *                          ["#{_}", {},          false, "#{_}", []]);
 *   console.log(arr);
 *   output: [100, {id: 1234}, false, "foo", [250, 500]]
 *
 * - And also like this:
 *   var arr = _.deepExtend([100,    {id:1234},   true,  "foo",  [250, 500]],
 *                          ["#{_}", {},          false]);
 *   console.log(arr);
 *   output: [100, {id: 1234}, false, "foo", [250, 500]]
 *
 * - Array order is important. example:
 *   var arr = _.deepExtend([1, 2, 3, 4], [1, 4, 3, 2]);
 *   console.log(arr);
 *   output: [1, 4, 3, 2]
 *
 * - You can remove an array element set in a parent object by setting the same index value to null in a child object.
 *   example:
 *   var obj = _.deepExtend({arr: [1, 2, 3, 4]}, {arr: ["#{_}", null]});
 *   console.log(obj.arr);
 *   output: [1, 3, 4]
 *
 **/

function isNumeric(n) {
	return !isNaN(parseFloat(n)) && isFinite(n);
}

function isArray(a) {
	return Array.isArray(a);
}

var assignPropVal = function(parts, result, val) {
	var part = parts.shift().replace(/\[|\]/g, "");
	if (parts.length) {
		if (isNumeric(parts[0].replace(/\[|\]/g, ""))) {
			result[part] = [];
		} else {
			result[part] = {};
		}
		assignPropVal(parts, result[part], val);
	} else {
		result[part] = val;
	}
	return result;
}

var deserialize = function(input) {
	console.log("INPUT", input);
	var result = {};
	var newobj = {};
	for (prop in input) {
		var parts = prop.match(allMatch);
		console.log(prop, parts);
		var val = input[prop];
		if (parts) {
			var tmp = assignPropVal(parts, result, val);
			newobj = _.deepExtend(newobj, tmp);
		} else {
			console.log("NOT ARRAY", parts);
			newobj[prop] = input[prop];
		}

	}
	// input = newobj;
	console.log("Munge!", newobj);
	return newobj;
}

var runTest = function(name, input, test) {
	var result = deserialize(input);
	if (_.isEqual(result, test)) {
		console.log("PASSED " + name);
	} else {
		console.log("FAILED " + name);
		console.log(result);
	}
}

var test = function() {
	runTest("Test 1", input1, test1);
	runTest("Test 2", input2, test2);
	runTest("Test 3", input3, test3);
	runTest("Test 4", input4, test4);
}

module.exports = {
	deserialize: deserialize
}<commit_msg>Object names can start with an integer<commit_after>var _ = require("underscore");

var input1 = {
	due_date: '2016-02-01T00:00:00Z',
	from_document: '',
	allow_online_payment: 'false',
	paid: 'true',
	locked: 'false',
	customer_id: '2446859',
	modified: '2016-02-02T15:09:06.983',
	created: '2016-02-01T21:09:37.873',
	date: '2016-02-01T00:00:00Z',
	inclusive: 'false',
	discount_percentage: '0',
	tax_reference: '',
	discount: '0',
	amount_due: '0',
	printed: 'false',
	editable: 'true',
	has_attachments: 'false',
	has_notes: 'false',
	has_anticipated_date: 'false',
	'lines[0][SelectionId]': '6824935',
	'lines[0][TaxTypeId]': '965302',
	'lines[0][ID]': '74465675',
	'lines[0][LineType]': '0',
	'lines[0][Quantity]': '1',
	'lines[0][Unit]': '',
	'lines[0][TaxPercentage]': '0.14',
	'lines[0][DiscountPercentage]': '0',
	'lines[0][Discount]': '0',
	'lines[1][ID]': '74465676',
	'lines[1][LineType]': '0',
	'lines[1][Quantity]': '4',
	'lines[1][Unit]': '',
	'lines[1][TaxPercentage]': '0.14',
	'lines[1][DiscountPercentage]': '0',
	'lines[1][Discount]': '0',
	'lines[1][Comments]': '',
	location: '547d506b184d110c0601314c',
	id: '93490358',
}

var test1 = { due_date: '2016-02-01T00:00:00Z',
  from_document: '',
  allow_online_payment: 'false',
  paid: 'true',
  locked: 'false',
  customer_id: '2446859',
  modified: '2016-02-02T15:09:06.983',
  created: '2016-02-01T21:09:37.873',
  date: '2016-02-01T00:00:00Z',
  inclusive: 'false',
  discount_percentage: '0',
  tax_reference: '',
  discount: '0',
  amount_due: '0',
  printed: 'false',
  editable: 'true',
  has_attachments: 'false',
  has_notes: 'false',
  has_anticipated_date: 'false',
  lines:
   [ { SelectionId: '6824935',
       TaxTypeId: '965302',
       ID: '74465675',
       LineType: '0',
       Quantity: '1',
       Unit: '',
       TaxPercentage: '0.14',
       DiscountPercentage: '0',
       Discount: '0' },
     { ID: '74465676',
       LineType: '0',
       Quantity: '4',
       Unit: '',
       TaxPercentage: '0.14',
       DiscountPercentage: '0',
       Discount: '0',
       Comments: '' } ],
  location: '547d506b184d110c0601314c',
  id: '93490358' }

input2 = {
	'test[0][Yo]': "0Yo",
	'test[0][No]': '0No',
	'test[1][Yo]': "1Yo",
	'test[1][No]': '1No',
	'test[3][No][Yo][0][Lo]': '2NoYo0Lo',
	normalProperty: 123
}

test2 = {
	test: [
		{
			Yo: "0Yo",
			No: "0No"
		},
		{
			Yo: "1Yo",
			No: "1No"
		},
		{
			No: {
				Yo: [
					{ Lo: "2NoYo0Lo" }
				]
			}
		}
	],
	normalProperty: 123
}

input3 = {
	'test[0][Yo]': "0Yo",
	'test[0][No]': '0No',
	'test[1][Yo]': "1Yo",
	'test[1][No]': '1No',
	normalProperty: 123
}

test3 = {
	test: [
		{
			Yo: "0Yo",
			No: "0No"
		},
		{
			Yo: "1Yo",
			No: "1No"
		}
	],
	normalProperty: 123
}

input4 = {
	'test[Blah][Yack]': 'BlahYack',
	'test[Blah][Blah]': 'BlahBlah'
}

test4 = {
	test: {
		Blah: {
			Yack: 'BlahYack',
			Blah: 'BlahBlah'
		}
	}
}

var arrayMatch = /\[\d+\]/;
var objMatch = /\[[a-zA-Z][a-zA-Z\d]*\]/;
var allMatch = /^[a-zA-Z\d_\-]+|\[[a-zA-Z\d_\-][a-zA-Z_\-\d]*\]|\[\d+\]/g;

/*  Copyright (C) 2012-2014  Kurt Milam - http://xioup.com | Source: https://gist.github.com/1868955
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
**/

// Based conceptually on the _.extend() function in underscore.js ( see http://documentcloud.github.com/underscore/#extend for more details )

function deepExtend(obj) {
  var parentRE = /#{\s*?_\s*?}/,
  slice = Array.prototype.slice;

  _.each(slice.call(arguments, 1), function(source) {
    for (var prop in source) {
      if (_.isUndefined(obj[prop]) || _.isFunction(obj[prop]) || _.isNull(source[prop]) || _.isDate(source[prop])) {
        obj[prop] = source[prop];
      }
      else if (_.isString(source[prop]) && parentRE.test(source[prop])) {
        if (_.isString(obj[prop])) {
          obj[prop] = source[prop].replace(parentRE, obj[prop]);
        }
      }
      else if (_.isArray(obj[prop]) || _.isArray(source[prop])){
        if (!_.isArray(obj[prop]) || !_.isArray(source[prop])){
          throw new Error('Trying to combine an array with a non-array (' + prop + ')');
        } else {
          obj[prop] = _.reject(_.deepExtend(_.clone(obj[prop]), source[prop]), function (item) { return _.isNull(item);});
        }
      }
      else if (_.isObject(obj[prop]) || _.isObject(source[prop])){
        if (!_.isObject(obj[prop]) || !_.isObject(source[prop])){
          throw new Error('Trying to combine an object with a non-object (' + prop + ')');
        } else {
          obj[prop] = _.deepExtend(_.clone(obj[prop]), source[prop]);
        }
      } else {
        obj[prop] = source[prop];
      }
    }
  });
  return obj;
};

_.mixin({ 'deepExtend': deepExtend });

/**
 * Dependency: underscore.js ( http://documentcloud.github.com/underscore/ )
 *
 * Mix it in with underscore.js:
 * _.mixin({deepExtend: deepExtend});
 *
 * Call it like this:
 * var myObj = _.deepExtend(grandparent, child, grandchild, greatgrandchild)
 *
 * Notes:
 * Keep it DRY.
 * This function is especially useful if you're working with JSON config documents. It allows you to create a default
 * config document with the most common settings, then override those settings for specific cases. It accepts any
 * number of objects as arguments, giving you fine-grained control over your config document hierarchy.
 *
 * Special Features and Considerations:
 * - parentRE allows you to concatenate strings. example:
 *   var obj = _.deepExtend({url: "www.example.com"}, {url: "http://#{_}/path/to/file.html"});
 *   console.log(obj.url);
 *   output: "http://www.example.com/path/to/file.html"
 *
 * - parentRE also acts as a placeholder, which can be useful when you need to change one value in an array, while
 *   leaving the others untouched. example:
 *   var arr = _.deepExtend([100,    {id: 1234}, true,  "foo",  [250, 500]],
 *                          ["#{_}", "#{_}",     false, "#{_}", "#{_}"]);
 *   console.log(arr);
 *   output: [100, {id: 1234}, false, "foo", [250, 500]]
 *
 * - The previous example can also be written like this:
 *   var arr = _.deepExtend([100,    {id:1234},   true,  "foo",  [250, 500]],
 *                          ["#{_}", {},          false, "#{_}", []]);
 *   console.log(arr);
 *   output: [100, {id: 1234}, false, "foo", [250, 500]]
 *
 * - And also like this:
 *   var arr = _.deepExtend([100,    {id:1234},   true,  "foo",  [250, 500]],
 *                          ["#{_}", {},          false]);
 *   console.log(arr);
 *   output: [100, {id: 1234}, false, "foo", [250, 500]]
 *
 * - Array order is important. example:
 *   var arr = _.deepExtend([1, 2, 3, 4], [1, 4, 3, 2]);
 *   console.log(arr);
 *   output: [1, 4, 3, 2]
 *
 * - You can remove an array element set in a parent object by setting the same index value to null in a child object.
 *   example:
 *   var obj = _.deepExtend({arr: [1, 2, 3, 4]}, {arr: ["#{_}", null]});
 *   console.log(obj.arr);
 *   output: [1, 3, 4]
 *
 **/

function isNumeric(n) {
	return !isNaN(parseFloat(n)) && isFinite(n);
}

function isArray(a) {
	return Array.isArray(a);
}

var assignPropVal = function(parts, result, val) {
	var part = parts.shift().replace(/\[|\]/g, "");
	if (parts.length) {
		if (isNumeric(parts[0].replace(/\[|\]/g, ""))) {
			result[part] = [];
		} else {
			result[part] = {};
		}
		assignPropVal(parts, result[part], val);
	} else {
		result[part] = val;
	}
	return result;
}

var deserialize = function(input) {
	// console.log("INPUT", input);
	var result = {};
	var newobj = {};
	for (prop in input) {
		var parts = prop.match(allMatch);
		// console.log(prop, parts);
		var val = input[prop];
		if (parts) {
			var tmp = assignPropVal(parts, result, val);
			newobj = _.deepExtend(newobj, tmp);
		} else {
			// console.log("NOT ARRAY", parts);
			newobj[prop] = input[prop];
		}

	}
	// input = newobj;
	// console.log("Munge!", newobj);
	return newobj;
}

var runTest = function(name, input, test) {
	var result = deserialize(input);
	if (_.isEqual(result, test)) {
		console.log("PASSED " + name);
	} else {
		console.log("FAILED " + name);
		console.log(result);
	}
}

var test = function() {
	runTest("Test 1", input1, test1);
	runTest("Test 2", input2, test2);
	runTest("Test 3", input3, test3);
	runTest("Test 4", input4, test4);
}

module.exports = {
	deserialize: deserialize
}<|endoftext|>


<commit_before>define("dijit/form/_DateTimeTextBox", ["dojo", "dijit", "text!dijit/form/templates/DropDownBox.html", "dojo/date", "dojo/date/locale", "dojo/date/stamp", "dijit/form/ValidationTextBox", "dijit/_HasDropDown"], function(dojo, dijit) {

new Date("X"); // workaround for #11279, new Date("") == NaN

/*=====
dojo.declare(
	"dijit.form._DateTimeTextBox.__Constraints",
	[dijit.form.RangeBoundTextBox.__Constraints, dojo.date.locale.__FormatOptions], {
	// summary:
	//		Specifies both the rules on valid/invalid values (first/last date/time allowed),
	//		and also formatting options for how the date/time is displayed.
	// example:
	//		To restrict to dates within 2004, displayed in a long format like "December 25, 2005":
	//	|		{min:'2004-01-01',max:'2004-12-31', formatLength:'long'}
});
=====*/

dojo.declare(
	"dijit.form._DateTimeTextBox",
	[ dijit.form.RangeBoundTextBox, dijit._HasDropDown ],
	{
		// summary:
		//		Base class for validating, serializable, range-bound date or time text box.

		templateString: dojo.cache("dijit.form", "templates/DropDownBox.html"),

		// hasDownArrow: [const] Boolean
		//		Set this textbox to display a down arrow button, to open the drop down list.
		hasDownArrow: true,

		// openOnClick: Boolean
		//		Set to true to open drop down upon clicking anywhere on the textbox.
		openOnClick: true,

		/*=====
		// constraints: dijit.form._DateTimeTextBox.__Constraints
		//		Despite the name, this parameter specifies both constraints on the input
		//		(including starting/ending dates/times allowed) as well as
		//		formatting options like whether the date is displayed in long (ex: December 25, 2005)
		//		or short (ex: 12/25/2005) format.  See `dijit.form._DateTimeTextBox.__Constraints` for details.
		constraints: {},
		======*/

		// Override ValidationTextBox.regExpGen().... we use a reg-ex generating function rather
		// than a straight regexp to deal with locale  (plus formatting options too?)
		regExpGen: dojo.date.locale.regexp,

		// datePackage: String
		//		JavaScript namespace to find calendar routines.	 Uses Gregorian calendar routines
		//		at dojo.date, by default.
		datePackage: "dojo.date",

		// Override _FormWidget.compare() to work for dates/times
		compare: function(/*Date*/ val1, /*Date*/ val2){
			return dojo.date.compare(val1, val2, this._selector);
		},

		// flag to _HasDropDown to make drop down Calendar width == <input> width
		forceWidth: true,

		format: function(/*Date*/ value, /*dojo.date.locale.__FormatOptions*/ constraints){
			// summary:
			//		Formats the value as a Date, according to specified locale (second argument)
			// tags:
			//		protected
			if(!value){ return ''; }
			return this.dateLocaleModule.format(value, constraints);
		},

		"parse": function(/*String*/ value, /*dojo.date.locale.__FormatOptions*/ constraints){
			// summary:
			//		Parses as string as a Date, according to constraints
			// tags:
			//		protected

			return this.dateLocaleModule.parse(value, constraints) || (this._isEmpty(value) ? null : undefined);	 // Date
		},

		// Overrides ValidationTextBox.serialize() to serialize a date in canonical ISO format.
		serialize: function(/*anything*/ val, /*Object?*/ options){
			if(val.toGregorian){
				val = val.toGregorian();
			}
			return dojo.date.stamp.toISOString(val, options);
		},

		// dropDownDefaultValue: Date
		//		The default value to focus in the popupClass widget when the textbox value is empty.
		dropDownDefaultValue : new Date(),

		// value: Date
		//		The value of this widget as a JavaScript Date object.  Use get("value") / set("value", val) to manipulate.
		//		When passed to the parser in markup, must be specified according to `dojo.date.stamp.fromISOString`
		value: new Date(""),	// value.toString()="NaN"

		_blankValue: null,	// used by filter() when the textbox is blank

		// popupClass: [protected extension] String
		//		Name of the popup widget class used to select a date/time.
		//		Subclasses should specify this.
		popupClass: "", // default is no popup = text only

		// _selector: [protected extension] String
		//		Specifies constraints.selector passed to dojo.date functions, should be either
		//		"date" or "time".
		//		Subclass must specify this.
		_selector: "",

		constructor: function(/*Object*/ args){
			var dateClass = args.datePackage ? args.datePackage + ".Date" : "Date";
			this.dateClassObj = dojo.getObject(dateClass, false);
			this.value = new this.dateClassObj("");

			this.datePackage = args.datePackage || this.datePackage;
			this.dateLocaleModule = dojo.getObject(this.datePackage + ".locale", false);
			this.regExpGen = this.dateLocaleModule.regexp;
			this._invalidDate = dijit.form._DateTimeTextBox.prototype.value.toString();
		},

		buildRendering: function(){
			this.inherited(arguments);

			// If openOnClick is true, we basically just want to treat the whole widget as the
			// button.  We need to do that also if the actual drop down button will be hidden,
			// so that there's a mouse method for opening the drop down.
			if(this.openOnClick || !this.hasDownArrow){
				this._buttonNode = this.domNode;
			}

			if(!this.hasDownArrow){
				this._buttonNode.style.display = "none";
			}
		},

		_setConstraintsAttr: function(/*Object*/ constraints){
			constraints.selector = this._selector;
			constraints.fullYear = true; // see #5465 - always format with 4-digit years
			var fromISO = dojo.date.stamp.fromISOString;
			if(typeof constraints.min == "string"){ constraints.min = fromISO(constraints.min); }
 			if(typeof constraints.max == "string"){ constraints.max = fromISO(constraints.max); }
			this.inherited(arguments, [constraints]);
		},

		_isInvalidDate: function(/*Date*/ value){
			// summary:
			//		Runs various tests on the value, checking for invalid conditions
			// tags:
			//		private
			return !value || isNaN(value) || typeof value != "object" || value.toString() == this._invalidDate;
		},

		_setValueAttr: function(/*Date|String*/ value, /*Boolean?*/ priorityChange, /*String?*/ formattedValue){
			// summary:
			//		Sets the date on this textbox. Note: value can be a JavaScript Date literal or a string to be parsed.
			if(value !== undefined){
				if(typeof value == "string"){
					value = dojo.date.stamp.fromISOString(value);
				}
				if(this._isInvalidDate(value)){
					value = null;
				}
				if(value instanceof Date && !(this.dateClassObj instanceof Date)){
					value = new this.dateClassObj(value);
				}
			}
			this.inherited(arguments, [value, priorityChange, formattedValue]);
			if(this.dropDown){
				this.dropDown.set('value', value, false);
			}
		},

		_set: function(attr, value){
			// Avoid spurious watch() notifications when value is changed to new Date object w/the same value
			if(attr == "value" && this.value instanceof Date && this.compare(value, this.value) == 0){
				return;
			}
			this.inherited(arguments);
		},

		_setDropDownDefaultValueAttr: function(/*Date*/ val){
			if(this._isInvalidDate(val)){
				// convert null setting into today's date, since there needs to be *some* default at all times.
				 val = new this.dateClassObj()
						}
			this.dropDownDefaultValue = val;
		},

		openDropDown: function(/*Function*/ callback){
			// rebuild drop down every time, so that constraints get copied (#6002)
			if(this.dropDown){
				this.dropDown.destroy();
			}
			var PopupProto = dojo.getObject(this.popupClass, false),
				textBox = this,
				value = this.get("value");
			this.dropDown = new PopupProto({
				onChange: function(value){
						// this will cause InlineEditBox and other handlers to do stuff so make sure it's last
						dijit.form._DateTimeTextBox.superclass._setValueAttr.call(textBox, value, true);
					},
					id: this.id + "_popup",
					dir: textBox.dir,
					lang: textBox.lang,
				value: value,
				currentFocus: !this._isInvalidDate(value) ? value : this.dropDownDefaultValue,
					constraints: textBox.constraints,
				filterString: textBox.filterString, // for TimeTextBox, to filter times shown

					datePackage: textBox.datePackage,

					isDisabledDate: function(/*Date*/ date){
						// summary:
						// 	disables dates outside of the min/max of the _DateTimeTextBox
						return !textBox.rangeCheck(date, textBox.constraints);
					}
				});

			this.inherited(arguments);
		},

		_getDisplayedValueAttr: function(){
			return this.textbox.value;
		},

		_setDisplayedValueAttr: function(/*String*/ value, /*Boolean?*/ priorityChange){
			this._setValueAttr(this.parse(value, this.constraints), priorityChange, value);
		}
	}
);

return dijit.form._DateTimeTextBox;
});
<commit_msg>Refs #11289 Fixes multiple bugs with the dojox.form.DateTextBox widget.  The base digit classes on which it is built changed the names of some private methods, breaking the dojox equivalents.  This fixes that, as well as making the widgets more fault tolerant when their values are set from a text box<commit_after>define("dijit/form/_DateTimeTextBox", ["dojo", "dijit", "text!dijit/form/templates/DropDownBox.html", "dojo/date", "dojo/date/locale", "dojo/date/stamp", "dijit/form/ValidationTextBox", "dijit/_HasDropDown"], function(dojo, dijit) {

new Date("X"); // workaround for #11279, new Date("") == NaN

/*=====
dojo.declare(
	"dijit.form._DateTimeTextBox.__Constraints",
	[dijit.form.RangeBoundTextBox.__Constraints, dojo.date.locale.__FormatOptions], {
	// summary:
	//		Specifies both the rules on valid/invalid values (first/last date/time allowed),
	//		and also formatting options for how the date/time is displayed.
	// example:
	//		To restrict to dates within 2004, displayed in a long format like "December 25, 2005":
	//	|		{min:'2004-01-01',max:'2004-12-31', formatLength:'long'}
});
=====*/

dojo.declare(
	"dijit.form._DateTimeTextBox",
	[ dijit.form.RangeBoundTextBox, dijit._HasDropDown ],
	{
		// summary:
		//		Base class for validating, serializable, range-bound date or time text box.

		templateString: dojo.cache("dijit.form", "templates/DropDownBox.html"),

		// hasDownArrow: [const] Boolean
		//		Set this textbox to display a down arrow button, to open the drop down list.
		hasDownArrow: true,

		// openOnClick: Boolean
		//		Set to true to open drop down upon clicking anywhere on the textbox.
		openOnClick: true,

		/*=====
		// constraints: dijit.form._DateTimeTextBox.__Constraints
		//		Despite the name, this parameter specifies both constraints on the input
		//		(including starting/ending dates/times allowed) as well as
		//		formatting options like whether the date is displayed in long (ex: December 25, 2005)
		//		or short (ex: 12/25/2005) format.  See `dijit.form._DateTimeTextBox.__Constraints` for details.
		constraints: {},
		======*/

		// Override ValidationTextBox.regExpGen().... we use a reg-ex generating function rather
		// than a straight regexp to deal with locale  (plus formatting options too?)
		regExpGen: dojo.date.locale.regexp,

		// datePackage: String
		//		JavaScript namespace to find calendar routines.	 Uses Gregorian calendar routines
		//		at dojo.date, by default.
		datePackage: "dojo.date",

		// Override _FormWidget.compare() to work for dates/times
		compare: function(/*Date*/ val1, /*Date*/ val2){
			return dojo.date.compare(val1, val2, this._selector);
		},

		// flag to _HasDropDown to make drop down Calendar width == <input> width
		forceWidth: true,

		format: function(/*Date*/ value, /*dojo.date.locale.__FormatOptions*/ constraints){
			// summary:
			//		Formats the value as a Date, according to specified locale (second argument)
			// tags:
			//		protected
			if(!value){ return ''; }
			return this.dateLocaleModule.format(value, constraints);
		},

		"parse": function(/*String*/ value, /*dojo.date.locale.__FormatOptions*/ constraints){
			// summary:
			//		Parses as string as a Date, according to constraints
			// tags:
			//		protected

			return this.dateLocaleModule.parse(value, constraints) || (this._isEmpty(value) ? null : undefined);	 // Date
		},

		// Overrides ValidationTextBox.serialize() to serialize a date in canonical ISO format.
		serialize: function(/*anything*/ val, /*Object?*/ options){
			if(val.toGregorian){
				val = val.toGregorian();
			}
			return dojo.date.stamp.toISOString(val, options);
		},

		// dropDownDefaultValue: Date
		//		The default value to focus in the popupClass widget when the textbox value is empty.
		dropDownDefaultValue : new Date(),

		// value: Date
		//		The value of this widget as a JavaScript Date object.  Use get("value") / set("value", val) to manipulate.
		//		When passed to the parser in markup, must be specified according to `dojo.date.stamp.fromISOString`
		value: new Date(""),	// value.toString()="NaN"

		_blankValue: null,	// used by filter() when the textbox is blank

		// popupClass: [protected extension] String
		//		Name of the popup widget class used to select a date/time.
		//		Subclasses should specify this.
		popupClass: "", // default is no popup = text only

		// _selector: [protected extension] String
		//		Specifies constraints.selector passed to dojo.date functions, should be either
		//		"date" or "time".
		//		Subclass must specify this.
		_selector: "",

		constructor: function(/*Object*/ args){
			var dateClass = args.datePackage ? args.datePackage + ".Date" : "Date";
			this.dateClassObj = dojo.getObject(dateClass, false);
			this.value = new this.dateClassObj("");

			this.datePackage = args.datePackage || this.datePackage;
			this.dateLocaleModule = dojo.getObject(this.datePackage + ".locale", false);
			this.regExpGen = this.dateLocaleModule.regexp;
			this._invalidDate = dijit.form._DateTimeTextBox.prototype.value.toString();
		},

		buildRendering: function(){
			this.inherited(arguments);

			// If openOnClick is true, we basically just want to treat the whole widget as the
			// button.  We need to do that also if the actual drop down button will be hidden,
			// so that there's a mouse method for opening the drop down.
			if(this.openOnClick || !this.hasDownArrow){
				this._buttonNode = this.domNode;
			}

			if(!this.hasDownArrow){
				this._buttonNode.style.display = "none";
			}
		},

		_setConstraintsAttr: function(/*Object*/ constraints){
			constraints.selector = this._selector;
			constraints.fullYear = true; // see #5465 - always format with 4-digit years
			var fromISO = dojo.date.stamp.fromISOString;
			if(typeof constraints.min == "string"){ constraints.min = fromISO(constraints.min); }
 			if(typeof constraints.max == "string"){ constraints.max = fromISO(constraints.max); }
			this.inherited(arguments, [constraints]);
		},

		_isInvalidDate: function(/*Date*/ value){
			// summary:
			//		Runs various tests on the value, checking for invalid conditions
			// tags:
			//		private
			return !value || isNaN(value) || typeof value != "object" || value.toString() == this._invalidDate;
		},

		_setValueAttr: function(/*Date|String*/ value, /*Boolean?*/ priorityChange, /*String?*/ formattedValue){
			// summary:
			//		Sets the date on this textbox. Note: value can be a JavaScript Date literal or a string to be parsed.
			if(value !== undefined){
				if(typeof value == "string"){
					value = dojo.date.stamp.fromISOString(value);
				}
				if(this._isInvalidDate(value)){
					value = null;
				}
				if(value instanceof Date && !(this.dateClassObj instanceof Date)){
					value = new this.dateClassObj(value);
				}
			}
			this.inherited(arguments, [value, priorityChange, formattedValue]);
			if(this.dropDown){
				this.dropDown.set('value', value, false);
			}
		},

		_set: function(attr, value){
			// Avoid spurious watch() notifications when value is changed to new Date object w/the same value
			if(attr == "value"
				&& this.value instanceof Date
				&& ((this._isInvalidDate(this.value) && this._isInvalidDate(value))
				  	|| this.compare(value, this.value) == 0)){
				return;
			}
			this.inherited(arguments);
		},

		_setDropDownDefaultValueAttr: function(/*Date*/ val){
			if(this._isInvalidDate(val)){
				// convert null setting into today's date, since there needs to be *some* default at all times.
				 val = new this.dateClassObj()
						}
			this.dropDownDefaultValue = val;
		},

		openDropDown: function(/*Function*/ callback){
			// rebuild drop down every time, so that constraints get copied (#6002)
			if(this.dropDown){
				this.dropDown.destroy();
			}
			var PopupProto = dojo.getObject(this.popupClass, false),
				textBox = this,
				value = this.get("value");
			this.dropDown = new PopupProto({
				onChange: function(value){
						// this will cause InlineEditBox and other handlers to do stuff so make sure it's last
						dijit.form._DateTimeTextBox.superclass._setValueAttr.call(textBox, value, true);
					},
					id: this.id + "_popup",
					dir: textBox.dir,
					lang: textBox.lang,
				value: value,
				currentFocus: !this._isInvalidDate(value) ? value : this.dropDownDefaultValue,
					constraints: textBox.constraints,
				filterString: textBox.filterString, // for TimeTextBox, to filter times shown

					datePackage: textBox.datePackage,

					isDisabledDate: function(/*Date*/ date){
						// summary:
						// 	disables dates outside of the min/max of the _DateTimeTextBox
						return !textBox.rangeCheck(date, textBox.constraints);
					}
				});

			this.inherited(arguments);
		},

		_getDisplayedValueAttr: function(){
			return this.textbox.value;
		},

		_setDisplayedValueAttr: function(/*String*/ value, /*Boolean?*/ priorityChange){
			this._setValueAttr(this.parse(value, this.constraints), priorityChange, value);
		}
	}
);

return dijit.form._DateTimeTextBox;
});
<|endoftext|>


<commit_before>(function() {
  class Storage {
    constructor(prefix) {
      this.PREFIX = prefix || "";
      this.META = "META/";
      this.EXIPIRY = '__expiry/'; // Key used for expiration storage
      this.default_expire_ms = 10*60*1000; // ten minutes
      this.defaults = {}; // table with default values
      this._schema = {};
      if (!this.test_supported()) {
        console.warn("Storage not supported, falling back to dummy storage");
        this.FAKE_STORAGE = {};
        this.set = function(key,value) { this.FAKE_STORAGE[key] = value; }
        this.get = function(key) { return this.FAKE_STORAGE[key]; }
        this.has = function(key) { this.FAKE_STORAGE.hasOwnProperty(key); }
        this.remove = function(key) { delete this.FAKE_STORAGE[key]; }
      }
      this.times = this.get(this.META+"times") || {};
      this.keys = this.get(this.META+"keys") || [];
    }

    _(key) { return this.PREFIX + key; }
    _getItem(key) { return localStorage.getItem(this._(key)); }
    _setItem(key,value) { return localStorage.setItem(this._(key),value); }
    _removeItem(key) { return localStorage.removeItem(this._(key)); }
    _hasOwnProperty(key) { return localStorage.hasOwnProperty(this._(key)) }

    get(key) {
      // pull a json from local storage or get an object from the defaults dict
      var value;
      if (this._hasOwnProperty(key)) {
        try { value = JSON.parse(this._getItem(key)); }
        catch(e) { } // we only allow JSON here, so parse errors can be ignored
      } else if (this.defaults.hasOwnProperty(key)) {
        value = this.defaults[key];
      }
      return value
    }

    update(data) {
      for (var key in data) {
        if (data.hasOwnProperty(key)) { this.set(key,data[key]) }
      }
    }

    set(key,value) {
      // store stringified json in localstorage
      if (!value && value !== 0) { value = undefined; } // null causes issues
      this._setItem(key,JSON.stringify(value))
      this.times[key] = new Date().valueOf();
      (this.keys.indexOf(key)==-1)?this.keys.push(key):undefined;
      this._save();
    }
    has(key) { return this.keys.indexOf(key) != -1 }

    remove(key) {
      // note, removing a key will revert to default (if present), not undefined
      this._removeItem(key);
      this.keys = this.keys.filter((k) => k != key );
      delete this.times[key];
      this._save();
    }

    clear() {
      for (var key in this.times) { this.remove(key); }
      this._save();
    }

    _save() {
      this._setItem(this.META+'times',JSON.stringify(this.times));
      this._setItem(this.META+'keys',JSON.stringify(this.keys));
    }

    test_supported() {
      // incognito safari and older browsers don't support local storage. Use an object in ram as a dummy
      try {
        localStorage.setItem('test', '1');
        localStorage.removeItem('test');
        return true;
      } catch(e) { }
    }

    // below this is the api for the timebomb remote data store, which isn't used anywhere yet.
    // a dummy version of this would just allways execute a remote lookup and then callback.
    isExpired(key) {
      var expire_ms = this.get(this.EXPIRY+key) || this.setExpire(key);
      return expire_ms < new Date().valueOf();
    }
    setExpire(key,epoch_ms) {
      epoch_ms = epoch_ms || this.default_expire_ms + new Date().valueOf();
      this.set(this.EXPIRY+key,epoch_ms);
      return epoch_ms;
    }
    remote(url,callback) {
      var stored = this.get(url);
      if (stored && !this.isExpired(url)) { callback(stored); return }
      uR.ajax({
        url: url,
        success: function(data) {
          this.set(url,data);
          this.setExpire(url);
          callback(data);
        }.bind(this),
      });
    }
  }

  uR._configs = [];
  class Config extends Storage {
    constructor(prefix,schema) {
      super(prefix);
      uR._configs.push(this);
      uR._configs[prefix] = this;
      schema && this.setSchema(schema);
    }
    getDefault(key,_default,schema) {
      if (!schema || typeof schema == "string") { schema = { type: schema, _default:_default } }
      if (schema && !this._schema[key]) {
        this._schema[key] = schema || {};
        this._schema[key].name = key;
      }
      if (_default && ! this.defaults[key]) {
        this.defaults[key] = _default;
        !this.has(key) && this.set(key,_default);
      }
      return this.get(key) || _default;
    }
    get(key) {
      var out = super.get(key);
      var type = this._schema[key] && this._schema[key].type;
      if (type == "boolean") { return out == "true" }
      if (type == "int" || type == "integer") { return parseInt(out) }
      if (type == "float") { return parseFloat(out) }
      return out;
    }
    getSchema(keys) {
      var self = this;
      return this._getSchemaKeys(keys).map(key => (self._schema[key] || key) );
    }

    setSchema(schema) {
      var self = this;
      uR.forEach(schema,function(s) {
        if (s.type == "color" && tinycolor) { s.initial = tinycolor(s.initial).toHexString(); }
        self.getDefault(s.name,s._default || s.value,s);
      })
      return this.getSchema();;
    }

    _getSchemaKeys(keys) {
      var out = [];
      for (var key in this._schema) {
        if (keys && keys.indexOf(key) == -1) { continue }
        this._schema.hasOwnProperty(key) && out.push(key)
      }
      return out
    }
    getData(keys) {
      var out = {};
      var self = this;
      uR.forEach(this._getSchemaKeys(keys),(key) => out[key] = self.get(key));
      return out;
    }

    openEditor(tag_opts) {
      var self=this, dirty;
      var opts = {
        schema: self.getSchema(),
        submit: function (riot_tag) {
          self.update(riot_tag.getData());
          dirty = true;
        },
        autosubmit: true,
        onUnmount: function() {
          tag_opts && tag_opts.cancel && tag_opts.cancel()
          dirty && window.location.reload()
        },
      }
      uR.forEach(opts.schema,(s)=> {
          s._default = s.value;
          s.value = self.get(s.name);
      });
      uR.alertElement("ur-form",opts);
    }
  }

  uR.Config = Config;

  uR.storage = new Storage();
  uR.Storage = Storage;
})();
<commit_msg>uR.Config.setSchema can now take in a name/value pair dictionary.<commit_after>(function() {
  class Storage {
    constructor(prefix) {
      this.PREFIX = prefix || "";
      this.META = "META/";
      this.EXIPIRY = '__expiry/'; // Key used for expiration storage
      this.default_expire_ms = 10*60*1000; // ten minutes
      this.defaults = {}; // table with default values
      this._schema = {};
      if (!this.test_supported()) {
        console.warn("Storage not supported, falling back to dummy storage");
        this.FAKE_STORAGE = {};
        this.set = function(key,value) { this.FAKE_STORAGE[key] = value; }
        this.get = function(key) { return this.FAKE_STORAGE[key]; }
        this.has = function(key) { this.FAKE_STORAGE.hasOwnProperty(key); }
        this.remove = function(key) { delete this.FAKE_STORAGE[key]; }
      }
      this.times = this.get(this.META+"times") || {};
      this.keys = this.get(this.META+"keys") || [];
    }

    _(key) { return this.PREFIX + key; }
    _getItem(key) { return localStorage.getItem(this._(key)); }
    _setItem(key,value) { return localStorage.setItem(this._(key),value); }
    _removeItem(key) { return localStorage.removeItem(this._(key)); }
    _hasOwnProperty(key) { return localStorage.hasOwnProperty(this._(key)) }

    get(key) {
      // pull a json from local storage or get an object from the defaults dict
      var value;
      if (this._hasOwnProperty(key)) {
        try { value = JSON.parse(this._getItem(key)); }
        catch(e) { } // we only allow JSON here, so parse errors can be ignored
      } else if (this.defaults.hasOwnProperty(key)) {
        value = this.defaults[key];
      }
      return value
    }

    update(data) {
      for (var key in data) {
        if (data.hasOwnProperty(key)) { this.set(key,data[key]) }
      }
    }

    set(key,value) {
      // store stringified json in localstorage
      if (!value && value !== 0) { value = undefined; } // null causes issues
      this._setItem(key,JSON.stringify(value))
      this.times[key] = new Date().valueOf();
      (this.keys.indexOf(key)==-1)?this.keys.push(key):undefined;
      this._save();
    }
    has(key) { return this.keys.indexOf(key) != -1 }

    remove(key) {
      // note, removing a key will revert to default (if present), not undefined
      this._removeItem(key);
      this.keys = this.keys.filter((k) => k != key );
      delete this.times[key];
      this._save();
    }

    clear() {
      for (var key in this.times) { this.remove(key); }
      this._save();
    }

    _save() {
      this._setItem(this.META+'times',JSON.stringify(this.times));
      this._setItem(this.META+'keys',JSON.stringify(this.keys));
    }

    test_supported() {
      // incognito safari and older browsers don't support local storage. Use an object in ram as a dummy
      try {
        localStorage.setItem('test', '1');
        localStorage.removeItem('test');
        return true;
      } catch(e) { }
    }

    // below this is the api for the timebomb remote data store, which isn't used anywhere yet.
    // a dummy version of this would just allways execute a remote lookup and then callback.
    isExpired(key) {
      var expire_ms = this.get(this.EXPIRY+key) || this.setExpire(key);
      return expire_ms < new Date().valueOf();
    }
    setExpire(key,epoch_ms) {
      epoch_ms = epoch_ms || this.default_expire_ms + new Date().valueOf();
      this.set(this.EXPIRY+key,epoch_ms);
      return epoch_ms;
    }
    remote(url,callback) {
      var stored = this.get(url);
      if (stored && !this.isExpired(url)) { callback(stored); return }
      uR.ajax({
        url: url,
        success: function(data) {
          this.set(url,data);
          this.setExpire(url);
          callback(data);
        }.bind(this),
      });
    }
  }

  uR._configs = [];
  class Config extends Storage {
    constructor(prefix,schema) {
      super(prefix);
      uR._configs.push(this);
      uR._configs[prefix] = this;
      schema && this.setSchema(schema);
    }
    getDefault(key,_default,schema) {
      if (!schema || typeof schema == "string") { schema = { type: schema, _default:_default } }
      if (schema && !this._schema[key]) {
        this._schema[key] = schema || {};
        this._schema[key].name = key;
      }
      if (_default && ! this.defaults[key]) {
        this.defaults[key] = _default;
        !this.has(key) && this.set(key,_default);
      }
      return this.get(key) || _default;
    }
    get(key) {
      var out = super.get(key);
      var type = this._schema[key] && this._schema[key].type;
      if (type == "boolean") { return out == "true" }
      if (type == "int" || type == "integer") { return parseInt(out) }
      if (type == "float") { return parseFloat(out) }
      return out;
    }
    getSchema(keys) {
      var self = this;
      return this._getSchemaKeys(keys).map(key => (self._schema[key] || key) );
    }

    setSchema(schema) {
      var self = this;
      // #! TODO: detect type and set to int/bool/char. Maybe if value is object extend object
      if (!Array.isArray(schema)) {// assume it's name/value object
        var obj = schema;
        schema = [];
        for (var key in obj) {
          schema.push({ name: key, value: obj[key] })
        }
      }
      uR.forEach(schema,function(s) {
        if (s.type == "color" && tinycolor) { s.initial = tinycolor(s.initial).toHexString(); }
        self.getDefault(s.name,s._default || s.value,s);
      })
      return this.getSchema();;
    }

    _getSchemaKeys(keys) {
      var out = [];
      for (var key in this._schema) {
        if (keys && keys.indexOf(key) == -1) { continue }
        this._schema.hasOwnProperty(key) && out.push(key)
      }
      return out
    }
    getData(keys) {
      var out = {};
      var self = this;
      uR.forEach(this._getSchemaKeys(keys),(key) => out[key] = self.get(key));
      return out;
    }

    openEditor(tag_opts) {
      var self=this, dirty;
      var opts = {
        schema: self.getSchema(),
        submit: function (riot_tag) {
          self.update(riot_tag.getData());
          dirty = true;
        },
        autosubmit: true,
        onUnmount: function() {
          tag_opts && tag_opts.cancel && tag_opts.cancel()
          dirty && window.location.reload()
        },
      }
      uR.forEach(opts.schema,(s)=> {
          s._default = s.value;
          s.value = self.get(s.name);
      });
      uR.alertElement("ur-form",opts);
    }
  }

  uR.Config = Config;

  uR.storage = new Storage();
  uR.Storage = Storage;
})();
<|endoftext|>


<commit_before>define([
	"dojo/_base/array", // array.forEach
	"dojo/_base/declare", // declare
	"dojo/dom-attr", // domAttr.set
	"dojo/dom-style", // domStyle.get
	"dojo/_base/lang", // lang.hitch lang.isArray
	"dojo/mouse", // mouse.isLeft
	"dojo/sniff", // has("webkit")
	"dojo/_base/window", // win.body
	"dojo/window", // winUtils.scrollIntoView
	"../a11y"	// a11y.hasDefaultTabStop
], function(array, declare, domAttr, domStyle, lang, mouse, has, win, winUtils, a11y){

// module:
//		dijit/form/_FormWidgetMixin
// summary:
//		Mixin for widgets corresponding to native HTML elements such as <checkbox> or <button>,
//		which can be children of a <form> node or a `dijit.form.Form` widget.

return declare("dijit.form._FormWidgetMixin", null, {
	// summary:
	//		Mixin for widgets corresponding to native HTML elements such as <checkbox> or <button>,
	//		which can be children of a <form> node or a `dijit.form.Form` widget.
	//
	// description:
	//		Represents a single HTML element.
	//		All these widgets should have these attributes just like native HTML input elements.
	//		You can set them during widget construction or afterwards, via `dijit._Widget.attr`.
	//
	//		They also share some common methods.

	// name: [const] String
	//		Name used when submitting form; same as "name" attribute or plain HTML elements
	name: "",

	// alt: String
	//		Corresponds to the native HTML <input> element's attribute.
	alt: "",

	// value: String
	//		Corresponds to the native HTML <input> element's attribute.
	value: "",

	// type: [const] String
	//		Corresponds to the native HTML <input> element's attribute.
	type: "text",

	// tabIndex: Integer
	//		Order fields are traversed when user hits the tab key
	tabIndex: "0",
	_setTabIndexAttr: "focusNode",	// force copy even when tabIndex default value, needed since Button is

	// disabled: Boolean
	//		Should this widget respond to user input?
	//		In markup, this is specified as "disabled='disabled'", or just "disabled".
	disabled: false,

	// intermediateChanges: Boolean
	//		Fires onChange for each value change or only on demand
	intermediateChanges: false,

	// scrollOnFocus: Boolean
	//		On focus, should this widget scroll into view?
	scrollOnFocus: true,

	// Override _WidgetBase mapping id to this.domNode, needs to be on focusNode so  etc.
	// works with screen reader
	_setIdAttr: "focusNode",

	postCreate: function(){
		this.inherited(arguments);
		this.connect(this.domNode, "onmousedown", "_onMouseDown");
	},

	_setDisabledAttr: function(/*Boolean*/ value){
		this._set("disabled", value);
		domAttr.set(this.focusNode, 'disabled', value);
		if(this.valueNode){
			domAttr.set(this.valueNode, 'disabled', value);
		}
		this.focusNode.setAttribute("aria-disabled", value ? "true" : "false");

		if(value){
			// reset these, because after the domNode is disabled, we can no longer receive
			// mouse related events, see #4200
			this._set("hovering", false);
			this._set("active", false);

			// clear tab stop(s) on this widget's focusable node(s)  (ComboBox has two focusable nodes)
			var attachPointNames = "tabIndex" in this.attributeMap ? this.attributeMap.tabIndex :
				("_setTabIndexAttr" in this) ? this._setTabIndexAttr : "focusNode";
			array.forEach(lang.isArray(attachPointNames) ? attachPointNames : [attachPointNames], function(attachPointName){
				var node = this[attachPointName];
				// complex code because tabIndex=-1 on a
 doesn't work on FF
				if(has("webkit") || a11y.hasDefaultTabStop(node)){	// see #11064 about webkit bug
					node.setAttribute('tabIndex', "-1");
				}else{
					node.removeAttribute('tabIndex');
				}
			}, this);
		}else{
			if(this.tabIndex != ""){
				this.set('tabIndex', this.tabIndex);
			}
		}
	},

	_onFocus: function(e){
		if(this.scrollOnFocus){
			this.defer(function(){ winUtils.scrollIntoView(this.domNode); }); // without defer, the input caret position can change on mouse click
		}
		this.inherited(arguments);
	},

	isFocusable: function(){
		// summary:
		//		Tells if this widget is focusable or not.  Used internally by dijit.
		// tags:
		//		protected
		return !this.disabled && this.focusNode && (domStyle.get(this.domNode, "display") != "none");
	},

	focus: function(){
		// summary:
		//		Put focus on this widget
		if(!this.disabled && this.focusNode.focus){
			try{ this.focusNode.focus(); }catch(e){}/*squelch errors from hidden nodes*/
		}
	},

	compare: function(/*anything*/ val1, /*anything*/ val2){
		// summary:
		//		Compare 2 values (as returned by get('value') for this widget).
		// tags:
		//		protected
		if(typeof val1 == "number" && typeof val2 == "number"){
			return (isNaN(val1) && isNaN(val2)) ? 0 : val1 - val2;
		}else if(val1 > val2){
			return 1;
		}else if(val1 < val2){
			return -1;
		}else{
			return 0;
		}
	},

	onChange: function(/*===== newValue =====*/){
		// summary:
		//		Callback when this widget's value is changed.
		// tags:
		//		callback
	},

	// _onChangeActive: [private] Boolean
	//		Indicates that changes to the value should call onChange() callback.
	//		This is false during widget initialization, to avoid calling onChange()
	//		when the initial value is set.
	_onChangeActive: false,

	_handleOnChange: function(/*anything*/ newValue, /*Boolean?*/ priorityChange){
		// summary:
		//		Called when the value of the widget is set.  Calls onChange() if appropriate
		// newValue:
		//		the new value
		// priorityChange:
		//		For a slider, for example, dragging the slider is priorityChange==false,
		//		but on mouse up, it's priorityChange==true.  If intermediateChanges==false,
		//		onChange is only called form priorityChange=true events.
		// tags:
		//		private
		if(this._lastValueReported == undefined && (priorityChange === null || !this._onChangeActive)){
			// this block executes not for a change, but during initialization,
			// and is used to store away the original value (or for ToggleButton, the original checked state)
			this._resetValue = this._lastValueReported = newValue;
		}
		this._pendingOnChange = this._pendingOnChange
			|| (typeof newValue != typeof this._lastValueReported)
			|| (this.compare(newValue, this._lastValueReported) != 0);
		if((this.intermediateChanges || priorityChange || priorityChange === undefined) && this._pendingOnChange){
			this._lastValueReported = newValue;
			this._pendingOnChange = false;
			if(this._onChangeActive){
				if(this._onChangeHandle){
					this._onChangeHandle.remove();
				}
				// defer allows hidden value processing to run and
				// also the onChange handler can safely adjust focus, etc
				this._onChangeHandle = this.defer(
					function(){
						this._onChangeHandle = null;
						this.onChange(newValue);
					}); // try to collapse multiple onChange's fired faster than can be processed
			}
		}
	},

	create: function(){
		// Overrides _Widget.create()
		this.inherited(arguments);
		this._onChangeActive = true;
	},

	destroy: function(){
		if(this._onChangeHandle){ // destroy called before last onChange has fired
			this._onChangeHandle.remove();
			this.onChange(this._lastValueReported);
		}
		this.inherited(arguments);
	},

	_onMouseDown: function(e){
		// If user clicks on the button, even if the mouse is released outside of it,
		// this button should get focus (to mimics native browser buttons).
		// This is also needed on chrome because otherwise buttons won't get focus at all,
		// which leads to bizarre focus restore on Dialog close etc.
		// IE exhibits strange scrolling behavior when focusing a node so only do it when !focused.
		// FF needs the extra help to make sure the mousedown actually gets to the focusNode
		if((!this.focused || !has('ie')) && !e.ctrlKey && mouse.isLeft(e) && this.isFocusable()){ // !e.ctrlKey to ignore right-click on mac
			// Set a global event to handle mouseup, so it fires properly
			// even if the cursor leaves this.domNode before the mouse up event.
			var mouseUpConnector = this.connect(win.body(), "onmouseup", function(){
				if(!this.focused && this.isFocusable()){
					this.focus();
				}
				this.disconnect(mouseUpConnector);
			});
		}
	}
});

});
<commit_msg>Refs #14968.  If mousedown causes focus before mouseup, don't attempt to refocus.<commit_after>define([
	"dojo/_base/array", // array.forEach
	"dojo/_base/declare", // declare
	"dojo/dom-attr", // domAttr.set
	"dojo/dom-style", // domStyle.get
	"dojo/_base/lang", // lang.hitch lang.isArray
	"dojo/mouse", // mouse.isLeft
	"dojo/sniff", // has("webkit")
	"dojo/_base/window", // win.body
	"dojo/window", // winUtils.scrollIntoView
	"../a11y"	// a11y.hasDefaultTabStop
], function(array, declare, domAttr, domStyle, lang, mouse, has, win, winUtils, a11y){

// module:
//		dijit/form/_FormWidgetMixin
// summary:
//		Mixin for widgets corresponding to native HTML elements such as <checkbox> or <button>,
//		which can be children of a <form> node or a `dijit.form.Form` widget.

return declare("dijit.form._FormWidgetMixin", null, {
	// summary:
	//		Mixin for widgets corresponding to native HTML elements such as <checkbox> or <button>,
	//		which can be children of a <form> node or a `dijit.form.Form` widget.
	//
	// description:
	//		Represents a single HTML element.
	//		All these widgets should have these attributes just like native HTML input elements.
	//		You can set them during widget construction or afterwards, via `dijit._Widget.attr`.
	//
	//		They also share some common methods.

	// name: [const] String
	//		Name used when submitting form; same as "name" attribute or plain HTML elements
	name: "",

	// alt: String
	//		Corresponds to the native HTML <input> element's attribute.
	alt: "",

	// value: String
	//		Corresponds to the native HTML <input> element's attribute.
	value: "",

	// type: [const] String
	//		Corresponds to the native HTML <input> element's attribute.
	type: "text",

	// tabIndex: Integer
	//		Order fields are traversed when user hits the tab key
	tabIndex: "0",
	_setTabIndexAttr: "focusNode",	// force copy even when tabIndex default value, needed since Button is

	// disabled: Boolean
	//		Should this widget respond to user input?
	//		In markup, this is specified as "disabled='disabled'", or just "disabled".
	disabled: false,

	// intermediateChanges: Boolean
	//		Fires onChange for each value change or only on demand
	intermediateChanges: false,

	// scrollOnFocus: Boolean
	//		On focus, should this widget scroll into view?
	scrollOnFocus: true,

	// Override _WidgetBase mapping id to this.domNode, needs to be on focusNode so  etc.
	// works with screen reader
	_setIdAttr: "focusNode",

	postCreate: function(){
		this.inherited(arguments);
		this.connect(this.domNode, "onmousedown", "_onMouseDown");
	},

	_setDisabledAttr: function(/*Boolean*/ value){
		this._set("disabled", value);
		domAttr.set(this.focusNode, 'disabled', value);
		if(this.valueNode){
			domAttr.set(this.valueNode, 'disabled', value);
		}
		this.focusNode.setAttribute("aria-disabled", value ? "true" : "false");

		if(value){
			// reset these, because after the domNode is disabled, we can no longer receive
			// mouse related events, see #4200
			this._set("hovering", false);
			this._set("active", false);

			// clear tab stop(s) on this widget's focusable node(s)  (ComboBox has two focusable nodes)
			var attachPointNames = "tabIndex" in this.attributeMap ? this.attributeMap.tabIndex :
				("_setTabIndexAttr" in this) ? this._setTabIndexAttr : "focusNode";
			array.forEach(lang.isArray(attachPointNames) ? attachPointNames : [attachPointNames], function(attachPointName){
				var node = this[attachPointName];
				// complex code because tabIndex=-1 on a
 doesn't work on FF
				if(has("webkit") || a11y.hasDefaultTabStop(node)){	// see #11064 about webkit bug
					node.setAttribute('tabIndex', "-1");
				}else{
					node.removeAttribute('tabIndex');
				}
			}, this);
		}else{
			if(this.tabIndex != ""){
				this.set('tabIndex', this.tabIndex);
			}
		}
	},

	_onFocus: function(e){
		if(this.scrollOnFocus){
			this.defer(function(){ winUtils.scrollIntoView(this.domNode); }); // without defer, the input caret position can change on mouse click
		}
		this.inherited(arguments);
	},

	isFocusable: function(){
		// summary:
		//		Tells if this widget is focusable or not.  Used internally by dijit.
		// tags:
		//		protected
		return !this.disabled && this.focusNode && (domStyle.get(this.domNode, "display") != "none");
	},

	focus: function(){
		// summary:
		//		Put focus on this widget
		if(!this.disabled && this.focusNode.focus){
			try{ this.focusNode.focus(); }catch(e){}/*squelch errors from hidden nodes*/
		}
	},

	compare: function(/*anything*/ val1, /*anything*/ val2){
		// summary:
		//		Compare 2 values (as returned by get('value') for this widget).
		// tags:
		//		protected
		if(typeof val1 == "number" && typeof val2 == "number"){
			return (isNaN(val1) && isNaN(val2)) ? 0 : val1 - val2;
		}else if(val1 > val2){
			return 1;
		}else if(val1 < val2){
			return -1;
		}else{
			return 0;
		}
	},

	onChange: function(/*===== newValue =====*/){
		// summary:
		//		Callback when this widget's value is changed.
		// tags:
		//		callback
	},

	// _onChangeActive: [private] Boolean
	//		Indicates that changes to the value should call onChange() callback.
	//		This is false during widget initialization, to avoid calling onChange()
	//		when the initial value is set.
	_onChangeActive: false,

	_handleOnChange: function(/*anything*/ newValue, /*Boolean?*/ priorityChange){
		// summary:
		//		Called when the value of the widget is set.  Calls onChange() if appropriate
		// newValue:
		//		the new value
		// priorityChange:
		//		For a slider, for example, dragging the slider is priorityChange==false,
		//		but on mouse up, it's priorityChange==true.  If intermediateChanges==false,
		//		onChange is only called form priorityChange=true events.
		// tags:
		//		private
		if(this._lastValueReported == undefined && (priorityChange === null || !this._onChangeActive)){
			// this block executes not for a change, but during initialization,
			// and is used to store away the original value (or for ToggleButton, the original checked state)
			this._resetValue = this._lastValueReported = newValue;
		}
		this._pendingOnChange = this._pendingOnChange
			|| (typeof newValue != typeof this._lastValueReported)
			|| (this.compare(newValue, this._lastValueReported) != 0);
		if((this.intermediateChanges || priorityChange || priorityChange === undefined) && this._pendingOnChange){
			this._lastValueReported = newValue;
			this._pendingOnChange = false;
			if(this._onChangeActive){
				if(this._onChangeHandle){
					this._onChangeHandle.remove();
				}
				// defer allows hidden value processing to run and
				// also the onChange handler can safely adjust focus, etc
				this._onChangeHandle = this.defer(
					function(){
						this._onChangeHandle = null;
						this.onChange(newValue);
					}); // try to collapse multiple onChange's fired faster than can be processed
			}
		}
	},

	create: function(){
		// Overrides _Widget.create()
		this.inherited(arguments);
		this._onChangeActive = true;
	},

	destroy: function(){
		if(this._onChangeHandle){ // destroy called before last onChange has fired
			this._onChangeHandle.remove();
			this.onChange(this._lastValueReported);
		}
		this.inherited(arguments);
	},

	_onMouseDown: function(e){
		// If user clicks on the button, even if the mouse is released outside of it,
		// this button should get focus (to mimics native browser buttons).
		// This is also needed on Chrome because otherwise buttons won't get focus at all,
		// which leads to bizarre focus restore on Dialog close etc.
		// FF needs the extra help to make sure the mousedown actually gets to the focusNode
		if(!this.focused && !e.ctrlKey && mouse.isLeft(e) && this.isFocusable()){ // !e.ctrlKey to ignore right-click on mac
			// IE exhibits strange scrolling behavior when refocusing a node so only do it when !focused.
			var focusConnector = this.connect(this.focusNode, "onfocus", function(){
				this.disconnect(mouseUpConnector); // cancel mouseup focus action since it's already happened
				this.disconnect(focusConnector);
			});
			// Set a global event to handle mouseup, so it fires properly
			// even if the cursor leaves this.domNode before the mouse up event.
			var mouseUpConnector = this.connect(win.body(), "onmouseup", function(){
				this.disconnect(mouseUpConnector);
				this.disconnect(focusConnector);
				// if here, then the mousedown did not focus the focusNode as the default action
				if(this.isFocusable()){
					this.focus();
				}
			});
		}
	}
});

});
<|endoftext|>