<commit_before>// @flow

import { chatMessageItemPropType } from 'lib/selectors/chat-selectors';
import type {
  MultimediaMessageInfo,
  LocalMessageInfo,
} from 'lib/types/message-types';
import type { Media } from 'lib/types/media-types';
import type { ImageStyle } from '../types/styles';
import type { ThreadInfo } from 'lib/types/thread-types';
import type { Navigate } from '../navigation/route-names';

import * as React from 'react';
import {
  Text,
  StyleSheet,
  TouchableWithoutFeedback,
  View,
} from 'react-native';
import PropTypes from 'prop-types';

import { messageKey } from 'lib/shared/message-utils';

import ComposedMessage from './composed-message.react';
import MultimediaMessageMultimedia from './multimedia-message-multimedia.react';

export type ChatMultimediaMessageInfoItem = {|
  itemType: "message",
  messageShapeType: "multimedia",
  messageInfo: MultimediaMessageInfo,
  localMessageInfo: ?LocalMessageInfo,
  threadInfo: ThreadInfo,
  startsConversation: bool,
  startsCluster: bool,
  endsCluster: bool,
  contentHeight: number,
|};

function multimediaMessageContentHeight(
  messageInfo: MultimediaMessageInfo,
  composedMessageMaxWidth: number,
) {
  let contentHeight;
  if (messageInfo.media.length === 1) {
    const [ media ] = messageInfo.media;
    const mediaDimensions = media.dimensions;
    if (composedMessageMaxWidth >= mediaDimensions.width) {
      contentHeight = mediaDimensions.height;
    } else {
      contentHeight = mediaDimensions.height *
        composedMessageMaxWidth / mediaDimensions.width;
    }
    if (contentHeight < 50) {
      contentHeight = 50;
    }
  } else if (messageInfo.media.length === 2) {
    contentHeight = composedMessageMaxWidth / 2;
  } else if (messageInfo.media.length === 3) {
    contentHeight = composedMessageMaxWidth / 3;
  } else if (messageInfo.media.length === 4) {
    contentHeight = composedMessageMaxWidth;
  } else {
    const numRows = Math.ceil(messageInfo.media.length / 3);
    contentHeight = numRows * composedMessageMaxWidth / 3;
  }
  return contentHeight;
}

function multimediaMessageItemHeight(
  item: ChatMultimediaMessageInfoItem,
  viewerID: ?string,
) {
  const { messageInfo, contentHeight, startsCluster, endsCluster } = item;
  const { id, creator } = messageInfo;
  const { isViewer } = creator;
  let height = 5 + contentHeight; // for margin and image
  if (!isViewer && startsCluster) {
    height += 25; // for username
  }
  if (endsCluster) {
    height += 7; // extra padding at the end of a cluster
  }
  if (
    isViewer &&
    id !== null && id !== undefined &&
    item.localMessageInfo &&
    item.localMessageInfo.sendFailed
  ) {
    height += 22; // extra padding for sendFailed
  }
  return height;
}

type Props = {|
  item: ChatMultimediaMessageInfoItem,
  navigate: Navigate,
  toggleFocus: (messageKey: string) => void,
|};
class MultimediaMessage extends React.PureComponent<Props> {

  static propTypes = {
    item: chatMessageItemPropType.isRequired,
    navigate: PropTypes.func.isRequired,
    toggleFocus: PropTypes.func.isRequired,
  };

  render() {
    const { messageInfo, contentHeight } = this.props.item;
    const { id, creator } = messageInfo;
    const { isViewer } = creator;
    const heightStyle = { height: contentHeight };

    const sendFailed =
      isViewer &&
      (id === null || id === undefined) &&
      this.props.item.localMessageInfo &&
      this.props.item.localMessageInfo.sendFailed;

    return (
      <ComposedMessage
        item={this.props.item}
        sendFailed={!!sendFailed}
        borderRadius={16}
        style={styles.row}
      >
        <TouchableWithoutFeedback onLongPress={this.onLongPress}>
          <View style={[heightStyle, styles.container]}>
            {this.renderContent()}
          </View>
        </TouchableWithoutFeedback>
      </ComposedMessage>
    );
  }

  renderContent(): React.Node {
    const { messageInfo } = this.props.item;
    if (messageInfo.media.length === 1) {
      return this.renderImage(messageInfo.media[0]);
    } else if (messageInfo.media.length === 2) {
      const [ one, two ] = messageInfo.media;
      return (
        <View style={styles.row}>
          {this.renderImage(one, styles.leftImage)}
          {this.renderImage(two, styles.rightImage)}
        </View>
      );
    } else if (messageInfo.media.length === 3) {
      const [ one, two, three ] = messageInfo.media;
      return (
        <View style={styles.row}>
          {this.renderImage(one, styles.leftImage)}
          {this.renderImage(two, styles.centerImage)}
          {this.renderImage(three, styles.rightImage)}
        </View>
      );
    } else if (messageInfo.media.length === 4) {
      const [ one, two, three, four ] = messageInfo.media;
      return (
        <View style={styles.grid}>
          <View style={styles.row}>
            {this.renderImage(one, styles.topLeftImage)}
            {this.renderImage(two, styles.topRightImage)}
          </View>
          <View style={styles.row}>
            {this.renderImage(three, styles.bottomLeftImage)}
            {this.renderImage(four, styles.bottomRightImage)}
          </View>
        </View>
      );
    } else {
      const rows = [];
      for (let i = 0; i < messageInfo.media.length; i += 3) {
        let rowStyle;
        if (i === 0) {
          rowStyle = rowStyles.top;
        } else if (i + 3 >= messageInfo.media.length) {
          rowStyle = rowStyles.bottom;
        } else {
          rowStyle = rowStyles.middle;
        }

        const rowMedia = messageInfo.media.slice(i, i + 3);
        const row = [];
        let j = 0;
        for (; j < rowMedia.length; j++) {
          const media = rowMedia[j];
          const style = rowStyle[j];
          row.push(this.renderImage(media, style));
        }
        for (; j < 3; j++) {
          const key = `filler${j}`;
          row.push(<View style={styles.filler} key={key} />);
        }

        rows.push(
          <View style={styles.row} key={i}>
            {row}
          </View>
        );
      }
      return <View style={styles.grid}>{rows}</View>;
    }
  }

  renderImage(media: Media, style?: ImageStyle): React.Node {
    return (
      <MultimediaMessageMultimedia
        media={media}
        navigate={this.props.navigate}
        style={style}
        key={media.id}
      />
    );
  }

  onLongPress = () => {
    this.props.toggleFocus(messageKey(this.props.item.messageInfo));
  }

}

const spaceBetweenImages = 2;
const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'row',
    justifyContent: 'center',
  },
  grid: {
    flex: 1,
  },
  filler: {
    flex: 1,
  },
  row: {
    flex: 1,
    flexDirection: 'row',
  },
  leftImage: {
    marginRight: spaceBetweenImages,
  },
  centerImage: {
    marginLeft: spaceBetweenImages,
    marginRight: spaceBetweenImages,
  },
  rightImage: {
    marginLeft: spaceBetweenImages,
  },
  topLeftImage: {
    marginRight: spaceBetweenImages,
    marginBottom: spaceBetweenImages,
  },
  topCenterImage: {
    marginLeft: spaceBetweenImages,
    marginRight: spaceBetweenImages,
    marginBottom: spaceBetweenImages,
  },
  topRightImage: {
    marginLeft: spaceBetweenImages,
    marginBottom: spaceBetweenImages,
  },
  bottomLeftImage: {
    marginRight: spaceBetweenImages,
    marginTop: spaceBetweenImages,
  },
  bottomCenterImage: {
    marginLeft: spaceBetweenImages,
    marginRight: spaceBetweenImages,
    marginTop: spaceBetweenImages,
  },
  bottomRightImage: {
    marginLeft: spaceBetweenImages,
    marginTop: spaceBetweenImages,
  },
});
const rowStyles = {
  top: [
    styles.topLeftImage,
    styles.topCenterImage,
    styles.topRightImage,
  ],
  bottom: [
    styles.bottomLeftImage,
    styles.bottomCenterImage,
    styles.bottomRightImage,
  ],
  middle: [
    styles.leftImage,
    styles.centerImage,
    styles.rightImage,
  ],
};

export {
  MultimediaMessage,
  multimediaMessageContentHeight,
  multimediaMessageItemHeight,
};
<commit_msg>[native] Make horizontal/vertical margin in MultimediaMessage image grid match<commit_after>// @flow

import { chatMessageItemPropType } from 'lib/selectors/chat-selectors';
import type {
  MultimediaMessageInfo,
  LocalMessageInfo,
} from 'lib/types/message-types';
import type { Media } from 'lib/types/media-types';
import type { ImageStyle } from '../types/styles';
import type { ThreadInfo } from 'lib/types/thread-types';
import type { Navigate } from '../navigation/route-names';

import * as React from 'react';
import {
  Text,
  StyleSheet,
  TouchableWithoutFeedback,
  View,
} from 'react-native';
import PropTypes from 'prop-types';

import { messageKey } from 'lib/shared/message-utils';

import ComposedMessage from './composed-message.react';
import MultimediaMessageMultimedia from './multimedia-message-multimedia.react';

export type ChatMultimediaMessageInfoItem = {|
  itemType: "message",
  messageShapeType: "multimedia",
  messageInfo: MultimediaMessageInfo,
  localMessageInfo: ?LocalMessageInfo,
  threadInfo: ThreadInfo,
  startsConversation: bool,
  startsCluster: bool,
  endsCluster: bool,
  contentHeight: number,
|};

