<commit_before>import {
  Fragment,
  useState,
  useReducer,
  useEffect,
  useCallback,
  useRef,
} from 'react';
import {
  Switch,
  Route,
  Redirect,
  useRouteMatch,
  useParams,
} from 'react-router-dom';
import {useAPI, useResource, selectAPINull} from '@xorkevin/substation';
import {useAuthValue, useAuthCall, useAuthResource} from '@xorkevin/turbine';
import {
  Grid,
  Column,
  ListGroup,
  ListItem,
  ModalSurface,
  useModal,
  useMenu,
  Menu,
  MenuItem,
  FieldDynMultiSelect,
  Form,
  useForm,
  useFormSearch,
  ButtonGroup,
  FaIcon,
  Time,
} from '@xorkevin/nuke';
import ButtonPrimary from '@xorkevin/nuke/src/component/button/primary';
import ButtonTertiary from '@xorkevin/nuke/src/component/button/tertiary';

const CHATS_LIMIT = 32;
const USERS_LIMIT = 8;

const selectAPIUsers = (api) => api.u.user.ids;
const selectAPIChats = (api) => api.conduit.chat.ids;
const selectAPILatestChats = (api) => api.conduit.chat.latest;
const selectAPICreateChat = (api) => api.conduit.chat.create;
const selectAPISearch = (api) => api.u.user.search;

const SelectAChat = () => {
  return
Select a chat
;
};

const Chat = () => {
  const {chatid} = useParams();

  return
Hello, World, chatid: {chatid}
;
};

const ChatRow = ({chat, usersCache}) => {
  const {userid} = useAuthValue();

  const match = useRouteMatch();
  const menu = useMenu();

  const menuToggle = menu.toggle;
  const toggleMenu = useCallback(
    (e) => {
      // prevent link from being clicked. anchor tag lies outside of event bubbling system.
      e.preventDefault();
      menuToggle();
    },
    [menuToggle],
  );

  const preventDefault = useCallback((e) => {
    // prevent link from being clicked. anchor tag lies outside of event bubbling system.
    e.preventDefault();
  }, []);

  return (
    <ListItem local link={`${match.url}/${chat.chatid}`}>
      <Grid justify="space-between" align="center" nowrap>
        <Column>

            {chat.name ||
              chat.members
                .filter((i) => i !== userid)
                .slice(0, 5)
                .map((i) => {
                  const k = usersCache[i];
                  if (!k) {
                    return '';
                  }
                  return k.username;
                })
                .join(', ')}

          <Time value={chat.last_updated} />
        </Column>
        <Column shrink="0">
          <ButtonTertiary forwardedRef={menu.anchorRef} onClick={toggleMenu}>
            <FaIcon icon="ellipsis-v" />
          </ButtonTertiary>
          {menu.show && (
            <Menu
              size="md"
              anchor={menu.anchor}
              close={menu.close}
              onClick={preventDefault}
            >
              <MenuItem icon={<FaIcon icon="bars" />}>Action</MenuItem>
            </Menu>
          )}
        </Column>
      </Grid>
    </ListItem>
  );
};

const CreateChat = ({close, addChat}) => {
  const {userid} = useAuthValue();

  const form = useForm({
    userids: [],
  });

  const posthook = useCallback(
    (_status, chat) => {
      close();
      // TODO: remove this branch after websockets
      if (chat && chat.chatid) {
        addChat(chat.chatid);
      }
    },
    [close, addChat],
  );
  const [create, execCreate] = useAuthCall(
    selectAPICreateChat,
    ['dm', '', '{}', form.state.userids],
    {},
    {posthook},
  );

  const apiSearch = useAPI(selectAPISearch);
  const searchUsers = useCallback(
    async (search) => {
      const [data, status, err] = await apiSearch(search, USERS_LIMIT);
      if (err || status < 200 || status >= 300 || !Array.isArray(data)) {
        return [];
      }
      return data
        .filter((i) => i.userid !== userid)
        .map((i) => ({value: i.userid, display: i.username}));
    },
    [userid, apiSearch],
  );
  const userSuggest = useFormSearch(searchUsers, 256);

  return (
    <Fragment>

New Chat

      <Form
        formState={form.state}
        onChange={form.update}
        onSubmit={execCreate}
        displays={form.displays}
        putDisplays={form.putDisplays}
        addDisplay={form.addDisplay}
        compactDisplays={form.compactDisplays}
      >
        <FieldDynMultiSelect
          name="userids"
          label="Users"
          onSearch={userSuggest.setSearch}
          options={userSuggest.opts}
          nohint
          fullWidth
        />
      </Form>
      <ButtonGroup>
        <ButtonTertiary onClick={close}>Cancel</ButtonTertiary>
        <ButtonPrimary onClick={execCreate}>Create Chat</ButtonPrimary>
      </ButtonGroup>
      {create.err &&
{create.err.message}
}
    </Fragment>
  );
};

const CHATS_RESET = Symbol('CHATS_RESET');
const CHATS_APPEND = Symbol('CHATS_APPEND');
const CHATS_INVALIDATE = Symbol('CHATS_INVALIDATE');

const USERS_APPEND = Symbol('USERS_APPEND');
const USERS_INVALIDATE = Symbol('USERS_INVALIDATE');

const ChatsReset = (chats) => ({
  type: CHATS_RESET,
  chats,
});

const ChatsAppend = (chats) => ({
  type: CHATS_APPEND,
  chats,
});

const ChatsInvalidate = (chatids) => ({
  type: CHATS_INVALIDATE,
  chatids,
});

const UsersAppend = (users) => ({
  type: USERS_APPEND,
  users,
});

const chatsReducer = (state, action) => {
  switch (action.type) {
    case CHATS_RESET: {
      if (!Array.isArray(action.chats)) {
        return state;
      }
      const chats = action.chats
        .filter((i) => i.chatid && i.last_updated > 0)
        .sort((a, b) => {
          // reverse sort
          return b.last_updated - a.last_updated;
        });
      const usersDiff = chats.flatMap((i) =>
        Array.isArray(i.members) ? i.members : [],
      );
      const chatids = chats.map((i) => i.chatid);
      return {
        chats,
        users: {},
        chatsDiff: [],
        usersDiff,
        validChatsSet: new Set(chatids),
        chatsSet: new Set(chatids),
        allChatsMap: new Map(chats.map((i) => [i.chatid, i.last_updated])),
        validUsersSet: new Set(),
        usersSet: new Set(usersDiff),
      };
    }
    case CHATS_APPEND: {
      if (!Array.isArray(action.chats) || action.chats.length === 0) {
        return state;
      }
      const addedChats = action.chats
        .filter((i) => {
          if (!i.chatid || !i.last_updated) {
            return false;
          }
          if (!state.allChatsMap.has(i.chatid)) {
            return true;
          }
          return i.last_updated > state.allChatsMap.get(i.chatid);
        })
        .sort((a, b) => {
          // reverse sort
          return b.last_updated - a.last_updated;
        });
      const addedChatidSet = new Set(addedChats.map((i) => i.chatid));
      const chats = state.chats.filter((i) => !addedChatidSet.has(i.chatid));
      addedChats.forEach((i) => {
        chats.push(i);
      });
      chats.sort((a, b) => {
        // reverse sort
        return b.last_updated - a.last_updated;
      });
      const validChatsSet = state.validChatsSet;
      const chatsSet = state.chatsSet;
      const allChatsMap = state.allChatsMap;
      const usersSet = state.usersSet;
      addedChats.forEach((i) => {
        validChatsSet.add(i.chatid);
        chatsSet.add(i.chatid);
        allChatsMap.set(i.chatid, i.last_updated);
        if (Array.isArray(i.members)) {
          i.members.forEach((j) => {
            usersSet.add(j);
          });
        }
      });
      return Object.assign({}, state, {
        chats,
        chatsDiff: Array.from(chatsSet).filter((i) => !validChatsSet.has(i)),
        usersDiff: Array.from(usersSet).filter(
          (i) => !state.validUsersSet.has(i),
        ),
        validChatsSet,
        chatsSet,
        allChatsMap,
        usersSet,
      });
    }
    case CHATS_INVALIDATE: {
      if (!Array.isArray(action.chatids) || action.chatids.length === 0) {
        return state;
      }
      const validChatsSet = state.validChatsSet;
      const chatsSet = state.chatsSet;
      action.chatids.forEach((i) => {
        validChatsSet.delete(i);
        chatsSet.add(i);
      });
      return Object.assign({}, state, {
        chatsDiff: Array.from(chatsSet).filter((i) => !validChatsSet.has(i)),
        validChatsSet,
        chatsSet,
      });
    }
    case USERS_APPEND: {
      if (!Array.isArray(action.users) || action.users.length === 0) {
        return state;
      }
      const users = Object.assign(
        {},
        state.users,
        Object.fromEntries(action.users.map((i) => [i.userid, i])),
      );
      const validUsersSet = state.validUsersSet;
      action.users.forEach((i) => {
        validUsersSet.add(i.userid);
      });
      return Object.assign({}, state, {
        users,
        usersDiff: Array.from(state.usersSet).filter(
          (i) => !validUsersSet.has(i),
        ),
        validUsersSet,
      });
    }
    case USERS_INVALIDATE: {
      if (!Array.isArray(action.userids) || action.userids.length === 0) {
        return state;
      }
      const validUsersSet = state.validUsersSet;
      action.userids.forEach((i) => {
        validUsersSet.delete(i);
      });
      return Object.assign({}, state, {
        usersDiff: Array.from(state.usersSet).filter(
          (i) => !validUsersSet.has(i),
        ),
        validUsersSet,
      });
    }
    default:
      return state;
  }
};

