<commit_before>import React                from 'react';
import PureRenderMixin      from 'react-addons-pure-render-mixin';
import {
  omit,
  merge,
}                           from 'timm';
import {
  bindAll,
  cancelEvent,
}                           from '../gral/helpers';
import {
  COLORS,
  MISC,
  IS_IOS,
}                           from '../gral/constants';
import { scrollIntoView }   from '../gral/visibility';
import { isDark }           from '../gral/styles';
import { isRequired }       from '../gral/validators';
import {
  floatAdd,
  floatDelete,
  floatUpdate,
  floatReposition,
  warnFloats,
}                           from '../components/floats';
import FocusCapture         from '../components/focusCapture';

const PROP_TYPES = {
  value:                  React.PropTypes.any,
  errors:                 React.PropTypes.array,
  required:               React.PropTypes.bool,   // also passed through
  validators:             React.PropTypes.array,
  noErrors:               React.PropTypes.bool,
  cmds:                   React.PropTypes.array,  // also passed through
  disabled:               React.PropTypes.bool,   // also passed through
  floatZ:                 React.PropTypes.number, // also passed through
  floatPosition:          React.PropTypes.string, // also passed through
  errorZ:                 React.PropTypes.number,
  errorPosition:          React.PropTypes.string,
  errorAlign:             React.PropTypes.string,
  onChange:               React.PropTypes.func,
  onFocus:                React.PropTypes.func,
  onBlur:                 React.PropTypes.func,
  styleOuter:             React.PropTypes.object,
  // all others are passed through unchanged
};
const PROP_KEYS = Object.keys(PROP_TYPES);

// **IMPORTANT**: must be the outermost HOC (i.e. closest to the
// user), for the imperative API to work.
function input(ComposedComponent, {
  toInternalValue = (o => o),
  toExternalValue = (o => o),
  isNull,
  valueAttr = 'value',
  fIncludeFocusCapture = false,
  defaultValidators = {},
  validatorContext,
  trappedKeys = [],
  className,
  fIncludeClipboardProps: fIncludeClipboardProps0,
} = {}) {
  const fIncludeClipboardProps = fIncludeClipboardProps0 != null
    ? fIncludeClipboardProps0
    : fIncludeFocusCapture;

  return class extends React.Component {
    static displayName = `Input(${ComposedComponent.name})`;
    static propTypes = PROP_TYPES;
    static defaultProps = {
      errors:                 [],
      validators:             [],
    };

    constructor(props) {
      super(props);
      this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);

      // NOTE: this.errors = this.props.errors + this.state.validationErrors
      this.errors = props.errors;
      this.prevErrors = this.errors;
      this.state = {
        curValue: toInternalValue(props.value, props),
        fFocused: false,
        keyDown: null,
        validationErrors: [],
        lastValidatedValue: toInternalValue(props.value, props),
      };
      bindAll(this, [
        'registerOuterRef',
        'registerFocusableRef',
        'renderErrorFloat',
        'onChange',
        'onFocus',
        'onBlur',
        'onCopyCut',
        'onPaste',
        'onMouseDownWrapper',
        'onKeyDown',
      ]);
    }

    componentDidMount() {
      warnFloats(this.constructor.name);
      this.renderErrorFloat();
    }

    componentWillReceiveProps(nextProps) {
      const { value, cmds } = nextProps;
      if (value !== this.props.value) {
        this.setState({ curValue: toInternalValue(value, nextProps) });
      }
      if (cmds !== this.props.cmds) this.processCmds(cmds);
    }

    componentWillUpdate(nextProps, nextState) {
      const { errors } = nextProps;
      const { validationErrors } = nextState;
      if (errors !== this.props.errors ||
          validationErrors !== this.state.validationErrors) {
        this.errors = errors.concat(validationErrors);
      }
    }

    componentDidUpdate(prevProps, prevState) {
      const { value } = this.props;
      const { curValue } = this.state;
      if (this.errors !== this.prevErrors ||
          value !== prevProps.value ||
          curValue !== prevState.curValue) {
        this.renderErrorFloat();
      }
      if (this.pendingFocusBlur) {
        // execute `FOCUS` and `BLUR` commands asynchronously, so that the owner
        // of the Input component doesn't find a `null` ref in a `focus`/`blur` handler
        setTimeout(() => {
          if (!this.pendingFocusBlur) return;
          if (!this[this.pendingFocusBlur]) return;
          this[this.pendingFocusBlur]();
          this.pendingFocusBlur = null;
        });
      }
    }

    componentWillUnmount() { floatDelete(this.errorFloatId); }

    // ==========================================
    // Imperative API (via props or directly)
    // ==========================================
    processCmds(cmds) {
      if (cmds == null) return;
      cmds.forEach(cmd => {
        switch (cmd.type) {
          case 'SET_VALUE':
            this.setState({ curValue: toInternalValue(cmd.value, this.props) });
            break;
          case 'REVERT':
            this.setState({ curValue: toInternalValue(this.props.value, this.props) });
            break;
          case 'VALIDATE':
            this.validate();
            break;
          case 'FOCUS':
            this.pendingFocusBlur = '_focus';
            break;
          case 'BLUR':
            this.pendingFocusBlur = '_blur';
            break;
          default:
            break;
        }
      });
    }

    // Alternative to using the `onChange` prop (e.g. if we want to delegate
    // state handling to the input and only want to retrieve the value when submitting a form)
    getValue() { return toExternalValue(this.state.curValue, this.props); }
    getErrors() { return this.errors; }
    validateAndGetValue() { return this._validate().then(() => this.getValue()); }

    // ==========================================
    // Render
    // ==========================================
    render() {
      const otherProps = omit(this.props, PROP_KEYS);
      let out = (
        <ComposedComponent
          registerOuterRef={this.registerOuterRef}
          registerFocusableRef={fIncludeFocusCapture ? undefined : this.registerFocusableRef}
          {...otherProps}
          curValue={this.state.curValue}
          errors={this.errors}
          required={this.props.required}
          cmds={this.props.cmds}
          keyDown={this.state.keyDown}
          disabled={this.props.disabled}
          fFocused={this.state.fFocused}
          floatZ={this.props.floatZ}
          floatPosition={this.props.floatPosition}
          onChange={this.onChange}
          onFocus={fIncludeFocusCapture ? undefined : this.onFocus}
          onBlur={fIncludeFocusCapture ? undefined : this.onBlur}
          onCopy={fIncludeClipboardProps ? this.onCopyCut : undefined}
          onCut={fIncludeClipboardProps ? this.onCopyCut : undefined}
          onPaste={fIncludeClipboardProps ? this.onPaste : undefined}
          onResizeOuter={floatReposition}
          styleOuter={fIncludeFocusCapture ? undefined : this.props.styleOuter}
        />
      );

      // Render FocusCapture if needed
      let focusCaptureEl;
      if (fIncludeFocusCapture) {
        focusCaptureEl = (
          <FocusCapture
            registerRef={this.registerFocusableRef}
            disabled={this.props.disabled}
            onFocus={this.onFocus} onBlur={this.onBlur}
            onCopy={this.onCopyCut} onCut={this.onCopyCut} onPaste={this.onPaste}
            onKeyDown={this.onKeyDown}
          />
        );
      }

      // Render errors if needed
      let errorsEl;
      const { errors } = this;
      if (IS_IOS && errors.length) errorsEl = this.renderErrors(this.errors);

      // Wrap element if needed
      if (focusCaptureEl || errorsEl) {
        out = (
          <span
            className={className}
            onMouseDown={focusCaptureEl ? this.onMouseDownWrapper : undefined}
            style={style.wrapper(this.props)}
          >
            {focusCaptureEl}
            {errorsEl}
            {out}
          </span>
        );
      }

      return out;
    }

    renderErrorFloat() {
      if (IS_IOS) return;
      const { errors } = this;

      // Remove float
      if (!errors.length && this.errorFloatId != null) {
        floatDelete(this.errorFloatId);
        this.errorFloatId = null;
        return;
      }

      // Create or update float
      // (if the `errorX` props are not set, spy on the pass-through `floatX` props for
      // hints on how to properly position the error float; e.g. if another float
      // will open `below`, position the error float `above`; also adjust `z-index`
      // to be below another float's level, so that it doesn't obscure the main float).
      if (errors.length) {
        const {
          floatZ, floatPosition,
          errorZ, errorPosition, errorAlign,
        } = this.props;
        let zIndex = errorZ;
        if (zIndex == null) {
          zIndex = floatZ != null ? floatZ - MISC.zErrorFloatDelta : MISC.zErrorFloatDelta;
        }
        let position = errorPosition;
        if (position == null) {
          position = floatPosition === 'below' ? 'above' : 'below';
        }
        const floatOptions = {
          position,
          align: errorAlign,
          zIndex,
          getAnchorNode: () => this.refOuter || this.refFocusable,
          children: this.renderErrors(errors),
        };
        if (this.errorFloatId == null) {
          this.errorFloatId = floatAdd(floatOptions);
        } else {
          floatUpdate(this.errorFloatId, floatOptions);
        }
      }
    }

    renderErrors(errors) {
      const { curValue, lastValidatedValue } = this.state;
      let fModified = false;
      if (curValue != null) fModified = curValue !== lastValidatedValue;
      return (
        <div style={style.errors(fModified, this.props)}>
          {errors.join(' | ')}
        </div>
      );
    }

    // ==========================================
    // Handlers
    // ==========================================
    registerOuterRef(c) { this.refOuter = c; }
    registerFocusableRef(c) { this.refFocusable = c; }

    onChange(ev, providedValue, options = {}) {
      const { onChange, disabled } = this.props;
      if (disabled) return;
      let curValue = providedValue;
      if (curValue === undefined) {
        curValue = ev.currentTarget[valueAttr];
      }
      this.setState({ curValue });
      if (onChange) onChange(ev, toExternalValue(curValue, this.props));
      if (!this.state.fFocused && !options.fDontFocus) this._focus();
    }

    onFocus(ev) {
      const { onFocus, disabled } = this.props;
      if (disabled) {
        this._blur();
        return;
      }
      if (this.refOuter) scrollIntoView(this.refOuter);
      this.setState({ fFocused: true });
      if (onFocus) onFocus(ev);
    }

    onBlur(ev) {
      const { onBlur } = this.props;
      this._validate();
      this.setState({ fFocused: false });
      if (onBlur) onBlur(ev);
    }

    onMouseDownWrapper(ev) {
      // Always cancel mousedowns: they blur the component. If they are interesting,
      // capture them at a lower level
      cancelEvent(ev);

      // If not focused, a mouse-down should focus the component and cancel the event
      if (this.state.fFocused) return;
      this._focus();
    }

    onKeyDown(ev) {
      const { which, keyCode, metaKey, shiftKey, altKey, ctrlKey } = ev;
      if (trappedKeys.indexOf(which) < 0) return;
      this.setState({ keyDown: { which, keyCode, metaKey, shiftKey, altKey, ctrlKey } });
    }

    onCopyCut(ev) {
      ev.clipboardData.setData('text/plain', this.state.curValue);
      ev.preventDefault();
    }

    onPaste(ev) {
      const nextValue = ev.clipboardData.getData('text/plain');
      ev.preventDefault();
      this.onChange(ev, nextValue);
    }

    // ==========================================
    // Helpers
    // ==========================================
    _focus() {
      if (this.refFocusable && this.refFocusable.focus) this.refFocusable.focus();
    }

    _blur() {
      if (this.refFocusable && this.refFocusable.blur) this.refFocusable.blur();
    }

    _validate() {
      const { noErrors } = this.props;
      if (noErrors) return Promise.resolve();
      let validators;
      if (this.props.validators.length) {
        validators = merge({}, defaultValidators);
        let cnt = 0;
        this.props.validators.forEach(validator => {
          validators[validator.id || `anon_${cnt++}`] = validator;
        });
      } else {
        validators = defaultValidators;
      }
      const { curValue: internalValue } = this.state;
      const externalValue = toExternalValue(internalValue, this.props);
      const fRequired = this.props.required || validators.isRequired != null;
      const fIsNull = isNull(internalValue);
      let pErrors; // promised errors

      // If `null` is allowed and input is `null`, bail out
      if (!fRequired && fIsNull) {
        pErrors = [];

      // If input is null (unallowed), get the corresponding message and bail out
      } else if (fIsNull) {
        const validator = validators.isRequired || isRequired();
        pErrors = [validator.getErrorMessage(internalValue)];

      // Otherwise, collect all validator errors (skipping `isRequired`)
      } else {
        pErrors = [];
        Object.keys(validators).forEach(id => {
          if (id === 'isRequired') return;
          const validator = validators[id];
          const validate = validator.validate || validator;
          const value = validator.fInternal ? internalValue : externalValue;
          const pError = validate(value, this.props, validatorContext);
          if (pError != null) pErrors.push(pError);
        });
      }

      // When all promises have resolved, changed the current state
      return Promise.all(pErrors).then(validationErrors0 => {
        const validationErrors = validationErrors0.filter(o => o != null);
        this && this.setState && this.setState({
          validationErrors,
          lastValidatedValue: internalValue,
        });
        if (validationErrors.length) {
          const exception = new Error('VALIDATION_ERROR');
          exception.errors = validationErrors;
          throw exception;
        }
      });
    }
  };
}