function multimediaMessageContentHeight(
  messageInfo: MultimediaMessageInfo,
  composedMessageMaxWidth: number,
) {
  let contentHeight;
  if (messageInfo.media.length === 1) {
    const [ media ] = messageInfo.media;
    const mediaDimensions = media.dimensions;
    if (composedMessageMaxWidth >= mediaDimensions.width) {
      contentHeight = mediaDimensions.height;
    } else {
      contentHeight = mediaDimensions.height *
        composedMessageMaxWidth / mediaDimensions.width;
    }
    if (contentHeight < 50) {
      contentHeight = 50;
    }
  } else if (messageInfo.media.length === 2) {
    contentHeight = composedMessageMaxWidth / 2;
  } else if (messageInfo.media.length === 3) {
    contentHeight = composedMessageMaxWidth / 3;
  } else if (messageInfo.media.length === 4) {
    contentHeight = composedMessageMaxWidth;
  } else {
    const numRows = Math.ceil(messageInfo.media.length / 3);
    contentHeight = numRows * composedMessageMaxWidth / 3;
  }
  return contentHeight;
}

function multimediaMessageItemHeight(
  item: ChatMultimediaMessageInfoItem,
  viewerID: ?string,
) {
  const { messageInfo, contentHeight, startsCluster, endsCluster } = item;
  const { id, creator } = messageInfo;
  const { isViewer } = creator;
  let height = 5 + contentHeight; // for margin and image
  if (!isViewer && startsCluster) {
    height += 25; // for username
  }
  if (endsCluster) {
    height += 7; // extra padding at the end of a cluster
  }
  if (
    isViewer &&
    id !== null && id !== undefined &&
    item.localMessageInfo &&
    item.localMessageInfo.sendFailed
  ) {
    height += 22; // extra padding for sendFailed
  }
  return height;
}

type Props = {|
  item: ChatMultimediaMessageInfoItem,
  navigate: Navigate,
  toggleFocus: (messageKey: string) => void,
|};
class MultimediaMessage extends React.PureComponent<Props> {

  static propTypes = {
    item: chatMessageItemPropType.isRequired,
    navigate: PropTypes.func.isRequired,
    toggleFocus: PropTypes.func.isRequired,
  };

  render() {
    const { messageInfo, contentHeight } = this.props.item;
    const { id, creator } = messageInfo;
    const { isViewer } = creator;
    const heightStyle = { height: contentHeight };

    const sendFailed =
      isViewer &&
      (id === null || id === undefined) &&
      this.props.item.localMessageInfo &&
      this.props.item.localMessageInfo.sendFailed;

    return (
      <ComposedMessage
        item={this.props.item}
        sendFailed={!!sendFailed}
        borderRadius={16}
        style={styles.row}
      >
        <TouchableWithoutFeedback onLongPress={this.onLongPress}>
          <View style={[heightStyle, styles.container]}>
            {this.renderContent()}
          </View>
        </TouchableWithoutFeedback>
      </ComposedMessage>
    );
  }

  renderContent(): React.Node {
    const { messageInfo } = this.props.item;
    if (messageInfo.media.length === 1) {
      return this.renderImage(messageInfo.media[0]);
    } else if (messageInfo.media.length === 2) {
      const [ one, two ] = messageInfo.media;
      return (
        <View style={styles.row}>
          {this.renderImage(one, styles.leftImage)}
          {this.renderImage(two, styles.rightImage)}
        </View>
      );
    } else if (messageInfo.media.length === 3) {
      const [ one, two, three ] = messageInfo.media;
      return (
        <View style={styles.row}>
          {this.renderImage(one, styles.leftImage)}
          {this.renderImage(two, styles.centerImage)}
          {this.renderImage(three, styles.rightImage)}
        </View>
      );
    } else if (messageInfo.media.length === 4) {
      const [ one, two, three, four ] = messageInfo.media;
      return (
        <View style={styles.grid}>
          <View style={styles.row}>
            {this.renderImage(one, styles.topLeftImage)}
            {this.renderImage(two, styles.topRightImage)}
          </View>
          <View style={styles.row}>
            {this.renderImage(three, styles.leftImage)}
            {this.renderImage(four, styles.rightImage)}
          </View>
        </View>
      );
    } else {
      const rows = [];
      for (let i = 0; i < messageInfo.media.length; i += 3) {
        let rowStyle;
        if (i === 0) {
          rowStyle = rowStyles.top;
        } else if (i + 3 >= messageInfo.media.length) {
          rowStyle = rowStyles.bottom;
        } else {
          rowStyle = rowStyles.middle;
        }

        const rowMedia = messageInfo.media.slice(i, i + 3);
        const row = [];
        let j = 0;
        for (; j < rowMedia.length; j++) {
          const media = rowMedia[j];
          const style = rowStyle[j];
          row.push(this.renderImage(media, style));
        }
        for (; j < 3; j++) {
          const key = `filler${j}`;
          row.push(<View style={styles.filler} key={key} />);
        }

        rows.push(
          <View style={styles.row} key={i}>
            {row}
          </View>
        );
      }
      return <View style={styles.grid}>{rows}</View>;
    }
  }

  renderImage(media: Media, style?: ImageStyle): React.Node {
    return (
      <MultimediaMessageMultimedia
        media={media}
        navigate={this.props.navigate}
        style={style}
        key={media.id}
      />
    );
  }

  onLongPress = () => {
    this.props.toggleFocus(messageKey(this.props.item.messageInfo));
  }

}

const horizontalSpaceBetweenImages = 2;
const verticalSpaceBetweenImages = 4;
const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'row',
    justifyContent: 'center',
  },
  grid: {
    flex: 1,
  },
  filler: {
    flex: 1,
  },
  row: {
    flex: 1,
    flexDirection: 'row',
  },
  leftImage: {
    marginRight: horizontalSpaceBetweenImages,
  },
  centerImage: {
    marginLeft: horizontalSpaceBetweenImages,
    marginRight: horizontalSpaceBetweenImages,
  },
  rightImage: {
    marginLeft: horizontalSpaceBetweenImages,
  },
  topLeftImage: {
    marginRight: horizontalSpaceBetweenImages,
    marginBottom: verticalSpaceBetweenImages,
  },
  topCenterImage: {
    marginLeft: horizontalSpaceBetweenImages,
    marginRight: horizontalSpaceBetweenImages,
    marginBottom: verticalSpaceBetweenImages,
  },
  topRightImage: {
    marginLeft: horizontalSpaceBetweenImages,
    marginBottom: verticalSpaceBetweenImages,
  },
  middleLeftImage: {
    marginRight: horizontalSpaceBetweenImages,
    marginBottom: verticalSpaceBetweenImages,
  },
  middleCenterImage: {
    marginRight: horizontalSpaceBetweenImages,
    marginLeft: horizontalSpaceBetweenImages,
    marginBottom: verticalSpaceBetweenImages,
  },
  middleRightImage: {
    marginLeft: horizontalSpaceBetweenImages,
    marginBottom: verticalSpaceBetweenImages,
  },
});
const rowStyles = {
  top: [
    styles.topLeftImage,
    styles.topCenterImage,
    styles.topRightImage,
  ],
  middle: [
    styles.middleLeftImage,
    styles.middleCenterImage,
    styles.middleRightImage,
  ],
  bottom: [
    styles.leftImage,
    styles.centerImage,
    styles.rightImage,
  ],
};

export {
  MultimediaMessage,
  multimediaMessageContentHeight,
  multimediaMessageItemHeight,
};
<|endoftext|>


<commit_before>/*jslint plusplus: true, continue: true*/
/*global hilary*/