const ConduitChat = () => {
  const match = useRouteMatch();

  const [chats, dispatchChats] = useReducer(chatsReducer, {
    chats: [],
    users: {},
    chatsDiff: [],
    usersDiff: [],
    validChatsSet: new Set(),
    chatsSet: new Set(),
    allChatsMap: new Map(),
    validUsersSet: new Set(),
    usersSet: new Set(),
  });

  const posthookInit = useCallback(
    (_status, chats) => {
      dispatchChats(ChatsReset(chats));
    },
    [dispatchChats],
  );
  const [initChats, _execInitChats] = useAuthResource(
    selectAPILatestChats,
    ['dm', 0, CHATS_LIMIT],
    [],
    {posthook: posthookInit},
  );

  const endElem = useRef(null);
  const firstLastUpdated =
    chats.chats.length === 0
      ? 0
      : chats.chats[chats.chats.length - 1].last_updated;
  const [before, setBefore] = useState(0);
  useEffect(() => {
    if (!endElem.current) {
      return;
    }
    if (firstLastUpdated === 0) {
      return;
    }
    if (firstLastUpdated === before) {
      return;
    }
    const observer = new IntersectionObserver((entries) => {
      if (entries.some((i) => i.isIntersecting)) {
        console.log('intersect');
        setBefore(firstLastUpdated);
      }
    });
    observer.observe(endElem.current);

    return () => {
      observer.disconnect();
    };
  }, [endElem, setBefore, before, firstLastUpdated]);

  const posthookLoadChats = useCallback(
    (_status, chats) => {
      dispatchChats(ChatsAppend(chats));
    },
    [dispatchChats],
  );
  const [loadChats, _execLoadChats] = useAuthResource(
    before === 0 ? selectAPINull : selectAPILatestChats,
    ['dm', before, CHATS_LIMIT],
    [],
    {posthook: posthookLoadChats},
  );

  const posthookChats = useCallback(
    (_status, chats) => {
      dispatchChats(ChatsAppend(chats));
    },
    [dispatchChats],
  );
  const chatsDiff = chats.chatsDiff;
  const [getChats, _execGetChats] = useAuthResource(
    chatsDiff.length > 0 ? selectAPIChats : selectAPINull,
    [chatsDiff],
    [],
    {posthook: posthookChats},
  );

  const posthookUsers = useCallback(
    (_status, users) => {
      dispatchChats(UsersAppend(users));
    },
    [dispatchChats],
  );
  const usersDiff = chats.usersDiff;
  const [getUsers, _execGetUsers] = useResource(
    usersDiff.length > 0 ? selectAPIUsers : selectAPINull,
    [usersDiff],
    [],
    {posthook: posthookUsers},
  );

  const addChat = useCallback(
    (chatid) => {
      dispatchChats(ChatsInvalidate([chatid]));
    },
    [dispatchChats],
  );

  const modal = useModal();

  return (
    <Grid className="conduit-chat-root">
      <Column fullWidth sm={6}>
        <Grid className="conduit-chat-sidebar" direction="column" nowrap>
          <Column>
            <Grid justify="space-between" align="flex-end">
              <Column grow="1">

Direct Messages

              </Column>
              <Column>
                <ButtonGroup>
                  <ButtonTertiary
                    forwardedRef={modal.anchorRef}
                    onClick={modal.toggle}
                  >
                    <FaIcon icon="plus" />
                  </ButtonTertiary>
                </ButtonGroup>
                {modal.show && (
                  <ModalSurface
                    size="md"
                    anchor={modal.anchor}
                    close={modal.close}
                  >
                    <CreateChat close={modal.close} addChat={addChat} />
                  </ModalSurface>
                )}
              </Column>
            </Grid>
            {initChats.err &&
{initChats.err.message}
}
            {loadChats.err &&
{loadChats.err.message}
}
            {getChats.err &&
{getChats.err.message}
}
            {getUsers.err &&
{getUsers.err.message}
}
          </Column>
          <Column className="conduit-chat-list-outer" grow="1" basis="0">
            <ListGroup className="conduit-chat-list">
              {chats.chats.map((i) => (
                <ChatRow key={i.chatid} chat={i} usersCache={chats.users} />
              ))}

            </ListGroup>
          </Column>
        </Grid>
      </Column>
      <Column fullWidth sm={18}>
        <Switch>
          <Route exact path={`${match.path}`}>
            <SelectAChat />
          </Route>
          <Route path={`${match.path}/:chatid`}>
            <Chat />
          </Route>
          <Redirect to={`${match.url}`} />
        </Switch>
      </Column>
    </Grid>
  );
};

export default ConduitChat;
<commit_msg>Remove debug<commit_after>import {
  Fragment,
  useState,
  useReducer,
  useEffect,
  useCallback,
  useRef,
} from 'react';
import {
  Switch,
  Route,
  Redirect,
  useRouteMatch,
  useParams,
} from 'react-router-dom';
import {useAPI, useResource, selectAPINull} from '@xorkevin/substation';
import {useAuthValue, useAuthCall, useAuthResource} from '@xorkevin/turbine';
import {
  Grid,
  Column,
  ListGroup,
  ListItem,
  ModalSurface,
  useModal,
  useMenu,
  Menu,
  MenuItem,
  FieldDynMultiSelect,
  Form,
  useForm,
  useFormSearch,
  ButtonGroup,
  FaIcon,
  Time,
} from '@xorkevin/nuke';
import ButtonPrimary from '@xorkevin/nuke/src/component/button/primary';
import ButtonTertiary from '@xorkevin/nuke/src/component/button/tertiary';

const CHATS_LIMIT = 32;
const USERS_LIMIT = 8;

const selectAPIUsers = (api) => api.u.user.ids;
const selectAPIChats = (api) => api.conduit.chat.ids;
const selectAPILatestChats = (api) => api.conduit.chat.latest;
const selectAPICreateChat = (api) => api.conduit.chat.create;
const selectAPISearch = (api) => api.u.user.search;

const SelectAChat = () => {
  return
Select a chat
;
};

const Chat = () => {
  const {chatid} = useParams();

  return
Hello, World, chatid: {chatid}
;
};

const ChatRow = ({chat, usersCache}) => {
  const {userid} = useAuthValue();

  const match = useRouteMatch();
  const menu = useMenu();

  const menuToggle = menu.toggle;
  const toggleMenu = useCallback(
    (e) => {
      // prevent link from being clicked. anchor tag lies outside of event bubbling system.
      e.preventDefault();
      menuToggle();
    },
    [menuToggle],
  );

  const preventDefault = useCallback((e) => {
    // prevent link from being clicked. anchor tag lies outside of event bubbling system.
    e.preventDefault();
  }, []);

  return (
    <ListItem local link={`${match.url}/${chat.chatid}`}>
      <Grid justify="space-between" align="center" nowrap>
        <Column>

            {chat.name ||
              chat.members
                .filter((i) => i !== userid)
                .slice(0, 5)
                .map((i) => {
                  const k = usersCache[i];
                  if (!k) {
                    return '';
                  }
                  return k.username;
                })
                .join(', ')}

          <Time value={chat.last_updated} />
        </Column>
        <Column shrink="0">
          <ButtonTertiary forwardedRef={menu.anchorRef} onClick={toggleMenu}>
            <FaIcon icon="ellipsis-v" />
          </ButtonTertiary>
          {menu.show && (
            <Menu
              size="md"
              anchor={menu.anchor}
              close={menu.close}
              onClick={preventDefault}
            >
              <MenuItem icon={<FaIcon icon="bars" />}>Action</MenuItem>
            </Menu>
          )}
        </Column>
      </Grid>
    </ListItem>
  );
};

const CreateChat = ({close, addChat}) => {
  const {userid} = useAuthValue();

  const form = useForm({
    userids: [],
  });

  const posthook = useCallback(
    (_status, chat) => {
      close();
      // TODO: remove this branch after websockets
      if (chat && chat.chatid) {
        addChat(chat.chatid);
      }
    },
    [close, addChat],
  );
  const [create, execCreate] = useAuthCall(
    selectAPICreateChat,
    ['dm', '', '{}', form.state.userids],
    {},
    {posthook},
  );

  const apiSearch = useAPI(selectAPISearch);
  const searchUsers = useCallback(
    async (search) => {
      const [data, status, err] = await apiSearch(search, USERS_LIMIT);
      if (err || status < 200 || status >= 300 || !Array.isArray(data)) {
        return [];
      }
      return data
        .filter((i) => i.userid !== userid)
        .map((i) => ({value: i.userid, display: i.username}));
    },
    [userid, apiSearch],
  );
  const userSuggest = useFormSearch(searchUsers, 256);

  return (
    <Fragment>

New Chat

      <Form
        formState={form.state}
        onChange={form.update}
        onSubmit={execCreate}
        displays={form.displays}
        putDisplays={form.putDisplays}
        addDisplay={form.addDisplay}
        compactDisplays={form.compactDisplays}
      >
        <FieldDynMultiSelect
          name="userids"
          label="Users"
          onSearch={userSuggest.setSearch}
          options={userSuggest.opts}
          nohint
          fullWidth
        />
      </Form>
      <ButtonGroup>
        <ButtonTertiary onClick={close}>Cancel</ButtonTertiary>
        <ButtonPrimary onClick={execCreate}>Create Chat</ButtonPrimary>
      </ButtonGroup>
      {create.err &&
{create.err.message}
}
    </Fragment>
  );
};

const CHATS_RESET = Symbol('CHATS_RESET');
const CHATS_APPEND = Symbol('CHATS_APPEND');
const CHATS_INVALIDATE = Symbol('CHATS_INVALIDATE');

const USERS_APPEND = Symbol('USERS_APPEND');
const USERS_INVALIDATE = Symbol('USERS_INVALIDATE');

const ChatsReset = (chats) => ({
  type: CHATS_RESET,
  chats,
});

const ChatsAppend = (chats) => ({
  type: CHATS_APPEND,
  chats,
});

const ChatsInvalidate = (chatids) => ({
  type: CHATS_INVALIDATE,
  chatids,
});

const UsersAppend = (users) => ({
  type: USERS_APPEND,
  users,
});