// ==========================================
// Styles
// ==========================================
const errorBgColorBase = COLORS.notifs.error;
const errorFgColorBase = COLORS[isDark(errorBgColorBase) ? 'lightText' : 'darkText'];
const errorBgColorModified = COLORS.notifs.warn;
const errorFgColorModified = COLORS[isDark(errorBgColorModified) ? 'lightText' : 'darkText'];
const style = {
  wrapper: ({ styleOuter }) => {
    let out = styleOuter || {};
    out = merge(out, {
      position: 'relative',
      display: 'inline-block',
    });
    return out;
  },
  errors: fModified => {
    const out = {
      padding: '1px 3px',
    };
    if (IS_IOS) {
      out.position = 'absolute';
      out.top = '100%';
      out.left = 0;
    }
    out.backgroundColor = fModified ? errorBgColorModified : errorBgColorBase;
    out.color = fModified ? errorFgColorModified : errorFgColorBase;
    return out;
  },
};

// ==========================================
// Public API
// ==========================================
export default input;
<commit_msg>iOS: input error floats: honour positioning props<commit_after>import React                from 'react';
import PureRenderMixin      from 'react-addons-pure-render-mixin';
import {
  omit,
  merge,
}                           from 'timm';
import {
  bindAll,
  cancelEvent,
}                           from '../gral/helpers';
import {
  COLORS,
  MISC,
  IS_IOS,
}                           from '../gral/constants';
import { scrollIntoView }   from '../gral/visibility';
import { isDark }           from '../gral/styles';
import { isRequired }       from '../gral/validators';
import {
  floatAdd,
  floatDelete,
  floatUpdate,
  floatReposition,
  warnFloats,
}                           from '../components/floats';
import FocusCapture         from '../components/focusCapture';
import IosFloatWrapper      from '../inputs/iosFloatWrapper';

const PROP_TYPES = {
  value:                  React.PropTypes.any,
  errors:                 React.PropTypes.array,
  required:               React.PropTypes.bool,   // also passed through
  validators:             React.PropTypes.array,
  noErrors:               React.PropTypes.bool,
  cmds:                   React.PropTypes.array,  // also passed through
  disabled:               React.PropTypes.bool,   // also passed through
  floatZ:                 React.PropTypes.number, // also passed through
  floatPosition:          React.PropTypes.string, // also passed through
  errorZ:                 React.PropTypes.number,
  errorPosition:          React.PropTypes.string,
  errorAlign:             React.PropTypes.string,
  onChange:               React.PropTypes.func,
  onFocus:                React.PropTypes.func,
  onBlur:                 React.PropTypes.func,
  styleOuter:             React.PropTypes.object,
  // all others are passed through unchanged
};
const PROP_KEYS = Object.keys(PROP_TYPES);