hilary.register('gutentyp::components', { init: function (config, dom, componentPipeline) {
    "use strict";

    var components = [],
        componentFactory,
        makeForm,
        appendMarkup,
        appendValidators,
        makeValidateFunc,
        makeComponentForm,
        attachToBtn,
        attachToForm,
        attachToCancel,
        addComponent,
        events = {},
        selectionCoordinates;

    addComponent = function (component) {
        if (component instanceof Array) {
            var i;

            for (i in component) {
                if (component.hasOwnProperty(i)) {
                    addComponent(component[i]);
                }
            }
        } else {
            components.push(component);
        }
    };

    componentFactory = function (definition) {
        var self = {};

        self.title = definition.title;
        self.cssClass = definition.cssClass || definition.pipelineName;
        self.pipelineName = definition.pipelineName;
        self.icon = definition.icon || undefined;
        self.textClass = definition.textClass + ' ' + config.cssClasses.toolbarBtnText || config.cssClasses.toolbarBtnText;
        self.displayHandler = definition.displayHandler;
        self.group = definition.group || undefined;
        self.execute = function (event, formData) {
            var i,
                beforeThis = config.prefixes.pipeline.beforeComponent + definition.pipelineName,
                afterThis = config.prefixes.pipeline.afterComponent + definition.pipelineName,
                selected = dom.getSelectedText(),
                output,
                gutenArea;

            // if the event is a form button click, capture the selected text (if any) and
            // do not continue
            if (dom.getAttribute(dom.getClosest(event.target, 'button'), config.attributes.formBtn.key)) {
                selectionCoordinates = dom.getCursorCoordinates();
                selectionCoordinates.text = selected;
                selectionCoordinates.isInEditor = dom.selectionIsInEditor(selectionCoordinates);
                event.gutenSelection = selectionCoordinates;

                return;
            }

            for (i = 0; i < componentPipeline.beforeAny.length; i++) {
                if (dom.isFunction(componentPipeline.beforeAny[i])) {
                    componentPipeline.beforeAny[i](event, selected, formData);
                }
            }

            if (dom.isFunction(componentPipeline[beforeThis])) {
                componentPipeline[beforeThis](event, selected, formData);
            }

            if (dom.isFunction(definition.func)) {
                output = definition.func(event, selected || selectionCoordinates.text, formData);

                // replace the selected text
                if (selected && selected.length > 0 && output) {
                    dom.replaceSelectedText(output);
                } else if (selectionCoordinates && selectionCoordinates.isInEditor) {
                    dom.pasteHtml(selectionCoordinates, output);
                } else if (output) {
                    dom.pasteHtmlAtCursor(output, false, event);
                }
            }

            for (i = 0; i < componentPipeline.afterAny.length; i++) {
                if (dom.isFunction(componentPipeline.afterAny[i])) {
                    componentPipeline.afterAny[i](event, selected, formData);
                }
            }

            if (dom.isFunction(componentPipeline[afterThis])) {
                componentPipeline[afterThis](event, selected, formData);
            }
        };

        if (definition.before) {
            componentPipeline.registerPipelineEvent.registerBeforeComponentHandler(definition.pipelineName, definition.before);
        }

        if (definition.after) {
            componentPipeline.registerPipelineEvent.registerAfterComponentHandler(definition.pipelineName, definition.after);
        }

        if (definition.form && !self.displayHandler) {
            self.displayHandler = function () { return makeForm(self, definition.form); };
        }

        // we're done using these coordinates, get rid of them, so they aren't accidentally used
        // by following actions
        selectionCoordinates = undefined;

        return self;
    };

    attachToBtn = function (component) {
        dom.attachEvent({
            primarySelector: document,
            secondarySelector: '.' + component.cssClass,
            eventType: 'click',
            eventHandler: function (event) {
                var btn = dom.getClosest(event.target, 'button'),
                    target = dom.getNext(btn, config.selectors.toolbarGroup),
                    btnCoords = dom.getCoordinates(event.target, target),
                    style;

                // set the coordinates
                style = 'left: ' + btnCoords.moveLeft + 'px';
                style += '; top: ' + btnCoords.moveTop + 'px';
                dom.setStyle(target, style);

                // show or hid this toolbar
                dom.toggleClass(target, 'active');
            }
        });
    };

    attachToForm = function (component, validate) {
        dom.attachEvent({
            primarySelector: document,
            secondarySelector: '.' + component.cssClass + '-form .btn-success',
            eventType: 'click',
            eventHandler: function (event) {
                var formData = dom.formToJson(event.target),
                    target;

                if (dom.isFunction(validate)) {
                    if (!validate(event, formData)) {
                        return false;
                    }
                }

                target = dom.getClosest(event.target, config.selectors.toolbarGroup);
                // show or hide this toolbar
                dom.toggleClass(target, 'active');
                event.fromGutenForm = true;
                component.execute(event, formData);
            }
        });
    };

    attachToCancel = function (component) {
        dom.attachEvent({
            primarySelector: document,
            secondarySelector: '.' + component.cssClass + '-form .btn-cancel',
            eventType: 'click',
            eventHandler: function (event) {
                var target = dom.getClosest(event.target, config.selectors.toolbarGroup),
                    alerts = dom.getClosestAdjacent(event.target, '.alert');
                // show or hide this toolbar
                dom.toggleClass(target, 'active');
                dom.clearForm(target);
                dom.addClass(alerts, config.cssClasses.hidden);
            }
        });
    };

    makeForm = function (component, formMeta) {
        var fields = formMeta.fields,
            i = 0,
            markup = '',
            validators = { names: [] },
            validation = {},
            current,
            uniqueIds = {};

        for (i; i < fields.length; i++) {
            // do NOT combine uniqueId with the previous statement, it needs to be a new reference every time.
            uniqueIds[('a' + i)] = dom.getRandomString();
            markup += appendMarkup(fields[i], uniqueIds[('a' + i)]);
            appendValidators(validators, fields[i], uniqueIds[('a' + i)]);
        }

        if (validators.names.length > 0) {
            validation.validate = makeValidateFunc(validators);
        }

        return makeComponentForm(component, markup, validation);
    };

    appendMarkup = function (field, uniqueId) {
        var markup = '',
            attributes,
            alertCss;

        if (!field || !field.elementType || !field.name) {
            return '';
        }

        if (field.validation && field.validation.message) {
            //
Please enter a valid Url.

            alertCss = 'alert alert-warning ' + config.cssClasses.hidden + ' ' + uniqueId;

            if (field.validation.cssClass) {
                alertCss += ' ' + field.validation.cssClass;
            }

            markup += dom.makeElement('div', alertCss, undefined, field.validation.message, true);
        }

        if (field.label) {
            // Url
            markup += dom.makeElement('label', undefined, undefined, field.label, true);
        }

        // <input type="text" name="" />
        attributes = dom.isArray(field.attributes) ? field.attributes : [];
        attributes.push({ key: 'name', value: field.name });

        if (field.elementType === 'input') {
            attributes.push({ key: 'type', value: field.inputType || 'text' });
        }

        markup += dom.makeElement(field.elementType, field.cssClass || undefined, attributes, undefined, true);
        markup += '
';

        return markup;
    };

    appendValidators = function (validators, field, uniqueId) {
        if (field.name && field.validation && dom.isFunction(field.validation.validate)) {
            validators.names.push(field.name);
            validators[field.name] = {
                messageId: uniqueId,
                validate: field.validation.validate
            };
        }
    };

    makeValidateFunc = function (validators) {
        return function (event, formData) {
            var i, validator, alert, isValid = true;

            for (i = 0; i < validators.names.length; i++) {
                validator = validators[validators.names[i]];

                if (!validator || !dom.isFunction(validator.validate) || !validator.messageId) {
                    continue;
                }

                if (!validator.validate(event, formData)) {
                    isValid = false;
                    alert = dom.getClosestAdjacent(event.target, '.' + validator.messageId);
                    alert.removeClass(config.cssClasses.hidden);
                }
            }

            return isValid;
        };
    };

    makeComponentForm = function (component, formMarkup, validation) {
        if (!events[component.pipelineName]) {
            attachToBtn(component);
            attachToForm(component, validation && validation.validate);
            attachToCancel(component);
            events[component.pipelineName] = true;
        }

        return '<button type="button" class="' + component.cssClass + '" data-form-btn="true">'
                    + ''
                    + '' + component.title + ''
                + '</button>'
                + '
'
                    + '
'
                        + formMarkup
                        + '<button class="btn btn-success btn-sm" type="button">Add</button>'
                        + '<button class="btn btn-cancel btn-sm" type="button">Cancel</button>'
                    + '
'
                + '
';
    };

    return {
        /*
         * The default components supported by gutentyp
         */
        components: components,

        /*
         * Returns a component object that will leverage the component pipeline
         * @param title (string): the display name of the component
         * @param pipelineName (string): the name that pipeline handlers would be bound to, with respect to
         *     this component (i.e. a pipelineName of 'bold' will be registered as 'before::bold' and 'after::bold')
         * @param func (function): the callback / function that will be executed when this component is used
         */
        makeComponent: componentFactory,

        makeComponentForm: makeComponentForm,

        /*
        * Adds a component (the result of makeComponent) to the components collection
        */
        addComponent: addComponent
    };
}});
<commit_msg>uses cssClass instead of uniqueId for val msgs<commit_after>/*jslint plusplus: true, continue: true*/
/*global hilary*/