const chatsReducer = (state, action) => {
  switch (action.type) {
    case CHATS_RESET: {
      if (!Array.isArray(action.chats)) {
        return state;
      }
      const chats = action.chats
        .filter((i) => i.chatid && i.last_updated > 0)
        .sort((a, b) => {
          // reverse sort
          return b.last_updated - a.last_updated;
        });
      const usersDiff = chats.flatMap((i) =>
        Array.isArray(i.members) ? i.members : [],
      );
      const chatids = chats.map((i) => i.chatid);
      return {
        chats,
        users: {},
        chatsDiff: [],
        usersDiff,
        validChatsSet: new Set(chatids),
        chatsSet: new Set(chatids),
        allChatsMap: new Map(chats.map((i) => [i.chatid, i.last_updated])),
        validUsersSet: new Set(),
        usersSet: new Set(usersDiff),
      };
    }
    case CHATS_APPEND: {
      if (!Array.isArray(action.chats) || action.chats.length === 0) {
        return state;
      }
      const addedChats = action.chats
        .filter((i) => {
          if (!i.chatid || !i.last_updated) {
            return false;
          }
          if (!state.allChatsMap.has(i.chatid)) {
            return true;
          }
          return i.last_updated > state.allChatsMap.get(i.chatid);
        })
        .sort((a, b) => {
          // reverse sort
          return b.last_updated - a.last_updated;
        });
      const addedChatidSet = new Set(addedChats.map((i) => i.chatid));
      const chats = state.chats.filter((i) => !addedChatidSet.has(i.chatid));
      addedChats.forEach((i) => {
        chats.push(i);
      });
      chats.sort((a, b) => {
        // reverse sort
        return b.last_updated - a.last_updated;
      });
      const validChatsSet = state.validChatsSet;
      const chatsSet = state.chatsSet;
      const allChatsMap = state.allChatsMap;
      const usersSet = state.usersSet;
      addedChats.forEach((i) => {
        validChatsSet.add(i.chatid);
        chatsSet.add(i.chatid);
        allChatsMap.set(i.chatid, i.last_updated);
        if (Array.isArray(i.members)) {
          i.members.forEach((j) => {
            usersSet.add(j);
          });
        }
      });
      return Object.assign({}, state, {
        chats,
        chatsDiff: Array.from(chatsSet).filter((i) => !validChatsSet.has(i)),
        usersDiff: Array.from(usersSet).filter(
          (i) => !state.validUsersSet.has(i),
        ),
        validChatsSet,
        chatsSet,
        allChatsMap,
        usersSet,
      });
    }
    case CHATS_INVALIDATE: {
      if (!Array.isArray(action.chatids) || action.chatids.length === 0) {
        return state;
      }
      const validChatsSet = state.validChatsSet;
      const chatsSet = state.chatsSet;
      action.chatids.forEach((i) => {
        validChatsSet.delete(i);
        chatsSet.add(i);
      });
      return Object.assign({}, state, {
        chatsDiff: Array.from(chatsSet).filter((i) => !validChatsSet.has(i)),
        validChatsSet,
        chatsSet,
      });
    }
    case USERS_APPEND: {
      if (!Array.isArray(action.users) || action.users.length === 0) {
        return state;
      }
      const users = Object.assign(
        {},
        state.users,
        Object.fromEntries(action.users.map((i) => [i.userid, i])),
      );
      const validUsersSet = state.validUsersSet;
      action.users.forEach((i) => {
        validUsersSet.add(i.userid);
      });
      return Object.assign({}, state, {
        users,
        usersDiff: Array.from(state.usersSet).filter(
          (i) => !validUsersSet.has(i),
        ),
        validUsersSet,
      });
    }
    case USERS_INVALIDATE: {
      if (!Array.isArray(action.userids) || action.userids.length === 0) {
        return state;
      }
      const validUsersSet = state.validUsersSet;
      action.userids.forEach((i) => {
        validUsersSet.delete(i);
      });
      return Object.assign({}, state, {
        usersDiff: Array.from(state.usersSet).filter(
          (i) => !validUsersSet.has(i),
        ),
        validUsersSet,
      });
    }
    default:
      return state;
  }
};

const ConduitChat = () => {
  const match = useRouteMatch();

  const [chats, dispatchChats] = useReducer(chatsReducer, {
    chats: [],
    users: {},
    chatsDiff: [],
    usersDiff: [],
    validChatsSet: new Set(),
    chatsSet: new Set(),
    allChatsMap: new Map(),
    validUsersSet: new Set(),
    usersSet: new Set(),
  });

  const posthookInit = useCallback(
    (_status, chats) => {
      dispatchChats(ChatsReset(chats));
    },
    [dispatchChats],
  );
  const [initChats, _execInitChats] = useAuthResource(
    selectAPILatestChats,
    ['dm', 0, CHATS_LIMIT],
    [],
    {posthook: posthookInit},
  );

  const endElem = useRef(null);
  const firstLastUpdated =
    chats.chats.length === 0
      ? 0
      : chats.chats[chats.chats.length - 1].last_updated;
  const [before, setBefore] = useState(0);
  useEffect(() => {
    if (!endElem.current) {
      return;
    }
    if (firstLastUpdated === 0) {
      return;
    }
    if (firstLastUpdated === before) {
      return;
    }
    const observer = new IntersectionObserver((entries) => {
      if (entries.some((i) => i.isIntersecting)) {
        setBefore(firstLastUpdated);
      }
    });
    observer.observe(endElem.current);

    return () => {
      observer.disconnect();
    };
  }, [endElem, setBefore, before, firstLastUpdated]);

  const posthookLoadChats = useCallback(
    (_status, chats) => {
      dispatchChats(ChatsAppend(chats));
    },
    [dispatchChats],
  );
  const [loadChats, _execLoadChats] = useAuthResource(
    before === 0 ? selectAPINull : selectAPILatestChats,
    ['dm', before, CHATS_LIMIT],
    [],
    {posthook: posthookLoadChats},
  );

  const posthookChats = useCallback(
    (_status, chats) => {
      dispatchChats(ChatsAppend(chats));
    },
    [dispatchChats],
  );
  const chatsDiff = chats.chatsDiff;
  const [getChats, _execGetChats] = useAuthResource(
    chatsDiff.length > 0 ? selectAPIChats : selectAPINull,
    [chatsDiff],
    [],
    {posthook: posthookChats},
  );

  const posthookUsers = useCallback(
    (_status, users) => {
      dispatchChats(UsersAppend(users));
    },
    [dispatchChats],
  );
  const usersDiff = chats.usersDiff;
  const [getUsers, _execGetUsers] = useResource(
    usersDiff.length > 0 ? selectAPIUsers : selectAPINull,
    [usersDiff],
    [],
    {posthook: posthookUsers},
  );

  const addChat = useCallback(
    (chatid) => {
      dispatchChats(ChatsInvalidate([chatid]));
    },
    [dispatchChats],
  );

  const modal = useModal();

  return (
    <Grid className="conduit-chat-root">
      <Column fullWidth sm={6}>
        <Grid className="conduit-chat-sidebar" direction="column" nowrap>
          <Column>
            <Grid justify="space-between" align="flex-end">
              <Column grow="1">

Direct Messages

              </Column>
              <Column>
                <ButtonGroup>
                  <ButtonTertiary
                    forwardedRef={modal.anchorRef}
                    onClick={modal.toggle}
                  >
                    <FaIcon icon="plus" />
                  </ButtonTertiary>
                </ButtonGroup>
                {modal.show && (
                  <ModalSurface
                    size="md"
                    anchor={modal.anchor}
                    close={modal.close}
                  >
                    <CreateChat close={modal.close} addChat={addChat} />
                  </ModalSurface>
                )}
              </Column>
            </Grid>
            {initChats.err &&
{initChats.err.message}
}
            {loadChats.err &&
{loadChats.err.message}
}
            {getChats.err &&
{getChats.err.message}
}
            {getUsers.err &&
{getUsers.err.message}
}
          </Column>
          <Column className="conduit-chat-list-outer" grow="1" basis="0">
            <ListGroup className="conduit-chat-list">
              {chats.chats.map((i) => (
                <ChatRow key={i.chatid} chat={i} usersCache={chats.users} />
              ))}

            </ListGroup>
          </Column>
        </Grid>
      </Column>
      <Column fullWidth sm={18}>
        <Switch>
          <Route exact path={`${match.path}`}>
            <SelectAChat />
          </Route>
          <Route path={`${match.path}/:chatid`}>
            <Chat />
          </Route>
          <Redirect to={`${match.url}`} />
        </Switch>
      </Column>
    </Grid>
  );
};

export default ConduitChat;
<|endoftext|>