// **IMPORTANT**: must be the outermost HOC (i.e. closest to the
// user), for the imperative API to work.
function input(ComposedComponent, {
  toInternalValue = (o => o),
  toExternalValue = (o => o),
  isNull,
  valueAttr = 'value',
  fIncludeFocusCapture = false,
  defaultValidators = {},
  validatorContext,
  trappedKeys = [],
  className,
  fIncludeClipboardProps: fIncludeClipboardProps0,
} = {}) {
  const fIncludeClipboardProps = fIncludeClipboardProps0 != null
    ? fIncludeClipboardProps0
    : fIncludeFocusCapture;

  return class extends React.Component {
    static displayName = `Input(${ComposedComponent.name})`;
    static propTypes = PROP_TYPES;
    static defaultProps = {
      errors:                 [],
      validators:             [],
    };

    constructor(props) {
      super(props);
      this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);

      // NOTE: this.errors = this.props.errors + this.state.validationErrors
      this.errors = props.errors;
      this.prevErrors = this.errors;
      this.state = {
        curValue: toInternalValue(props.value, props),
        fFocused: false,
        keyDown: null,
        validationErrors: [],
        lastValidatedValue: toInternalValue(props.value, props),
      };
      bindAll(this, [
        'registerOuterRef',
        'registerFocusableRef',
        'renderErrorFloat',
        'onChange',
        'onFocus',
        'onBlur',
        'onCopyCut',
        'onPaste',
        'onMouseDownWrapper',
        'onKeyDown',
      ]);
    }

    componentDidMount() {
      warnFloats(this.constructor.name);
      this.renderErrorFloat();
    }

    componentWillReceiveProps(nextProps) {
      const { value, cmds } = nextProps;
      if (value !== this.props.value) {
        this.setState({ curValue: toInternalValue(value, nextProps) });
      }
      if (cmds !== this.props.cmds) this.processCmds(cmds);
    }

    componentWillUpdate(nextProps, nextState) {
      const { errors } = nextProps;
      const { validationErrors } = nextState;
      if (errors !== this.props.errors ||
          validationErrors !== this.state.validationErrors) {
        this.errors = errors.concat(validationErrors);
      }
    }

    componentDidUpdate(prevProps, prevState) {
      const { value } = this.props;
      const { curValue } = this.state;
      if (this.errors !== this.prevErrors ||
          value !== prevProps.value ||
          curValue !== prevState.curValue) {
        this.renderErrorFloat();
      }
      if (this.pendingFocusBlur) {
        // execute `FOCUS` and `BLUR` commands asynchronously, so that the owner
        // of the Input component doesn't find a `null` ref in a `focus`/`blur` handler
        setTimeout(() => {
          if (!this.pendingFocusBlur) return;
          if (!this[this.pendingFocusBlur]) return;
          this[this.pendingFocusBlur]();
          this.pendingFocusBlur = null;
        });
      }
    }

    componentWillUnmount() { floatDelete(this.errorFloatId); }

    // ==========================================
    // Imperative API (via props or directly)
    // ==========================================
    processCmds(cmds) {
      if (cmds == null) return;
      cmds.forEach(cmd => {
        switch (cmd.type) {
          case 'SET_VALUE':
            this.setState({ curValue: toInternalValue(cmd.value, this.props) });
            break;
          case 'REVERT':
            this.setState({ curValue: toInternalValue(this.props.value, this.props) });
            break;
          case 'VALIDATE':
            this.validate();
            break;
          case 'FOCUS':
            this.pendingFocusBlur = '_focus';
            break;
          case 'BLUR':
            this.pendingFocusBlur = '_blur';
            break;
          default:
            break;
        }
      });
    }

    // Alternative to using the `onChange` prop (e.g. if we want to delegate
    // state handling to the input and only want to retrieve the value when submitting a form)
    getValue() { return toExternalValue(this.state.curValue, this.props); }
    getErrors() { return this.errors; }
    validateAndGetValue() { return this._validate().then(() => this.getValue()); }

    // ==========================================
    // Render
    // ==========================================
    render() {
      const otherProps = omit(this.props, PROP_KEYS);
      let out = (
        <ComposedComponent
          registerOuterRef={this.registerOuterRef}
          registerFocusableRef={fIncludeFocusCapture ? undefined : this.registerFocusableRef}
          {...otherProps}
          curValue={this.state.curValue}
          errors={this.errors}
          required={this.props.required}
          cmds={this.props.cmds}
          keyDown={this.state.keyDown}
          disabled={this.props.disabled}
          fFocused={this.state.fFocused}
          floatZ={this.props.floatZ}
          floatPosition={this.props.floatPosition}
          onChange={this.onChange}
          onFocus={fIncludeFocusCapture ? undefined : this.onFocus}
          onBlur={fIncludeFocusCapture ? undefined : this.onBlur}
          onCopy={fIncludeClipboardProps ? this.onCopyCut : undefined}
          onCut={fIncludeClipboardProps ? this.onCopyCut : undefined}
          onPaste={fIncludeClipboardProps ? this.onPaste : undefined}
          onResizeOuter={floatReposition}
          styleOuter={fIncludeFocusCapture ? undefined : this.props.styleOuter}
        />
      );

      // Render FocusCapture if needed
      let focusCaptureEl;
      if (fIncludeFocusCapture) {
        focusCaptureEl = (
          <FocusCapture
            registerRef={this.registerFocusableRef}
            disabled={this.props.disabled}
            onFocus={this.onFocus} onBlur={this.onBlur}
            onCopy={this.onCopyCut} onCut={this.onCopyCut} onPaste={this.onPaste}
            onKeyDown={this.onKeyDown}
          />
        );
      }

      // Render errors if needed
      let errorsEl;
      const { errors } = this;
      if (IS_IOS && errors.length) {
        const { position, align, zIndex } = this.calcFloatPosition();
        errorsEl = (
          <IosFloatWrapper
            floatPosition={position}
            floatAlign={align}
            floatZ={zIndex}
          >
            {this.renderErrors(this.errors)}
          </IosFloatWrapper>
        );
      }

      // Wrap element if needed
      if (focusCaptureEl || errorsEl) {
        out = (
          <span
            className={className}
            onMouseDown={focusCaptureEl ? this.onMouseDownWrapper : undefined}
            style={style.wrapper(this.props)}
          >
            {focusCaptureEl}
            {errorsEl}
            {out}
          </span>
        );
      }

      return out;
    }

    renderErrorFloat() {
      if (IS_IOS) return;
      const { errors } = this;

      // Remove float
      if (!errors.length && this.errorFloatId != null) {
        floatDelete(this.errorFloatId);
        this.errorFloatId = null;
        return;
      }

      // Create or update float
      // (if the `errorX` props are not set, spy on the pass-through `floatX` props for
      // hints on how to properly position the error float; e.g. if another float
      // will open `below`, position the error float `above`; also adjust `z-index`
      // to be below another float's level, so that it doesn't obscure the main float).
      if (errors.length) {
        const floatOptions = merge(this.calcFloatPosition(), {
          getAnchorNode: () => this.refOuter || this.refFocusable,
          children: this.renderErrors(errors),
        });
        if (this.errorFloatId == null) {
          this.errorFloatId = floatAdd(floatOptions);
        } else {
          floatUpdate(this.errorFloatId, floatOptions);
        }
      }
    }

    renderErrors(errors) {
      const { curValue, lastValidatedValue } = this.state;
      let fModified = false;
      if (curValue != null) fModified = curValue !== lastValidatedValue;
      return (
        <div style={style.errors(fModified)}>
          {errors.join(' | ')}
        </div>
      );
    }

    // ==========================================
    // Handlers
    // ==========================================
    registerOuterRef(c) { this.refOuter = c; }
    registerFocusableRef(c) { this.refFocusable = c; }

    onChange(ev, providedValue, options = {}) {
      const { onChange, disabled } = this.props;
      if (disabled) return;
      let curValue = providedValue;
      if (curValue === undefined) {
        curValue = ev.currentTarget[valueAttr];
      }
      this.setState({ curValue });
      if (onChange) onChange(ev, toExternalValue(curValue, this.props));
      if (!this.state.fFocused && !options.fDontFocus) this._focus();
    }

    onFocus(ev) {
      const { onFocus, disabled } = this.props;
      if (disabled) {
        this._blur();
        return;
      }
      if (this.refOuter) scrollIntoView(this.refOuter);
      this.setState({ fFocused: true });
      if (onFocus) onFocus(ev);
    }

    onBlur(ev) {
      const { onBlur } = this.props;
      this._validate();
      this.setState({ fFocused: false });
      if (onBlur) onBlur(ev);
    }

    onMouseDownWrapper(ev) {
      // Always cancel mousedowns: they blur the component. If they are interesting,
      // capture them at a lower level
      cancelEvent(ev);

      // If not focused, a mouse-down should focus the component and cancel the event
      if (this.state.fFocused) return;
      this._focus();
    }

    onKeyDown(ev) {
      const { which, keyCode, metaKey, shiftKey, altKey, ctrlKey } = ev;
      if (trappedKeys.indexOf(which) < 0) return;
      this.setState({ keyDown: { which, keyCode, metaKey, shiftKey, altKey, ctrlKey } });
    }

    onCopyCut(ev) {
      ev.clipboardData.setData('text/plain', this.state.curValue);
      ev.preventDefault();
    }

    onPaste(ev) {
      const nextValue = ev.clipboardData.getData('text/plain');
      ev.preventDefault();
      this.onChange(ev, nextValue);
    }

    // ==========================================
    // Helpers
    // ==========================================
    _focus() {
      if (this.refFocusable && this.refFocusable.focus) this.refFocusable.focus();
    }

    _blur() {
      if (this.refFocusable && this.refFocusable.blur) this.refFocusable.blur();
    }

    _validate() {
      const { noErrors } = this.props;
      if (noErrors) return Promise.resolve();
      let validators;
      if (this.props.validators.length) {
        validators = merge({}, defaultValidators);
        let cnt = 0;
        this.props.validators.forEach(validator => {
          validators[validator.id || `anon_${cnt++}`] = validator;
        });
      } else {
        validators = defaultValidators;
      }
      const { curValue: internalValue } = this.state;
      const externalValue = toExternalValue(internalValue, this.props);
      const fRequired = this.props.required || validators.isRequired != null;
      const fIsNull = isNull(internalValue);
      let pErrors; // promised errors

      // If `null` is allowed and input is `null`, bail out
      if (!fRequired && fIsNull) {
        pErrors = [];

      // If input is null (unallowed), get the corresponding message and bail out
      } else if (fIsNull) {
        const validator = validators.isRequired || isRequired();
        pErrors = [validator.getErrorMessage(internalValue)];

      // Otherwise, collect all validator errors (skipping `isRequired`)
      } else {
        pErrors = [];
        Object.keys(validators).forEach(id => {
          if (id === 'isRequired') return;
          const validator = validators[id];
          const validate = validator.validate || validator;
          const value = validator.fInternal ? internalValue : externalValue;
          const pError = validate(value, this.props, validatorContext);
          if (pError != null) pErrors.push(pError);
        });
      }

      // When all promises have resolved, changed the current state
      return Promise.all(pErrors).then(validationErrors0 => {
        const validationErrors = validationErrors0.filter(o => o != null);
        this && this.setState && this.setState({
          validationErrors,
          lastValidatedValue: internalValue,
        });
        if (validationErrors.length) {
          const exception = new Error('VALIDATION_ERROR');
          exception.errors = validationErrors;
          throw exception;
        }
      });
    }

    calcFloatPosition() {
      const {
        floatZ, floatPosition,
        errorZ, errorPosition, errorAlign,
      } = this.props;
      let zIndex = errorZ;
      if (zIndex == null) {
        zIndex = floatZ != null ? floatZ - MISC.zErrorFloatDelta : MISC.zErrorFloatDelta;
      }
      let position = errorPosition;
      if (position == null) {
        position = floatPosition === 'below' ? 'above' : 'below';
      }
      return { position, align: errorAlign, zIndex };
    }
  };
}