hilary.register('gutentyp::components', { init: function (config, dom, componentPipeline) {
    "use strict";

    var components = [],
        componentFactory,
        makeForm,
        appendMarkup,
        appendValidators,
        makeValidateFunc,
        makeComponentForm,
        attachToBtn,
        attachToForm,
        attachToCancel,
        addComponent,
        events = {},
        selectionCoordinates;

    addComponent = function (component) {
        if (component instanceof Array) {
            var i;

            for (i in component) {
                if (component.hasOwnProperty(i)) {
                    addComponent(component[i]);
                }
            }
        } else {
            components.push(component);
        }
    };

    componentFactory = function (definition) {
        var self = {};

        self.title = definition.title;
        self.cssClass = definition.cssClass || definition.pipelineName;
        self.pipelineName = definition.pipelineName;
        self.icon = definition.icon || undefined;
        self.textClass = definition.textClass + ' ' + config.cssClasses.toolbarBtnText || config.cssClasses.toolbarBtnText;
        self.displayHandler = definition.displayHandler;
        self.group = definition.group || undefined;
        self.execute = function (event, formData) {
            var i,
                beforeThis = config.prefixes.pipeline.beforeComponent + definition.pipelineName,
                afterThis = config.prefixes.pipeline.afterComponent + definition.pipelineName,
                selected = dom.getSelectedText(),
                output,
                gutenArea;

            // if the event is a form button click, capture the selected text (if any) and
            // do not continue
            if (dom.getAttribute(dom.getClosest(event.target, 'button'), config.attributes.formBtn.key)) {
                selectionCoordinates = dom.getCursorCoordinates();
                selectionCoordinates.text = selected;
                selectionCoordinates.isInEditor = dom.selectionIsInEditor(selectionCoordinates);
                event.gutenSelection = selectionCoordinates;

                return;
            }

            for (i = 0; i < componentPipeline.beforeAny.length; i++) {
                if (dom.isFunction(componentPipeline.beforeAny[i])) {
                    componentPipeline.beforeAny[i](event, selected, formData);
                }
            }

            if (dom.isFunction(componentPipeline[beforeThis])) {
                componentPipeline[beforeThis](event, selected, formData);
            }

            if (dom.isFunction(definition.func)) {
                output = definition.func(event, selected || selectionCoordinates.text, formData);

                // replace the selected text
                if (selected && selected.length > 0 && output) {
                    dom.replaceSelectedText(output);
                } else if (selectionCoordinates && selectionCoordinates.isInEditor) {
                    dom.pasteHtml(selectionCoordinates, output);
                } else if (output) {
                    dom.pasteHtmlAtCursor(output, false, event);
                }
            }

            for (i = 0; i < componentPipeline.afterAny.length; i++) {
                if (dom.isFunction(componentPipeline.afterAny[i])) {
                    componentPipeline.afterAny[i](event, selected, formData);
                }
            }

            if (dom.isFunction(componentPipeline[afterThis])) {
                componentPipeline[afterThis](event, selected, formData);
            }
        };

        if (definition.before) {
            componentPipeline.registerPipelineEvent.registerBeforeComponentHandler(definition.pipelineName, definition.before);
        }

        if (definition.after) {
            componentPipeline.registerPipelineEvent.registerAfterComponentHandler(definition.pipelineName, definition.after);
        }

        if (definition.form && !self.displayHandler) {
            self.displayHandler = function () { return makeForm(self, definition.form); };
        }

        // we're done using these coordinates, get rid of them, so they aren't accidentally used
        // by following actions
        selectionCoordinates = undefined;

        return self;
    };

    attachToBtn = function (component) {
        dom.attachEvent({
            primarySelector: document,
            secondarySelector: '.' + component.cssClass,
            eventType: 'click',
            eventHandler: function (event) {
                var btn = dom.getClosest(event.target, 'button'),
                    target = dom.getNext(btn, config.selectors.toolbarGroup),
                    btnCoords = dom.getCoordinates(event.target, target),
                    style;

                // set the coordinates
                style = 'left: ' + btnCoords.moveLeft + 'px';
                style += '; top: ' + btnCoords.moveTop + 'px';
                dom.setStyle(target, style);

                // show or hid this toolbar
                dom.toggleClass(target, 'active');
            }
        });
    };

    attachToForm = function (component, validate) {
        dom.attachEvent({
            primarySelector: document,
            secondarySelector: '.' + component.cssClass + '-form .btn-success',
            eventType: 'click',
            eventHandler: function (event) {
                var formData = dom.formToJson(event.target),
                    target;

                if (dom.isFunction(validate)) {
                    if (!validate(event, formData)) {
                        return false;
                    }
                }

                target = dom.getClosest(event.target, config.selectors.toolbarGroup);
                // show or hide this toolbar
                dom.toggleClass(target, 'active');
                event.fromGutenForm = true;
                component.execute(event, formData);
            }
        });
    };

    attachToCancel = function (component) {
        dom.attachEvent({
            primarySelector: document,
            secondarySelector: '.' + component.cssClass + '-form .btn-cancel',
            eventType: 'click',
            eventHandler: function (event) {
                var target = dom.getClosest(event.target, config.selectors.toolbarGroup),
                    alerts = dom.getClosestAdjacent(event.target, '.alert');
                // show or hide this toolbar
                dom.toggleClass(target, 'active');
                dom.clearForm(target);
                dom.addClass(alerts, config.cssClasses.hidden);
            }
        });
    };

    makeForm = function (component, formMeta) {
        var fields = formMeta.fields,
            i = 0,
            markup = '',
            validators = { names: [] },
            validation = {},
            current,
            uniqueIds = {};

        for (i; i < fields.length; i++) {
            // do NOT combine uniqueId with the previous statement, it needs to be a new reference every time.
            uniqueIds[('a' + i)] = dom.getRandomString();
            markup += appendMarkup(fields[i], uniqueIds[('a' + i)]);
            appendValidators(validators, fields[i], uniqueIds[('a' + i)]);
        }

        if (validators.names.length > 0) {
            validation.validate = makeValidateFunc(validators);
        }

        return makeComponentForm(component, markup, validation);
    };

    appendMarkup = function (field, uniqueId) {
        var markup = '',
            attributes,
            alertCss;

        if (!field || !field.elementType || !field.name) {
            return '';
        }

        if (field.validation && field.validation.message) {
            //
Please enter a valid Url.

            alertCss = 'alert alert-warning ' + config.cssClasses.hidden + ' ' + uniqueId;

            if (field.validation.cssClass) {
                alertCss += ' ' + field.validation.cssClass;
            }

            markup += dom.makeElement('div', alertCss, undefined, field.validation.message, true);
        }

        if (field.label) {
            // Url
            markup += dom.makeElement('label', undefined, undefined, field.label, true);
        }

        // <input type="text" name="" />
        attributes = dom.isArray(field.attributes) ? field.attributes : [];
        attributes.push({ key: 'name', value: field.name });

        if (field.elementType === 'input') {
            attributes.push({ key: 'type', value: field.inputType || 'text' });
        }

        markup += dom.makeElement(field.elementType, field.cssClass || undefined, attributes, undefined, true);
        markup += '
';

        return markup;
    };

    appendValidators = function (validators, field, uniqueId) {
        if (field.name && field.validation && dom.isFunction(field.validation.validate)) {
            validators.names.push(field.name);
            validators[field.name] = field.validation;
            validators[field.name].messageId = uniqueId;
        }
    };

    makeValidateFunc = function (validators) {
        return function (event, formData) {
            var i, validator, alert, isValid = true;

            for (i = 0; i < validators.names.length; i++) {
                validator = validators[validators.names[i]];

                if (!validator || !dom.isFunction(validator.validate) || !validator.messageId) {
                    continue;
                }

                if (!validator.validate(event, formData)) {
                    isValid = false;
                    alert = dom.getClosestAdjacent(event.target, '.' + validator.cssClass);
                    alert.removeClass(config.cssClasses.hidden);
                }
            }

            return isValid;
        };
    };

    makeComponentForm = function (component, formMarkup, validation) {
        if (!events[component.pipelineName]) {
            attachToBtn(component);
            attachToForm(component, validation && validation.validate);
            attachToCancel(component);
            events[component.pipelineName] = true;
        }

        return '<button type="button" class="' + component.cssClass + '" data-form-btn="true">'
                    + ''
                    + '' + component.title + ''
                + '</button>'
                + '
'
                    + '
'
                        + formMarkup
                        + '<button class="btn btn-success btn-sm" type="button">Add</button>'
                        + '<button class="btn btn-cancel btn-sm" type="button">Cancel</button>'
                    + '
'
                + '
';
    };

    return {
        /*
         * The default components supported by gutentyp
         */
        components: components,

        /*
         * Returns a component object that will leverage the component pipeline
         * @param title (string): the display name of the component
         * @param pipelineName (string): the name that pipeline handlers would be bound to, with respect to
         *     this component (i.e. a pipelineName of 'bold' will be registered as 'before::bold' and 'after::bold')
         * @param func (function): the callback / function that will be executed when this component is used
         */
        makeComponent: componentFactory,

        makeComponentForm: makeComponentForm,

        /*
        * Adds a component (the result of makeComponent) to the components collection
        */
        addComponent: addComponent
    };
}});
<|endoftext|>