<commit_before>(function(define){

define(function() {
"use strict";

/* Forward declarations
 ******************************************************************************/
var gen;

/* Helper functions
 ******************************************************************************/
var slice = Array.prototype.slice;

var bind = function(f) { return f.bind.apply(f, [undefined].concat(slice.call(arguments, 1))); };

var identity = function(v) { return v; };

var constant = function(v) { return bind(identity, v); };

var noop = constant(function() { });

/* Built-In Generators
 ******************************************************************************/
/**
 * Factory for generator that greedily iterates over a source generator.
 *
 * Returned generator does not return a value.
 *
 * @param {gen} source Source generator being iterated over.
 * @param {function(value, index)} callback Function called for each element of
 *     source.
 *
 * @return {function(yield, break)} Generator function that performs
 *    the forEach operation when invoked.
 */
var forEach = function(source, callback, t) {
    var i;
    var innerY = function(v) {
        callback.call(t, v, i++);
    };
    return gen(function(y, b) {
        if (i !== undefined) return b();
        i = 0;
        source(innerY, noop);
        return y();
    });
};

/**
 * Factory for a generator that reduces a source generator left to right.
 *
 * Returned generator yields a single value.
 *
 * @param {gen} source Source generator to reduce.
 * @param {function(previous, current, index)} callback Function that reduces
 *     the source.
 * @param initial The initial value used for reduce.
 *
 * @return {function(yield, break)} Generator function that reduces the
 *     source generator when invoked.
 */
var reduce = function(source, callback, initial) {
    var hasRun;
    return gen(function(y, b) {
        if (hasRun) return b();
        var p = initial;
        forEach(source, function(v, i) {
            p = callback(p, v, i);
        })(noop, noop);
        hasRun = true;
        return y(p);
    });
};

/**
 * Factory for generator that transforms values from a source generator.
 *
 * @param {gen} source Source generator being transformed.
 * @param {function(value)} callback Function that transforms source values and
 *     returns the transformed value.
 * @param [thisObj] 'this' object used for callback.
 *
 * @return {function(yield, break)} Transformed generator that yields transformed
 *     values when invoked.
 */
var map = function(source, callback, thisObj) {
    var innerY = function(v) {
        return constant(callback.call(thisObj, v));
    };
    return gen(function(y, b) {
        return y(source(innerY, b));
    });
};

/**
 * Factory for generators that filter a source generator.
 *
 * Returned generator only yields values from the source generator that meet the
 * given condition.
 *
 * @param {gen} source Source generator being filtered.
 * @param {function(value): boolean} predicate Predicate function used to filter
 *     source generator results.
 * @param [thisObj] 'this' object used for predicate.
 *
 * @return {function(yield, break)} Generator that yields filtered source
 *     generator values.
 */
var filter = function(source, predicate, thisObj) {
    var innerY = function(v) {
        if (predicate.call(thisObj, v))
            return constant(v);
    };
    return gen(function(y, b) {
        return y(source(innerY, b));
    });
};

/**
 * Factory for generators that greedily transform a source generator into an array.
 *
 * Returned generator yields a single value, an array of all source generator
 * values.
 *
 * @param {gen} source Source generator
 *
 * @return {function(yield, break)} Generator that yields all source generator
 *     values as an array when invoked.
 */
var toArray = (function(){
    function arrayReduce(p, c) {
        p.push(c);
        return p;
    }
    return function(source) {
        return reduce(source, arrayReduce, []);
    };
}());

/* Exported Objects
 ******************************************************************************/
/**
 * Exception thrown by default when a generator function breaks.
 */
var GenBreak = function(){};
GenBreak.prototype = new Error();
GenBreak.prototype.name = "GenBreak";

/**
 * Properties exported on gen instances.
 *
 * Because we cannot cleanly define a prototype of a function object using
 * standard code, any properties for gen instances must be set using properties.
 * Unlike prototype, adding or changing properties only effects instances created
 * after the property is set.
 */
var properties = {
    'map': {
        'value': function(c, t) { return map(this, c, t ); },
        'writable': true
    },
    'filter': {
        'value': function(p, t) { return filter(this, p, t); },
        'writable': true
    },
    'reduce': {
        'value': function(c, i) { return reduce(this, c, i); },
        'writable': true
    },
    'toArray': {
        'value': function() { return toArray(this); },
        'writable': true
    },
    'forEach': {
        'value': function(c, t) { return forEach(this, c, t); },
        'writable': true
    }

};

/**
 * Factory for gen style generators.
 *
 * Adds flow control logic to generator and default values for yield and break
 * continuations.
 *
 * @param {function(yield, break)} source A generator function that produces
 *     values for this generator.
 *
 * @return {function(yield, break)} A gen style generator function that yields
 *    values from the source generator.
 */
gen = (function(){
    var defaultY = constant;
    var defaultB = constant(function() { throw new GenBreak(); });

    return function(source) {
        var generator = function(y, b) {
            y = y || defaultY;
            b = b || defaultB;
            var continuation;
            do
            {
                continuation = source(y, b);
            } while (continuation === undefined);
            return continuation();
        };
        return Object.defineProperties(generator, properties);
    };
}());

/* Export
 ******************************************************************************/
return Object.defineProperties(gen, {
    'properties': {
        'value': properties
    },
    'GenBreak': GenBreak,
    'map': {
        'value': function(s, c, t) { return map(gen(s), c, t); },
        'writable': true
    },
    'filter': {
        'value': function(s, p, t) { return filter(gen(s), p, t); },
        'writable': true
    },
    'reduce': {
        'value': function(s, c, i) { return reduce(gen(s), c, i); },
        'writable': true
     },
    'toArray': {
        'value': function(s) { return toArray(gen(s)); },
        'writable': true
    },
    'forEach': {
        'value': function(s, c, t) { return forEach(gen(s), c, t); },
        'writable': true
    },
});

});

}(
    typeof define !== 'undefined' ? define : function(factory) { gen = factory(); }
));<commit_msg>JShint formatting fixes<commit_after>(function(define){

define(function() {
"use strict";

/* Forward declarations
 ******************************************************************************/
var gen;

/* Helper functions
 ******************************************************************************/
var slice = Array.prototype.slice;

var bind = function(f) { return f.bind.apply(f, [undefined].concat(slice.call(arguments, 1))); };

var identity = function(v) { return v; };

var constant = function(v) { return bind(identity, v); };

var noop = constant(function() { });

/* Built-In Generators
 ******************************************************************************/
/**
 * Factory for generator that greedily iterates over a source generator.
 *
 * Returned generator does not return a value.
 *
 * @param {gen} source Source generator being iterated over.
 * @param {function(value, index)} callback Function called for each element of
 *     source.
 *
 * @return {function(yield, break)} Generator function that performs
 *    the forEach operation when invoked.
 */
var forEach = function(source, callback, t) {
    var i;
    var innerY = function(v) {
        callback.call(t, v, i++);
    };
    return gen(function(y, b) {
        if (i !== undefined) return b();
        i = 0;
        source(innerY, noop);
        return y();
    });
};

/**
 * Factory for a generator that reduces a source generator left to right.
 *
 * Returned generator yields a single value.
 *
 * @param {gen} source Source generator to reduce.
 * @param {function(previous, current, index)} callback Function that reduces
 *     the source.
 * @param initial The initial value used for reduce.
 *
 * @return {function(yield, break)} Generator function that reduces the
 *     source generator when invoked.
 */
var reduce = function(source, callback, initial) {
    var hasRun;
    return gen(function(y, b) {
        if (hasRun) return b();
        var p = initial;
        forEach(source, function(v, i) {
            p = callback(p, v, i);
        })(noop, noop);
        hasRun = true;
        return y(p);
    });
};

/**
 * Factory for generator that transforms values from a source generator.
 *
 * @param {gen} source Source generator being transformed.
 * @param {function(value)} callback Function that transforms source values and
 *     returns the transformed value.
 * @param [thisObj] 'this' object used for callback.
 *
 * @return {function(yield, break)} Transformed generator that yields transformed
 *     values when invoked.
 */
var map = function(source, callback, thisObj) {
    var innerY = function(v) {
        return constant(callback.call(thisObj, v));
    };
    return gen(function(y, b) {
        return y(source(innerY, b));
    });
};

/**
 * Factory for generators that filter a source generator.
 *
 * Returned generator only yields values from the source generator that meet the
 * given condition.
 *
 * @param {gen} source Source generator being filtered.
 * @param {function(value): boolean} predicate Predicate function used to filter
 *     source generator results.
 * @param [thisObj] 'this' object used for predicate.
 *
 * @return {function(yield, break)} Generator that yields filtered source
 *     generator values.
 */
var filter = function(source, predicate, thisObj) {
    var innerY = function(v) {
        if (predicate.call(thisObj, v))
            return constant(v);
    };
    return gen(function(y, b) {
        return y(source(innerY, b));
    });
};

/**
 * Factory for generators that greedily transform a source generator into an array.
 *
 * Returned generator yields a single value, an array of all source generator
 * values.
 *
 * @param {gen} source Source generator
 *
 * @return {function(yield, break)} Generator that yields all source generator
 *     values as an array when invoked.
 */
var toArray = (function(){
    function arrayReduce(p, c) {
        p.push(c);
        return p;
    }
    return function(source) {
        return reduce(source, arrayReduce, []);
    };
}());

/* Exported Objects
 ******************************************************************************/
/**
 * Exception thrown by default when a generator function breaks.
 */
var GenBreak = function(){};
GenBreak.prototype = new Error();
GenBreak.prototype.name = "GenBreak";

/**
 * Properties exported on gen instances.
 *
 * Because we cannot cleanly define a prototype of a function object using
 * standard code, any properties for gen instances must be set using properties.
 * Unlike prototype, adding or changing properties only effects instances created
 * after the property is set.
 */
var properties = {
    'map': {
        'value': function(c, t) { return map(this, c, t ); },
        'writable': true
    },
    'filter': {
        'value': function(p, t) { return filter(this, p, t); },
        'writable': true
    },
    'reduce': {
        'value': function(c, i) { return reduce(this, c, i); },
        'writable': true
    },
    'toArray': {
        'value': function() { return toArray(this); },
        'writable': true
    },
    'forEach': {
        'value': function(c, t) { return forEach(this, c, t); },
        'writable': true
    }
};

/**
 * Factory for gen style generators.
 *
 * Adds flow control logic to generator and default values for yield and break
 * continuations.
 *
 * @param {function(yield, break)} source A generator function that produces
 *     values for this generator.
 *
 * @return {function(yield, break)} A gen style generator function that yields
 *    values from the source generator.
 */
gen = (function(){
    var defaultY = constant;
    var defaultB = constant(function() { throw new GenBreak(); });

    return function(source) {
        var generator = function(y, b) {
            y = y || defaultY;
            b = b || defaultB;
            var continuation;
            do
            {
                continuation = source(y, b);
            } while (continuation === undefined);
            return continuation();
        };
        return Object.defineProperties(generator, properties);
    };
}());

/* Export
 ******************************************************************************/
return Object.defineProperties(gen, {
    'properties': {
        'value': properties
    },
    'GenBreak': GenBreak,
    'map': {
        'value': function(s, c, t) { return map(gen(s), c, t); },
        'writable': true
    },
    'filter': {
        'value': function(s, p, t) { return filter(gen(s), p, t); },
        'writable': true
    },
    'reduce': {
        'value': function(s, c, i) { return reduce(gen(s), c, i); },
        'writable': true
     },
    'toArray': {
        'value': function(s) { return toArray(gen(s)); },
        'writable': true
    },
    'forEach': {
        'value': function(s, c, t) { return forEach(gen(s), c, t); },
        'writable': true
    }
});

});

}(
    typeof define !== 'undefined' ? define : function(factory) { gen = factory(); }
));<|endoftext|>


<commit_before>var fs = require('fs');

module.exports = function (context, req, res) {
    context.log('function triggered');
    context.log(req);

    fs.writeFile('message.txt', 'Hello Node.js', (err) => {
        if (err) throw err;
        console.log('It\'s saved!');
        context.done();
    });
}<commit_msg>Add print out of environment variables, cwd, dirname, and change file write destination to local directory.<commit_after>var fs = require('fs');

var environmentVariables = [
    "WEBSITE_SITE_NAME",
    "WEBSITE_SKU",
    "WEBSITE_COMPUTE_MODE",
    "WEBSITE_HOSTNAME",
    "WEBSITE_INSTANCE_ID",
    "WEBSITE_NODE_DEFAULT_VERSION",
    "WEBSOCKET_CONCURRENT_REQUEST_LIMIT",
    "%APPDATA%",
    "%TMP%"
]

module.exports = function (context, req, res) {
    context.log('function triggered');
    context.log(req);

    context.log('Environment Variables');
    environmentVariables
        .map(getEnvironmentVarible)
        .forEach(context.log);

    context.log('process.cwd()', process.cwd());
    context.log('__dirname', __dirname);

    fs.writeFile('D:/local/Temp/message.txt', 'Hello Node.js', (err) => {
        if (err) throw err;
        context.log("It's saved!");
        context.done();
    });
}

function getEnvironmentVarible(name) {
    return name + ": " + process.env[name];
}<|endoftext|>


<commit_before>
function get(property) {
  return function (object) {
    return object[property];
  };
}

module.exports = get;
<commit_msg>Added get<commit_after>function get(property) {
  return function (object) {
    return object[property];
  };
}

module.exports = get;
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import Immutable from 'immutable';

import { FormControl, FormGroup, ControlLabel, Button, Alert } from 'react-bootstrap';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import AsyncContent from '../../components/asynccontent/AsyncContent';
import { editAppRequest } from './AppActionFactory';

class EditApp extends React.Component {

  static propTypes = {
    actions: PropTypes.shape({
      editAppRequest: PropTypes.func.isRequired
    }).isRequired,
    editAppAsyncState: PropTypes.instanceOf(Immutable.Map).isRequired,
    id: PropTypes.string.isRequired,
    name: PropTypes.string.isRequired,
    title: PropTypes.string.isRequired,
    description: PropTypes.string.isRequired,
    url: PropTypes.string.isRequired
  }