// ==========================================
// Styles
// ==========================================
const errorBgColorBase = COLORS.notifs.error;
const errorFgColorBase = COLORS[isDark(errorBgColorBase) ? 'lightText' : 'darkText'];
const errorBgColorModified = COLORS.notifs.warn;
const errorFgColorModified = COLORS[isDark(errorBgColorModified) ? 'lightText' : 'darkText'];
const style = {
  wrapper: ({ styleOuter }) => {
    let out = styleOuter || {};
    out = merge(out, {
      position: 'relative',
      display: 'inline-block',
    });
    return out;
  },
  errors: fModified => ({
    padding: '1px 3px',
    backgroundColor: fModified ? errorBgColorModified : errorBgColorBase,
    color: fModified ? errorFgColorModified : errorFgColorBase,
  }),
};

// ==========================================
// Public API
// ==========================================
export default input;
<|endoftext|>


<commit_before>"use strict";

import CONFIG from '../config';

export const LANGUAGES = {
    en_US: {
        name: 'english',
        translators: [
            // {name: '', icon: '', link: '',},
        ],
        translations: Object.assign(
            require('./en_US.items.json'),
            require('./en_US.events.json'),
            require('./en_US.heroes.json'),
            require('./en_US.interface.json')
        ),
    },
    ru_RU: {
        name: 'русский',
        translators: [
            {name: '/u/FixKun', icon: 'reddit-alien', link: 'https://www.reddit.com/user/FixKun',},
            {name: 'BurzZzum', icon: 'twitter', link: 'https://twitter.com/BurzZzum',},
        ],
        translations: Object.assign(
            require('./ru_RU.items.json'),
            require('./ru_RU.events.json'),
            require('./ru_RU.heroes.json'),
            require('./ru_RU.interface.json')
        ),
    },
};

export default function (key, defaultReturn = null) {
    if (defaultReturn === null) {
        defaultReturn = `_${key}_`
    }

    if (!LANGUAGES[CONFIG.LANGUAGE]) {
        return LANGUAGES.en_US.translations[key] || defaultReturn;
    }

    return LANGUAGES[CONFIG.LANGUAGE].translations[key] || defaultReturn;
};
<commit_msg>fix import<commit_after>"use strict";

import CONFIG from './../config';

export const LANGUAGES = {
    en_US: {
        name: 'english',
        translators: [
            // {name: '', icon: '', link: '',},
        ],
        translations: Object.assign(
            require('./en_US.items.json'),
            require('./en_US.events.json'),
            require('./en_US.heroes.json'),
            require('./en_US.interface.json')
        ),
    },
    ru_RU: {
        name: 'русский',
        translators: [
            {name: '/u/FixKun', icon: 'reddit-alien', link: 'https://www.reddit.com/user/FixKun',},
            {name: 'BurzZzum', icon: 'twitter', link: 'https://twitter.com/BurzZzum',},
        ],
        translations: Object.assign(
            require('./ru_RU.items.json'),
            require('./ru_RU.events.json'),
            require('./ru_RU.heroes.json'),
            require('./ru_RU.interface.json')
        ),
    },
};

export default function (key, defaultReturn = null) {
    if (defaultReturn === null) {
        defaultReturn = `_${key}_`
    }

    if (!LANGUAGES[CONFIG.LANGUAGE]) {
        return LANGUAGES.en_US.translations[key] || defaultReturn;
    }

    return LANGUAGES[CONFIG.LANGUAGE].translations[key] || defaultReturn;
};
<|endoftext|>


<commit_before>$.Redactor.prototype.WoltLabInlineCode = function() {
	"use strict";

	var _environment;

	return {
		init: function() {
			this.opts.activeButtonsStates.kbd = 'tt';

			require(['Environment', 'EventHandler'], (function (Environment, EventHandler) {
				_environment = Environment;

				EventHandler.add('com.woltlab.wcf.redactor2', 'bbcode_tt_' + this.$element[0].id, this.WoltLabInlineCode._toggle.bind(this));
			}).bind(this));
		},

		_toggle: function (data) {
			data.cancel = true;

			this.button.toggle({}, 'kbd', 'func', 'inline.format');

			var selection = window.getSelection();
			var node = selection.anchorNode;
			if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;

			if (node.nodeName === 'KBD') {
				var nextSibling = node.nextSibling;

				if (_environment.platform() === 'ios' && _environment.browser() === 'safari') {
					// iOS Safari work-around to allow caret placement after <kbd>
					if (nextSibling && nextSibling.nodeName === 'BR') {
						node.parentNode.insertBefore(document.createTextNode('\u200B'), nextSibling);
					}

					// fix selection position
					/*if (node.innerHTML === '\u200B') {
						// the caret must be at offset 0 (before the whitespace)
						var range = selection.getRangeAt(0);
						if (range.collapsed && range.startOffset === 1) {
							node.innerHTML = this.marker.html() + '\u200B';

							this.selection.restore();
						}
					}*/

					return;
				}

				while (nextSibling) {
					if (nextSibling.nodeType !== Node.TEXT_NODE || nextSibling.textContent.length) {
						return;
					}

					nextSibling = nextSibling.nextSibling;
				}

				if (nextSibling) {
					nextSibling.textContent = '\u200B';
				}
				else {
					node.parentNode.appendChild(document.createTextNode('\u200B'));
				}
			}
		}
	};
};
<commit_msg>Using the inline code at <kbd> start now places caret before it<commit_after>$.Redactor.prototype.WoltLabInlineCode = function() {
	"use strict";

	var _environment;

	return {
		init: function() {
			this.opts.activeButtonsStates.kbd = 'tt';

			require(['Environment', 'EventHandler'], (function (Environment, EventHandler) {
				_environment = Environment;

				EventHandler.add('com.woltlab.wcf.redactor2', 'bbcode_tt_' + this.$element[0].id, this.WoltLabInlineCode._toggle.bind(this));
			}).bind(this));
		},

		_toggle: function (data) {
			var node;

			data.cancel = true;

			var selection = window.getSelection();

			// check if the caret is at the front position of a non-empty `<kbd>`
			if (selection.isCollapsed) {
				node = null;

				var kbd = selection.anchorNode;
				if (kbd.nodeType === Node.TEXT_NODE) kbd = kbd.parentNode;

				var isAtStart = false;
				if (kbd.nodeName === 'KBD' && kbd.textContent.replace(/\u200b/g, '') !== '') {
					var anchorNode = selection.anchorNode;
					var anchorOffset = selection.anchorOffset;
					if (anchorNode.nodeType === Node.TEXT_NODE && anchorOffset === 0) {
						node = anchorNode;
					}
					else if (anchorNode === kbd) {
						if (anchorOffset === 0) {
							isAtStart = true;
						}
						else {
							node = kbd.childNodes[anchorOffset - 1];
						}
					}
				}

				if (isAtStart === false && node !== null) {
					var childNode;
					for (var i = 0, length = kbd.childNodes.length; i < length; i++) {
						childNode = kbd.childNodes[i];
						if (childNode === node) {
							isAtStart = true;
							break;
						}
						else if (childNode.nodeType !== Node.TEXT_NODE || childNode.textContent.replace(/\u200b/g, '') !== '') {
							break;
						}
					}
				}

				if (isAtStart) {
					var sibling = kbd.previousSibling;
					if (sibling === null || sibling.nodeType !== Node.TEXT_NODE || sibling.textContent !== '\u200b') {
						sibling = document.createTextNode('\u200b');
						kbd.parentNode.insertBefore(sibling, kbd);
					}

					this.caret.before(kbd);
					return;
				}
			}

			this.button.toggle({}, 'kbd', 'func', 'inline.format');

			node = selection.anchorNode;
			if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;

			if (node.nodeName === 'KBD') {
				var nextSibling = node.nextSibling;

				if (_environment.platform() === 'ios' && _environment.browser() === 'safari') {
					// iOS Safari work-around to allow caret placement after <kbd>
					if (nextSibling && nextSibling.nodeName === 'BR') {
						node.parentNode.insertBefore(document.createTextNode('\u200B'), nextSibling);
					}

					// fix selection position
					/*if (node.innerHTML === '\u200B') {
						// the caret must be at offset 0 (before the whitespace)
						var range = selection.getRangeAt(0);
						if (range.collapsed && range.startOffset === 1) {
							node.innerHTML = this.marker.html() + '\u200B';

							this.selection.restore();
						}
					}*/

					return;
				}

				while (nextSibling) {
					if (nextSibling.nodeType !== Node.TEXT_NODE || nextSibling.textContent.length) {
						return;
					}

					nextSibling = nextSibling.nextSibling;
				}

				if (nextSibling) {
					nextSibling.textContent = '\u200B';
				}
				else {
					node.parentNode.appendChild(document.createTextNode('\u200B'));
				}
			}
		}
	};
};
<|endoftext|>


<commit_before>/**
 * Created by ksinger on 24.08.2015.
 */
import angular from "angular";
import ngAnimate from "angular-animate";

