<commit_before>// @flow

import { Component, useEffect } from 'react';
import { Keyboard } from '../../../utils/constants';
import GalleryDetailsRow from './GalleryDetailsRow';
import { Flex } from 'app/components/Layout';
import { Content } from 'app/components/Content';
import Icon from 'app/components/Icon';
import throttle from 'lodash/throttle';
import ProgressiveImage from 'app/components/ProgressiveImage';
import Dropdown from 'app/components/Dropdown';
import { Link } from 'react-router-dom';
import CommentView from 'app/components/Comments/CommentView';
import Modal from 'app/components/Modal';
import styles from './GalleryPictureModal.css';
import { Swipeable, RIGHT, LEFT } from 'react-swipeable';
import type { EntityID } from 'app/types';
import type { ID } from 'app/models';
import Button from 'app/components/Button';
import { Image } from 'app/components/Image';

type Props = {
  picture: Object,
  pictureId: number,
  currentUser: Object,
  loggedIn: boolean,
  gallery: Object,
  push: (string) => void,
  updateGalleryCover: (number, number) => Promise<*>,
  deletePicture: (number, number) => Promise<*>,
  comments: Array<Object>,
  actionGrant: Array<string>,
  pictures: Array<Object>,
  hasMore: Boolean,
  fetchSiblingGallerPicture: (EntityID, EntityID, boolean) => Promise<*>,
  isFirstImage: Boolean,
  isLastImage: Boolean,
  deleteComment: (id: ID, contentTarget: string) => Promise<*>,
};