  constructor(props) {
    super(props);
    this.state = {
      title: '',
      description: '',
      name: '',
      url: ''
    };
  }

  onTitleChange = (event) => {
    this.setState({
      title: event.target.value
    });
  };

  onNameChange = (event) => {
    this.setState({
      name: event.target.value
    });
  };

  onDescriptionChange = (event) => {
    this.setState({
      description: event.target.value
    });
  };

  onUrlChange = (event) => {
    this.setState({
      url: event.target.value
    });
  };

  onSubmit = () => {
    const { title, description, name, url } = this.state;

    // We need to collect only items that have been changed. AKA NOT an empty string.
    // Having issues with this because of js and iterables.
    // Collect the items, check each for change, add changed to appData object.
    const tempMap = new Map([['title', title], ['description', description], ['name', name], ['url', url]]);
    const keys = tempMap.keys();
    const appData = {};

    for (const item of keys) {
      if (tempMap.get(item) !== '') {
        appData[item] = tempMap.get(item);
      }
    }

    const appId = this.props.id;
    this.props.actions.editAppRequest(appId, appData);
  }

  renderPending = () => {
    return (
      <form onSubmit={this.onSubmit}>
        <FormGroup>
          <ControlLabel>Name</ControlLabel>
          <FormControl type="text" onChange={this.onNameChange} placeholder={this.props.name} />
        </FormGroup>

        <FormGroup>
          <ControlLabel>Title</ControlLabel>
          <FormControl type="text" onChange={this.onTitleChange} placeholder={this.props.title} />
        </FormGroup>

        <FormGroup>
          <ControlLabel>Description</ControlLabel>
          <FormControl componentClass="textarea" onChange={this.onDescriptionChange} placeholder={this.props.description} />
        </FormGroup>

        <FormGroup>
          <ControlLabel>Url</ControlLabel>
          <FormControl type="text" onChange={this.onUrlChange} placeholder={this.props.url} />
        </FormGroup>

        <Button type="submit" bsStyle="primary">Update App</Button>
      </form>
    );
  };

  renderSuccess = () => {
    return (
      <Alert bsStyle="success">
        Successfully saved changes
      </Alert>
    );
  };

  render() {
    return (
      <AsyncContent
          status={this.props.editAppAsyncState.get('status')}
          errorMessage={this.props.editAppAsyncState.get('errorMessage')}
          pendingContent={this.renderPending()}
          content={this.renderSuccess} />
    );
  }

}

function mapStateToProps(state) {
  const editAppAsyncState = state.getIn(['app', 'editAppAsyncState']);

  return {
    editAppAsyncState
  };
}