import "ng-redux";
import labelledHrModule from "./labelled-hr.js";
import imageDropzoneModule from "./image-dropzone.js";
import matchingContextModule from "./details/matching-context-picker.js"; // TODO: should be renamed
import createIsseeksModule from "./create-isseeks.js";
import { postTitleCharacterLimit } from "config";
import { get, getIn, attach, delay } from "../utils.js";
import { actionCreators } from "../actions/actions.js";
import won from "../won-es6.js";
import { connect2Redux } from "../won-utils.js";
import { selectIsConnected } from "../selectors.js";

// import { details } from "detailDefinitions";
import { useCases } from "useCaseDefinitions";
// TODO: these should be replaced by importing defintions from config
import descriptionPickerModule from "./details/description-picker.js";
import locationPickerModule from "./details/location-picker.js";
import personPickerModule from "./details/person-picker.js";
import routePickerModule from "./details/route-picker.js";
import tagsPickerModule from "./details/tags-picker.js";
import ttlPickerModule from "./details/ttl-picker.js";

const postTypeTexts = [
  {
    type: won.WON.BasicNeedTypeDemand,
    text: "I'm looking for",
    helpText:
      "Select this if you are looking for things or services other people offer",
  },
  {
    type: won.WON.BasicNeedTypeSupply,
    text: "I'm offering",
    helpText:
      "Use this if you are offering an item or a service. You will find people who said" +
      " that they're looking for something.",
  },
  {
    type: won.WON.BasicNeedTypeDotogether,
    text: "I want to find someone to",
    helpText:
      "Select this if you are looking to find other people who share your interest. You will be matched" +
      " with other people who chose this option as well.",
  },
  {
    type: won.WON.BasicNeedTypeCombined,
    text: "I'm offering and looking for",
    helpText:
      "Select this if you are looking for things or services other people offer + Use this if you are offering an item or a service. You will find people who said" +
      " that they're looking for something.",
  },
];

//TODO can't inject $scope with the angular2-router, preventing redux-cleanup
const serviceDependencies = [
  "$ngRedux",
  "$scope",
  "$element" /*'$routeParams' /*injections as strings here*/,
];

function genComponentConf() {
  const template = `

                <svg style="--local-primary:var(--won-primary-color);"
                    class="cp__header__back__icon">
                    <use xlink:href="#ico36_backarrow" href="#ico36_backarrow"></use>
                </svg>

            {{self.useCase.label}}

            <won-create-isseeks
                ng-if="self.useCase.isDetails"
                is-or-seeks="::'Description'"
                detail-list="self.useCase.isDetails"
                initial-draft="self.useCase.draft.is"
                on-update="::self.updateDraft(draft, 'is')"
                on-scroll="::self.scrollToBottom(element)">
            </won-create-isseeks>
            <won-create-isseeks
                ng-if="self.useCase.seeksDetails"
                is-or-seeks="::'Search'"
                detail-list="self.useCase.seeksDetails"
                initial-draft="self.useCase.draft.seeks"
                on-update="::self.updateDraft(draft, 'seeks')"
                on-scroll="::self.scrollToBottom(element)">
            </won-create-isseeks>

                    Tune Matching Behaviour
                    <svg class="cp__content__tuning__title__carret" ng-show="!self.showTuningOptions">
                        <use xlink:href="#ico16_arrow_down" href="#ico16_arrow_down"></use>
                    </svg>
                    <svg class="cp__content__tuning__title__carret" ng-show="self.showTuningOptions">
                        <use xlink:href="#ico16_arrow_up" href="#ico16_arrow_up"></use>
                    </svg>

                    <won-matching-context-picker
                      ng-if="self.showTuningOptions"
                      default-matching-context="::self.defaultMatchingContext"
                      initial-matching-context="::self.draftObject.matchingContext"
                      on-matching-context-updated="::self.updateMatchingContext(matchingContext)">
                    </won-matching-context-picker>

            <won-labelled-hr label="::'done?'" class="cp__content__labelledhr show-in-responsive"></won-labelled-hr>
            <button type="submit" class="won-button--filled red cp__content__publish show-in-responsive"
                    ng-disabled="!self.isValid()"
                    ng-click="::self.publish()">

                    Publish

                    Publishing …

            </button>

            <won-labelled-hr label="::'done?'" class="cp__footer__labelledhr"></won-labelled-hr>
            <button type="submit" class="won-button--filled red cp__footer__publish"
                    ng-disabled="!self.isValid()"
                    ng-click="::self.publish()">

                    Publish

                    Publishing …

            </button>

    `;

  class Controller {
    constructor(/* arguments <- serviceDependencies */) {
      attach(this, serviceDependencies, arguments);

      this.SEARCH = "search";
      this.POST = "post";

      this.focusedElement = null;
      //TODO debug; deleteme
      window.cnc4dbg = this;

      this.postTypeTexts = postTypeTexts;
      this.characterLimit = postTitleCharacterLimit;

      this.windowHeight = window.screen.height;
      this.scrollContainer().addEventListener("scroll", e => this.onResize(e));

      this.draftObject = {};

      this.showTuningOptions = false;

      this.pendingPublishing = false;
      this.details = { is: [], seeks: [] };
      this.isNew = true;

      const selectFromState = state => {
        const showCreateView = getIn(state, [
          "router",
          "currentParams",
          "showCreateView",
        ]);
        const useCaseString = getIn(state, [
          "router",
          "currentParams",
          "useCase",
        ]);
        const isSearch = showCreateView === this.SEARCH;
        const isPost = showCreateView && !isSearch;

        // needed to be able to reset matching context to default
        // TODO: is there an easier way to do this?
        const defaultMatchingContextList = getIn(state, [
          "config",
          "theme",
          "defaultContext",
        ]);
        const defaultMatchingContext = defaultMatchingContextList
          ? defaultMatchingContextList.toJS()
          : [];

        return {
          connectionHasBeenLost: !selectIsConnected(state),
          showCreateView,
          useCaseString,
          useCase: this.getUseCase(useCaseString),
          isSearch,
          isPost,
          defaultMatchingContext: defaultMatchingContext,
        };
      };

      // TODO: think about how to deal with contexts predefined in usecases
      delay(0).then(() => {
        this.updateMatchingContext(this.defaultMatchingContext);
        this.loadInitialDraft();
      });

      // Using actionCreators like this means that every action defined there is available in the template.
      connect2Redux(selectFromState, actionCreators, [], this);
    }

    onResize() {
      //TODO: delete
      //console.log("ResizeEvent: ", window.screen.height);
      if (this.focusedElement) {
        if (this.windowHeight < window.screen.height) {
          this.windowHeight < window.screen.height;
          this.scrollToBottom(this.focusedElement);
        } else {
          this.windowHeight = window.screen.height;
        }
      }
    }

    scrollToBottom(element) {
      this._programmaticallyScrolling = true;

      if (element) {
        let heightHeader =
          this.$element[0].querySelector(".cp__header").offsetHeight + 10;
        let scrollTop = this.$element[0].querySelector(element).offsetTop;
        this.scrollContainer().scrollTop = scrollTop - heightHeader;

        this.focusedElement = element;
        //TODO: debug: delete me
        //console.log("ScrollTop: ", this.scrollContainer().scrollTop);
      }
    }

    scrollContainer() {
      if (!this._scrollContainer) {
        this._scrollContainer = this.$element[0].querySelector(".cp__content");
      }
      return this._scrollContainer;
    }

    getUseCase(useCaseString) {
      if (useCaseString) {
        for (const useCaseName in useCases) {
          if (useCaseString === useCases[useCaseName]["identifier"]) {
            return useCases[useCaseName];
          }
        }
      }
      return undefined;
    }

    toggleTuningOptions() {
      // if (!this.showTuningOptions) {
      //   this.onScroll({ element: ".cis__addDetail__header.b" });
      // }
      this.showTuningOptions = !this.showTuningOptions;
    }

    isValid() {
      const draft = this.draftObject;
      const hasContent = get(draft, "is") || get(draft, "seeks");
      const hasValidIs = this.isOrSeeksIsValid(get(draft, "is"));
      const hasValidSeeks = this.isOrSeeksIsValid(get(draft, "seeks"));
      return (
        !this.connectionHasBeenLost &&
        hasContent &&
        (hasValidIs || hasValidSeeks)
      );
    }

    loadInitialDraft() {
      // just to be sure this is loaded already
      if (!this.useCase) {
        this.getUseCase(this.useCaseString);
      }

      if (this.useCase && this.useCase.draft) {
        // deep clone of draft
        this.draftObject = JSON.parse(JSON.stringify(this.useCase.draft));
      }
    }

    updateMatchingContext(matchingContext) {
      // also accepts []!
      if (matchingContext && this.draftObject.matchingContext) {
        const combinedContext = [
          ...matchingContext,
          ...this.draftObject.matchingContext,
        ].reduce(function(a, b) {
          if (a.indexOf(b) < 0) a.push(b);
          return a;
        }, []);

        this.draftObject.matchingContext = combinedContext;
      } else if (matchingContext) {
        this.draftObject.matchingContext = matchingContext;
      }
    }

    updateDraft(updatedDraft, isSeeks) {
      if (this.isNew) {
        this.isNew = false;
      }

      this.draftObject[isSeeks] = updatedDraft;
    }

    publish() {
      // Post both needs
      if (!this.pendingPublishing) {
        this.pendingPublishing = true;

        // TODO: This should be a usecase/done via a different component
        // Check if this is a search
        const searchString = this.checkForSearchString(this.draftObject);
        if (searchString) {
          this.draftObject.searchString = searchString;
          //delete this.draftObject.is;
        }

        const draft = this.getPublishObject(this.draftObject);

        this.needs__create(
          draft,
          this.$ngRedux.getState().getIn(["config", "defaultNodeUri"])
        );
      }
    }

    getPublishObject(draft) {
      if (!this.isOrSeeksIsValid(draft.is)) {
        delete draft.is;
      }
      if (!this.isOrSeeksIsValid(draft.seeks)) {
        delete draft.seeks;
      }
      return draft;
    }

    // returns true if the part has a valid title or any other detail
    isOrSeeksIsValid(isOrSeeks) {
      if (!isOrSeeks) {
        return false;
      }

      const title = get(isOrSeeks, "title");
      const hasValidTitle = title && title.length < this.characterLimit;

      let hasDetail = false;
      const details = Object.keys(isOrSeeks);
      for (let d of details) {
        if (isOrSeeks[d] && d !== "type") {
          hasDetail = true;
        }
      }
      return !!(hasValidTitle || hasDetail);
    }

    checkForSearchString(draft) {
      // draft has an is part -> not a pure search
      if (this.isOrSeeksIsValid(draft.is)) {
        return undefined;
      }

      // draft has no valid seeks part -> not a pure search
      if (!draft.seeks || !draft.seeks.title) {
        return undefined;
      }

      for (let detail of Object.keys(draft.seeks)) {
        if (detail !== "title" && detail !== "type" && draft.seeks[detail]) {
          return undefined;
        }
      }

      return draft.seeks.title;
    }

    createWhatsNew() {
      if (!this.pendingPublishing) {
        this.pendingPublishing = true;
        this.needs__whatsNew();
      }
    }
  }

  Controller.$inject = serviceDependencies;

  return {
    restrict: "E",
    controller: Controller,
    controllerAs: "self",
    bindToController: true, //scope-bindings -> ctrl
    scope: {
      /*scope-isolation*/
    },
    template: template,
  };
}