type State = {
  showMore: boolean,
  clickedDeletePicture: number,
  hasNext: boolean,
  hasPrevious: boolean,
};
const OnKeyDownHandler = ({
  handler,
}: {
  handler: (KeyboardEvent) => void,
}) => {
  useEffect(() => {
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [handler]);
  return null;
};
const Taggees = ({ taggees }: { taggees: Array<Object> }) => {
  if (taggees.length === 1) {
    return (

        med
        <Link key={taggees[0].id} to={`/users/${taggees[0].username}`}>
          {taggees[0].fullName}
        </Link>

    );
  } else {
    return (

        med
        {taggees.map((taggee, index) => (

            {taggees.length === index + 1 ? (

                {'og '}
                <Link to={`/users/${taggee.username}`}>{taggee.fullName}</Link>

            ) : (

                <Link to={`/users/${taggee.username}`}>{taggee.fullName}</Link>
                {taggees.length === index + 2 ? null : ,}

            )}

        ))}

    );
  }
};

const RenderGalleryPicture = ({
  id,
  handleDelete,
  clickedDeletePicture,
}: {
  id: number,
  handleDelete: (number) => void,
  clickedDeletePicture: number,
}) => (

    <Button flat onClick={() => handleDelete(id)}>

      {clickedDeletePicture === id ? 'Er du sikker?' : 'Slett'}
    </Button>

);

export default class GalleryPictureModal extends Component<Props, State> {
  state = {
    showMore: false,
    clickedDeletePicture: 0,
    hasNext: !this.props.isLastImage,
    hasPrevious: !this.props.isFirstImage,
  };

  toggleDropdown = () => {
    this.setState({ showMore: !this.state.showMore, clickedDeletePicture: 0 });
  };

  onUpdate = () => {
    this.props.push(
      `/photos/${this.props.gallery.id}/picture/${this.props.picture.id}/edit`
    );
  };

  onUpdateGalleryCover = () => {
    this.props.updateGalleryCover(this.props.gallery.id, this.props.picture.id);
    this.toggleDropdown();
  };

  handleDelete = (clickedDeletePicture: number) => {
    if (this.state.clickedDeletePicture === clickedDeletePicture) {
      this.props
        .deletePicture(this.props.gallery.id, this.props.picture.id)
        .then(() => this.props.push(`/photos/${this.props.gallery.id}`));
    } else {
      this.setState({
        clickedDeletePicture,
      });
    }
  };

  siblingGalleryPicture = (next: boolean) => {
    const { pictureId, gallery, push } = this.props;
    return this.props
      .fetchSiblingGallerPicture(gallery.id, pictureId, next)
      .then((result) => {
        this.setState({
          hasNext: !!result.payload.next,
          hasPrevious: !!result.payload.previous,
        });
        return (
          result.payload.result.length > 0 &&
          push(`/photos/${gallery.id}/picture/${result.payload.result[0]}`)
        );
      });
  };

  previousGalleryPicture = throttle(
    () => this.siblingGalleryPicture(false),
    500,
    { trailing: false }
  );
  nextGalleryPicture = throttle(() => this.siblingGalleryPicture(true), 500, {
    trailing: false,
  });

  handleKeyDown = (e: KeyboardEvent): void => {
    // Dont handle events inside the comment form... :smile:
    // $FlowFixMe
    if (e.target.className === 'notranslate public-DraftEditor-content') {
      return;
    }
    switch (e.which) {
      case Keyboard.LEFT:
        e.preventDefault();
        this.previousGalleryPicture();
        break;

      case Keyboard.RIGHT:
        e.preventDefault();
        this.nextGalleryPicture();
        break;

      default:
    }
  };

  handleSwipe = ({ dir }: { dir: string }) => {
    dir === RIGHT && this.previousGalleryPicture();
    dir === LEFT && this.nextGalleryPicture();
  };

  render() {
    const {
      picture,
      pictureId,
      comments,
      currentUser,
      loggedIn,
      push,
      gallery,
      actionGrant,
      deleteComment,
    } = this.props;
    const { showMore } = this.state;

    return (
      <Swipeable onSwiping={this.handleSwipe}>
        <Modal
          onHide={() => push(`/photos/${gallery.id}`)}
          show
          backdrop
          contentClassName={styles.content}
        >
          <OnKeyDownHandler handler={this.handleKeyDown} />
          <Content className={styles.topContent}>
            <Flex
              width="100%"
              justifyContent="space-between"
              alignItems="center"
            >
              <Flex justifyContent="space-between">
                <Image
                  className={styles.galleryThumbnail}
                  alt="some alt"
                  src={gallery.cover.thumbnail}
                />

                <Flex column justifyContent="space-around">

                    <Link to={`/photos/${gallery.id}`}>{gallery.title}</Link>

                  <GalleryDetailsRow size="small" gallery={gallery} />
                </Flex>
              </Flex>

              <Dropdown
                show={showMore}
                placement="bottom"
                toggle={this.toggleDropdown}
                className={styles.dropdown}
                iconName="more"
              >
                <Dropdown.List>
                  <Dropdown.ListItem>

                      Last ned
                      <Icon name="download-outline" size={24} />

                  </Dropdown.ListItem>
                  {actionGrant &&
                    actionGrant.includes('edit') && [
                      <Dropdown.ListItem key="edit">
                        <Link
                          to="#"
                          onClick={this.onUpdate}
                          style={{ color: 'var(--lego-color-gray)' }}
                        >
                          Rediger
                          <Icon name="gear" size={24} />
                        </Link>
                      </Dropdown.ListItem>,
                      <Dropdown.ListItem key="cover">
                        <Link
                          onClick={this.onUpdateGalleryCover}
                          to="#"
                          style={{ color: 'var(--lego-color-gray)' }}
                        >
                          Sett som album cover
                          <Icon name="image" size={24} />
                        </Link>
                      </Dropdown.ListItem>,
                      <Dropdown.Divider key="divider" />,
                      <Dropdown.ListItem
                        key="delete"
                        onClick={(e) => {
                          e.preventDefault();
                          e.stopPropagation();
                        }}
                      >
                        <RenderGalleryPicture
                          handleDelete={this.handleDelete}
                          id={pictureId}
                          clickedDeletePicture={this.state.clickedDeletePicture}
                        />
                      </Dropdown.ListItem>,
                    ]}
                </Dropdown.List>
              </Dropdown>
            </Flex>
          </Content>
          <Flex className={styles.pictureContainer}>
            <ProgressiveImage
              key={picture.id}
              src={picture.file}
              alt="some alt"
            />
          </Flex>
          <Content className={styles.bottomContent}>
            <Flex justifyContent="center">
              {this.state.hasPrevious && (
                <Link
                  to="#"
                  onClick={this.previousGalleryPicture}
                  style={{ marginRight: '50px' }}
                >
                  <Icon name="arrow-dropleft" size={64} />
                </Link>
              )}
              {this.state.hasNext && (
                <Link to="#" onClick={this.nextGalleryPicture}>
                  <Icon name="arrow-dropright" size={64} />
                </Link>
              )}
            </Flex>
            <Flex className={styles.pictureDescription}>

                {picture.description}
                {picture.taggees.length > 0 && (
                  <Taggees taggees={picture.taggees} />
                )}

            </Flex>
            {picture.contentTarget && (
              <Flex className={styles.pictureDescription}>
                <CommentView
                  style={{ width: '100%' }}
                  formEnabled
                  user={currentUser}
                  contentTarget={picture.contentTarget}
                  loggedIn={loggedIn}
                  comments={comments}
                  deleteComment={deleteComment}
                />
              </Flex>
            )}
          </Content>
        </Modal>
      </Swipeable>
    );
  }
}
<commit_msg>Fix swiping and bump swipable<commit_after>// @flow

import { Component, useEffect } from 'react';
import { Keyboard } from '../../../utils/constants';
import GalleryDetailsRow from './GalleryDetailsRow';
import { Flex } from 'app/components/Layout';
import { Content } from 'app/components/Content';
import Icon from 'app/components/Icon';
import throttle from 'lodash/throttle';
import ProgressiveImage from 'app/components/ProgressiveImage';
import Dropdown from 'app/components/Dropdown';
import { Link } from 'react-router-dom';
import CommentView from 'app/components/Comments/CommentView';
import Modal from 'app/components/Modal';
import styles from './GalleryPictureModal.css';
import { useSwipeable, RIGHT, LEFT } from 'react-swipeable';
import type { EntityID } from 'app/types';
import type { ID } from 'app/models';
import Button from 'app/components/Button';
import { Image } from 'app/components/Image';

type Props = {
  picture: Object,
  pictureId: number,
  currentUser: Object,
  loggedIn: boolean,
  gallery: Object,
  push: (string) => void,
  updateGalleryCover: (number, number) => Promise<*>,
  deletePicture: (number, number) => Promise<*>,
  comments: Array<Object>,
  actionGrant: Array<string>,
  pictures: Array<Object>,
  hasMore: Boolean,
  fetchSiblingGallerPicture: (EntityID, EntityID, boolean) => Promise<*>,
  isFirstImage: Boolean,
  isLastImage: Boolean,
  deleteComment: (id: ID, contentTarget: string) => Promise<*>,
};

type State = {
  showMore: boolean,
  clickedDeletePicture: number,
  hasNext: boolean,
  hasPrevious: boolean,
};
const OnKeyDownHandler = ({
  handler,
}: {
  handler: (KeyboardEvent) => void,
}) => {
  useEffect(() => {
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [handler]);
  return null;
};
const Taggees = ({ taggees }: { taggees: Array<Object> }) => {
  if (taggees.length === 1) {
    return (

        med
        <Link key={taggees[0].id} to={`/users/${taggees[0].username}`}>
          {taggees[0].fullName}
        </Link>

    );
  } else {
    return (

        med
        {taggees.map((taggee, index) => (

            {taggees.length === index + 1 ? (

                {'og '}
                <Link to={`/users/${taggee.username}`}>{taggee.fullName}</Link>

            ) : (

                <Link to={`/users/${taggee.username}`}>{taggee.fullName}</Link>
                {taggees.length === index + 2 ? null : ,}

            )}

        ))}

    );
  }
};

const RenderGalleryPicture = ({
  id,
  handleDelete,
  clickedDeletePicture,
}: {
  id: number,
  handleDelete: (number) => void,
  clickedDeletePicture: number,
}) => (

    <Button flat onClick={() => handleDelete(id)}>

      {clickedDeletePicture === id ? 'Er du sikker?' : 'Slett'}
    </Button>

);

function Swipeable(props: {
  onSwiping: ({ dir: string }) => void,
  children: any,
}) {
  const handlers = useSwipeable({
    onSwiped: (eventData) => props.onSwiping(eventData),
  });
  return
{props.children}
;
}

export default class GalleryPictureModal extends Component<Props, State> {
  state = {
    showMore: false,
    clickedDeletePicture: 0,
    hasNext: !this.props.isLastImage,
    hasPrevious: !this.props.isFirstImage,
  };

  toggleDropdown = () => {
    this.setState({ showMore: !this.state.showMore, clickedDeletePicture: 0 });
  };

  onUpdate = () => {
    this.props.push(
      `/photos/${this.props.gallery.id}/picture/${this.props.picture.id}/edit`
    );
  };

  onUpdateGalleryCover = () => {
    this.props.updateGalleryCover(this.props.gallery.id, this.props.picture.id);
    this.toggleDropdown();
  };

  handleDelete = (clickedDeletePicture: number) => {
    if (this.state.clickedDeletePicture === clickedDeletePicture) {
      this.props
        .deletePicture(this.props.gallery.id, this.props.picture.id)
        .then(() => this.props.push(`/photos/${this.props.gallery.id}`));
    } else {
      this.setState({
        clickedDeletePicture,
      });
    }
  };

  siblingGalleryPicture = (next: boolean) => {
    const { pictureId, gallery, push } = this.props;
    return this.props
      .fetchSiblingGallerPicture(gallery.id, pictureId, next)
      .then((result) => {
        this.setState({
          hasNext: !!result.payload.next,
          hasPrevious: !!result.payload.previous,
        });
        return (
          result.payload.result.length > 0 &&
          push(`/photos/${gallery.id}/picture/${result.payload.result[0]}`)
        );
      });
  };

  previousGalleryPicture = throttle(
    () => this.siblingGalleryPicture(false),
    500,
    { trailing: false }
  );
  nextGalleryPicture = throttle(() => this.siblingGalleryPicture(true), 500, {
    trailing: false,
  });

  handleKeyDown = (e: KeyboardEvent): void => {
    // Dont handle events inside the comment form... :smile:
    // $FlowFixMe
    if (e.target.className === 'notranslate public-DraftEditor-content') {
      return;
    }
    switch (e.which) {
      case Keyboard.LEFT:
        e.preventDefault();
        this.previousGalleryPicture();
        break;

      case Keyboard.RIGHT:
        e.preventDefault();
        this.nextGalleryPicture();
        break;

      default:
    }
  };

  handleSwipe = ({ dir }: { dir: string }) => {
    dir === RIGHT && this.previousGalleryPicture();
    dir === LEFT && this.nextGalleryPicture();
  };

  render() {
    const {
      picture,
      pictureId,
      comments,
      currentUser,
      loggedIn,
      push,
      gallery,
      actionGrant,
      deleteComment,
    } = this.props;
    const { showMore } = this.state;

    return (
      <Modal
        onHide={() => push(`/photos/${gallery.id}`)}
        show
        backdrop
        contentClassName={styles.content}
      >
        <Swipeable onSwiping={this.handleSwipe}>
          <OnKeyDownHandler handler={this.handleKeyDown} />
          <Content className={styles.topContent}>
            <Flex
              width="100%"
              justifyContent="space-between"
              alignItems="center"
            >
              <Flex justifyContent="space-between">
                <Image
                  className={styles.galleryThumbnail}
                  alt="some alt"
                  src={gallery.cover.thumbnail}
                />

                <Flex column justifyContent="space-around">

                    <Link to={`/photos/${gallery.id}`}>{gallery.title}</Link>

                  <GalleryDetailsRow size="small" gallery={gallery} />
                </Flex>
              </Flex>

              <Dropdown
                show={showMore}
                placement="bottom"
                toggle={this.toggleDropdown}
                className={styles.dropdown}
                iconName="more"
              >
                <Dropdown.List>
                  <Dropdown.ListItem>

                      Last ned
                      <Icon name="download-outline" size={24} />

                  </Dropdown.ListItem>
                  {actionGrant &&
                    actionGrant.includes('edit') && [
                      <Dropdown.ListItem key="edit">
                        <Link
                          to="#"
                          onClick={this.onUpdate}
                          style={{ color: 'var(--lego-color-gray)' }}
                        >
                          Rediger
                          <Icon name="gear" size={24} />
                        </Link>
                      </Dropdown.ListItem>,
                      <Dropdown.ListItem key="cover">
                        <Link
                          onClick={this.onUpdateGalleryCover}
                          to="#"
                          style={{ color: 'var(--lego-color-gray)' }}
                        >
                          Sett som album cover
                          <Icon name="image" size={24} />
                        </Link>
                      </Dropdown.ListItem>,
                      <Dropdown.Divider key="divider" />,
                      <Dropdown.ListItem
                        key="delete"
                        onClick={(e) => {
                          e.preventDefault();
                          e.stopPropagation();
                        }}
                      >
                        <RenderGalleryPicture
                          handleDelete={this.handleDelete}
                          id={pictureId}
                          clickedDeletePicture={this.state.clickedDeletePicture}
                        />
                      </Dropdown.ListItem>,
                    ]}
                </Dropdown.List>
              </Dropdown>
            </Flex>
          </Content>
          <Flex className={styles.pictureContainer}>
            <ProgressiveImage
              key={picture.id}
              src={picture.file}
              alt="some alt"
            />
          </Flex>
          <Content className={styles.bottomContent}>
            <Flex justifyContent="center">
              {this.state.hasPrevious && (
                <Link
                  to="#"
                  onClick={this.previousGalleryPicture}
                  style={{ marginRight: '50px' }}
                >
                  <Icon name="arrow-dropleft" size={64} />
                </Link>
              )}
              {this.state.hasNext && (
                <Link to="#" onClick={this.nextGalleryPicture}>
                  <Icon name="arrow-dropright" size={64} />
                </Link>
              )}
            </Flex>
            <Flex className={styles.pictureDescription}>

                {picture.description}
                {picture.taggees.length > 0 && (
                  <Taggees taggees={picture.taggees} />
                )}

            </Flex>
            {picture.contentTarget && (
              <Flex className={styles.pictureDescription}>
                <CommentView
                  style={{ width: '100%' }}
                  formEnabled
                  user={currentUser}
                  contentTarget={picture.contentTarget}
                  loggedIn={loggedIn}
                  comments={comments}
                  deleteComment={deleteComment}
                />
              </Flex>
            )}
          </Content>
        </Swipeable>
      </Modal>
    );
  }
}
<|endoftext|>


<commit_before>/**
 * Used for adding, editing, and copying earnings entries.
 * @module kocomponents/earnings/editor
 *
 */
import '../../client/editor';
import '../../client/list';
import '../../utilities/icon-dec';
import '../../input/duration';
import '../../../utils/autofocusBindingHandler';
import '../../input/date';
import * as suggestedPlatformsList from '../../../data/suggestedPlatforms';
import { ActionForValue } from '../../job-title-autocomplete';
import Komponent from '../../helpers/KnockoutComponent';
import UserEarningsEntry from '../../../models/UserEarningsEntry';
import { item as clientsDataItem } from '../../../data/clients';
import ko from 'knockout';
import { show as showConfirm } from '../../../modals/confirm';
import { show as showError } from '../../../modals/error';
import template from './template.html';
import { item as userEarningsItem } from '../../../data/userEarnings';
import { list as userExternalListingsList } from '../../../data/userExternalListings';
import { list as userListingsList } from '../../../data/userListings';

const TAG_NAME = 'earnings-editor';

/**
 * @enum {string} Supported displaying modes
 */
const EditorMode = {
    add: 'add',
    edit: 'edit',
    copy: 'copy'
};

/**
 * Component
 */
export default class EarningsEditor extends Komponent {

    static get template() { return template; }

    /**
     * Parameters allowed are 'input only' when the value given is read at constructor
     * and keeps constant internally. If is an observable, any change from outside is
     * not read.
     * @param {object} params
     * @param {KnockoutObservable<EditorMode>} [params.editorMode] Input only value setting-up the mode in use
     * @param {(number|KnockoutObservable<number>)} [params.earningsEntryID] Input only ID to be edited or copied, or zero for new.
     * @param {(number|KnockoutObservable<number>)} [params.userExternalListingID] Input only ID to preset the external listing,
     * this let's add an earning from a listing component as a shortcut.
     * @param {KnockoutObservable<integer>} [params.startAtStep] Input only value setting-up the initial step for the component.
     * @param {function} [params.onSaved] Callback to notify after save the item, with the updated data included
     * @param {function} [params.onDeleted] Callback to notify after delete the item
     */
    constructor(params) {
        super();

        /**
         * Editable earnings entry. Same instance is used all the time, just
         * updating content, simplifying working with the form and summary.
         * @member {UserEarningsEntry}
         */
        this.earningsEntry = new UserEarningsEntry({
            earningsEntryID: ko.unwrap(params.earningsEntryID) || 0,
            userExternalListingID: ko.unwrap(params.userExternalListingID)
        });

        /**
         * Captures from the activity which "mode" the editor
         * component is to be used.
         * @member {EditorMode}
         */
        this.editorMode = ko.observable(ko.unwrap(params.editorMode));

        /**
         * @member {KnockoutObservable<object>}
         */
        this.selectedClient = ko.observable(null);

        /**
         * Callback executed when the form is saved successfully, giving
         * a copy of the server data
         * @member {function}
         */
        this.onSaved = params.onSaved;

        /**
         * Callback executed when the entry was deleted successfully
         * @member {function}
         */
        this.onDeleted = params.onDeleted;

        /**
         * Holds a list of the user external listings, available to be selected
         * as the earnings entry listing.
         */
        this.userExternalListings = ko.observableArray([]);

        /**
         * Holds a list of the user listings at Loconomics, available to allow
         * quick selection of job title.
         */
        this.userListings = ko.observableArray([]);

        /**
         * Holds a list of suggested platforms, available to be selected
         * as the earnigns entry listing, but while is not a listing it will
         * create a listing for that platform automatically.
         */
        this.suggestedPlatforms = ko.observableArray([]);

        // Starts in review mode when we are editing or copying an entry,
        // so anything else but 'add'
        this.__setupStepsManagement(params.editorMode !== EditorMode.add);

        this.__setupStatusFlags();

        /**
         * Label text for the 'delete' button
         * @member {KnockoutComputed<string>}
         */
        this.deleteButtonText = ko.pureComputed(() => {
            var itIs = this.isDeleting();
            return itIs ? 'Deleting..' : 'Delete entry';
        });

        /**
         * Label text for the 'save' button
         * @member {KnockoutComputed<string>}
         */
        this.saveButtonText = ko.pureComputed(() => {
            var itIs = this.isSaving();
            return itIs ? 'Submitting..' : 'Submit';
        });

        this.__setupDataOperations();
    }

    /**
     * Define members to implement a step based interface.
     * @param {boolean} [startInReview=false] Whether the interface must start at the
     * summary step with review enabled.
     * @private
     */
    __setupStepsManagement(startInReview) {
        /**
         * Magic number for the step where is the summary.
         * @const {number}
         * @private
         */
        const SUMMARY_STEP = 0;
        /**
         * Keeps track of the current step being displayed.
         * By default is 1, first step, except when starting at review that will
         * be 0 (the summary).
         * This default is in sync with isAtReview initialization.
         * @member {KnockoutObservable<integer>}
         */
        this.currentStep = ko.observable(startInReview ? SUMMARY_STEP : 1);

        /**
         * Returns which step the user is on in the form.
         * @member {KnockoutComputed<boolean>}
         */
        this.isAtStep = function(number) {
            return ko.pureComputed(() => this.currentStep() === number);
        };

        /**
         * Whether the user has completed the steps almost once and is free
         * to jump between the summary and steps in order to do touch ups.
         * By default is false, which means the user is restricted to follow
         * the steps in order until finalize and reach the summary, when
         * this flag switchs to true.
         * Can be set.
         * By default is false, except when starting at review that will be true.
         * This default is in sync with currentStep initialization.
         * @member {KnockoutObservable<boolean>}
         */
        this.isAtReview = ko.observable(!!startInReview);

        /**
         * Takes the user to the next step in the form.
         * @member {KnockoutComputed<number>}
         */
        this.goNextStep = () => {
            this.currentStep(this.currentStep() + 1);
        };

        /**
         * Takes the user to the specified step
         * @param {number} step Step number
         */
        this.goToStep = (step) => {
            this.currentStep(step);
        };

        /**
         * Takes the user to the summary.
         * Will enable review mode too.
         */
        this.goToSummary = () => {
            this.currentStep(SUMMARY_STEP);
            // Reached the summary, all steps done so enters review mode
            this.isAtReview(true);
        };

         /**
         * Takes the user to the next step in the form.
         * @member {KnockoutComputed<string>}
         */
        this.stepButtonLabel = ko.pureComputed(() => {
            if (this.isAtReview()) {
                return 'Save';
            }
            else {
                return 'Save and Continue';
            }
        });

        /**
         * Takes the user to the next step in the form (in standard mode),
         * or to the summary (when in review mode).
         * @member {KnockoutComputed<number>}
         */
        this.saveStep = () => {
            if (this.isAtReview()) {
                this.currentStep(SUMMARY_STEP);
            }
            else {
                this.goNextStep();
            }
        };
    }

    /**
     * Define members for all the status flags needed.
     * @private
     */
    __setupStatusFlags() {
        /**
         * When a loading request it's on the works
         * @member {KnockoutObservable<boolean>}
         */
        this.isLoading = ko.observable(false);

        /**
         * When a saving request it's on the works
         * @member {KnockoutObservable<boolean>}
         */
        this.isSaving = ko.observable(false);

        /**
         * When a deletion request it's on the works
         * @member {KnockoutObservable<boolean>}
         */
        this.isDeleting = ko.observable(false);

        /**
         * When edition must be locked because of in progress
         * operations.
         * @member {KnockoutComputed<boolean>}
         */
        this.isLocked = ko.pureComputed(() => this.isSaving() || this.isLoading() || this.isDeleting());

        /**
         * Whether the item is a new record or is being edited.
         * @member {KnockoutObservable<boolean>}
         */
        this.isNew = ko.pureComputed(() => this.editorMode() !== EditorMode.edit);
    }

    /**
     * Define members, prepare subscriptions to work with the code
     * and start any initial request for data
     * @private
     */
    __setupDataOperations() {
        /**
         * We create an item manager to operate on the data for the requested ID
         * (allows to load, save, delete).
         */
        this.dataManager = userEarningsItem(this.earningsEntry.earningsEntryID());

        /**
         * Suscribe to data coming for the list and put them in our
         * userListings propery.
         */
        this.subscribeTo(userListingsList.onData, this.userListings);

        /**
         * Notify data load errors
         */
        this.subscribeTo(userListingsList.onDataError, (err) => {
            showError({
                title: 'There was an error loading your listings',
                error: err
            });
        });

        /**
         * Suscribe to data coming for the list and put them in our
         * userExternalListings propery.
         */
        this.subscribeTo(userExternalListingsList.onData, this.userExternalListings);

        /**
         * Notify data load errors
         */
        this.subscribeTo(userExternalListingsList.onDataError, (err) => {
            showError({
                title: 'There was an error loading your external listings',
                error: err
            });
        });

        /**
         * Load suggestions.
         */
        this.subscribeTo(suggestedPlatformsList.onData, this.suggestedPlatforms);

        /// Notify data load errors
        this.subscribeTo(suggestedPlatformsList.onDataError, (err) => {
            showError({
                title: 'There was an error loading the platforms',
                error: err
            });
        });

        // When we have an ID, we need to load it first
        if (this.earningsEntry.earningsEntryID()) {

            this.isLoading(true);

            this.dataManager.onceLoaded()
            .then((data) => {
                this.isLoading(false);
                if (this.editorMode() === EditorMode.copy) {
                    // On copy mode, we need to reset the ID and dataManager
                    // so it forces to create a new entry (otherwise it will
                    // actually update the original one)
                    data.earningsEntryID = 0;
                    this.dataManager = userEarningsItem(0);
                }
                this.earningsEntry.model.updateWith(data);
                // Load info about the client of this entry
                // It's optional, so careful since can be null
                if (data.clientUserID) {
                    clientsDataItem(data.clientUserID)
                    .onceLoaded()
                    .then((client) => {
                        // IMPORTANT: Be careful (again), because if something was slow
                        // loading, user could have picked (edited/added) a new client
                        // in the editor, and we do not want to replace that info,
                        // check if there is no data, or that data has the same ID
                        // that the one we have loaded
                        if (!this.selectedClient() ||
                            this.selectedClient().clientUserID === client.clientUserID) {
                            this.selectedClient(client);
                        }
                    })
                    .catch((error) => {
                        showError({
                            title: 'There was an error loading the client information at the entry',
                            error
                        });
                    });
                }
            })
            .catch((error) => {
                this.isLoading(false);
                showError({
                    title: 'There was an error loading the earnings entry',
                    error
                });
            });
        }
    }

    /**
     * Makes given client current one selected.
     * @param {Object} client
     * @method
     */
    selectClient(client) {
        // Updates edited entry with the client ID selected
        this.earningsEntry.clientUserID(client.clientUserID);
        // Save a reference, to display name and other info
        this.selectedClient(client);
        this.goToSummary();
    }

    /**
     * Makes given listing the one selected
     * @param {Object} listing
     * @method
     */
    selectListing(listing) {
        this.earningsEntry.userExternalListingID(listing.userExternalListingID);
        this.earningsEntry.listingTitle(listing.title);
        // for integrity, makes platform matches listing (not needed to save the data)
        this.earningsEntry.platformID(listing.platformID);
        this.saveStep();
    }

    /**
     * Gets the job title from the given user listings and use it as the
     * selected job title of the earnings entry
     * @param {Object} listing
     * @method
     */
    selectListingJobTitle(listing) {
        this.earningsEntry.jobTitleID(listing.jobTitleID);
        this.earningsEntry.jobTitleName(listing.jobTitleSingularName);
        this.goToSummary();
    }

    /**
     * Gets the job title from a job title selected from the autocomplete
     * and use it as the selected job title of the earnings entry
     * @param {string} textValue User input text searching a job title
     * @param {models/JobTitle} data Selected job title model
     */
    selectJobTitle(textValue, data) {
        if (!data || !data.jobTitleID) return;

        const id = data.jobTitleID();
        this.earningsEntry.jobTitleID(id);
        this.earningsEntry.jobTitleName(data.singularName());
        this.goToSummary();
        return {
            value: ActionForValue.clear
        };
    }

    /**
     * Makes given platform as the selected listing, which means a listing
     * will be created for that platform
     * @param {rest/Platform} platform
     */
    selectPlatform(platform) {
        this.earningsEntry.platformID(platform.platformID);
        // resets listingID or will not take effect
        this.earningsEntry.userExternalListingID(null);
        // Something to display to the user
        this.earningsEntry.listingTitle(`My ${platform.name} listing`);
        this.saveStep();
    }

    /**
     * Save data in the server
     * @returns {Promise<object>}
     */
    save() {
        if (this.isSaving()) return Promise.reject();

        this.isSaving(true);

        const data = this.earningsEntry.model.toPlainObject(true);

        return this.dataManager
        .save(data)
        .then((freshData) => {
            this.isSaving(false);
            if (this.onSaved) {
                // Notify
                this.onSaved(freshData);
            }
            else {
                // Use updated/created data
                this.earningsEntry.model.updateWith(freshData);
            }
        })
        .catch((error) => {
            this.isSaving(false);
            showError({
                title: 'There was an error saving the earnings entry',
                error
            });
        });
    }

    /**
     * Delete the entry being edited after confirmation
     * @returns {Promise}
     */
    confirmDelete() {
        if (this.isDeleting()) return Promise.reject();

        this.isDeleting(true);
        const id = this.earningsEntry.earningsEntryID();

        return showConfirm({
            title: 'Are you sure?',
            message: 'Delete ernings entry #' + id,
            yes: 'Delete',
            no: 'Keep'
        })
        .then(() =>  this.dataManager.delete())
        .then(() => {
            this.isDeleting(false);
            if (this.onDeleted) {
                // Notify
                this.onDeleted();
            }
            else {
                // Reset to new item
                this.earningsEntry.model.reset();
                this.editorMode(EditorMode.add);
                this.dataManager = userEarningsItem(0);
            }
        })
        .catch((error) => {
            this.isDeleting(false);
            if (error) {
                showError({
                    title: 'There was an error deleting the earnings entry',
                    error
                });
            }
        });
    }
}

ko.components.register(TAG_NAME, EarningsEditor);

<commit_msg>Usability earnings editor, press enter at 'total' must react as a submit/next step #776<commit_after>/**
 * Used for adding, editing, and copying earnings entries.
 * @module kocomponents/earnings/editor
 *
 */
import '../../client/editor';
import '../../client/list';
import '../../utilities/icon-dec';
import '../../input/duration';
import '../../../utils/autofocusBindingHandler';
import '../../../utils/pressEnterBindingHandler';
import '../../input/date';
import * as suggestedPlatformsList from '../../../data/suggestedPlatforms';
import { ActionForValue } from '../../job-title-autocomplete';
import Komponent from '../../helpers/KnockoutComponent';
import UserEarningsEntry from '../../../models/UserEarningsEntry';
import { item as clientsDataItem } from '../../../data/clients';
import ko from 'knockout';
import { show as showConfirm } from '../../../modals/confirm';
import { show as showError } from '../../../modals/error';
import template from './template.html';
import { item as userEarningsItem } from '../../../data/userEarnings';
import { list as userExternalListingsList } from '../../../data/userExternalListings';
import { list as userListingsList } from '../../../data/userListings';

const TAG_NAME = 'earnings-editor';

/**
 * @enum {string} Supported displaying modes
 */
const EditorMode = {
    add: 'add',
    edit: 'edit',
    copy: 'copy'
};

/**
 * Component
 */
export default class EarningsEditor extends Komponent {

    static get template() { return template; }

    /**
     * Parameters allowed are 'input only' when the value given is read at constructor
     * and keeps constant internally. If is an observable, any change from outside is
     * not read.
     * @param {object} params
     * @param {KnockoutObservable<EditorMode>} [params.editorMode] Input only value setting-up the mode in use
     * @param {(number|KnockoutObservable<number>)} [params.earningsEntryID] Input only ID to be edited or copied, or zero for new.
     * @param {(number|KnockoutObservable<number>)} [params.userExternalListingID] Input only ID to preset the external listing,
     * this let's add an earning from a listing component as a shortcut.
     * @param {KnockoutObservable<integer>} [params.startAtStep] Input only value setting-up the initial step for the component.
     * @param {function} [params.onSaved] Callback to notify after save the item, with the updated data included
     * @param {function} [params.onDeleted] Callback to notify after delete the item
     */
    constructor(params) {
        super();

        /**
         * Editable earnings entry. Same instance is used all the time, just
         * updating content, simplifying working with the form and summary.
         * @member {UserEarningsEntry}
         */
        this.earningsEntry = new UserEarningsEntry({
            earningsEntryID: ko.unwrap(params.earningsEntryID) || 0,
            userExternalListingID: ko.unwrap(params.userExternalListingID)
        });

        /**
         * Captures from the activity which "mode" the editor
         * component is to be used.
         * @member {EditorMode}
         */
        this.editorMode = ko.observable(ko.unwrap(params.editorMode));

        /**
         * @member {KnockoutObservable<object>}
         */
        this.selectedClient = ko.observable(null);

        /**
         * Callback executed when the form is saved successfully, giving
         * a copy of the server data
         * @member {function}
         */
        this.onSaved = params.onSaved;

        /**
         * Callback executed when the entry was deleted successfully
         * @member {function}
         */
        this.onDeleted = params.onDeleted;

        /**
         * Holds a list of the user external listings, available to be selected
         * as the earnings entry listing.
         */
        this.userExternalListings = ko.observableArray([]);

        /**
         * Holds a list of the user listings at Loconomics, available to allow
         * quick selection of job title.
         */
        this.userListings = ko.observableArray([]);

        /**
         * Holds a list of suggested platforms, available to be selected
         * as the earnigns entry listing, but while is not a listing it will
         * create a listing for that platform automatically.
         */
        this.suggestedPlatforms = ko.observableArray([]);

        // Starts in review mode when we are editing or copying an entry,
        // so anything else but 'add'
        this.__setupStepsManagement(params.editorMode !== EditorMode.add);

        this.__setupStatusFlags();

        /**
         * Label text for the 'delete' button
         * @member {KnockoutComputed<string>}
         */
        this.deleteButtonText = ko.pureComputed(() => {
            var itIs = this.isDeleting();
            return itIs ? 'Deleting..' : 'Delete entry';
        });

        /**
         * Label text for the 'save' button
         * @member {KnockoutComputed<string>}
         */
        this.saveButtonText = ko.pureComputed(() => {
            var itIs = this.isSaving();
            return itIs ? 'Submitting..' : 'Submit';
        });

        this.__setupDataOperations();
    }

    /**
     * Define members to implement a step based interface.
     * @param {boolean} [startInReview=false] Whether the interface must start at the
     * summary step with review enabled.
     * @private
     */
    __setupStepsManagement(startInReview) {
        /**
         * Magic number for the step where is the summary.
         * @const {number}
         * @private
         */
        const SUMMARY_STEP = 0;
        /**
         * Keeps track of the current step being displayed.
         * By default is 1, first step, except when starting at review that will
         * be 0 (the summary).
         * This default is in sync with isAtReview initialization.
         * @member {KnockoutObservable<integer>}
         */
        this.currentStep = ko.observable(startInReview ? SUMMARY_STEP : 1);

        /**
         * Returns which step the user is on in the form.
         * @member {KnockoutComputed<boolean>}
         */
        this.isAtStep = function(number) {
            return ko.pureComputed(() => this.currentStep() === number);
        };

        /**
         * Whether the user has completed the steps almost once and is free
         * to jump between the summary and steps in order to do touch ups.
         * By default is false, which means the user is restricted to follow
         * the steps in order until finalize and reach the summary, when
         * this flag switchs to true.
         * Can be set.
         * By default is false, except when starting at review that will be true.
         * This default is in sync with currentStep initialization.
         * @member {KnockoutObservable<boolean>}
         */
        this.isAtReview = ko.observable(!!startInReview);

        /**
         * Takes the user to the next step in the form.
         * @member {KnockoutComputed<number>}
         */
        this.goNextStep = () => {
            this.currentStep(this.currentStep() + 1);
        };

        /**
         * Takes the user to the specified step
         * @param {number} step Step number
         */
        this.goToStep = (step) => {
            this.currentStep(step);
        };

        /**
         * Takes the user to the summary.
         * Will enable review mode too.
         */
        this.goToSummary = () => {
            this.currentStep(SUMMARY_STEP);
            // Reached the summary, all steps done so enters review mode
            this.isAtReview(true);
        };

         /**
         * Takes the user to the next step in the form.
         * @member {KnockoutComputed<string>}
         */
        this.stepButtonLabel = ko.pureComputed(() => {
            if (this.isAtReview()) {
                return 'Save';
            }
            else {
                return 'Save and Continue';
            }
        });

        /**
         * Takes the user to the next step in the form (in standard mode),
         * or to the summary (when in review mode).
         * @member {KnockoutComputed<number>}
         */
        this.saveStep = () => {
            if (this.isAtReview()) {
                this.currentStep(SUMMARY_STEP);
            }
            else {
                this.goNextStep();
            }
        };
    }

    /**
     * Define members for all the status flags needed.
     * @private
     */
    __setupStatusFlags() {
        /**
         * When a loading request it's on the works
         * @member {KnockoutObservable<boolean>}
         */
        this.isLoading = ko.observable(false);

        /**
         * When a saving request it's on the works
         * @member {KnockoutObservable<boolean>}
         */
        this.isSaving = ko.observable(false);

        /**
         * When a deletion request it's on the works
         * @member {KnockoutObservable<boolean>}
         */
        this.isDeleting = ko.observable(false);

        /**
         * When edition must be locked because of in progress
         * operations.
         * @member {KnockoutComputed<boolean>}
         */
        this.isLocked = ko.pureComputed(() => this.isSaving() || this.isLoading() || this.isDeleting());

        /**
         * Whether the item is a new record or is being edited.
         * @member {KnockoutObservable<boolean>}
         */
        this.isNew = ko.pureComputed(() => this.editorMode() !== EditorMode.edit);
    }

    /**
     * Define members, prepare subscriptions to work with the code
     * and start any initial request for data
     * @private
     */
    __setupDataOperations() {
        /**
         * We create an item manager to operate on the data for the requested ID
         * (allows to load, save, delete).
         */
        this.dataManager = userEarningsItem(this.earningsEntry.earningsEntryID());

        /**
         * Suscribe to data coming for the list and put them in our
         * userListings propery.
         */
        this.subscribeTo(userListingsList.onData, this.userListings);

        /**
         * Notify data load errors
         */
        this.subscribeTo(userListingsList.onDataError, (err) => {
            showError({
                title: 'There was an error loading your listings',
                error: err
            });
        });

        /**
         * Suscribe to data coming for the list and put them in our
         * userExternalListings propery.
         */
        this.subscribeTo(userExternalListingsList.onData, this.userExternalListings);

        /**
         * Notify data load errors
         */
        this.subscribeTo(userExternalListingsList.onDataError, (err) => {
            showError({
                title: 'There was an error loading your external listings',
                error: err
            });
        });

        /**
         * Load suggestions.
         */
        this.subscribeTo(suggestedPlatformsList.onData, this.suggestedPlatforms);

        /// Notify data load errors
        this.subscribeTo(suggestedPlatformsList.onDataError, (err) => {
            showError({
                title: 'There was an error loading the platforms',
                error: err
            });
        });

        // When we have an ID, we need to load it first
        if (this.earningsEntry.earningsEntryID()) {

            this.isLoading(true);

            this.dataManager.onceLoaded()
            .then((data) => {
                this.isLoading(false);
                if (this.editorMode() === EditorMode.copy) {
                    // On copy mode, we need to reset the ID and dataManager
                    // so it forces to create a new entry (otherwise it will
                    // actually update the original one)
                    data.earningsEntryID = 0;
                    this.dataManager = userEarningsItem(0);
                }
                this.earningsEntry.model.updateWith(data);
                // Load info about the client of this entry
                // It's optional, so careful since can be null
                if (data.clientUserID) {
                    clientsDataItem(data.clientUserID)
                    .onceLoaded()
                    .then((client) => {
                        // IMPORTANT: Be careful (again), because if something was slow
                        // loading, user could have picked (edited/added) a new client
                        // in the editor, and we do not want to replace that info,
                        // check if there is no data, or that data has the same ID
                        // that the one we have loaded
                        if (!this.selectedClient() ||
                            this.selectedClient().clientUserID === client.clientUserID) {
                            this.selectedClient(client);
                        }
                    })
                    .catch((error) => {
                        showError({
                            title: 'There was an error loading the client information at the entry',
                            error
                        });
                    });
                }
            })
            .catch((error) => {
                this.isLoading(false);
                showError({
                    title: 'There was an error loading the earnings entry',
                    error
                });
            });
        }
    }

    /**
     * Makes given client current one selected.
     * @param {Object} client
     * @method
     */
    selectClient(client) {
        // Updates edited entry with the client ID selected
        this.earningsEntry.clientUserID(client.clientUserID);
        // Save a reference, to display name and other info
        this.selectedClient(client);
        this.goToSummary();
    }

    /**
     * Makes given listing the one selected
     * @param {Object} listing
     * @method
     */
    selectListing(listing) {
        this.earningsEntry.userExternalListingID(listing.userExternalListingID);
        this.earningsEntry.listingTitle(listing.title);
        // for integrity, makes platform matches listing (not needed to save the data)
        this.earningsEntry.platformID(listing.platformID);
        this.saveStep();
    }

    /**
     * Gets the job title from the given user listings and use it as the
     * selected job title of the earnings entry
     * @param {Object} listing
     * @method
     */
    selectListingJobTitle(listing) {
        this.earningsEntry.jobTitleID(listing.jobTitleID);
        this.earningsEntry.jobTitleName(listing.jobTitleSingularName);
        this.goToSummary();
    }

    /**
     * Gets the job title from a job title selected from the autocomplete
     * and use it as the selected job title of the earnings entry
     * @param {string} textValue User input text searching a job title
     * @param {models/JobTitle} data Selected job title model
     */
    selectJobTitle(textValue, data) {
        if (!data || !data.jobTitleID) return;

        const id = data.jobTitleID();
        this.earningsEntry.jobTitleID(id);
        this.earningsEntry.jobTitleName(data.singularName());
        this.goToSummary();
        return {
            value: ActionForValue.clear
        };
    }

    /**
     * Makes given platform as the selected listing, which means a listing
     * will be created for that platform
     * @param {rest/Platform} platform
     */
    selectPlatform(platform) {
        this.earningsEntry.platformID(platform.platformID);
        // resets listingID or will not take effect
        this.earningsEntry.userExternalListingID(null);
        // Something to display to the user
        this.earningsEntry.listingTitle(`My ${platform.name} listing`);
        this.saveStep();
    }

    /**
     * Save data in the server
     * @returns {Promise<object>}
     */
    save() {
        if (this.isSaving()) return Promise.reject();

        this.isSaving(true);

        const data = this.earningsEntry.model.toPlainObject(true);

        return this.dataManager
        .save(data)
        .then((freshData) => {
            this.isSaving(false);
            if (this.onSaved) {
                // Notify
                this.onSaved(freshData);
            }
            else {
                // Use updated/created data
                this.earningsEntry.model.updateWith(freshData);
            }
        })
        .catch((error) => {
            this.isSaving(false);
            showError({
                title: 'There was an error saving the earnings entry',
                error
            });
        });
    }

    /**
     * Delete the entry being edited after confirmation
     * @returns {Promise}
     */
    confirmDelete() {
        if (this.isDeleting()) return Promise.reject();

        this.isDeleting(true);
        const id = this.earningsEntry.earningsEntryID();

        return showConfirm({
            title: 'Are you sure?',
            message: 'Delete ernings entry #' + id,
            yes: 'Delete',
            no: 'Keep'
        })
        .then(() =>  this.dataManager.delete())
        .then(() => {
            this.isDeleting(false);
            if (this.onDeleted) {
                // Notify
                this.onDeleted();
            }
            else {
                // Reset to new item
                this.earningsEntry.model.reset();
                this.editorMode(EditorMode.add);
                this.dataManager = userEarningsItem(0);
            }
        })
        .catch((error) => {
            this.isDeleting(false);
            if (error) {
                showError({
                    title: 'There was an error deleting the earnings entry',
                    error
                });
            }
        });
    }
}

ko.components.register(TAG_NAME, EarningsEditor);

<|endoftext|>


<commit_before>'use strict';

const urlParser = require('url');

function joinNonEmpty(strings, delimeter) {
  return strings.filter(Boolean).join(delimeter);
}

function toSafeKey(key) {
  return key.replace(/[.~ /+|,:?&%]|%7C/g, '_');
}

module.exports = {
  keypathFromUrl(url, includeQueryParams)
  {
    function flattenQueryParams(params) {
      return Object.keys(params).reduce((result, key) =>
          joinNonEmpty([result, key, params[key]], '_'),
        '');
    }

    url = urlParser.parse(url, !!includeQueryParams);

    let path = toSafeKey(url.pathname);

    if (includeQueryParams) {
      path = joinNonEmpty([path, toSafeKey(flattenQueryParams(url.query))], '_');
    }

    const keys = [
      toSafeKey(url.hostname),
      path
    ];

    return joinNonEmpty(keys, '.');
  },

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

    function recursiveFlatten(target, keyPrefix, value, type) {
      // super simple version to avoid flatten HAR and screenshot data
      if (keyPrefix.match(/(screenshots\.|har\.)/)) {
          return;
      }

      // Google is overloading User Timing marks
      // See https://github.com/sitespeedio/browsertime/issues/257
      if (keyPrefix.indexOf('userTimings.marks.goog_') > -1) {
        return;
      }

      // Google is overloading User Timing marks = the same using WebPageTest
      // See https://github.com/sitespeedio/browsertime/issues/257
      if (keyPrefix.indexOf('userTimes.goog_') > -1) {
         return;
      }

      const valueType = typeof value;

      switch (valueType) {
        case 'number':
        {
          target[keyPrefix] = value;
        }
          break;
        case 'object':
        {
          if (value === null) {
            break;
          }

          Object.keys(value).forEach((key) => {

            // Hey are you coming to the future from 1980s? Please don't
            // look at this code, it's a ugly hack to make sure we can send assets
            // to Graphite and don't send them with array position, instead
            // use the url to generate the key

            if (type === 'pagexray.pageSummary' && keyPrefix === 'assets') {
                recursiveFlatten(target, joinNonEmpty([keyPrefix, toSafeKey(value[key].url)], '.'), value[key], type);
            }
            else {
              recursiveFlatten(target, joinNonEmpty([keyPrefix, toSafeKey(key)], '.'), value[key], type);
            }
          });
        }
          break;
        case 'string':
        {
          if (isNumeric(value)) {
            target[keyPrefix] = parseFloat(value);
          }
        }
          break;
        case 'boolean':
        {
          target[keyPrefix] = value ? 1 : 0;
        }
          break;
        case 'undefined':
        {
          // take care of faulty values, add a log in the future
        }
          break;
        default:
          throw new Error('Unhandled value type ' + valueType + ' found when flattening data for prefix ' + keyPrefix);
      }
    }

    let retVal = {};
    recursiveFlatten(retVal, '', message.data, message.type);
    return retVal;
  }
};
<commit_msg>Simplify using ES6 destructuring syntax.<commit_after>'use strict';

const urlParser = require('url');

function joinNonEmpty(strings, delimeter) {
  return strings.filter(Boolean).join(delimeter);
}

function toSafeKey(key) {
  return key.replace(/[.~ /+|,:?&%]|%7C/g, '_');
}

module.exports = {
  keypathFromUrl(url, includeQueryParams)
  {
    function flattenQueryParams(params) {
      return Object.keys(params).reduce((result, key) =>
          joinNonEmpty([result, key, params[key]], '_'),
        '');
    }

    url = urlParser.parse(url, !!includeQueryParams);

    let path = toSafeKey(url.pathname);

    if (includeQueryParams) {
      path = joinNonEmpty([path, toSafeKey(flattenQueryParams(url.query))], '_');
    }

    const keys = [
      toSafeKey(url.hostname),
      path
    ];

    return joinNonEmpty(keys, '.');
  },

  flattenMessageData({data, type}) {
    function isNumeric(n) {
      return !isNaN(parseFloat(n)) && isFinite(n);
    }

    function recursiveFlatten(target, keyPrefix, value) {
      // super simple version to avoid flatten HAR and screenshot data
      if (keyPrefix.match(/(screenshots\.|har\.)/)) {
          return;
      }

      // Google is overloading User Timing marks
      // See https://github.com/sitespeedio/browsertime/issues/257
      if (keyPrefix.indexOf('userTimings.marks.goog_') > -1) {
        return;
      }

      // Google is overloading User Timing marks = the same using WebPageTest
      // See https://github.com/sitespeedio/browsertime/issues/257
      if (keyPrefix.indexOf('userTimes.goog_') > -1) {
         return;
      }

      const valueType = typeof value;

      switch (valueType) {
        case 'number':
        {
          target[keyPrefix] = value;
        }
          break;
        case 'object':
        {
          if (value === null) {
            break;
          }

          Object.keys(value).forEach((key) => {

            // Hey are you coming to the future from 1980s? Please don't
            // look at this code, it's a ugly hack to make sure we can send assets
            // to Graphite and don't send them with array position, instead
            // use the url to generate the key

            if (type === 'pagexray.pageSummary' && keyPrefix === 'assets') {
                recursiveFlatten(target, joinNonEmpty([keyPrefix, toSafeKey(value[key].url)], '.'), value[key]);
            }
            else {
              recursiveFlatten(target, joinNonEmpty([keyPrefix, toSafeKey(key)], '.'), value[key]);
            }
          });
        }
          break;
        case 'string':
        {
          if (isNumeric(value)) {
            target[keyPrefix] = parseFloat(value);
          }
        }
          break;
        case 'boolean':
        {
          target[keyPrefix] = value ? 1 : 0;
        }
          break;
        case 'undefined':
        {
          // take care of faulty values, add a log in the future
        }
          break;
        default:
          throw new Error('Unhandled value type ' + valueType + ' found when flattening data for prefix ' + keyPrefix);
      }
    }

    let retVal = {};
    recursiveFlatten(retVal, '', data);
    return retVal;
  }
};
<|endoftext|>


<commit_before>'use babel';

import { CompositeDisposable } from 'atom';
const path = require('path');

//TODO: what if multiple instances of a file are open in a single pane? can that happen?

export default {
  subscriptions: null,
  sourceExtensions: null,
  headerExtensions: null,
  useAdvancedMatch: null,
  pairRegExp: null,

  config: {
    headerExtensions: {
      type: "array",
      "default": ["h", "hh", "hpp", "hxx", "h++", "cuh"],
      order: 1,
      items: {
        "type": "string",
      }
    },
    sourceExtensions: {
      type: "array",
      "default": ["c", "cc", "cpp", "cp", "cxx", "c++", "cu"],
      order: 2,
      items: {
        type: "string",
      }
    },
    matchFullPath: {
      type: "boolean",
      order: 3,
      default: false
    },
    //TODO: add option for compressing to icon only, no text

    advancedMatch: {
      type: "boolean",
      order: 4,
      default: false
    },
    advancedMatchRegExp: {
      type: "array",
      "default": ["([^/]+?)(?:\.c|\.cc|\.cpp|\.cp|\.cxx|\.c++|\.cu|\.h|\.hh|\.hpp|\.hxx|\.h++|\.cuh)",
                  "([^/]+?)(?:\.js|\.css|\.html)"],
      order: 5,
      items: {
        type: "string",
      }
    }
  },

  activate(state) {
    this.updateConfig();

    this.subscriptions = new CompositeDisposable();
    this.subscriptions.add(atom.config.onDidChange(this.updateConfig));

    // subscribe to events that change editor path, or panelitem position
    this.subscriptions.add(atom.workspace.onDidChangeActivePaneItem((ev)=>{this.changedActiveItem(ev)}));

    this.subscriptions.add(atom.workspace.observeTextEditors((editor) => {
        this.subscriptions.add(editor.onDidChangePath(()=>{this.changedPath(editor)}));
    }));

    this.subscriptions.add(atom.workspace.observePanes((pane) => {
      this.subscriptions.add(pane.onDidMoveItem((ev) => {
        setTimeout(()=>{this.movedInSamePane(pane, ev)}, 5);
      }));
      this.subscriptions.add(pane.onWillRemoveItem((ev)=>{
        this.removedPaneItem(pane, ev);
      }));
      this.subscriptions.add(pane.onDidAddItem((ev)=>{
        // wait a moment so the DOM has time to reflow
        // otherwise if we move the paneitem, it may not take
        setTimeout(()=>{this.addedPaneItem(pane, ev)}, 5);
      }));
    }));
  },

  deactivate() {
    this.subscriptions.dispose();
  },

  // serialize() {
  //   return {
  //     tabPairHeaderSourceViewState: this.tabPairHeaderSourceView.serialize()
  //   };
  // },

  updateConfig() {
    this.useFullPath = atom.config.get('tab-pair-header-source.matchFullPath');
    this.sourceExtensions = atom.config.get('tab-pair-header-source.sourceExtensions');
    this.headerExtensions = atom.config.get('tab-pair-header-source.headerExtensions');
    for(let index=0; index < this.sourceExtensions.length; index++) {
      this.sourceExtensions[index] = "."+this.sourceExtensions[index].toLowerCase();
    }
    for(let index=0; index < this.headerExtensions.length; index++) {
      this.headerExtensions[index] = "."+this.headerExtensions[index].toLowerCase();
    }

    this.useAdvancedMatch = atom.config.get('tab-pair-header-source.advancedMatch');
    var pairPatterns = atom.config.get('tab-pair-header-source.advancedMatchRegEx');
    this.pairRegExp = []
    for(let index=0; index < pairPatterns.length; index++) {
      this.pairRegExp[index] = RegExp(pairPatterns[index]);
    }
  },

  isPair(path1, path2) {
    for(let index=0; index < this.pairRegExp.length; index++) {
      var match1 = this.pairRegExp[index].exec(path1)
      var match2 = this.pairRegExp[index].exec(path2)
      if (match1 !== null && match2 !== null)
      {
        return JSON.stringify(match1.slice(1)) === JSON.stringify(match2.slice(1))
      }
    }
    return false;
  },

  isSource(ext) {
    var retval = false;
    if(this.sourceExtensions.indexOf(ext.toLowerCase()) >= 0) {
      retval = true;
    }
    return retval;
  },

  isHeader(ext) {
    var retval = false;
    if(this.headerExtensions.indexOf(ext.toLowerCase()) >= 0) {
      retval = true;
    }
    return retval;
  },

  isSourceHeaderPair(path1, path2) {
    var retval = false;

    if ( ( path1.name == path2.name ) &&
         ( path1.dir == path2.dir || !this.useFullPath ) &&
         ( (this.isSource(path1.ext) && this.isHeader(path2.ext)) ||
           (this.isSource(path2.ext) && this.isHeader(path1.ext)) ) ) {
          retval = true;
    }
    return retval;
  },

  setTabTitle(item, compress=false){
    if (atom.inDevMode()) {
      if (compress) {
        console.log("compressing tab: " + item.getPath());
      }
      else {
        console.log("expanding tab: " + item.getPath());
      }
    }
    var pItems = atom.workspace.getPaneItems();
    var index;
    for ( let i = 0; i < pItems.length; i++ ) {
      if (pItems[i] == item) {
        index = i;
        break;
      }
    }
    if (typeof index === "undefined")
      return;

    let tab = atom.views.getView(atom.workspace).querySelectorAll('li.tab .title')[index];
    if (typeof tab === "undefined") {
      return;
    }

    let filename = tab.getAttribute('data-name');

    if(tab && filename)
    {
      if (compress) {
        filename = path.extname(filename)
        tab.parentElement.classList.add('tab-pair-header-source')
      }
      else {
        tab.parentElement.classList.remove('tab-pair-header-source')
      }
      tab.innerText = filename;
    }
  },

  findPaneItem(paneitem) {
    var index;
    var items;
    var pane;
    var panes = atom.workspace.getPanes();
    for(let i=0; i < panes.length; i++) {
      pane = panes[i]
      items = pane.getItems()
      for (let j=0; j < items.length; j++) {
        if (items[j] == paneitem) {
          index = j;
          break;
        }
      }
      if(index) {
        break;
      }
    }

    return {pane, index};
  },

  checkNeighbors(pane, index, parsedPath, compress=false) {
    if(typeof index === "undefined" || index < 0) {
      return; // bail if we couldn't find the item
    }

    var items = pane.getItems();
    if (items.length > index+1) {
      let editor = items[index+1];
      if (atom.workspace.isTextEditor(editor)) {
        let filepath = editor.getPath();
        if (typeof filepath !== "undefined") {
          let editor_parsedPath = path.parse(filepath);
          if (this.isSourceHeaderPair(parsedPath, editor_parsedPath)) {
            this.setTabTitle(editor, compress);
          }
        }
      }
    }
    if (index-1 >= 0) {
      let editor = items[index-1];
      if (atom.workspace.isTextEditor(editor)) {
        let filepath = editor.getPath();
        if (typeof filepath !== "undefined") {
          let editor_parsedPath = path.parse(editor.getPath());
          if (this.isSourceHeaderPair(parsedPath, editor_parsedPath)) {
            this.setTabTitle(editor, compress);
          }
        }
      }
    }
  },

  addedPaneItem(pane, ev) {
    if (atom.inDevMode()) {
      console.log("addedPaneItem");
    }
    //only worry about text editors
    if (!atom.workspace.isTextEditor(ev.item)) {
      return;
      //TODO: need to check to see, if we opened a new item in the middle of
      // and existing matched pair, if so then we should move the item, or
      // expand the tabs of the newly seperated pair
    }

    var filepath = ev.item.getPath();
    if (typeof filepath === "undefined") {
      return;
    }

    var parsedPath = path.parse(filepath);
    if( !(this.isSource(parsedPath.ext) || this.isHeader(parsedPath.ext)) ){
      return;
    }

    if (atom.inDevMode()) {
      console.log("  added at index: " + ev.index);
    }

    var newIndex = -1;
    var panes = pane.getItems();
    for (let index=0; index < panes.length; index++)
    {
      //Only check text editors
      if (!atom.workspace.isTextEditor(panes[index])) {
        continue;
      }
      filepath = panes[index].getPath();
      if (typeof filepath === "undefined") {
        continue;
      }

      let editor_parsedPath = path.parse(filepath);
      if (this.isSourceHeaderPair(parsedPath, editor_parsedPath)) {
        if (this.isSource(parsedPath.ext))
        {
          newIndex = index; //put source on left side
        }
        else { //isHeader
          newIndex = index+1; //put header on right side
        }
        if (newIndex > ev.index) {
          newIndex -= 1; // account for the affect of removing this tab
        }
        break;
      }
    }
    if (newIndex >= 0 && newIndex != ev.index) {
      // console.log("Item was moved to new location: " + newIndex);
      pane.moveItem(ev.item, newIndex);
    }
    else if (newIndex == ev.index) {
      //TODO: opened the file in the correct location, need to adjust tab size
      // since moveitem event will not be triggered.
    }
    else {
      //TODO: need to check to see, if we opened a new item in the middle of
      // and existing matched pair, if so then we should move the item, or
      // expand the tabs of the newly seperated pair
      // maybe handled by changed active item? however that event is called before add event
    }
  },

  //TODO: may need to add removedPaneItem subscription,
  // if removed item causes pair to be next to each other
  // than that pair should be compressed somehow
  removedPaneItem(pane, ev) {
    if (atom.inDevMode()) {
      console.log("removedPaneItem");
    }

    //only worry about text editors
    if (!atom.workspace.isTextEditor(ev.item)) {
      return;
    }

    var filepath = ev.item.getPath();
    if (typeof filepath === "undefined") {
      return;
    }
    var parsedPath = path.parse(filepath);
    if( !(this.isSource(parsedPath.ext) || this.isHeader(parsedPath.ext)) ){
      return;
    }

    this.checkNeighbors(pane, ev.index, parsedPath, false)
  },

  //TODO: handle tab expand/compress from this function, (addedPaneItem() is replying on this function to do the job)
  movedInSamePane(pane, ev) {
    if (atom.inDevMode()) {
      console.log("movedInSamePane");
    }
    //can you tell if the user requested it moved or the application moved it?

    // TODO: moved to same spot, should something be done?
    // if(ev.nexIndex == ev.oldIndex) {
    //   return;
    // }

    //only worry about text editors
    if (!atom.workspace.isTextEditor(ev.item)) {
      return;
      //TODO: need to check to see, if we moved an item in the middle of
      // and existing matched pair, than expand the tabs of the newly
      // seperated pair
    }

    var filepath = ev.item.getPath();
    if (typeof filepath === "undefined") {
      return;
    }
    var parsedPath = path.parse(filepath);
    if( !(this.isSource(parsedPath.ext) || this.isHeader(parsedPath.ext)) ){
      return;
    }

    this.setTabTitle(ev.item, false); //TODO: does this need to be done here?

    // if we moved an item in the middle of
    // and existing matched pair, than expand the tabs of the newly
    // seperated pair
    //TODO: should we check to see if one of the tabs is compressed?
    // or does that just add unnecessary computing?
    var items = pane.getItems();
    if (items.length > ev.newIndex+1 && ev.newIndex > 0) {
      this.setTabTitle(items[ev.newIndex-1], false);
      this.setTabTitle(items[ev.newIndex+1], false);
    }

    // if we moved the active tab away from its pair, then we need to expand its pair
    this.checkNeighbors(pane, ev.oldIndex, parsedPath, false);

    // compress new pair
    this.checkNeighbors(pane, ev.newIndex, parsedPath, true);
  },

  changedActiveItem(paneitem) {
    if (atom.inDevMode()) {
      console.log("changedActiveItem");
    }

    if(!atom.workspace.isTextEditor(paneitem)) {
      return;
    }

    this.setTabTitle(paneitem, false);

    var result = this.findPaneItem(paneitem);
    var filepath = paneitem.getPath();
    if (typeof filepath === "undefined") {
      return;
    }
    var parsedPath = path.parse(filepath);
    this.checkNeighbors(result.pane, result.index, parsedPath, true);

    //TODO: handle tab expand/compress from this function
    // in the case were the compressed tab is picked, it should be expanded
    // and the matching pair compressed, in this use case, this is the only
    // event to trigger
  },

  changedPath(editor) {
    if (atom.inDevMode()) {
      console.log("changedPath");
    }

    var filepath = editor.getPath();
    if (typeof filepath === "undefined") {
      return;
    }
    var parsedPath = path.parse(filepath);

    this.setTabTitle(editor, false);

    var result = this.findPaneItem(editor);
    this.checkNeighbors(result.pane, result.index, parsedPath, true);
  },
};
<commit_msg>adding logic to group pane items into paired paths<commit_after>'use babel';

import { CompositeDisposable } from 'atom';
const path = require('path');

//TODO: what if multiple instances of a file are open in a single pane? can that happen?

export default {
  subscriptions: null,
  sourceExtensions: null,
  headerExtensions: null,
  useAdvancedMatch: null,
  pairRegExp: null,

  config: {
    pairExtensions: {
      type: "array",
      "default": ["c", "cc", "cpp", "cp", "cxx", "c++", "cu", "h", "hh", "hpp", "hxx", "h++", "cuh"],
      order: 1,
      items: {
        "type": "string",
      }
    },
    matchFullPath: {
      type: "boolean",
      order: 3,
      default: false
    },
    //TODO: add option for compressing to icon only, no text

    advancedMatch: {
      type: "boolean",
      order: 4,
      default: false
    },
    advancedMatchRegExp: {
      type: "array",
      "default": ["([^/]+?)(?:\.c|\.cc|\.cpp|\.cp|\.cxx|\.c++|\.cu|\.h|\.hh|\.hpp|\.hxx|\.h++|\.cuh)",
                  "([^/]+?)(?:\.js|\.css|\.html)"],
      order: 5,
      items: {
        type: "string",
      }
    }
  },

  activate(state) {
    this.updateConfig();

    this.subscriptions = new CompositeDisposable();
    this.subscriptions.add(atom.config.onDidChange(this.updateConfig));

    // subscribe to events that change editor path, or panelitem position
    this.subscriptions.add(atom.workspace.onDidChangeActivePaneItem((ev)=>{this.changedActiveItem(ev)}));

    this.subscriptions.add(atom.workspace.observeTextEditors((editor) => {
        this.subscriptions.add(editor.onDidChangePath(()=>{this.changedPath(editor)}));
    }));

    this.subscriptions.add(atom.workspace.observePanes((pane) => {
      this.subscriptions.add(pane.onDidMoveItem((ev) => {
        setTimeout(()=>{this.movedInSamePane(pane, ev)}, 5);
      }));
      this.subscriptions.add(pane.onWillRemoveItem((ev)=>{
        this.removedPaneItem(pane, ev);
      }));
      this.subscriptions.add(pane.onDidAddItem((ev)=>{
        // wait a moment so the DOM has time to reflow
        // otherwise if we move the paneitem, it may not take
        setTimeout(()=>{this.addedPaneItem(pane, ev)}, 5);
      }));
    }));
  },

  deactivate() {
    this.subscriptions.dispose();
  },

  // serialize() {
  //   return {
  //     tabPairHeaderSourceViewState: this.tabPairHeaderSourceView.serialize()
  //   };
  // },

  updateConfig() {
    this.pairRegExp = []
    var useAdvancedMatch = atom.config.get('tab-pair-header-source.advancedMatch');
    if (useAdvancedMatch) {
      var pairPatterns = atom.config.get('tab-pair-header-source.advancedMatchRegEx');
      for(let index=0; index < pairPatterns.length; index++) {
        this.pairRegExp.push( RegExp(pairPatterns[index]) );
      }
    }
    else { //basic mode
      var useFullPath = atom.config.get('tab-pair-header-source.matchFullPath');
      var regstr;
      if (useFullPath) {
        regstr = "([^/]+?)(?:";
      } else {
        regstr = "(?:";
      }
      pairExtensions = atom.config.get('tab-pair-header-source.pairExtensions');
      for(let index=0; index < pairExtensions.length; index++) {
        // ([^/]+?)(?:.c|.cc|.cpp|.cp|.cxx|.c++|.cu|.h|.hh|.hpp|.hxx|.h++|.cuh), ([^/]+?)(?:.js|.css|.html)
        regstr += "\\." + pairExtensions[index].toLowerCase() + "|";
      }
      regstr[regstr.lenght-1] = ")";
      this.pairRegExp.push( regstr) );
    }
  },

  isPair(curr_path, other_paths) {
    //coerce other_paths to an array
    if !Array.isArray(other_paths) {
      if (typeof other_paths === 'string' ||  other_paths instanceof String) {
        other_paths = [other_paths]
      }
      else {
        return null;
      }
    }
    matches = Array(other_paths.length).fill(false)
    for(let index=0; index < this.pairRegExp.length; index++) {
      let curr_match = this.pairRegExp[index].exec(curr_path)
      if (curr_match !== null)
      {
        let curr_match_str = JSON.stringify(curr_match.slice(1))
        for(let jndex=0; jndex < other_paths.length; jndex++) {
          let other_match = this.pairRegExp[index].exec(other_paths[jndex])
          if (other_match !== null && curr_match_str === JSON.stringify(other_match.slice(1)))
          {
            matches[jndex] = true;
          }
        }
      }
    }
    return matches;
  },

  getPathsForPane(pane) {
    paneitems = pane.getItems();
    let item_paths = Array(paneitems.length).fill('');
    for (paneitem_index in paneitems) { // TODO: verify, this assumes that getItems returns items in same order as they are displayed
      paneitem = paneitems[paneitem_index];
      if (atom.isTextEditor(paneitem)) {
        item_paths[pane_index] = paneitem.getPath();
      }
    }
  },

  checkEverything() {
    var panes = atom.workspace.getPanes();
    for (let pane_index in panes) {
      var pane = panes[pane_index];
      pane_paths = getPathsForPane(pane);

      // go through item_paths and split it into group numbers depending on matches. ex [1 1 0 2 2 2 0 3 0 4], here groups 1 and 2 would have tabs shortened

    }
  },

  setTabTitle(item, compress=false){
    if (atom.inDevMode()) {
      if (compress) {
        console.log("compressing tab: " + item.getPath());
      }
      else {
        console.log("expanding tab: " + item.getPath());
      }
    }
    var pItems = atom.workspace.getPaneItems();
    var index;
    for ( let i = 0; i < pItems.length; i++ ) {
      if (pItems[i] == item) {
        index = i;
        break;
      }
    }
    if (typeof index === "undefined")
      return;

    let tab = atom.views.getView(atom.workspace).querySelectorAll('li.tab .title')[index];
    if (typeof tab === "undefined") {
      return;
    }

    let filename = tab.getAttribute('data-name');

    if(tab && filename)
    {
      if (compress) {
        filename = path.extname(filename)
        tab.parentElement.classList.add('tab-pair-header-source')
      }
      else {
        tab.parentElement.classList.remove('tab-pair-header-source')
      }
      tab.innerText = filename;
    }
  },

  findPaneItem(paneitem) {
    var index;
    var items;
    var pane;
    var panes = atom.workspace.getPanes();
    for(let i=0; i < panes.length; i++) {
      pane = panes[i]
      items = pane.getItems()
      for (let j=0; j < items.length; j++) {
        if (items[j] == paneitem) {
          index = j;
          break;
        }
      }
      if(index) {
        break;
      }
    }

    return {pane, index};
  },

  checkNeighbors(pane, index, parsedPath, compress=false) {
    if(typeof index === "undefined" || index < 0) {
      return; // bail if we couldn't find the item
    }

    var items = pane.getItems();
    if (items.length > index+1) {
      let editor = items[index+1];
      if (atom.workspace.isTextEditor(editor)) {
        let filepath = editor.getPath();
        if (typeof filepath !== "undefined") {
          let editor_parsedPath = path.parse(filepath);
          if (this.isSourceHeaderPair(parsedPath, editor_parsedPath)) {
            this.setTabTitle(editor, compress);
          }
        }
      }
    }
    if (index-1 >= 0) {
      let editor = items[index-1];
      if (atom.workspace.isTextEditor(editor)) {
        let filepath = editor.getPath();
        if (typeof filepath !== "undefined") {
          let editor_parsedPath = path.parse(editor.getPath());
          if (this.isSourceHeaderPair(parsedPath, editor_parsedPath)) {
            this.setTabTitle(editor, compress);
          }
        }
      }
    }
  },

  addedPaneItem(pane, ev) {
    if (atom.inDevMode()) {
      console.log("addedPaneItem");
    }
    //only worry about text editors
    if (!atom.workspace.isTextEditor(ev.item)) {
      return;
      //TODO: need to check to see, if we opened a new item in the middle of
      // and existing matched pair, if so then we should move the item, or
      // expand the tabs of the newly seperated pair
    }

    var filepath = ev.item.getPath();
    if (typeof filepath === "undefined") {
      return;
    }

    var parsedPath = path.parse(filepath);
    if( !(this.isSource(parsedPath.ext) || this.isHeader(parsedPath.ext)) ){
      return;
    }

    if (atom.inDevMode()) {
      console.log("  added at index: " + ev.index);
    }

    var newIndex = -1;
    var panes = pane.getItems();
    for (let index=0; index < panes.length; index++)
    {
      //Only check text editors
      if (!atom.workspace.isTextEditor(panes[index])) {
        continue;
      }
      filepath = panes[index].getPath();
      if (typeof filepath === "undefined") {
        continue;
      }

      let editor_parsedPath = path.parse(filepath);
      if (this.isSourceHeaderPair(parsedPath, editor_parsedPath)) {
        if (this.isSource(parsedPath.ext))
        {
          newIndex = index; //put source on left side
        }
        else { //isHeader
          newIndex = index+1; //put header on right side
        }
        if (newIndex > ev.index) {
          newIndex -= 1; // account for the affect of removing this tab
        }
        break;
      }
    }
    if (newIndex >= 0 && newIndex != ev.index) {
      // console.log("Item was moved to new location: " + newIndex);
      pane.moveItem(ev.item, newIndex);
    }
    else if (newIndex == ev.index) {
      //TODO: opened the file in the correct location, need to adjust tab size
      // since moveitem event will not be triggered.
    }
    else {
      //TODO: need to check to see, if we opened a new item in the middle of
      // and existing matched pair, if so then we should move the item, or
      // expand the tabs of the newly seperated pair
      // maybe handled by changed active item? however that event is called before add event
    }
  },

  //TODO: may need to add removedPaneItem subscription,
  // if removed item causes pair to be next to each other
  // than that pair should be compressed somehow
  removedPaneItem(pane, ev) {
    if (atom.inDevMode()) {
      console.log("removedPaneItem");
    }

    //only worry about text editors
    if (!atom.workspace.isTextEditor(ev.item)) {
      return;
    }

    var filepath = ev.item.getPath();
    if (typeof filepath === "undefined") {
      return;
    }
    var parsedPath = path.parse(filepath);
    if( !(this.isSource(parsedPath.ext) || this.isHeader(parsedPath.ext)) ){
      return;
    }

    this.checkNeighbors(pane, ev.index, parsedPath, false)
  },

  //TODO: handle tab expand/compress from this function, (addedPaneItem() is replying on this function to do the job)
  movedInSamePane(pane, ev) {
    if (atom.inDevMode()) {
      console.log("movedInSamePane");
    }
    //can you tell if the user requested it moved or the application moved it?

    // TODO: moved to same spot, should something be done?
    // if(ev.nexIndex == ev.oldIndex) {
    //   return;
    // }

    //only worry about text editors
    if (!atom.workspace.isTextEditor(ev.item)) {
      return;
      //TODO: need to check to see, if we moved an item in the middle of
      // and existing matched pair, than expand the tabs of the newly
      // seperated pair
    }

    var filepath = ev.item.getPath();
    if (typeof filepath === "undefined") {
      return;
    }
    var parsedPath = path.parse(filepath);
    if( !(this.isSource(parsedPath.ext) || this.isHeader(parsedPath.ext)) ){
      return;
    }

    this.setTabTitle(ev.item, false); //TODO: does this need to be done here?

    // if we moved an item in the middle of
    // and existing matched pair, than expand the tabs of the newly
    // seperated pair
    //TODO: should we check to see if one of the tabs is compressed?
    // or does that just add unnecessary computing?
    var items = pane.getItems();
    if (items.length > ev.newIndex+1 && ev.newIndex > 0) {
      this.setTabTitle(items[ev.newIndex-1], false);
      this.setTabTitle(items[ev.newIndex+1], false);
    }

    // if we moved the active tab away from its pair, then we need to expand its pair
    this.checkNeighbors(pane, ev.oldIndex, parsedPath, false);

    // compress new pair
    this.checkNeighbors(pane, ev.newIndex, parsedPath, true);
  },

  changedActiveItem(paneitem) {
    if (atom.inDevMode()) {
      console.log("changedActiveItem");
    }

    if(!atom.workspace.isTextEditor(paneitem)) {
      return;
    }

    this.setTabTitle(paneitem, false);

    var result = this.findPaneItem(paneitem);
    var filepath = paneitem.getPath();
    if (typeof filepath === "undefined") {
      return;
    }
    var parsedPath = path.parse(filepath);
    this.checkNeighbors(result.pane, result.index, parsedPath, true);

    //TODO: handle tab expand/compress from this function
    // in the case were the compressed tab is picked, it should be expanded
    // and the matching pair compressed, in this use case, this is the only
    // event to trigger
  },

  changedPath(editor) {
    if (atom.inDevMode()) {
      console.log("changedPath");
    }

    var filepath = editor.getPath();
    if (typeof filepath === "undefined") {
      return;
    }
    var parsedPath = path.parse(filepath);

    this.setTabTitle(editor, false);

    var result = this.findPaneItem(editor);
    this.checkNeighbors(result.pane, result.index, parsedPath, true);
  },
};
<|endoftext|>