function mapDispatchToProps(dispatch) {

  const actions = {
    editAppRequest
  };

  return {
    actions: bindActionCreators(actions, dispatch)
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(EditApp);
<commit_msg>Updated the onSubmit method to be more simple in EditApp.<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import Immutable from 'immutable';

import { FormControl, FormGroup, ControlLabel, Button, Alert } from 'react-bootstrap';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import AsyncContent from '../../components/asynccontent/AsyncContent';
import { editAppRequest } from './AppActionFactory';

class EditApp extends React.Component {

  static propTypes = {
    actions: PropTypes.shape({
      editAppRequest: PropTypes.func.isRequired
    }).isRequired,
    editAppAsyncState: PropTypes.instanceOf(Immutable.Map).isRequired,
    id: PropTypes.string.isRequired,
    name: PropTypes.string.isRequired,
    title: PropTypes.string.isRequired,
    description: PropTypes.string.isRequired,
    url: PropTypes.string.isRequired
  }

  constructor(props) {
    super(props);
    this.state = {
      title: '',
      description: '',
      name: '',
      url: ''
    };
  }

  onTitleChange = (event) => {
    this.setState({
      title: event.target.value
    });
  };

  onNameChange = (event) => {
    this.setState({
      name: event.target.value
    });
  };

  onDescriptionChange = (event) => {
    this.setState({
      description: event.target.value
    });
  };

  onUrlChange = (event) => {
    this.setState({
      url: event.target.value
    });
  };

  onSubmit = () => {
    const { title, description, name, url } = this.state;
    const appData = {};

    // Collect only items that have been changed. AKA NOT an empty string.
    if (title && title.length > 0) {
      appData['title'] = title;
    }
    if (description && description.length > 0) {
      appData['description'] = description;
    }
    if (name && name.length > 0) {
      appData['name'] = name;
    }
    if (url && url.length > 0) {
      appData['url'] = url;
    }

    const appId = this.props.id;
    this.props.actions.editAppRequest(appId, appData);
  }

  renderPending = () => {
    return (
      <form onSubmit={this.onSubmit}>
        <FormGroup>
          <ControlLabel>Name</ControlLabel>
          <FormControl type="text" onChange={this.onNameChange} placeholder={this.props.name} />
        </FormGroup>

        <FormGroup>
          <ControlLabel>Title</ControlLabel>
          <FormControl type="text" onChange={this.onTitleChange} placeholder={this.props.title} />
        </FormGroup>

        <FormGroup>
          <ControlLabel>Description</ControlLabel>
          <FormControl componentClass="textarea" onChange={this.onDescriptionChange} placeholder={this.props.description} />
        </FormGroup>

        <FormGroup>
          <ControlLabel>Url</ControlLabel>
          <FormControl type="text" onChange={this.onUrlChange} placeholder={this.props.url} />
        </FormGroup>

        <Button type="submit" bsStyle="primary">Update App</Button>
      </form>
    );
  };

  renderSuccess = () => {
    return (
      <Alert bsStyle="success">
        Successfully saved changes
      </Alert>
    );
  };

  render() {
    return (
      <AsyncContent
          status={this.props.editAppAsyncState.get('status')}
          errorMessage={this.props.editAppAsyncState.get('errorMessage')}
          pendingContent={this.renderPending()}
          content={this.renderSuccess} />
    );
  }

}

function mapStateToProps(state) {
  const editAppAsyncState = state.getIn(['app', 'editAppAsyncState']);

  return {
    editAppAsyncState
  };
}

function mapDispatchToProps(dispatch) {

  const actions = {
    editAppRequest
  };

  return {
    actions: bindActionCreators(actions, dispatch)
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(EditApp);
<|endoftext|>


<commit_before>// Base class for all models.
module.exports = Backbone.Model.extend({

    original_model:new Backbone.Model(),

    original:function (value) {
        if (!arguments.length) return this.original_collection;
        this.original_collection = value;
        return this;
    },

    make_copy: function(ModelClass, options) {
        if (ModelClass.prototype.add) {
            // is this a collection
            var copy = new ModelClass(options);
            copy.add(this.toJSON(), {silent:true});
            this.original_collection = copy;
        } else {
            //nope its a model
            this.original_collection = new ModelClass(this.toJSON());
        }
    },

    standard_fetch:function () {
        this.fetch({
            success:function (m) {
                m.trigger("load");
            },
            error: function(m, resp) {
                m.trigger("error", resp);
            }
        });
        return this;
    }
});
<commit_msg>removed standard_fetch<commit_after>// Base class for all models.
module.exports = Backbone.Model.extend({

    original_model:new Backbone.Model(),

    original:function (value) {
        if (!arguments.length) return this.original_collection;
        this.original_collection = value;
        return this;
    },

    make_copy: function(ModelClass, options) {
        if (ModelClass.prototype.add) {
            // is this a collection
            var copy = new ModelClass(options);
            copy.add(this.toJSON(), {silent:true});
            this.original_collection = copy;
        } else {
            //nope its a model
            this.original_collection = new ModelClass(this.toJSON());
        }
    }
});
<|endoftext|>


<commit_before>import React from 'react'
import ReactDOM from 'react-dom'
import _ from 'lodash'
import when from 'when'
import whenKeys from 'when/keys'
import API from '../../api'

const UNINITIALIZED = 'uninitialized'
const AWAITING = 'awaiting'
const READY = 'ready'
const ERROR = 'error'

export default class FetchContainer extends React.Component {
  static propTypes = {
    endpoint: React.PropTypes.string.isRequired,
    endpointOptions: React.PropTypes.object.isRequired,
    blocking: React.PropTypes.bool,
    single: React.PropTypes.bool,
    mapper: React.PropTypes.func,
    transformer: React.PropTypes.func,
    element: React.PropTypes.string,
    flatten: React.PropTypes.bool,
    injectAs: React.PropTypes.string
  }

  static defaultProps = {
    endpointOptions: {},
    single: false,
    blocking: false,
    mapper: _.identity,
    transformer: (a, v, k) => _.set(a, k, v),
    transformAccumulator: {},
    element: 'div',
    client: undefined, // Causes API to use default client
    flatten: false,
    injectAs: 'data'
  }

  constructor(props) {
    super(props)
    this.state = {
      status: AWAITING,
      endpointData: null,
      propsData: null,
      message: null
    }

    this.api = new API(this.props.client)

    const propsPromises = _(this.props)
      .omit([ 'className', 'endpoint', 'client', 'element', 'endpointOptions', 'children' ])
      .mapValues(p => when(p))
      .value()

    const endpointPromise = this.api.loadJSON(this.props.endpoint, this.props.endpointOptions)
    const propsPromiseObject = whenKeys.all(propsPromises)

    const attributesPromise = when.all([ endpointPromise, propsPromiseObject ])
      .then(([endpointData, propsData]) => this.setState({ status: READY, endpointData, propsData }))
      .catch(e => this.setState({ status: ERROR, message: e }))
      .finally(_.noop)
  }

  render() {
    let finalProps = {}

    if ( this.state.endpointData ) {

      _.assign(finalProps, this.state.propsData)

      if ( this.props.single ) {
        if ( this.props.flatten ) {
          _.assign(
            finalProps,
            _.transform(this.state.endpointData, this.props.transformer, this.props.transformAccumulator)
          )
        }
        else {
          _.assign(
            finalProps,
            {
              [this.props.injectAs]: _.transform(this.state.endpointData, this.props.transformer, this.props.transformAccumulator)
            }
          )
        }
      }
      else {
        _.assign(finalProps, { [this.props.injectAs]: _.map(this.state.endpointData, this.props.mapper) })
      }

    }

    let mappedChildren = []

    if ( !this.props.blocking || this.state.status === READY ) {
      mappedChildren = React.Children.map(this.props.children, c => React.cloneElement(c, finalProps))
    }

    const className = this.props.className
    return React.createElement(this.props.element, { className }, ...mappedChildren)
  }
}

//export {PresentState, LoadingState, EmptyState, ErrorState} from 'snax/containers/loading'
<commit_msg>Make blocking the default FetchContainer behavior<commit_after>import React from 'react'
import ReactDOM from 'react-dom'
import _ from 'lodash'
import when from 'when'
import whenKeys from 'when/keys'
import API from '../../api'

const UNINITIALIZED = 'uninitialized'
const AWAITING = 'awaiting'
const READY = 'ready'
const ERROR = 'error'

export default class FetchContainer extends React.Component {
  static propTypes = {
    endpoint: React.PropTypes.string.isRequired,
    endpointOptions: React.PropTypes.object.isRequired,
    blocking: React.PropTypes.bool,
    single: React.PropTypes.bool,
    mapper: React.PropTypes.func,
    transformer: React.PropTypes.func,
    element: React.PropTypes.string,
    flatten: React.PropTypes.bool,
    injectAs: React.PropTypes.string
  }

  static defaultProps = {
    endpointOptions: {},
    single: false,
    blocking: true,
    mapper: _.identity,
    transformer: (a, v, k) => _.set(a, k, v),
    transformAccumulator: {},
    element: 'div',
    client: undefined, // Causes API to use default client
    flatten: false,
    injectAs: 'data'
  }

  constructor(props) {
    super(props)
    this.state = {
      status: AWAITING,
      endpointData: null,
      propsData: null,
      message: null
    }

    this.api = new API(this.props.client)

    const propsPromises = _(this.props)
      .omit([ 'className', 'endpoint', 'client', 'element', 'endpointOptions', 'children' ])
      .mapValues(p => when(p))
      .value()

    const endpointPromise = this.api.loadJSON(this.props.endpoint, this.props.endpointOptions)
    const propsPromiseObject = whenKeys.all(propsPromises)

    const attributesPromise = when.all([ endpointPromise, propsPromiseObject ])
      .then(([endpointData, propsData]) => this.setState({ status: READY, endpointData, propsData }))
      .catch(e => this.setState({ status: ERROR, message: e }))
      .finally(_.noop)
  }

  render() {
    let finalProps = {}

    if ( this.state.endpointData ) {

      _.assign(finalProps, this.state.propsData)

      if ( this.props.single ) {
        if ( this.props.flatten ) {
          _.assign(
            finalProps,
            _.transform(this.state.endpointData, this.props.transformer, this.props.transformAccumulator)
          )
        }
        else {
          _.assign(
            finalProps,
            {
              [this.props.injectAs]: _.transform(this.state.endpointData, this.props.transformer, this.props.transformAccumulator)
            }
          )
        }
      }
      else {
        _.assign(finalProps, { [this.props.injectAs]: _.map(this.state.endpointData, this.props.mapper) })
      }

    }

    let mappedChildren = []

    if ( !this.props.blocking || this.state.status === READY ) {
      mappedChildren = React.Children.map(this.props.children, c => React.cloneElement(c, finalProps))
    }

    const className = this.props.className
    return React.createElement(this.props.element, { className }, ...mappedChildren)
  }
}

//export {PresentState, LoadingState, EmptyState, ErrorState} from 'snax/containers/loading'
<|endoftext|>


<commit_before>function UnfinishedToggler(options) {

  var uft = this,
  s = uft.settings = $.extend({}, uft.defaults, options);
  uft.onCount = 0;
  uft.$root = $(s.root);
  uft.$triggers = uft.$root.find(s.triggerSelector);
  // $items contains the elements whose onClass will
  // be toggled. If `scattered` is false, $items only
  // include the containing items; if `scattered` is
  // true, items include triggers and contents.
  uft.$items = (!s.scattered) ? uft.$root.find(s.groupSelector)
                              : uft.$triggers.add(uft.$root.find(s.contentSelector));

  uft.init();
}

UnfinishedToggler.prototype = {

  /*============================
  INITIALIZATION
  ==============================*/

  init: function() {
    var uft = this,
        s = uft.settings;

    uft.data = {
      isDebouncing: false
    };

    // nmEvents is a namespaced version of the triggering events.
    uft.nmEvents = $.map(s.events, function(ev) {
      return uft.namespaceEvent(ev);
    }).join(' ');

    // Enable!
    uft.enable();

    // If nothing is turned on in the markup,
    // but settings say all cannot be off,
    // trigger the first trigger with the first event.
    if ((!s.allOff && !uft.getOnItems().length))
      uft.$triggers.first().trigger(uft.nmEvents.split(' ')[0]);

    // NEXT AND PREV
    // groupIds will contain the IDs of all groups, so methods
    // next() and prev() can move in order.
    uft.groupIds = [];
    uft.groupIds = $.map(uft.$items, function(group, i) {
      var thisGroupId = $(group).data('uft-group');
      if (thisGroupId && uft.groupIds.indexOf(thisGroupId) === -1)
        return thisGroupId;
    });

    // INNER FOCUS
    // Enable innerFocus, if settings say to do that.
    if (s.innerFocus) {
      uft.$root.find(s.innerFocus).on(uft.namespaceEvent('focus'), function(e) {
        if (!uft.data.isDebouncing) {
          // Firefox
          uft.data.isDebouncing = true;
          uft.innerFocus.call(uft, e);
        }
      });
    }
  },

  /*============================
  LITTLE UTIL FUNCTIONS
  ==============================*/

  namespaceEvent: function(ev) {
    return ev + '.' + this.settings.eventNamespace;
  },

  isOn: function($el) {
    // Check if an element is on.
    return $el.hasClass(this.settings.onClass);
  },

  getGroupById: function(id) {
    // `id` is a number.
    // Get all items idenfitified in the group of that number.
    return this.$items.filter('[data-uft-group="' + id + '"]');
  },

  getOnItems: function() {
    // Get all relevant elements marked by the onClass.
    return this.$items.filter('.' + this.settings.onClass);
  },

  getGroup: function(input) {
    // Get the group related to `input`.
    // `input` can be a selector or a number.
    var uft = this,
        s = uft.settings;
    if (typeof input === 'number')
      // If input is a number, get the group with that number.
      return uft.getGroupById(input);
    // If input is a selector ...
    else if (s.scattered)
      // ... and scattered is true, get the group by its number.
      return uft.getGroupById($(input).data('uft-group'));
    else
      // ... and scattered is false, get the group-containing element.
      return $(input).closest(s.groupSelector);
  },

  debounceDone: function() {
    // Rudimentary debouncing.
    var uft = this;
    setTimeout(function() {
      uft.data.isDebouncing = false;
    }, 200);
  },

  optionalDelay: function(delay, func) {
    if (delay > 0)
      window.setTimeout(func, delay);
    else
      func();
  },

  /*============================
  CORE FUNCTIONS
  ==============================*/

  enable: function() {
    var uft = this,
        s = uft.settings;

    // Bind triggers.
    uft.$triggers.on(uft.nmEvents, function(e) {
      if (!uft.data.isDebouncing) {
        uft.data.isDebouncing = true;
        e.preventDefault();
        uft.trigger.call(uft, this);
      }
    });

    // Bind next and prev, if they exist.
    if (s.nextSelector) {
      $(s.nextSelector).on(uft.namespaceEvent('click'), function(e) {
        e.preventDefault();
        uft.next.call(uft, this);
      });
    }
    if (s.prevSelector) {
      $(s.prevSelector).on(uft.namespaceEvent('click'), function(e) {
        e.preventDefault();
        uft.prev.call(uft, this);
      });
    }
  },

  disable: function() {
    var s = this.settings;
    // Unbind triggers.
    this.$triggers.off(uft.namespaceEvent(''));
    if (s.nextSelector)
      $(s.nextSelector).off(uft.namespaceEvent(''));
    if (s.prevSelector)
      $(s.prevSelector).off(uft.namespaceEvent(''));
  },

  trigger: function(input) {
    // `input` can be a selector or a number.

    var uft = this,
        s = uft.settings,
        $group = uft.getGroup(input),
        turningOn = !uft.isOn($group);

    console.log(turningOn, $group);

    if (turningOn)
      uft.turnOn($group);

    // If settings say so, don't allow the last turned-on
    // group to turn off.
    else if (s.allOff || (!s.allOff && uft.onCount > 1))
      uft.turnOff($group);

    // If outsideTurnsOff, enable it.
    if (s.outsideTurnsOff)
      uft.outsideTurnsOff($group, turningOn);
  },

  turnOn: function($group) {
    var uft = this,
        s = uft.settings,
        $onItems;

    if (s.onlyOneOn) {
      // If we can only turn one on at a time,
      // make sure others are turned off.
      $onItems = uft.getOnItems();
      if (s.transOverlap) {
        // If overlap is allowed, turn off others
        // and turn on $group at the same time.
        uft.turnOff($onItems);
        doTheTurningOn();
      } else {
        // If overlap is not allowed, turn on $group
        // only after others are done turning off.
        uft.turnOff($onItems, doTheTurningOn);
      }
    } else {
      // If onlyOneOn is false, just turn $group on with no worries.
      doTheTurningOn();
    }

    // If freezeScroll is part of this, now is the time to
    // turn it on.
    if (s.freezeScroll)
      uft.freezeScrollOn();

    function doTheTurningOn() {
      // Remove offClass, add onClass.
      $group.removeClass(s.offClass)
        .addClass(s.onClass);
      // After specified delay, add transClass.
      uft.optionalDelay(s.onTransDelay, function() {
        $group.addClass(s.transClass);
      });
      // Call user-defined callback, passing some data.
      s.onCallback({ '$group': $group, 'action': 'on'});
      // Add one to the onCount.
      uft.onCount++;
      // Open the gates again.
      uft.debounceDone();
    }
  },

  turnOff: function($group, callback) {
    var uft = this,
        s = uft.settings,
        cb = callback || function(){},
        offTransClass = s.transClass + '-off';

    if ($group.length) {

      // First, remove transClass and add
      // offTransClass.
      $group.removeClass(s.transClass)
        .addClass(offTransClass);

      uft.optionalDelay(s.offTransTime, function() {
        // After the specificied delay, do the rest.
        cb();
        $group.removeClass(s.onClass)
          .removeClass(offTransClass)
          .addClass(s.offClass);
        // Call user-defined callback, passing some data.
        s.offCallback({ '$group': $group, 'action': 'off'});
        // Subtract from onCount.
        uft.onCount--;
        // Open the gates again.
        uft.debounceDone();
        // If the scroll was frozen, unfreeze.
        if (s.freezeScroll)
          uft.freezeScrollOff();
      });

    } else {
      // If $group is empty, just call the callback.
      cb();
    }
  },

  turnAllOn: function() {
    // If onlyOneOn is not true, turn on all items.
    if (!this.settings.onlyOneOn)
      this.turnOn(this.$items);
    else
      throw new Error('UnfinishedToggler will not turnAllOn with the setting {onlyOneOn: true}.');
  },

  turnAllOff: function() {
    // If allOff is allowed, turn off all items.
    if (this.settings.allOff)
      this.turnOff(this.$items);
    else
      throw new Error('UnfinishedToggler will not turnAllOff with the setting {allOff: true}.');
  },

  /*============================
  NEXT AND PREVIOUS
  ==============================*/

  nextOrPrev: function(dir) {
    var uft = this,
        s = uft.settings,
        nextPrevErrorStart = 'UnfinishedToggler cannot use next() and prev() ',
        currentGroup, firstGroup, lastGroup, targetGroup;

    // First, check that next() or prev() make sense with the setup.
    if (!s.onlyOneOn)
      throw new Error(nextPrevErrorStart + 'with the setting {onlyOneOn: false}.');

    currentGroup = uft.getOnItems().first().data('uft-group');
    if (typeof currentGroup === 'undefined')
      throw new Error(nextPrevErrorStart + 'unless data-uft-group values are defined.');
    else if (typeof currentGroup !== 'number')
      throw new Error(nextPrevErrorStart + 'unless data-uft-group values are integers.');

    firstGroup = Math.min.apply(Math, uft.groupIds);
    lastGroup = Math.max.apply(Math, uft.groupIds);
    if (dir === 'next')
      targetGroup = (currentGroup + 1 <= lastGroup) ? currentGroup + 1 : firstGroup;
    else if (dir === 'prev')
      targetGroup = (currentGroup - 1 >= firstGroup) ? currentGroup - 1 : lastGroup;
    uft.trigger(targetGroup);
  },

  next: function() {
    this.nextOrPrev('next');
  },

  prev: function() {
    this.nextOrPrev('prev');
  },

  /*============================
  FREEZE SCROLL
  ==============================*/

  freezeScrollOn: function() {
    var uft = this,
        rootStyles = { overflow: 'hidden' };
    // If there is a scrollbar
    if (uft.hasScrollbar) {
      // ... get its size
      var scrollbarSize = uft.getScrollbarSize();
      // ... and if there's any size, offset margin to account.
      if (scrollbarSize)
        rootStyles['margin-right'] = scrollbarSize;
    }
    $('html').css(rootStyles);
  },

  freezeScrollOff: function() {
    $('html').css({ 'overflow': '', 'margin-right': '' });
  },

  getScrollbarSize: function() {
    // Thanks to code from MagnificPopup
    if(this.data.scrollbarSize === undefined) {
      var scrollDiv = document.createElement("div");
      scrollDiv.style.cssText = 'width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;';
      document.body.appendChild(scrollDiv);
      this.data.scrollbarSize = scrollDiv.offsetWidth - scrollDiv.clientWidth;
      document.body.removeChild(scrollDiv);
    }
    return this.data.scrollbarSize;
  },

  hasScrollbar: function() {
    return document.body.scrollHeight > $(window).height();
  },

  /*============================
  INNER FOCUS
  ==============================*/

  innerFocus: function(e) {
    // Find the focus-target's group, and trigger it.
    var uft = this,
        s = uft.settings,
        $el = $(e.target),
        $groupPart = (!s.scattered) ? $el : $el.closest(s.contentSelector),
        groupIsOn = uft.isOn(uft.getGroup($groupPart));

    if (!groupIsOn)
      uft.trigger($groupPart);
    else
      uft.data.isDebouncing = false;
  },

  /*============================
  OUTSIDE TURNS OFF
  ==============================*/

  outsideTurnsOff: function($group, turningOn) {
    var uft = this,
        s = uft.settings,
        // touchend is necessary for iOS.
        ev = uft.namespaceEvent('click') + ' ' + uft.namespaceEvent('touchend'),
        $inside = (typeof s.outsideTurnsOff === 'string') ? $(s.outsideTurnsOff) : $group;

    if (turningOn)
      outsideEnable();
    else
      outsideDisable();

    function outsideEnable() {
      // Make it so that clicking anywhere outside $inside
      // turns off $group.
      $inside.on(ev, function(e) {
        e.stopPropagation();
      });
      // Only allow the event once: it will get
      // re-assign if $group opens again.
      // Timeout ensures that assignment happens
      // after triggering event is done.
      setTimeout(function() {
        $('html').on(ev, outsideTriggered);
      }, 40);
    }

    function outsideDisable() {
      $('html').off(uft.namespaceEvent(''));
    }

    function outsideTriggered(e) {
      if (!$inside.is($(e.target))) {
        uft.turnOff($group);
        outsideDisable();
      }
    }
  }
};

/*============================
DEFAULT SETTINGS
==============================*/

UnfinishedToggler.prototype.defaults = {
  // selector for a context-element containing all the others,
  // within which to find the toggler's parts
  'root': 'body',
  // class to turn groups and elements on
  'onClass': 'uft-on',
  // class to turn groups and elements off
  'offClass': 'uft-off',
  // scattered is true if the triggers and content
  // are not children of group-containing elements.
  // scattered groups are identified by data-uft-group attributes.
  'scattered': false,
  // selector for trigger elements
  'triggerSelector': '.uft-trigger',
  // selector for group-containing elements
  // (only used when {scattered: false})
  'groupSelector': '.uft-group',
  // selector for content elements
  // (only used when {scattered: true})
  'contentSelector': '.uft-content',
  // allow only one item to be on at a time
  'onlyOneOn' : true,
  // allow all items to be turned off at the same time
  'allOff' : true,
  // the events that trigger a change
  'events': ['click'],
  // a namespace for uft-related events
  'eventNamespace': 'uft',
  // a callback to perform after something is turned on
  'onCallback': function(){},
  // a callback to perform after something is turned off
  'offCallback': function(){},
  // class that is added just after turning on
  // and removed just before turning off,
  // useful for adding extra CSS transitions
  'transClass': 'uft-trans',
  // delay between turning on and adding transClass.
  // 40ms is minimum, in case turning on involves a `display` switch
  'onTransDelay': 40,
  // delay between removing transClass and turning off
  'offTransTime': 0,
  // transClasses of multiple groups can overlap,
  // so one group can be transitioning off while another
  // transitions on
  'transOverlap': true,

  // NEXT AND PREV
  'nextSelector': false,
  'prevSelector': false,

  // FREEZE SCROLL
  'freezeScroll': false,

  // INNER FOCUS
  'innerFocus': false,

  // OUTSIDE TUNRS OFF
  'outsideTurnsOff': false

};<commit_msg>Some clarifications and comments<commit_after>function UnfinishedToggler(options) {
  var uft = this,
  s = uft.settings = $.extend({}, uft.defaults, options);
  uft.onCount = 0;
  uft.$root = $(s.root);
  uft.$triggers = uft.$root.find(s.triggerSelector);
  // `$items` contains the elements whose on and off state will
  // be toggled. If `scattered` is false, `$items` only
  // includes the group-container items; if `scattered` is
  // true, `$items` includes triggers and contents.
  uft.$items = (!s.scattered) ? uft.$root.find(s.groupSelector)
                              : uft.$triggers.add(uft.$root.find(s.contentSelector));
  uft.init();
}

UnfinishedToggler.prototype = {

  /*============================
  INITIALIZATION
  ==============================*/

  init: function() {
    var uft = this,
        s = uft.settings;

    uft.data = {
      // `isDebouncing` is used to ensure that toggling
      // doesn't overlap already-toggling-toggling.
      isDebouncing: false
    };

    // `nmEvents` is a namespaced version of the triggering event(s).
    uft.nmEvents = $.map(s.events, function(ev) {
      return uft.namespaceEvent(ev);
    }).join(' ');

    // Enable!
    uft.enable();

    // If nothing is turned on in the markup,
    // but settings say all cannot be off,
    // trigger the first trigger with the first event.
    if ((!s.allOff && !uft.getOnItems().length))
      uft.$triggers.first().trigger(uft.nmEvents.split(' ')[0]);

    // NEXT AND PREV
    // `groupIds` will contain the IDs of all groups, so methods
    // .next() and .prev() can move in order.
    uft.groupIds = [];
    uft.groupIds = $.map(uft.$items, function(group, i) {
      var thisGroupId = $(group).data('uft-group');
      if (thisGroupId && uft.groupIds.indexOf(thisGroupId) === -1)
        return thisGroupId;
    });

    // INNER FOCUS
    // Enable innerFocus functionality, if settings say to do that.
    if (s.innerFocus) {
      uft.$root.find(s.innerFocus).on(uft.namespaceEvent('focus'), function(e) {
        if (!uft.data.isDebouncing) {
          // Firefox
          uft.data.isDebouncing = true;
          uft.innerFocus.call(uft, e);
        }
      });
    }
  },

  /*============================
  LITTLE UTIL FUNCTIONS
  ==============================*/

  namespaceEvent: function(ev) {
    // Just add a namespace to an event.
    return ev + '.' + this.settings.eventNamespace;
  },

  isOn: function($el) {
    // Check if an element is on.
    return $el.hasClass(this.settings.onClass);
  },

  getGroupById: function(id) {
    // `id` is a number.
    // Get all items idenfitified in the group of that number.
    return this.$items.filter('[data-uft-group="' + id + '"]');
  },

  getOnItems: function() {
    // Get all relevant elements marked by the `onClass`.
    return this.$items.filter('.' + this.settings.onClass);
  },

  getGroup: function(input) {
    // `input` can be a selector or a number.
    // Get the group related to `input`.
    var uft = this,
        s = uft.settings;
    if (typeof input === 'number')
      // If `input` is a number, get the group with that number.
      return uft.getGroupById(input);
    // If `input` is a selector ...
    else if (s.scattered)
      // ... and `scattered` is true, get the group by its number.
      return uft.getGroupById($(input).data('uft-group'));
    else
      // ... and `scattered` is false, get the group-containing element.
      return $(input).closest(s.groupSelector);
  },

  debounceDone: function() {
    // Rudimentary debouncing. 200ms should be sufficient.
    var uft = this;
    setTimeout(function() {
      uft.data.isDebouncing = false;
    }, 200);
  },

  optionalDelay: function(delay, func) {
    // Don't bother creating a setTimeout if there's no delay.
    if (delay)
      window.setTimeout(func, delay);
    else
      func();
  },

  /*============================
  CORE FUNCTIONALITY
  ==============================*/

  enable: function() {
    var uft = this,
        s = uft.settings;

    // Bind triggers.
    uft.$triggers.on(uft.nmEvents, function(e) {
      if (!uft.data.isDebouncing) {
        uft.data.isDebouncing = true;
        uft.trigger.call(uft, this);
      }
    });

    // Bind next- and prev-triggers, if they exist.
    if (s.nextSelector) {
      $(s.nextSelector).on(uft.namespaceEvent('click'), function(e) {
        uft.next.call(uft, this);
      });
    }
    if (s.prevSelector) {
      $(s.prevSelector).on(uft.namespaceEvent('click'), function(e) {
        uft.prev.call(uft, this);
      });
    }
  },

  disable: function() {
    var s = this.settings;
    // Unbind all triggers.
    this.$triggers.off(uft.namespaceEvent(''));
    if (s.nextSelector)
      $(s.nextSelector).off(uft.namespaceEvent(''));
    if (s.prevSelector)
      $(s.prevSelector).off(uft.namespaceEvent(''));
  },

  trigger: function(input) {
    // `input` can be a selector or a number.

    var uft = this,
        s = uft.settings,
        $group = uft.getGroup(input),
        // If the group does not already have the `onClass`,
        // we'll assumed it needs to be turned on.
        turningOn = !uft.isOn($group);

    if (turningOn)
      uft.turnOn($group);

    // If settings don't permit all to be off,
    // don't allow the last remaining turned-on group to turn off.
    else if (s.allOff || (!s.allOff && uft.onCount > 1))
      uft.turnOff($group);

    // If outsideTurnsOff, trigger it.
    if (s.outsideTurnsOff)
      uft.outsideTurnsOff($group, turningOn);
  },

  turnOn: function($group) {
    var uft = this,
        s = uft.settings,
        $onItems;

    // If we can only turn one on at a time,
    // make sure others are turned off.
    if (s.onlyOneOn) {
      $onItems = uft.getOnItems();
      if (s.transOverlap) {
        // If overlap is allowed, turn off others
        // and turn on `$group` at the same time.
        uft.turnOff($onItems);
        doTheTurningOn();
      } else {
        // If overlap is not allowed, turn on `$group`
        // only after others are done turning off.
        uft.turnOff($onItems, doTheTurningOn);
      }
    // If `onlyOneOn` is false, just turn on `$group` with no worries.
    } else {
      doTheTurningOn();
    }

    // If `freezeScroll` is set, turn it on now.
    if (s.freezeScroll)
      uft.freezeScrollOn();

    function doTheTurningOn() {
      // Remove `offClass`, add `onClass`.
      $group.removeClass(s.offClass)
        .addClass(s.onClass);
      // After specified delay, add `transClass`.
      uft.optionalDelay(s.onTransDelay, function() {
        $group.addClass(s.transClass);
      });
      // Call user-defined callback, passing some data.
      s.onCallback({ '$group': $group, 'action': 'on'});
      // Add one to the `onCount`.
      uft.onCount++;
      // Open the gates again.
      uft.debounceDone();
    }
  },

  turnOff: function($group, callback) {
    var uft = this,
        s = uft.settings,
        cb = callback || function(){},
        offTransClass = s.transClass + '-off';

    if ($group.length) {

      // First, remove `transClass`
      // and add `offTransClass`.
      $group.removeClass(s.transClass)
        .addClass(offTransClass);

      uft.optionalDelay(s.offTransTime, function() {
        // After the specificied delay, finish switching up classes.
        cb();
        $group.removeClass(s.onClass)
          .removeClass(offTransClass)
          .addClass(s.offClass);
        // Call user-defined callback, passing some data.
        s.offCallback({ '$group': $group, 'action': 'off'});
        // Subtract from `onCount`.
        uft.onCount--;
        // Open the gates again.
        uft.debounceDone();

        // If the scroll was frozen, unfreeze it.
        if (s.freezeScroll)
          uft.freezeScrollOff();
      });

    // If `$group` is empty, just call the callback.
    } else {
      cb();
    }
  },

  turnAllOn: function() {
    // If `onlyOneOn` is not true, turn on all items.
    if (!this.settings.onlyOneOn)
      this.turnOn(this.$items);
    else
      throw new Error('UnfinishedToggler will not turnAllOn with the setting {onlyOneOn: true}.');
  },

  turnAllOff: function() {
    // If `allOff` is allowed, turn off all items.
    if (this.settings.allOff)
      this.turnOff(this.$items);
    else
      throw new Error('UnfinishedToggler will not turnAllOff with the setting {allOff: true}.');
  },

  /*============================
  NEXT AND PREVIOUS
  ==============================*/

  nextOrPrev: function(dir) {
    var uft = this,
        s = uft.settings,
        nextPrevErrorStart = 'UnfinishedToggler cannot use next() and prev() ',
        currentGroup, firstGroup, lastGroup, targetGroup;

    // First, check that next() or prev() make sense with the settings and markup.
    if (!s.onlyOneOn)
      throw new Error(nextPrevErrorStart + 'with the setting {onlyOneOn: false}.');

    currentGroup = uft.getOnItems().first().data('uft-group');
    if (typeof currentGroup === 'undefined')
      throw new Error(nextPrevErrorStart + 'unless data-uft-group values are defined.');
    else if (typeof currentGroup !== 'number')
      throw new Error(nextPrevErrorStart + 'unless data-uft-group values are integers.');

    firstGroup = Math.min.apply(Math, uft.groupIds);
    lastGroup = Math.max.apply(Math, uft.groupIds);
    if (dir === 'next')
      targetGroup = (currentGroup + 1 <= lastGroup) ? currentGroup + 1 : firstGroup;
    else
      targetGroup = (currentGroup - 1 >= firstGroup) ? currentGroup - 1 : lastGroup;
    uft.trigger(targetGroup);
  },

  // next() and prev() are just public-facing methods.
  next: function() {
    this.nextOrPrev('next');
  },
  prev: function() {
    this.nextOrPrev('prev');
  },

  /*============================
  FREEZE SCROLL
  ==============================*/

  freezeScrollOn: function() {
    var uft = this,
        rootStyles = { overflow: 'hidden' };
    // If there is a scrollbar
    if (uft.hasScrollbar) {
      // ... get its size
      var scrollbarSize = uft.getScrollbarSize();
      // ... and if there's any size, offset right margin to account.
      if (scrollbarSize)
        rootStyles['margin-right'] = scrollbarSize;
    }
    $('html').css(rootStyles);
  },

  freezeScrollOff: function() {
    $('html').css({ 'overflow': '', 'margin-right': '' });
  },

  getScrollbarSize: function() {
    // Thanks to code from MagnificPopup.
    if(this.data.scrollbarSize === undefined) {
      var scrollDiv = document.createElement("div");
      scrollDiv.style.cssText = 'width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;';
      document.body.appendChild(scrollDiv);
      this.data.scrollbarSize = scrollDiv.offsetWidth - scrollDiv.clientWidth;
      document.body.removeChild(scrollDiv);
    }
    return this.data.scrollbarSize;
  },

  hasScrollbar: function() {
    return document.body.scrollHeight > $(window).height();
  },

  /*============================
  INNER FOCUS
  ==============================*/

  innerFocus: function(e) {
    // Find the focus-target's group, and trigger it.
    var uft = this,
        s = uft.settings,
        $el = $(e.target),
        $groupPart = (!s.scattered) ? $el : $el.closest(s.contentSelector),
        groupIsOn = uft.isOn(uft.getGroup($groupPart));

    if (!groupIsOn)
      uft.trigger($groupPart);
    else
      uft.debounceDone();
  },

  /*============================
  OUTSIDE TURNS OFF
  ==============================*/

  outsideTurnsOff: function($group, turningOn) {
    var uft = this,
        s = uft.settings,
        // `touchend` event is necessary for iOS, it seems.
        ev = uft.namespaceEvent('click') + ' ' + uft.namespaceEvent('touchend'),
        $inside = (typeof s.outsideTurnsOff === 'string') ? $(s.outsideTurnsOff) : $group;

    if (turningOn)
      outsideEnable();
    else
      outsideDisable();

    function outsideEnable() {
      // Make it so that clicking anywhere outside `$inside`
      // turns off `$group`.
      $inside.on(ev, function(e) {
        e.stopPropagation();
      });
      // Only allow the event once: it will get
      // re-assigned if `$group` turns on again.
      // Timeout ensures that assignment happens
      // after the triggering event is done.
      setTimeout(function() {
        $('html').on(ev, outsideTriggered);
      }, 40);
    }

    function outsideDisable() {
      $('html').off(uft.namespaceEvent(''));
    }

    function outsideTriggered(e) {
      if (!$inside.is($(e.target))) {
        uft.turnOff($group);
        outsideDisable();
      }
    }
  }
};

/*============================
DEFAULT SETTINGS
==============================*/

UnfinishedToggler.prototype.defaults = {
  // selector for a context-element containing all the others,
  // within which to find the toggler's parts
  'root': 'body',
  // class to turn groups and elements on
  'onClass': 'uft-on',
  // class to turn groups and elements off
  'offClass': 'uft-off',
  // `scattered` is true if the triggers and content
  // are not children of group-containing elements.
  // scattered groups are identified by data-uft-group attributes.
  'scattered': false,
  // selector for trigger elements
  'triggerSelector': '.uft-trigger',
  // selector for group-containing elements
  // (only used if `{ scattered: false }`)
  'groupSelector': '.uft-group',
  // selector for content elements
  // (only used if `{ scattered: true }`)
  'contentSelector': '.uft-content',
  // allow only one item to be on at a time
  'onlyOneOn' : true,
  // allow all items to be turned off at the same time
  'allOff' : true,
  // array of events that trigger a toggle
  'events': ['click'],
  // namespace for uft-related events
  'eventNamespace': 'uft',
  // callback to perform after something is turned on
  'onCallback': function(){},
  // callback to perform after something is turned off
  'offCallback': function(){},
  // class that is added just after turning on
  // and removed just before turning off,
  // useful for adding extra CSS transitions
  'transClass': 'uft-trans',
  // delay between turning on and adding `transClass`;
  // 40ms is minimum, in case turning on involves a `display` switch
  'onTransDelay': 40,
  // delay between removing `transClass` and turning off
  'offTransTime': 0,
  // `transOverlap` is true if groups are allowed to
  // tranisiong on while others are still transitioning off
  'transOverlap': true,

  // NEXT AND PREV
  // selector for elements that will turn on the next group
  'nextSelector': false,
  // selector for elements that will turn on the previous group
  'prevSelector': false,

  // FREEZE SCROLL
  // freeze scrolling when a group is turned on;
  // basically used only for popups/modals
  'freezeScroll': false,

  // INNER FOCUS
  // selector for elements within groups that if focused
  // will trigger their containing group
  'innerFocus': false,

  // OUTSIDE TURNS OFF
  // either `true`, to indicate that clicking anywhere
  // outside the turned-on group should turn it off,
  // or selector for the region in which you are welcome to
  // click without turning off the group
  'outsideTurnsOff': false

};<|endoftext|>