export default //.controller('CreateNeedController', [...serviceDependencies, CreateNeedController])
angular
  .module("won.owner.components.createPost", [
    labelledHrModule,
    imageDropzoneModule,
    descriptionPickerModule,
    locationPickerModule,
    personPickerModule,
    routePickerModule,
    tagsPickerModule,
    ttlPickerModule,
    createIsseeksModule,
    matchingContextModule,
    ngAnimate,
  ])
  .directive("wonCreatePost", genComponentConf).name;
<commit_msg>implemented correct back button link of createpost (goes back to the usecasepicker) - always visible now, not only in responsive mode<commit_after>/**
 * Created by ksinger on 24.08.2015.
 */
import angular from "angular";
import ngAnimate from "angular-animate";

import "ng-redux";
import labelledHrModule from "./labelled-hr.js";
import imageDropzoneModule from "./image-dropzone.js";
import matchingContextModule from "./details/matching-context-picker.js"; // TODO: should be renamed
import createIsseeksModule from "./create-isseeks.js";
import { postTitleCharacterLimit } from "config";
import { get, getIn, attach, delay } from "../utils.js";
import { actionCreators } from "../actions/actions.js";
import won from "../won-es6.js";
import { connect2Redux } from "../won-utils.js";
import { selectIsConnected } from "../selectors.js";

// import { details } from "detailDefinitions";
import { useCases } from "useCaseDefinitions";
// TODO: these should be replaced by importing defintions from config
import descriptionPickerModule from "./details/description-picker.js";
import locationPickerModule from "./details/location-picker.js";
import personPickerModule from "./details/person-picker.js";
import routePickerModule from "./details/route-picker.js";
import tagsPickerModule from "./details/tags-picker.js";
import ttlPickerModule from "./details/ttl-picker.js";

const postTypeTexts = [
  {
    type: won.WON.BasicNeedTypeDemand,
    text: "I'm looking for",
    helpText:
      "Select this if you are looking for things or services other people offer",
  },
  {
    type: won.WON.BasicNeedTypeSupply,
    text: "I'm offering",
    helpText:
      "Use this if you are offering an item or a service. You will find people who said" +
      " that they're looking for something.",
  },
  {
    type: won.WON.BasicNeedTypeDotogether,
    text: "I want to find someone to",
    helpText:
      "Select this if you are looking to find other people who share your interest. You will be matched" +
      " with other people who chose this option as well.",
  },
  {
    type: won.WON.BasicNeedTypeCombined,
    text: "I'm offering and looking for",
    helpText:
      "Select this if you are looking for things or services other people offer + Use this if you are offering an item or a service. You will find people who said" +
      " that they're looking for something.",
  },
];

//TODO can't inject $scope with the angular2-router, preventing redux-cleanup
const serviceDependencies = [
  "$ngRedux",
  "$scope",
  "$element" /*'$routeParams' /*injections as strings here*/,
];