<commit_before>popup.event = {

  bindAll: function() {
    this.bindSpecialNews();
    this.bindTipsLinks();
    this.bindHeaderButtonsAndLogo();
    this.bindRealtimeBus();
    this.bindOracle();
    this.bindKonami();
  },

  //
  // Event handler: Special news
  //

  bindSpecialNews: function() {
    $('div#specialNews a').click(function() {
      Analytics.trackEvent('clickSpecialNews', $('#specialNews').text().trim());
      Browser.openTab($('#specialNews a').attr('href'));
      window.close();
    });
    $('div#specialNews img').click(function() {
      Analytics.trackEvent('closeSpecialNews', $('#specialNews').text().trim());
      localStorage.closedSpecialNews = $('#specialNews a').attr('href');
      $('#specialNews').slideUp();
    });
  },

  //
  // Event handler: Links in div#tips (in the )
  //

  bindTipsLinks: function() {
    $('#tips a').click(function() {
      var link = $(this).attr('href');
      Browser.openTab(link);
      Analytics.trackEvent('clickTipsLink', link);
      window.close();
    });
  },

  //
  // Event handler: Header buttons and logo
  //

  bindHeaderButtonsAndLogo: function() {

    // Bind buttons to button hover text

    var toggleButtonText = function(buttons) {
      for (var selector in buttons) {
        var textToShow = buttons[selector];
        $(selector).attr('data', textToShow);
        $(selector).hover(function() {
          var textToShowInner = $(this).attr('data');
          $('#buttontext').html(textToShowInner);
          $('#buttontext').toggle();
        });
      };
    };
    var buttons = {
      '#editButton': 'Endre innstillinger',
      '#tipsButton': 'Om appKom, changelog, ++',
      '#colorButton': 'Bytt fargepalett',
    }
    var slack = Affiliation.org[ls.affiliationKey1].slack;
    if (slack) {
      slack = 'Join us at ' + slack.match(/https?:\/\/(.*?)\//)[1];
      buttons['#chatterButton'] = slack;
    }
    toggleButtonText(buttons);

    // Button and logo clicks

    $('#editButton').click(function() {
      var toggled = ('img/popup-edit-done.png' === $(this).attr('src'));
      if (!toggled) {
        // Switch image and fade out other buttons
        $(this).attr('src', 'img/popup-edit-done.png').addClass('glow');
        $("img.popupbutton").not(this).each(function(index, value) {
          $(this).fadeOut();
        });
        // Show Done?-question in buttontext, leave it there until done
        $('#editButton').unbind('mouseenter mouseleave');
        $('#buttontext').text('Trykk på knappen når du er ferdig');
        // Slide in all options
        $("div.options").slideDown();
        $("div.content").slideUp();
        $("img#logo").css('opacity', '0.1');
        $("img#atbLogo").css('opacity', '0.1');
        $("div#oracle").slideUp();
        setTimeout(function() {
          $("div#bigOptions").fadeIn();
        }, 600);
        // Analytics
        Analytics.trackEvent('clickEdit');
      }
      else {
        // Switch image and fade in other buttons
        $(this).attr('src', 'img/popup-edit.png').removeClass('glow');
        $("img.popupbutton").not(this).each(function(index, value) {
          $(this).fadeIn();
        });
        // Switch back to regular hover texts for buttons
        $('#buttontext').html('Sweet! <3');
        toggleButtonText({'#editButton': 'Endre innstillinger'});
        // Slide away all options
        $("div.options").slideUp();
        $("div.content").slideDown();
        $("img#logo").css('opacity', '');
        $("img#atbLogo").css('opacity', '');
        $("div#oracle").slideDown();
        setTimeout(function() {
          $("div#bigOptions").fadeOut();
        }, 600);
        // Analytics
        Analytics.trackEvent('clickEditDone');
      }
    });

    $('#chatterButton').click(function() {
      var slack = Affiliation.org[ls.affiliationKey1].slack;
      Browser.openTab(slack);
      Analytics.trackEvent('clickChatter', ls.affiliationKey1);
      window.close();
    });

    $('#tipsButton').click(function() {
      $('div#tips').toggle();
      $('body').css('overflow-y', 'hidden');
      Analytics.trackEvent('clickTips');
    });
    $('div#tips:not(a)').click(function() {
      $(this).toggle();
      $('body').css('overflow-y', '');
    });

    $('#colorButton').click(function() {
      // Get all palettes
      var colors = Object.keys(Palettes.palettes);
      // And any special affiliation palette as well
      var affPalette = Affiliation.org[ls.affiliationKey1].palette;
      if (colors.indexOf(affPalette) === -1) {
        colors.push(affPalette);
      }
      // Find current palette
      var currentPalette = $('#palette').attr('href').match(/\w+\/\w+\/(\w+)\.\w+/)[1];
      var index = colors.indexOf(currentPalette);
      // Goto next palette
      index++;
      if (!colors[index])
        index = 0;
      ls.affiliationPalette = colors[index];
      Palettes.load();
      // Text feedback, fades out after timeout
      $('#buttontext').text('Fargepalett satt til "' + colors[index].capitalize() + '"');
      // And track it
      Analytics.trackEvent('clickColor');
    });

    $('#logo').click(function() {
      var name = Affiliation.org[ls.affiliationKey1].name;
      Analytics.trackEvent('clickLogo', name);
      var web = Affiliation.org[ls.affiliationKey1].web;
      Browser.openTab(web);
      window.close();
    });
  },

  //
  // Event handler: Realtime bus
  //

  bindRealtimeBus: function() {

    // Bind realtimebus lines to their timetables
    var timetables = JSON.parse(localStorage.busTimetables);
    var clickBus = function() {
      try {
        var line = $(this).find('.line').text().trim().split(' ')[0];
        var link = timetables[line];
        Browser.openTab(link);
        Analytics.trackEvent('clickTimetable');
        window.close();
      }
      catch (e) {
        console.error('Failed at clickBus', e);
      }
    }

    // Register click event for all lines
    var busLanes = ['.first', '.second', '.third'];
    for (var i in busLanes) {
      $('#bus #firstBus '+busLanes[i]).click(clickBus);
      $('#bus #secondBus '+busLanes[i]).click(clickBus);
    }

    // Bind AtB logo and any realtimebus error messages to atb.no/…
    var openAtb = function() {
      Browser.openTab('http://www.atb.no');
      Analytics.trackEvent('clickAtb');
      window.close();
    };
    $('#bus #atbLogo').click(openAtb);
    var openOracle = function() {
      Browser.openTab('http://www.atb.no/bussorakelet');
      Analytics.trackEvent('clickAtbError');
      window.close();
    };
    $('#bus .error').click(openOracle);
  },

  //
  // Event handler: Oracle
  //

  bindOracle: function() {

    var changeOracleAnswer = function(answer) {
      console.log('changeOracleAnswer to "' + answer + '"');
      // Stop previous changeOracleAnswer instance, if any
      clearTimeout(Number(ls.animateOracleAnswerTimeoutId));
      // If answer contains HTML, just insert it as HTML
      if (answer.match(/<\/?\w+>/g) !== null) {
        if ($('#oracle #answer .piece').size() === 0) {
          $('#oracle #answer').append('
' + answer + '
');
          $('#oracle #answer .piece a').attr('target', '_blank');
        }
        else {
          // Remove all preexisting pieces
          $('#oracle #answer .piece').fadeOut(400, function() {
            $('#oracle #answer .piece').remove();
            $('#oracle #answer').append('
' + answer + '
');
            $('#oracle #answer .piece a').attr('target', '_blank');
          });
        }
      }
      // Animate oracle answer name change
      else {
        var func = function(answer) {
          var pieces = answer.split('@');
          // Insert piece placeholders
          for (var i in pieces) {
            $('#oracle #answer').append('

');
          }
          for (var i in pieces) {
            animateOracleAnswer(pieces[i], i);
          }
        }
        // Check for preexisting pieces
        if ($('#oracle #answer .piece').size() === 0) {
          func(answer);
        }
        else {
          // Remove all preexisting pieces
          $('#oracle #answer .piece').fadeOut(400, function() {
            $('#oracle #answer .piece').remove();
            func(answer);
          });
        }
      }
    }

    var animateOracleAnswer = function(line, index, callback, build) {
      // Animate it
      var text = $('#oracle #answer .piece').eq(index).text();
      if (text.length === 0) {
        build = true;
      }
      var millisecs = 6;
      if (!build) {
        $('#oracle #answer .piece').eq(index).text(text.slice(0, text.length-1));
        ls.animateOracleAnswerTimeoutId = setTimeout(function() {
          animateOracleAnswer(line, index, callback);
        }, millisecs);
      }
      else {
        if (text.length !== line.length) {
          if (text.length === 0) {
            $('#oracle #answer .piece').eq(index).text(line.slice(0, 1));
          }
          else {
            $('#oracle #answer .piece').eq(index).text(line.slice(0, text.length+1));
          }
          ls.animateOracleAnswerTimeoutId = setTimeout(function() {
            animateOracleAnswer(line, index, callback, true);
          }, millisecs);
        }
        else {
          // Animation for this element is complete
          if (typeof callback != 'undefined') {
            callback(index);
          }
        }
      }
    }

    var oraclePrediction = function() {
      var question = Oracle.predict();
      if (question !== null) {
        // Add question
        changeOracleQuestion(question);
        // Add answer
        Oracle.ask(question, function(answer) {
          changeOracleAnswer(answer);
          $('#oracle #question').focus();
        });
      }
      else {
        // Tell the user to use the oracle more before using predictions
        $('#oracle #question').focus();
        // The timeout is...well...the timeout is a hack.
        setTimeout( function() {
          changeOracleAnswer(Oracle.msgAboutPredict);
        }, 200);
      }
    }

    var changeOracleQuestion = function(question) {
      // Stop previous changeOracleAnswer instance, if any
      clearTimeout(Number(ls.animateOracleQuestionTimeoutId));
      // Animate oracle question name change
      animateOracleQuestion(question);
    }

    var animateOracleQuestion = function(line) {
      // Animate it
      var text = $('#oracle #question').val();
      var random = Math.floor(100 * Math.random() + 10);
      if (text.length !== line.length) {
        if (text.length === 0) {
          $('#oracle #question').val(line.slice(0, 1));
        }
        else {
          $('#oracle #question').val(line.slice(0, text.length+1));
        }
        ls.animateOracleQuestionTimeoutId = setTimeout(function() {
          animateOracleQuestion(line);
        }, random);
      }
    }

    // Suggest prediction
    if (Oracle.predict() !== null) {
      $('#oracle #question').attr('placeholder', Oracle.predict() + Oracle.msgPredictPostfix);
      Analytics.trackEvent('oracleSuggest');
    }
    // User input
    $('#oracle').on('keyup', '#question', function(e) {
      var question = $('#oracle #question').val()
      // Clicked enter
      if (e.which === 13) {
        if (question !== '') {
          Oracle.ask(question, function(answer) {
            changeOracleAnswer(answer);
            Analytics.trackEvent('oracleAnswer');
            // Update suggested prediction
            if (Oracle.predict() !== null) {
              $('#oracle #question').attr('placeholder', Oracle.predict() + Oracle.msgPredictPostfix);
            }
          });
        }
        else {
          changeOracleAnswer(Oracle.greet());
          Analytics.trackEvent('oracleGreet');
        }
      }
      // Cleared field
      else if (question === '' && e.which !== 9) { // Tab is reserved
        changeOracleAnswer('');
        Analytics.trackEvent('oracleClear');
      }
    });
    // Keydown works better with tab
    $('#oracle').on('keydown', '#question', function(e) {
      // Clicked tab: Predict question
      if (e.which === 9) {
        e.preventDefault();
        // Prevent spam
        if (window.lastClickedTabInOracle && (Date.now() - window.lastClickedTabInOracle) < 1500) {
          console.warn('Do not hold down tab, the Oracle is very old and does not enjoy spam.');
        }
        else {
          oraclePrediction();
          Analytics.trackEvent('oraclePrediction');
        }
        window.lastClickedTabInOracle = Date.now();
      }
    });

  },

  //
  // Event handler: Konami code
  //

  bindKonami: function() {
    // React to Konami code
    $(document).konami({
      code: ['up', 'up', 'down', 'down', 'left', 'right', 'left', 'right', 'b', 'a'],
      callback: function() {
        Analytics.trackEvent('toggleKonami');

        // Remove #bus background color and any #background filters
        $('#bus').attr('style', 'background-color:transparent; box-shadow:none;');
        $('#background').attr('style', '-webkit-filter: none');

        // A list of the vids available. Add more vids here if you want more
        var links = ['eh7lp9umG2I', 'qyXTgqJtoGM', 'z9Uz1icjwrM', 'sTSA_sWGM44', 'asnZjyWDFlQ', 'NL6CDFn2i3I', 'f4l_MxTMq-4'];

        // Gets a random number, builds the html and displays the vid
        var konamiNum = Math.floor(Math.random() * links.length);
        var htmlVid = ''
        $("#background").html(htmlVid);
      },
    });
  },

};
<commit_msg>popup: refining some animations<commit_after>popup.event = {

  bindAll: function() {
    this.bindSpecialNews();
    this.bindTipsLinks();
    this.bindHeaderButtonsAndLogo();
    this.bindRealtimeBus();
    this.bindOracle();
    this.bindKonami();
  },

  //
  // Event handler: Special news
  //

  bindSpecialNews: function() {
    $('div#specialNews a').click(function() {
      Analytics.trackEvent('clickSpecialNews', $('#specialNews').text().trim());
      Browser.openTab($('#specialNews a').attr('href'));
      window.close();
    });
    $('div#specialNews img').click(function() {
      Analytics.trackEvent('closeSpecialNews', $('#specialNews').text().trim());
      localStorage.closedSpecialNews = $('#specialNews a').attr('href');
      $('#specialNews').slideUp();
    });
  },

  //
  // Event handler: Links in div#tips (in the )
  //

  bindTipsLinks: function() {
    $('#tips a').click(function() {
      var link = $(this).attr('href');
      Browser.openTab(link);
      Analytics.trackEvent('clickTipsLink', link);
      window.close();
    });
  },

  //
  // Event handler: Header buttons and logo
  //

  bindHeaderButtonsAndLogo: function() {

    // Bind buttons to button hover text

    var toggleButtonText = function(buttons) {
      for (var selector in buttons) {
        var textToShow = buttons[selector];
        $(selector).attr('data', textToShow);
        $(selector).hover(function() {
          var textToShowInner = $(this).attr('data');
          $('#buttontext').html(textToShowInner);
          $('#buttontext').toggle();
        });
      };
    };
    var buttons = {
      '#editButton': 'Endre innstillinger',
      '#tipsButton': 'Om appKom, changelog, ++',
      '#colorButton': 'Bytt fargepalett',
    }
    var slack = Affiliation.org[ls.affiliationKey1].slack;
    if (slack) {
      slack = 'Join us at ' + slack.match(/https?:\/\/(.*?)\//)[1];
      buttons['#chatterButton'] = slack;
    }
    toggleButtonText(buttons);

    // Button and logo clicks

    $('#editButton').click(function() {
      var toggled = ('img/popup-edit-done.png' === $(this).attr('src'));
      if (!toggled) {
        // Switch image and change other buttons
        $(this).attr('src', 'img/popup-edit-done.png').addClass('glow');
        $("img.popupbutton").not(this).each(function(index, value) {
          $(this).fadeOut();
        });
        setTimeout(function() {
          $("div#bigOptions").fadeIn();
        }, 600);
        // Show Done?-question in buttontext, leave it there until done
        $('#editButton').unbind('mouseenter mouseleave');
        $('#buttontext').text('Trykk på knappen når du er ferdig');
        // Slide in all options
        $("div.options").slideDown();
        $("div.content").slideUp();
        $("img#logo").animate({'opacity': '0.1'});
        $("img#atbLogo").animate({'opacity': '0.1'});
        $("div#oracle").slideUp();
        // Analytics
        Analytics.trackEvent('clickEdit');
      }
      else {
        // Switch image and change other buttons
        $(this).attr('src', 'img/popup-edit.png').removeClass('glow');
        $("div#bigOptions").fadeOut(function() {
          $("img.popupbutton").not(this).each(function(index, value) {
            $(this).fadeIn();
          });
        });
        // Switch back to regular hover texts for buttons
        $('#buttontext').html('Sweet! <3');
        toggleButtonText({'#editButton': 'Endre innstillinger'});
        // Slide away all options
        $("div.options").slideUp();
        $("div.content").slideDown();
        $("img#logo").animate({'opacity': '1.0'});
        $("img#atbLogo").animate({'opacity': '1.0'});
        $("div#oracle").slideDown();
        // Analytics
        Analytics.trackEvent('clickEditDone');
      }
    });

    $('#chatterButton').click(function() {
      var slack = Affiliation.org[ls.affiliationKey1].slack;
      Browser.openTab(slack);
      Analytics.trackEvent('clickChatter', ls.affiliationKey1);
      window.close();
    });

    $('#tipsButton').click(function() {
      $('div#tips').toggle();
      $('body').css('overflow-y', 'hidden');
      Analytics.trackEvent('clickTips');
    });
    $('div#tips:not(a)').click(function() {
      $(this).toggle();
      $('body').css('overflow-y', '');
    });

    $('#colorButton').click(function() {
      // Get all palettes
      var colors = Object.keys(Palettes.palettes);
      // And any special affiliation palette as well
      var affPalette = Affiliation.org[ls.affiliationKey1].palette;
      if (colors.indexOf(affPalette) === -1) {
        colors.push(affPalette);
      }
      // Find current palette
      var currentPalette = $('#palette').attr('href').match(/\w+\/\w+\/(\w+)\.\w+/)[1];
      var index = colors.indexOf(currentPalette);
      // Goto next palette
      index++;
      if (!colors[index])
        index = 0;
      ls.affiliationPalette = colors[index];
      Palettes.load();
      // Text feedback, fades out after timeout
      $('#buttontext').text('Fargepalett satt til "' + colors[index].capitalize() + '"');
      // And track it
      Analytics.trackEvent('clickColor');
    });

    $('#logo').click(function() {
      var name = Affiliation.org[ls.affiliationKey1].name;
      Analytics.trackEvent('clickLogo', name);
      var web = Affiliation.org[ls.affiliationKey1].web;
      Browser.openTab(web);
      window.close();
    });
  },

  //
  // Event handler: Realtime bus
  //

  bindRealtimeBus: function() {

    // Bind realtimebus lines to their timetables
    var timetables = JSON.parse(localStorage.busTimetables);
    var clickBus = function() {
      try {
        var line = $(this).find('.line').text().trim().split(' ')[0];
        var link = timetables[line];
        Browser.openTab(link);
        Analytics.trackEvent('clickTimetable');
        window.close();
      }
      catch (e) {
        console.error('Failed at clickBus', e);
      }
    }

    // Register click event for all lines
    var busLanes = ['.first', '.second', '.third'];
    for (var i in busLanes) {
      $('#bus #firstBus '+busLanes[i]).click(clickBus);
      $('#bus #secondBus '+busLanes[i]).click(clickBus);
    }

    // Bind AtB logo and any realtimebus error messages to atb.no/…
    var openAtb = function() {
      Browser.openTab('http://www.atb.no');
      Analytics.trackEvent('clickAtb');
      window.close();
    };
    $('#bus #atbLogo').click(openAtb);
    var openOracle = function() {
      Browser.openTab('http://www.atb.no/bussorakelet');
      Analytics.trackEvent('clickAtbError');
      window.close();
    };
    $('#bus .error').click(openOracle);
  },

  //
  // Event handler: Oracle
  //

  bindOracle: function() {

    var changeOracleAnswer = function(answer) {
      console.log('changeOracleAnswer to "' + answer + '"');
      // Stop previous changeOracleAnswer instance, if any
      clearTimeout(Number(ls.animateOracleAnswerTimeoutId));
      // If answer contains HTML, just insert it as HTML
      if (answer.match(/<\/?\w+>/g) !== null) {
        if ($('#oracle #answer .piece').size() === 0) {
          $('#oracle #answer').append('
' + answer + '
');
          $('#oracle #answer .piece a').attr('target', '_blank');
        }
        else {
          // Remove all preexisting pieces
          $('#oracle #answer .piece').fadeOut(400, function() {
            $('#oracle #answer .piece').remove();
            $('#oracle #answer').append('
' + answer + '
');
            $('#oracle #answer .piece a').attr('target', '_blank');
          });
        }
      }
      // Animate oracle answer name change
      else {
        var func = function(answer) {
          var pieces = answer.split('@');
          // Insert piece placeholders
          for (var i in pieces) {
            $('#oracle #answer').append('

');
          }
          for (var i in pieces) {
            animateOracleAnswer(pieces[i], i);
          }
        }
        // Check for preexisting pieces
        if ($('#oracle #answer .piece').size() === 0) {
          func(answer);
        }
        else {
          // Remove all preexisting pieces
          $('#oracle #answer .piece').fadeOut(400, function() {
            $('#oracle #answer .piece').remove();
            func(answer);
          });
        }
      }
    }

    var animateOracleAnswer = function(line, index, callback, build) {
      // Animate it
      var text = $('#oracle #answer .piece').eq(index).text();
      if (text.length === 0) {
        build = true;
      }
      var millisecs = 6;
      if (!build) {
        $('#oracle #answer .piece').eq(index).text(text.slice(0, text.length-1));
        ls.animateOracleAnswerTimeoutId = setTimeout(function() {
          animateOracleAnswer(line, index, callback);
        }, millisecs);
      }
      else {
        if (text.length !== line.length) {
          if (text.length === 0) {
            $('#oracle #answer .piece').eq(index).text(line.slice(0, 1));
          }
          else {
            $('#oracle #answer .piece').eq(index).text(line.slice(0, text.length+1));
          }
          ls.animateOracleAnswerTimeoutId = setTimeout(function() {
            animateOracleAnswer(line, index, callback, true);
          }, millisecs);
        }
        else {
          // Animation for this element is complete
          if (typeof callback != 'undefined') {
            callback(index);
          }
        }
      }
    }

    var oraclePrediction = function() {
      var question = Oracle.predict();
      if (question !== null) {
        // Add question
        changeOracleQuestion(question);
        // Add answer
        Oracle.ask(question, function(answer) {
          changeOracleAnswer(answer);
          $('#oracle #question').focus();
        });
      }
      else {
        // Tell the user to use the oracle more before using predictions
        $('#oracle #question').focus();
        // The timeout is...well...the timeout is a hack.
        setTimeout( function() {
          changeOracleAnswer(Oracle.msgAboutPredict);
        }, 200);
      }
    }

    var changeOracleQuestion = function(question) {
      // Stop previous changeOracleAnswer instance, if any
      clearTimeout(Number(ls.animateOracleQuestionTimeoutId));
      // Animate oracle question name change
      animateOracleQuestion(question);
    }

    var animateOracleQuestion = function(line) {
      // Animate it
      var text = $('#oracle #question').val();
      var random = Math.floor(100 * Math.random() + 10);
      if (text.length !== line.length) {
        if (text.length === 0) {
          $('#oracle #question').val(line.slice(0, 1));
        }
        else {
          $('#oracle #question').val(line.slice(0, text.length+1));
        }
        ls.animateOracleQuestionTimeoutId = setTimeout(function() {
          animateOracleQuestion(line);
        }, random);
      }
    }

    // Suggest prediction
    if (Oracle.predict() !== null) {
      $('#oracle #question').attr('placeholder', Oracle.predict() + Oracle.msgPredictPostfix);
      Analytics.trackEvent('oracleSuggest');
    }
    // User input
    $('#oracle').on('keyup', '#question', function(e) {
      var question = $('#oracle #question').val()
      // Clicked enter
      if (e.which === 13) {
        if (question !== '') {
          Oracle.ask(question, function(answer) {
            changeOracleAnswer(answer);
            Analytics.trackEvent('oracleAnswer');
            // Update suggested prediction
            if (Oracle.predict() !== null) {
              $('#oracle #question').attr('placeholder', Oracle.predict() + Oracle.msgPredictPostfix);
            }
          });
        }
        else {
          changeOracleAnswer(Oracle.greet());
          Analytics.trackEvent('oracleGreet');
        }
      }
      // Cleared field
      else if (question === '' && e.which !== 9) { // Tab is reserved
        changeOracleAnswer('');
        Analytics.trackEvent('oracleClear');
      }
    });
    // Keydown works better with tab
    $('#oracle').on('keydown', '#question', function(e) {
      // Clicked tab: Predict question
      if (e.which === 9) {
        e.preventDefault();
        // Prevent spam
        if (window.lastClickedTabInOracle && (Date.now() - window.lastClickedTabInOracle) < 1500) {
          console.warn('Do not hold down tab, the Oracle is very old and does not enjoy spam.');
        }
        else {
          oraclePrediction();
          Analytics.trackEvent('oraclePrediction');
        }
        window.lastClickedTabInOracle = Date.now();
      }
    });

  },

  //
  // Event handler: Konami code
  //

  bindKonami: function() {
    // React to Konami code
    $(document).konami({
      code: ['up', 'up', 'down', 'down', 'left', 'right', 'left', 'right', 'b', 'a'],
      callback: function() {
        Analytics.trackEvent('toggleKonami');

        // Remove #bus background color and any #background filters
        $('#bus').attr('style', 'background-color:transparent; box-shadow:none;');
        $('#background').attr('style', '-webkit-filter: none');

        // A list of the vids available. Add more vids here if you want more
        var links = ['eh7lp9umG2I', 'qyXTgqJtoGM', 'z9Uz1icjwrM', 'sTSA_sWGM44', 'asnZjyWDFlQ', 'NL6CDFn2i3I', 'f4l_MxTMq-4'];

        // Gets a random number, builds the html and displays the vid
        var konamiNum = Math.floor(Math.random() * links.length);
        var htmlVid = ''
        $("#background").html(htmlVid);
      },
    });
  },

};
<|endoftext|>


<commit_before>String.prototype.toProperCase = function () {
  return this.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
};

var bgPage;
var mappings = [
  ['givenName', 'first-name'],
  ['familyName', 'last-name'],
  ['birthDate', 'birth-date'],
  ['birthPlace', 'birth-place'],
  ['deathDate', 'death-date'],
  ['deathPlace', 'death-place'],
  ['spouseGivenName', 'spouse-first-name'],
  ['spouseFamilyName', 'spouse-last-name'],
  ['marriageDate', 'marriage-date'],
  ['marriagePlace', 'marriage-place'],
  ['fatherGivenName', 'father-first-name'],
  ['fatherFamilyName', 'father-last-name'],
  ['motherGivenName', 'mother-first-name'],
  ['motherFamilyName', 'mother-last-name']
];

$(document).ready(function(){

  // Create input fields
  $.each(mappings, function(i, vals){
    var fieldWrap = $('
').addClass('field-wrap span2')
      .append( $('
').addClass('field-label').html(vals[1].replace(/\-/g,' ').toProperCase()) )
      .append( $('<input>').addClass('span2').attr({'id': vals[1], 'type': 'text'}) )
      .appendTo('#form');
  });

  // Get the id of the tab
  chrome.tabs.query({active: true}, function(tabs){

    bgPage = chrome.extension.getBackgroundPage();

    var personData = bgPage.personDataObjects[tabs[0].id];

    fillForm(personData);

    $('#update-button').attr('disabled', false).click(function(){
      updateLinks();
    });

    updateLinks();

  });

});

function fillForm(personData) {
  $.each(mappings, function(i, vals){
    $('#'+vals[1]).val(personData[vals[0]]);
  });
}

function updateLinks() {

  // Delete previous links and show ajax loader for a moment
  $('#search-links .btn').addClass('disabled');

  setTimeout(buildLinks, 300);

}

function buildLinks() {

  var personData = {};
  $.each(mappings, function(i, vals){
    personData[vals[0]] = $('#'+vals[1]).val();
  });

  var searchLinks = bgPage.rs.executeLinkBuilders(personData);

  // Remove ajax loader and add search links
  $('#search-links').html('');
  $.each(searchLinks, function(i, link){
      $('').addClass('btn btn-info').html(link.text).attr('href', link.url).appendTo('#search-links');
  });

}<commit_msg>Add event tracking<commit_after>String.prototype.toProperCase = function () {
  return this.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
};

var bgPage;
var mappings = [
  ['givenName', 'first-name'],
  ['familyName', 'last-name'],
  ['birthDate', 'birth-date'],
  ['birthPlace', 'birth-place'],
  ['deathDate', 'death-date'],
  ['deathPlace', 'death-place'],
  ['spouseGivenName', 'spouse-first-name'],
  ['spouseFamilyName', 'spouse-last-name'],
  ['marriageDate', 'marriage-date'],
  ['marriagePlace', 'marriage-place'],
  ['fatherGivenName', 'father-first-name'],
  ['fatherFamilyName', 'father-last-name'],
  ['motherGivenName', 'mother-first-name'],
  ['motherFamilyName', 'mother-last-name']
];

$(document).ready(function(){

  // Create input fields
  $.each(mappings, function(i, vals){
    var properLabel = vals[1].replace(/\-/g,' ').toProperCase();
    var fieldWrap = $('
').addClass('field-wrap span2')
      .append( $('
').addClass('field-label').html(properLabel) )
      .append(
        $('<input>').addClass('span2').attr({'id': vals[1], 'type': 'text'}).change(function(){
          _gaq.push(['_trackEvent', 'Data', 'Change', properLabel]);
        })
      )
      .appendTo('#form');
  });

  // Get the id of the tab
  chrome.tabs.query({active: true}, function(tabs){

    bgPage = chrome.extension.getBackgroundPage();

    var personData = bgPage.personDataObjects[tabs[0].id];

    fillForm(personData);

    $('#update-button').attr('disabled', false).click(function(){
      _gaq.push(['_trackEvent', 'Data', 'Update Links']);
      updateLinks();
    });

    updateLinks();

  });

});

function fillForm(personData) {
  $.each(mappings, function(i, vals){
    $('#'+vals[1]).val(personData[vals[0]]);
  });
}

function updateLinks() {

  // Delete previous links and show ajax loader for a moment
  $('#search-links .btn').addClass('disabled');

  setTimeout(buildLinks, 300);

}

function buildLinks() {

  var personData = {};
  $.each(mappings, function(i, vals){
    personData[vals[0]] = $('#'+vals[1]).val();
  });

  var searchLinks = bgPage.rs.executeLinkBuilders(personData);

  // Remove ajax loader and add search links
  $('#search-links').html('');
  $.each(searchLinks, function(i, link){
      $('').addClass('btn btn-info').html(link.text).attr('href', link.url).appendTo('#search-links').click(function(){
        _gaq.push(['_trackEvent', 'Links', 'Click', link.text]);
      });
  });

}<|endoftext|>


<commit_before>/**
 * Attaches an event listener that looks for barcode scans from a keyboard wedge and
 * executes supplied function upon successful scan. The listener will be attached to
 * the object supplied by the "object" parameter. The "options" parameter configures
 * the behavior of the listener. The default options are defined below:
 *
 * {
 *   startSeq: [],
 *   endSeq: [],
 *   onScanStart: undefined,
 *   onScanEnd: undefined,
 * }
 *
 * The "startSeq" and "endSeq" options are arrays of key codes that a keyboard wedge
 * barcode scanner will send before and after barcode data. The "onScanStart" option
 * is not required.
 */
function Wedge(object, options) {

    var scanning = false;
    var seqIndex = 0;
    var barcodeData = [];
    object.onkeypress = keyHandler;

    // Check to see if we're scanning a start or end sequence. "func" is a callback
    // to execute when we get to the end of the sequence.
    function seqCompare(keyCode, seq, func) {
        if (keyCode != seq[seqIndex]) {
            seqIndex = (seq[0] == keyCode) ? 1 : 0;
        } else if (++seqIndex == seq.length) {
            func();
            seqIndex = 0;
        }
    }

    function keyHandler(e) {
        if (scanning) {
            barcodeData.push(e.keyCode);
            seqCompare(e.keyCode, options.endSeq, function() {
                scanning = false;

                // Trim end sequence from barcode data and invoke end callback.
                var l = barcodeData.length - options.endSeq.length;
                options.onScanEnd(barcodeData.slice(0, l));
                barcodeData = [];
            });
        } else {
            seqCompare(e.keyCode, options.startSeq, function() {
                scanning = true;
                if (options.onScanStart !== undefined) {
                    options.onScanStart();
                }
            });
        }
    }

}
<commit_msg>Rearranging stuff for no good reason.<commit_after>/**
 * Attaches an event listener that looks for barcode scans from a keyboard wedge and
 * executes supplied function upon successful scan. The listener will be attached to
 * the object supplied by the "object" parameter. The "options" parameter configures
 * the behavior of the listener. The default options are defined below:
 *
 * {
 *   startSeq: [],
 *   endSeq: [],
 *   onScanStart: undefined,
 *   onScanEnd: undefined,
 * }
 *
 * The "startSeq" and "endSeq" options are arrays of key codes that a keyboard wedge
 * barcode scanner will send before and after barcode data. The "onScanStart" option
 * is not required.
 */
function Wedge(object, options) {

    var scanning = false;
    var seqIndex = 0;
    var barcodeData = [];
    object.onkeypress = keyHandler;

    function keyHandler(e) {
        if (scanning) {
            barcodeData.push(e.keyCode);
            seqCompare(e.keyCode, options.endSeq, function() {
                scanning = false;

                // Trim end sequence from barcode data and invoke end callback.
                var l = barcodeData.length - options.endSeq.length;
                options.onScanEnd(barcodeData.slice(0, l));
                barcodeData = [];
            });
        } else {
            seqCompare(e.keyCode, options.startSeq, function() {
                scanning = true;
                if (options.onScanStart !== undefined) {
                    options.onScanStart();
                }
            });
        }
    }

    // Check to see if we're scanning a start or end sequence. "func" is a callback
    // to execute when we get to the end of the sequence.
    function seqCompare(keyCode, seq, func) {
        if (keyCode != seq[seqIndex]) {
            seqIndex = (seq[0] == keyCode) ? 1 : 0;
        } else if (++seqIndex == seq.length) {
            func();
            seqIndex = 0;
        }
    }
}
<|endoftext|>


<commit_before>/**
 * Creates a demo WebAuthn credential, optionally setting the 'payment'
 * extension. The created credential will always have the name 'Troy ····
 * 1234', matching the demo payment instrument used in authentication.
 *
 * @param {boolean} setPaymentExtension - whether or not to enable the 'payment'
 *     extension in the created credential.
 * @param {object} optionalOverrides - a set of optional overrides for the
 *     default credential creation parameters.
 * @return {PublicKeyCredential} the created credential.
 */
async function createCredential(setPaymentExtension, optionalOverrides = {}) {
  const {userIdOverride, residentKeyOverride} = optionalOverrides;
  const rp = {
    id: window.location.hostname,
    name: 'Rouslan Solomakhin',
  };
  const pubKeyCredParams = [{
    type: 'public-key',
    alg: -7, // ECDSA, not supported on Windows.
  }, {
    type: 'public-key',
    alg: -257, // RSA, supported on Windows.
  }];
  const userId = (userIdOverride !== undefined) ? userIdOverride : String(Math.random()*999999999);
  const user = {
    // Set an understandable 'username' in case the WebAuthn UX displays it
    // (e.g., the Passkeys UX on Chrome MacOS 108+). This is for display ONLY,
    // and has no bearing on SPC's functionality in general. (For example, it
    // is NOT shown in the SPC transaction dialog.)
    name: 'Troy ···· 1234',
    displayName: '',
    id: Uint8Array.from(userId, c => c.charCodeAt(0)),
  }

  let residentKey = spcSupportsPreferred() ? 'preferred' : 'required';
  if (residentKeyOverride !== undefined)
    residentKey = residentKeyOverride;

  const publicKey = {
    rp,
    user,
    challenge: new TextEncoder().encode('Enrollment challenge'),
    pubKeyCredParams,
    authenticatorSelection: {
      userVerification: 'required',
      residentKey,
      authenticatorAttachment: 'platform',
    },
  };

  if (setPaymentExtension)
    publicKey['extensions'] = {payment: {isPayment: true}};

  return await navigator.credentials.create({publicKey});
}

/**
 * Returns whether or not SPC supports residentKey 'preferred' (instead of just
 * 'required'). There is unfortunately no way to feature detect this, so we
 * have to do a version check.
 *
 * @return {boolean} true if SPC supports 'preferred' for the residentKey
 *     parameter, false otherwise.
 */
function spcSupportsPreferred() {
  // This will be true for not just Chrome but also Edge/etc, but that's fine.
  const match = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
  if (!match)
    return false;

  const version = parseInt(match[2], 10);
  // https://crrev.com/130fada41 landed in 106.0.5228.0, but we assume that any
  // 106 will do for simplicity.
  return version >= 106;
}

/**
 * Creates a PaymentRequest object for SPC.
 *
 * @param {SecurePaymentConfirmationRequest} spcData - the input SPC data. The
 *     credentialIds field *MUST* be set. Any other SecurePaymentConfirmationRequest
 *     fields not set in this object will be initialized to a default value.
 * @return {PaymentRequest} The payment request object.
 */
function createSPCPaymentRequest(spcData) {
  if (!window.PaymentRequest) {
    throw new Error('PaymentRequest API is not supported.');
  }
  if (spcData === undefined || spcData.credentialIds === undefined) {
    throw new Error('credentialIds must be set in the input spcData object.');
  }

  // https://w3c.github.io/secure-payment-confirmation/#sctn-securepaymentconfirmationrequest-dictionary
  if (spcData.challenge === undefined)
    spcData.challenge = new TextEncoder().encode('network_data');
  if (spcData.rpId === undefined)
    spcData.rpId = window.location.hostname;
  if (spcData.instrument === undefined)
    spcData.instrument = {};
  if (spcData.instrument.displayName === undefined)
    spcData.instrument.displayName = 'Troy ···· 1234';
  if (spcData.instrument.icon === undefined)
    spcData.instrument.icon = 'https://rsolomakhin.github.io/pr/spc/troy-alt-logo.png';
  if (spcData.timeout === undefined)
    spcData.timeout = 60000;
  // We only set a default payeeOrigin if *both* payeeName and payeeOrigin are
  // undefined, as the spec deliberately allows either/or to be null.
  if (spcData.payeeName === undefined && spcData.payeeOrigin === undefined)
    spcData.payeeOrigin = window.location.origin;

  const supportedInstruments = [{supportedMethods: 'secure-payment-confirmation', data: spcData}];
  const details = {
    total: {
      label: 'Total',
      amount: {
        currency: 'USD',
        value: '0.01',
      },
    },
  };

  return new PaymentRequest(supportedInstruments, details);
}
<commit_msg>Attempted bugfix for pr/spc_util.js, undefined vs not set<commit_after>/**
 * Creates a demo WebAuthn credential, optionally setting the 'payment'
 * extension. The created credential will always have the name 'Troy ····
 * 1234', matching the demo payment instrument used in authentication.
 *
 * @param {boolean} setPaymentExtension - whether or not to enable the 'payment'
 *     extension in the created credential.
 * @param {object} optionalOverrides - a set of optional overrides for the
 *     default credential creation parameters.
 * @return {PublicKeyCredential} the created credential.
 */
async function createCredential(setPaymentExtension, optionalOverrides = {}) {
  const {userIdOverride, residentKeyOverride} = optionalOverrides;
  const rp = {
    id: window.location.hostname,
    name: 'Rouslan Solomakhin',
  };
  const pubKeyCredParams = [{
    type: 'public-key',
    alg: -7, // ECDSA, not supported on Windows.
  }, {
    type: 'public-key',
    alg: -257, // RSA, supported on Windows.
  }];
  const userId = (userIdOverride !== undefined) ? userIdOverride : String(Math.random()*999999999);
  const user = {
    // Set an understandable 'username' in case the WebAuthn UX displays it
    // (e.g., the Passkeys UX on Chrome MacOS 108+). This is for display ONLY,
    // and has no bearing on SPC's functionality in general. (For example, it
    // is NOT shown in the SPC transaction dialog.)
    name: 'Troy ···· 1234',
    displayName: '',
    id: Uint8Array.from(userId, c => c.charCodeAt(0)),
  }

  let residentKey = spcSupportsPreferred() ? 'preferred' : 'required';
  if (residentKeyOverride !== undefined)
    residentKey = residentKeyOverride;

  const publicKey = {
    rp,
    user,
    challenge: new TextEncoder().encode('Enrollment challenge'),
    pubKeyCredParams,
    authenticatorSelection: {
      userVerification: 'required',
      residentKey,
      authenticatorAttachment: 'platform',
    },
  };

  if (setPaymentExtension)
    publicKey['extensions'] = {payment: {isPayment: true}};

  return await navigator.credentials.create({publicKey});
}

/**
 * Returns whether or not SPC supports residentKey 'preferred' (instead of just
 * 'required'). There is unfortunately no way to feature detect this, so we
 * have to do a version check.
 *
 * @return {boolean} true if SPC supports 'preferred' for the residentKey
 *     parameter, false otherwise.
 */
function spcSupportsPreferred() {
  // This will be true for not just Chrome but also Edge/etc, but that's fine.
  const match = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
  if (!match)
    return false;

  const version = parseInt(match[2], 10);
  // https://crrev.com/130fada41 landed in 106.0.5228.0, but we assume that any
  // 106 will do for simplicity.
  return version >= 106;
}

/**
 * Creates a PaymentRequest object for SPC.
 *
 * @param {SecurePaymentConfirmationRequest} spcData - the input SPC data. The
 *     credentialIds field *MUST* be set. Any other SecurePaymentConfirmationRequest
 *     fields not set in this object will be initialized to a default value.
 * @return {PaymentRequest} The payment request object.
 */
function createSPCPaymentRequest(spcData) {
  if (!window.PaymentRequest) {
    throw new Error('PaymentRequest API is not supported.');
  }
  if (spcData === undefined || spcData.credentialIds === undefined) {
    throw new Error('credentialIds must be set in the input spcData object.');
  }

  // https://w3c.github.io/secure-payment-confirmation/#sctn-securepaymentconfirmationrequest-dictionary
  if (spcData.challenge === undefined)
    spcData.challenge = new TextEncoder().encode('network_data');
  if (spcData.rpId === undefined)
    spcData.rpId = window.location.hostname;
  if (spcData.instrument === undefined)
    spcData.instrument = {};
  if (spcData.instrument.displayName === undefined)
    spcData.instrument.displayName = 'Troy ···· 1234';
  if (spcData.instrument.icon === undefined)
    spcData.instrument.icon = 'https://rsolomakhin.github.io/pr/spc/troy-alt-logo.png';
  if (spcData.timeout === undefined)
    spcData.timeout = 60000;
  // We only set a default payeeOrigin if *both* payeeName and payeeOrigin are
  // not set, as the spec deliberately allows either/or to be null.
  if (!('payeeName' in spcData) && !('payeeOrigin' in spcData))
    spcData.payeeOrigin = window.location.origin;

  const supportedInstruments = [{supportedMethods: 'secure-payment-confirmation', data: spcData}];
  const details = {
    total: {
      label: 'Total',
      amount: {
        currency: 'USD',
        value: '0.01',
      },
    },
  };

  return new PaymentRequest(supportedInstruments, details);
}
<|endoftext|>


<commit_before>import { SELECT } from '../../actions/desktop/types';
import { ADD_WINDOW } from '../../actions/windows/types';

const initialState = {
  active: null,
  isMobile: true
};

export default function windowsReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_WINDOW:
    case SELECT: {
      return {
        active: action.windowId,
        isMobile: state.isMobile
      };
    }
    default:
      return state;
  }
}
<commit_msg>[newclient] Initial small screen detection<commit_after>import { SELECT } from '../../actions/desktop/types';
import { ADD_WINDOW } from '../../actions/windows/types';

const isMobileDevice = document.documentElement.clientWidth < 770;

const initialState = {
  active: null,
  isMobile: isMobileDevice
};

export default function windowsReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_WINDOW:
    case SELECT: {
      return {
        active: action.windowId,
        isMobile: state.isMobile
      };
    }
    default:
      return state;
  }
}
<|endoftext|>