function genComponentConf() {
  const template = `

                <svg style="--local-primary:var(--won-primary-color);"
                    class="cp__header__back__icon">
                    <use xlink:href="#ico36_backarrow" href="#ico36_backarrow"></use>
                </svg>

            {{self.useCase.label}}

            <won-create-isseeks
                ng-if="self.useCase.isDetails"
                is-or-seeks="::'Description'"
                detail-list="self.useCase.isDetails"
                initial-draft="self.useCase.draft.is"
                on-update="::self.updateDraft(draft, 'is')"
                on-scroll="::self.scrollToBottom(element)">
            </won-create-isseeks>
            <won-create-isseeks
                ng-if="self.useCase.seeksDetails"
                is-or-seeks="::'Search'"
                detail-list="self.useCase.seeksDetails"
                initial-draft="self.useCase.draft.seeks"
                on-update="::self.updateDraft(draft, 'seeks')"
                on-scroll="::self.scrollToBottom(element)">
            </won-create-isseeks>

                    Tune Matching Behaviour
                    <svg class="cp__content__tuning__title__carret" ng-show="!self.showTuningOptions">
                        <use xlink:href="#ico16_arrow_down" href="#ico16_arrow_down"></use>
                    </svg>
                    <svg class="cp__content__tuning__title__carret" ng-show="self.showTuningOptions">
                        <use xlink:href="#ico16_arrow_up" href="#ico16_arrow_up"></use>
                    </svg>

                    <won-matching-context-picker
                      ng-if="self.showTuningOptions"
                      default-matching-context="::self.defaultMatchingContext"
                      initial-matching-context="::self.draftObject.matchingContext"
                      on-matching-context-updated="::self.updateMatchingContext(matchingContext)">
                    </won-matching-context-picker>

            <won-labelled-hr label="::'done?'" class="cp__content__labelledhr show-in-responsive"></won-labelled-hr>
            <button type="submit" class="won-button--filled red cp__content__publish show-in-responsive"
                    ng-disabled="!self.isValid()"
                    ng-click="::self.publish()">

                    Publish

                    Publishing …

            </button>

            <won-labelled-hr label="::'done?'" class="cp__footer__labelledhr"></won-labelled-hr>
            <button type="submit" class="won-button--filled red cp__footer__publish"
                    ng-disabled="!self.isValid()"
                    ng-click="::self.publish()">

                    Publish

                    Publishing …

            </button>

    `;

  class Controller {
    constructor(/* arguments <- serviceDependencies */) {
      attach(this, serviceDependencies, arguments);

      this.SEARCH = "search";
      this.POST = "post";

      this.focusedElement = null;
      //TODO debug; deleteme
      window.cnc4dbg = this;

      this.postTypeTexts = postTypeTexts;
      this.characterLimit = postTitleCharacterLimit;

      this.windowHeight = window.screen.height;
      this.scrollContainer().addEventListener("scroll", e => this.onResize(e));

      this.draftObject = {};

      this.showTuningOptions = false;

      this.pendingPublishing = false;
      this.details = { is: [], seeks: [] };
      this.isNew = true;

      const selectFromState = state => {
        const showCreateView = getIn(state, [
          "router",
          "currentParams",
          "showCreateView",
        ]);
        const useCaseString = getIn(state, [
          "router",
          "currentParams",
          "useCase",
        ]);
        const isSearch = showCreateView === this.SEARCH;
        const isPost = showCreateView && !isSearch;

        // needed to be able to reset matching context to default
        // TODO: is there an easier way to do this?
        const defaultMatchingContextList = getIn(state, [
          "config",
          "theme",
          "defaultContext",
        ]);
        const defaultMatchingContext = defaultMatchingContextList
          ? defaultMatchingContextList.toJS()
          : [];

        return {
          connectionHasBeenLost: !selectIsConnected(state),
          showCreateView,
          useCaseString,
          useCase: this.getUseCase(useCaseString),
          isSearch,
          isPost,
          defaultMatchingContext: defaultMatchingContext,
        };
      };

      // TODO: think about how to deal with contexts predefined in usecases
      delay(0).then(() => {
        this.updateMatchingContext(this.defaultMatchingContext);
        this.loadInitialDraft();
      });

      // Using actionCreators like this means that every action defined there is available in the template.
      connect2Redux(selectFromState, actionCreators, [], this);
    }

    onResize() {
      //TODO: delete
      //console.log("ResizeEvent: ", window.screen.height);
      if (this.focusedElement) {
        if (this.windowHeight < window.screen.height) {
          this.windowHeight < window.screen.height;
          this.scrollToBottom(this.focusedElement);
        } else {
          this.windowHeight = window.screen.height;
        }
      }
    }

    scrollToBottom(element) {
      this._programmaticallyScrolling = true;

      if (element) {
        let heightHeader =
          this.$element[0].querySelector(".cp__header").offsetHeight + 10;
        let scrollTop = this.$element[0].querySelector(element).offsetTop;
        this.scrollContainer().scrollTop = scrollTop - heightHeader;

        this.focusedElement = element;
        //TODO: debug: delete me
        //console.log("ScrollTop: ", this.scrollContainer().scrollTop);
      }
    }

    scrollContainer() {
      if (!this._scrollContainer) {
        this._scrollContainer = this.$element[0].querySelector(".cp__content");
      }
      return this._scrollContainer;
    }

    getUseCase(useCaseString) {
      if (useCaseString) {
        for (const useCaseName in useCases) {
          if (useCaseString === useCases[useCaseName]["identifier"]) {
            return useCases[useCaseName];
          }
        }
      }
      return undefined;
    }

    toggleTuningOptions() {
      // if (!this.showTuningOptions) {
      //   this.onScroll({ element: ".cis__addDetail__header.b" });
      // }
      this.showTuningOptions = !this.showTuningOptions;
    }

    isValid() {
      const draft = this.draftObject;
      const hasContent = get(draft, "is") || get(draft, "seeks");
      const hasValidIs = this.isOrSeeksIsValid(get(draft, "is"));
      const hasValidSeeks = this.isOrSeeksIsValid(get(draft, "seeks"));
      return (
        !this.connectionHasBeenLost &&
        hasContent &&
        (hasValidIs || hasValidSeeks)
      );
    }

    loadInitialDraft() {
      // just to be sure this is loaded already
      if (!this.useCase) {
        this.getUseCase(this.useCaseString);
      }

      if (this.useCase && this.useCase.draft) {
        // deep clone of draft
        this.draftObject = JSON.parse(JSON.stringify(this.useCase.draft));
      }
    }

    updateMatchingContext(matchingContext) {
      // also accepts []!
      if (matchingContext && this.draftObject.matchingContext) {
        const combinedContext = [
          ...matchingContext,
          ...this.draftObject.matchingContext,
        ].reduce(function(a, b) {
          if (a.indexOf(b) < 0) a.push(b);
          return a;
        }, []);

        this.draftObject.matchingContext = combinedContext;
      } else if (matchingContext) {
        this.draftObject.matchingContext = matchingContext;
      }
    }

    updateDraft(updatedDraft, isSeeks) {
      if (this.isNew) {
        this.isNew = false;
      }

      this.draftObject[isSeeks] = updatedDraft;
    }

    publish() {
      // Post both needs
      if (!this.pendingPublishing) {
        this.pendingPublishing = true;

        // TODO: This should be a usecase/done via a different component
        // Check if this is a search
        const searchString = this.checkForSearchString(this.draftObject);
        if (searchString) {
          this.draftObject.searchString = searchString;
          //delete this.draftObject.is;
        }

        const draft = this.getPublishObject(this.draftObject);

        this.needs__create(
          draft,
          this.$ngRedux.getState().getIn(["config", "defaultNodeUri"])
        );
      }
    }

    getPublishObject(draft) {
      if (!this.isOrSeeksIsValid(draft.is)) {
        delete draft.is;
      }
      if (!this.isOrSeeksIsValid(draft.seeks)) {
        delete draft.seeks;
      }
      return draft;
    }

    // returns true if the part has a valid title or any other detail
    isOrSeeksIsValid(isOrSeeks) {
      if (!isOrSeeks) {
        return false;
      }

      const title = get(isOrSeeks, "title");
      const hasValidTitle = title && title.length < this.characterLimit;

      let hasDetail = false;
      const details = Object.keys(isOrSeeks);
      for (let d of details) {
        if (isOrSeeks[d] && d !== "type") {
          hasDetail = true;
        }
      }
      return !!(hasValidTitle || hasDetail);
    }

    checkForSearchString(draft) {
      // draft has an is part -> not a pure search
      if (this.isOrSeeksIsValid(draft.is)) {
        return undefined;
      }

      // draft has no valid seeks part -> not a pure search
      if (!draft.seeks || !draft.seeks.title) {
        return undefined;
      }

      for (let detail of Object.keys(draft.seeks)) {
        if (detail !== "title" && detail !== "type" && draft.seeks[detail]) {
          return undefined;
        }
      }

      return draft.seeks.title;
    }

    createWhatsNew() {
      if (!this.pendingPublishing) {
        this.pendingPublishing = true;
        this.needs__whatsNew();
      }
    }
  }

  Controller.$inject = serviceDependencies;

  return {
    restrict: "E",
    controller: Controller,
    controllerAs: "self",
    bindToController: true, //scope-bindings -> ctrl
    scope: {
      /*scope-isolation*/
    },
    template: template,
  };
}

export default //.controller('CreateNeedController', [...serviceDependencies, CreateNeedController])
angular
  .module("won.owner.components.createPost", [
    labelledHrModule,
    imageDropzoneModule,
    descriptionPickerModule,
    locationPickerModule,
    personPickerModule,
    routePickerModule,
    tagsPickerModule,
    ttlPickerModule,
    createIsseeksModule,
    matchingContextModule,
    ngAnimate,
  ])
  .directive("wonCreatePost", genComponentConf).name;
<|endoftext|>


<commit_before>let {
  parseTests,
  parseTestsWithQueryString,
  formatTests,
  relativeTests,
  relativeTests2,
} = require('../third_party/test_cases');
let url = require('../dist');

parseTests = {
  ...parseTests,
  'https://google.com/hello?test=space%20': {
    protocol: 'https:',
    slashes: true,
    host: 'google.com',
    hostname: 'google.com',
    search: '?test=space%20',
    query: 'test=space%20',
    pathname: '/hello',
    path: '/hello?test=space%20',
    href: 'https://google.com/hello?test=space%20',
  },
  '?percent=%25': {
    protocol: null,
    slashes: null,
    auth: null,
    host: null,
    port: null,
    hostname: null,
    hash: null,
    search: '?percent=%25',
    query: 'percent=%25',
    pathname: null,
    path: '?percent=%25',
    href: '?percent=%25',
  },
  [`?lf=before${encodeURI('\n')}after`]: {
    protocol: null,
    slashes: null,
    auth: null,
    host: null,
    port: null,
    hostname: null,
    hash: null,
    search: '?lf=before%0Aafter',
    query: 'lf=before%0Aafter',
    pathname: null,
    path: '?lf=before%0Aafter',
    href: '?lf=before%0Aafter',
  },
  'https://www.w.org': {
    protocol: 'https:',
    slashes: true,
    auth: null,
    host: 'www.w.org',
    port: null,
    hostname: 'www.w.org',
    hash: null,
    search: null,
    query: null,
    pathname: '/',
    path: '/',
    href: 'https://www.w.org/',
  },
  'https://www.wikipedia.org/': {
    protocol: 'https:',
    slashes: true,
    auth: null,
    host: 'www.wikipedia.org',
    port: null,
    hostname: 'www.wikipedia.org',
    hash: null,
    search: null,
    query: null,
    pathname: '/',
    path: '/',
    href: 'https://www.wikipedia.org/',
  },
};

describe('Basic parse and format:', () => {
  Object.keys(parseTests).forEach(function (u) {
    it(`parse(${u}):`, () => {
      const actual = url.parse(u);
      const spaced = url.parse('     \t  ' + u + '\n\t');
      const expected = parseTests[u];

      Object.keys(actual).forEach((i) => {
        if (expected[i] === undefined && actual[i] === null) {
          expected[i] = null;
        }
      });

      // The parsed object is an instanceof Url
      // Jasmine's toEqual fails when comparing an instance with and object
      expect({ ...actual }).toEqual(expected);
      expect({ ...spaced }).toEqual(expected);
    });

    it(`format(${u}):`, () => {
      const expected = parseTests[u].href;
      const actual = url.format(parseTests[u]);

      expect(actual).toEqual(expected);
    });
  });
});

describe('With querystring:', () => {
  Object.keys(parseTestsWithQueryString).forEach(function (u) {
    it(`parse(${u}):`, () => {
      const actual = url.parse(u, true);
      const expected = parseTestsWithQueryString[u];

      Object.keys(actual).forEach((i) => {
        if (expected[i] === undefined && actual[i] === null) {
          expected[i] = null;
        }
      });

      expect({ ...actual }).toEqual(expected);
    });
  });
});

describe('Test format():', () => {
  Object.keys(formatTests).forEach(function (u) {
    const expected = formatTests[u].href;
    it(`format(${u}):`, () => {
      const actual = url.format(u);

      expect(actual).toBe(expected);
    });

    it(`format(${JSON.stringify(formatTests[u])})`, () => {
      delete formatTests[u].href;
      const actualObj = url.format(formatTests[u]);

      expect(actualObj).toBe(expected);
    });
  });
});

describe('Test resolve():', () => {
  relativeTests.forEach(function (relativeTest) {
    it(`resolve(${relativeTest[0]}, ${relativeTest[1]}):`, () => {
      const actual = url.resolve(relativeTest[0], relativeTest[1]);
      const expected = relativeTest[2];

      expect(actual).toBe(expected);
    });
  });
});

// https://github.com/joyent/node/issues/568
[undefined, null, true, false, 0.0, 0, [], {}].forEach(function (val) {
  describe('Test [undefined, null, true, false, 0.0, 0, [], {}] values:', () => {
    it(`parse(${val})`, () => {
      expect(() => {
        url.parse(val);
      }).toThrow();
    });
  });
});

describe('Test wonky resolves:', () => {
  relativeTests2.forEach(function (relativeTest) {
    it(`resolve(${relativeTest[1]}, ${relativeTest[0]}):`, () => {
      const actual = url.resolve(relativeTest[1], relativeTest[0]);
      const expected = relativeTest[2];

      expect(actual).toBe(expected);
    });
  });
});

describe('Test resolveObject:', () => {
  relativeTests.forEach(function (relativeTest) {
    it(`resolveObject(${relativeTest[0]}, ${relativeTest[1]}):`, () => {
      let actual = url.resolveObject(relativeTest[0], relativeTest[1]);
      let expected = url.parse(relativeTest[2]);

      expect(actual).toEqual(expected);

      expected = relativeTest[2];
      actual = url.format(actual);

      expect(actual).toBe(expected);
    });
  });
});

//format: [to, from, result]
// the test: ['.//g', 'f:/a', 'f://g'] is a fundamental problem
// url.parse('f:/a') does not have a host
// url.resolve('f:/a', './/g') does not have a host because you have moved
// down to the g directory.  i.e. f:     //g, however when this url is parsed
// f:// will indicate that the host is g which is not the case.
// it is unclear to me how to keep this information from being lost
// it may be that a pathname of ////g should collapse to /g but this seems
// to be a lot of work for an edge case.  Right now I remove the test
if (
  relativeTests2[181][0] === './/g' &&
  relativeTests2[181][1] === 'f:/a' &&
  relativeTests2[181][2] === 'f://g'
) {
  relativeTests2.splice(181, 1);
}

describe('Test wonky resolveObject:', () => {
  relativeTests2.forEach(function (relativeTest) {
    it(`resolveObject(${relativeTest[1]}, ${relativeTest[0]}):`, () => {
      let actual = url.resolveObject(
        url.parse(relativeTest[1]),
        relativeTest[0]
      );
      let expected = url.parse(relativeTest[2]);

      expect(actual).toEqual(expected);

      expected = relativeTest[2];
      actual = url.format(actual);

      expect(actual).toBe(expected);
    });
  });
});
<commit_msg>Adding test to reparse Url instance<commit_after>let {
  parseTests,
  parseTestsWithQueryString,
  formatTests,
  relativeTests,
  relativeTests2,
} = require('../third_party/test_cases');
let url = require('../dist');

parseTests = {
  ...parseTests,
  'https://google.com/hello?test=space%20': {
    protocol: 'https:',
    slashes: true,
    host: 'google.com',
    hostname: 'google.com',
    search: '?test=space%20',
    query: 'test=space%20',
    pathname: '/hello',
    path: '/hello?test=space%20',
    href: 'https://google.com/hello?test=space%20',
  },
  '?percent=%25': {
    protocol: null,
    slashes: null,
    auth: null,
    host: null,
    port: null,
    hostname: null,
    hash: null,
    search: '?percent=%25',
    query: 'percent=%25',
    pathname: null,
    path: '?percent=%25',
    href: '?percent=%25',
  },
  [`?lf=before${encodeURI('\n')}after`]: {
    protocol: null,
    slashes: null,
    auth: null,
    host: null,
    port: null,
    hostname: null,
    hash: null,
    search: '?lf=before%0Aafter',
    query: 'lf=before%0Aafter',
    pathname: null,
    path: '?lf=before%0Aafter',
    href: '?lf=before%0Aafter',
  },
  'https://www.w.org': {
    protocol: 'https:',
    slashes: true,
    auth: null,
    host: 'www.w.org',
    port: null,
    hostname: 'www.w.org',
    hash: null,
    search: null,
    query: null,
    pathname: '/',
    path: '/',
    href: 'https://www.w.org/',
  },
  'https://www.wikipedia.org/': {
    protocol: 'https:',
    slashes: true,
    auth: null,
    host: 'www.wikipedia.org',
    port: null,
    hostname: 'www.wikipedia.org',
    hash: null,
    search: null,
    query: null,
    pathname: '/',
    path: '/',
    href: 'https://www.wikipedia.org/',
  },
};

describe('Basic parse and format:', () => {
  Object.keys(parseTests).forEach(function (u) {
    it(`parse(${u}):`, () => {
      const actual = url.parse(u);
      const spaced = url.parse('     \t  ' + u + '\n\t');
      const expected = parseTests[u];
      const reparsed = url.parse(actual);

      Object.keys(actual).forEach((i) => {
        if (expected[i] === undefined && actual[i] === null) {
          expected[i] = null;
        }
      });

      // The parsed object is an instanceof Url
      // Jasmine's toEqual fails when comparing an instance with and object
      expect({ ...actual }).toEqual(expected);
      expect({ ...spaced }).toEqual(expected);
      expect({ ...reparsed }).toEqual(expected);
    });

    it(`format(${u}):`, () => {
      const expected = parseTests[u].href;
      const actual = url.format(parseTests[u]);

      expect(actual).toEqual(expected);
    });
  });
});

describe('With querystring:', () => {
  Object.keys(parseTestsWithQueryString).forEach(function (u) {
    it(`parse(${u}):`, () => {
      const actual = url.parse(u, true);
      const expected = parseTestsWithQueryString[u];

      Object.keys(actual).forEach((i) => {
        if (expected[i] === undefined && actual[i] === null) {
          expected[i] = null;
        }
      });

      expect({ ...actual }).toEqual(expected);
    });
  });
});

describe('Test format():', () => {
  Object.keys(formatTests).forEach(function (u) {
    const expected = formatTests[u].href;
    it(`format(${u}):`, () => {
      const actual = url.format(u);

      expect(actual).toBe(expected);
    });

    it(`format(${JSON.stringify(formatTests[u])})`, () => {
      delete formatTests[u].href;
      const actualObj = url.format(formatTests[u]);

      expect(actualObj).toBe(expected);
    });
  });
});

describe('Test resolve():', () => {
  relativeTests.forEach(function (relativeTest) {
    it(`resolve(${relativeTest[0]}, ${relativeTest[1]}):`, () => {
      const actual = url.resolve(relativeTest[0], relativeTest[1]);
      const expected = relativeTest[2];

      expect(actual).toBe(expected);
    });
  });
});

// https://github.com/joyent/node/issues/568
[undefined, null, true, false, 0.0, 0, [], {}].forEach(function (val) {
  describe('Test [undefined, null, true, false, 0.0, 0, [], {}] values:', () => {
    it(`parse(${val})`, () => {
      expect(() => {
        url.parse(val);
      }).toThrow();
    });
  });
});

describe('Test wonky resolves:', () => {
  relativeTests2.forEach(function (relativeTest) {
    it(`resolve(${relativeTest[1]}, ${relativeTest[0]}):`, () => {
      const actual = url.resolve(relativeTest[1], relativeTest[0]);
      const expected = relativeTest[2];

      expect(actual).toBe(expected);
    });
  });
});

describe('Test resolveObject:', () => {
  relativeTests.forEach(function (relativeTest) {
    it(`resolveObject(${relativeTest[0]}, ${relativeTest[1]}):`, () => {
      let actual = url.resolveObject(relativeTest[0], relativeTest[1]);
      let expected = url.parse(relativeTest[2]);

      expect(actual).toEqual(expected);

      expected = relativeTest[2];
      actual = url.format(actual);

      expect(actual).toBe(expected);
    });
  });
});

//format: [to, from, result]
// the test: ['.//g', 'f:/a', 'f://g'] is a fundamental problem
// url.parse('f:/a') does not have a host
// url.resolve('f:/a', './/g') does not have a host because you have moved
// down to the g directory.  i.e. f:     //g, however when this url is parsed
// f:// will indicate that the host is g which is not the case.
// it is unclear to me how to keep this information from being lost
// it may be that a pathname of ////g should collapse to /g but this seems
// to be a lot of work for an edge case.  Right now I remove the test
if (
  relativeTests2[181][0] === './/g' &&
  relativeTests2[181][1] === 'f:/a' &&
  relativeTests2[181][2] === 'f://g'
) {
  relativeTests2.splice(181, 1);
}

describe('Test wonky resolveObject:', () => {
  relativeTests2.forEach(function (relativeTest) {
    it(`resolveObject(${relativeTest[1]}, ${relativeTest[0]}):`, () => {
      let actual = url.resolveObject(
        url.parse(relativeTest[1]),
        relativeTest[0]
      );
      let expected = url.parse(relativeTest[2]);

      expect(actual).toEqual(expected);

      expected = relativeTest[2];
      actual = url.format(actual);

      expect(actual).toBe(expected);
    });
  });
});
<|endoftext|>