<commit_before>const { Plugin } = require('@uppy/core')
const Translator = require('@uppy/utils/lib/Translator')
const toArray = require('@uppy/utils/lib/toArray')
const dragDrop = require('drag-drop')
const { h } = require('preact')

/**
 * Drag & Drop plugin
 *
 */
module.exports = class DragDrop extends Plugin {
  constructor (uppy, opts) {
    super(uppy, opts)
    this.type = 'acquirer'
    this.id = this.opts.id || 'DragDrop'
    this.title = 'Drag & Drop'

    this.defaultLocale = {
      strings: {
        dropHereOr: 'Drop files here or %{browse}',
        browse: 'browse'
      }
    }

    // Default options
    const defaultOpts = {
      target: null,
      inputName: 'files[]',
      width: '100%',
      height: '100%',
      note: null
    }

    // Merge default options with the ones set by user
    this.opts = Object.assign({}, defaultOpts, opts)

    // Check for browser dragDrop support
    this.isDragDropSupported = this.checkDragDropSupport()

    // i18n
    this.translator = new Translator([ this.defaultLocale, this.uppy.locale, this.opts.locale ])
    this.i18n = this.translator.translate.bind(this.translator)
    this.i18nArray = this.translator.translateArray.bind(this.translator)

    // Bind `this` to class methods
    this.handleDrop = this.handleDrop.bind(this)
    this.handleInputChange = this.handleInputChange.bind(this)
    this.checkDragDropSupport = this.checkDragDropSupport.bind(this)
    this.render = this.render.bind(this)
  }

  /**
   * Checks if the browser supports Drag & Drop (not supported on mobile devices, for example).
   * @return {Boolean}
   */
  checkDragDropSupport () {
    const div = document.createElement('div')

    if (!('draggable' in div) || !('ondragstart' in div && 'ondrop' in div)) {
      return false
    }

    if (!('FormData' in window)) {
      return false
    }

    if (!('FileReader' in window)) {
      return false
    }

    return true
  }

  handleDrop (files) {
    this.uppy.log('[DragDrop] Files dropped')

    files.forEach((file) => {
      try {
        this.uppy.addFile({
          source: this.id,
          name: file.name,
          type: file.type,
          data: file
        })
      } catch (err) {
        // Nothing, restriction errors handled in Core
      }
    })
  }

  handleInputChange (ev) {
    this.uppy.log('[DragDrop] Files selected through input')

    const files = toArray(ev.target.files)

    files.forEach((file) => {
      try {
        this.uppy.addFile({
          source: this.id,
          name: file.name,
          type: file.type,
          data: file
        })
      } catch (err) {
        // Nothing, restriction errors handled in Core
      }
    })
  }

  render (state) {
    /* http://tympanus.net/codrops/2015/09/15/styling-customizing-file-inputs-smart-way/ */
    const hiddenInputStyle = {
      width: '0.1px',
      height: '0.1px',
      opacity: 0,
      overflow: 'hidden',
      position: 'absolute',
      zIndex: -1
    }
    const DragDropClass = `uppy-Root uppy-DragDrop-container ${this.isDragDropSupported ? 'uppy-DragDrop--is-dragdrop-supported' : ''}`
    const DragDropStyle = {
      width: this.opts.width,
      height: this.opts.height
    }
    const restrictions = this.uppy.opts.restrictions

    // empty value="" on file input, so that the input is cleared after a file is selected,
    // because Uppy will be handling the upload and so we can select same file
    // after removing — otherwise browser thinks it’s already selected
    return (

          <svg aria-hidden="true" class="UppyIcon uppy-DragDrop-arrow" width="16" height="16" viewBox="0 0 16 16">
            <path d="M11 10V0H5v10H2l6 6 6-6h-3zm0 0" fill-rule="evenodd" />
          </svg>

            <input style={hiddenInputStyle}
              class="uppy-DragDrop-input"
              type="file"
              name={this.opts.inputName}
              multiple={restrictions.maxNumberOfFiles !== 1}
              accept={restrictions.allowedFileTypes}
              ref={(input) => {
                this.input = input
              }}
              onchange={this.handleInputChange}
              value="" />
            {this.i18nArray('dropHereOr', {
              browse: {this.i18n('browse')}
            })}

          {this.opts.note}

    )
  }

  install () {
    const target = this.opts.target
    if (target) {
      this.mount(target, this)
    }
    this.removeDragDropListener = dragDrop(this.el, (files) => {
      this.handleDrop(files)
      this.uppy.log(files)
    })
  }

  uninstall () {
    this.unmount()
    this.removeDragDropListener()
  }
}
<commit_msg>@uppy/drag-drop - made .onBeforeFileAdded() be called twice even if file is the same<commit_after>const { Plugin } = require('@uppy/core')
const Translator = require('@uppy/utils/lib/Translator')
const toArray = require('@uppy/utils/lib/toArray')
const dragDrop = require('drag-drop')
const { h } = require('preact')

/**
 * Drag & Drop plugin
 *
 */
module.exports = class DragDrop extends Plugin {
  constructor (uppy, opts) {
    super(uppy, opts)
    this.type = 'acquirer'
    this.id = this.opts.id || 'DragDrop'
    this.title = 'Drag & Drop'

    this.defaultLocale = {
      strings: {
        dropHereOr: 'Drop files here or %{browse}',
        browse: 'browse'
      }
    }

    // Default options
    const defaultOpts = {
      target: null,
      inputName: 'files[]',
      width: '100%',
      height: '100%',
      note: null
    }

    // Merge default options with the ones set by user
    this.opts = Object.assign({}, defaultOpts, opts)

    // Check for browser dragDrop support
    this.isDragDropSupported = this.checkDragDropSupport()

    // i18n
    this.translator = new Translator([ this.defaultLocale, this.uppy.locale, this.opts.locale ])
    this.i18n = this.translator.translate.bind(this.translator)
    this.i18nArray = this.translator.translateArray.bind(this.translator)

    // Bind `this` to class methods
    this.handleDrop = this.handleDrop.bind(this)
    this.handleInputChange = this.handleInputChange.bind(this)
    this.checkDragDropSupport = this.checkDragDropSupport.bind(this)
    this.render = this.render.bind(this)
  }

  /**
   * Checks if the browser supports Drag & Drop (not supported on mobile devices, for example).
   * @return {Boolean}
   */
  checkDragDropSupport () {
    const div = document.createElement('div')

    if (!('draggable' in div) || !('ondragstart' in div && 'ondrop' in div)) {
      return false
    }

    if (!('FormData' in window)) {
      return false
    }

    if (!('FileReader' in window)) {
      return false
    }

    return true
  }

  handleDrop (files) {
    this.uppy.log('[DragDrop] Files dropped')

    files.forEach((file) => {
      try {
        this.uppy.addFile({
          source: this.id,
          name: file.name,
          type: file.type,
          data: file
        })
      } catch (err) {
        // Nothing, restriction errors handled in Core
      }
    })
  }

  handleInputChange (ev) {
    this.uppy.log('[DragDrop] Files selected through input')

    const files = toArray(ev.target.files)

    files.forEach((file) => {
      try {
        this.uppy.addFile({
          source: this.id,
          name: file.name,
          type: file.type,
          data: file
        })
      } catch (err) {
        // Nothing, restriction errors handled in Core
      }
    })

    // ___Why not use value="" on <input/> instead?
    //    Because if we use that method of clearing the input, Chrome will not trigger onChange={} if we drop the same file twice (Issue #768).
    event.target.value = null
  }

  render (state) {
    /* http://tympanus.net/codrops/2015/09/15/styling-customizing-file-inputs-smart-way/ */
    const hiddenInputStyle = {
      width: '0.1px',
      height: '0.1px',
      opacity: 0,
      overflow: 'hidden',
      position: 'absolute',
      zIndex: -1
    }
    const DragDropClass = `uppy-Root uppy-DragDrop-container ${this.isDragDropSupported ? 'uppy-DragDrop--is-dragdrop-supported' : ''}`
    const DragDropStyle = {
      width: this.opts.width,
      height: this.opts.height
    }
    const restrictions = this.uppy.opts.restrictions

    // empty value="" on file input, so that the input is cleared after a file is selected,
    // because Uppy will be handling the upload and so we can select same file
    // after removing — otherwise browser thinks it’s already selected
    return (

          <svg aria-hidden="true" class="UppyIcon uppy-DragDrop-arrow" width="16" height="16" viewBox="0 0 16 16">
            <path d="M11 10V0H5v10H2l6 6 6-6h-3zm0 0" fill-rule="evenodd" />
          </svg>

            <input style={hiddenInputStyle}
              class="uppy-DragDrop-input"
              type="file"
              name={this.opts.inputName}
              multiple={restrictions.maxNumberOfFiles !== 1}
              accept={restrictions.allowedFileTypes}
              onchange={this.handleInputChange} />
            {this.i18nArray('dropHereOr', {
              browse: {this.i18n('browse')}
            })}

          {this.opts.note}

    )
  }

  install () {
    const target = this.opts.target
    if (target) {
      this.mount(target, this)
    }
    this.removeDragDropListener = dragDrop(this.el, (files) => {
      this.handleDrop(files)
      this.uppy.log(files)
    })
  }

  uninstall () {
    this.unmount()
    this.removeDragDropListener()
  }
}
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2020 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.nanos.menu',
  name: 'VerticalMenu',
  extends: 'foam.u2.View',

  implements: [
    'foam.mlang.Expressions'
  ],

  imports: [
    'currentMenu',
    'menuListener',
    'loginSuccess',
    'menuDAO',
    'pushMenu'
  ],

  requires: [
    'foam.nanos.menu.Menu',
    'foam.nanos.menu.VerticalMenu',
    'foam.dao.ArraySink'
  ],

  css: `
  ^ input[type="search"] {
    width: 210px;
  }

  ^ .side-nav-view {
    font-size: medium!important;
    font-weight: normal;
    display: inline-block;
    position: absolute;
    height: calc(100vh - 80px);
    width: 250px;
    overflow-x: hidden;
    z-index: 100;
    font-size: 26px;
    color: /*%GREY2%*/ #9ba1a6;
    border-right: 1px solid /*%GREY4%*/ #e7eaec;
    background: /*%GREY5%*/ #f5f7fas;
  }

  .foam-u2-search-TextSearchView {
    text-align: center;
    margin: 14px 0 0;
  }

  ^ .foam-u2-view-TreeViewRow-label {
    font-weight: 300;
  }

  ^ .foam-u2-view-TreeViewRow {
    width: 100%;
  }
  ^ .tree-view-height-manager {
    margin-bottom: 40px;
  }
  `,

  properties: [
    {
      class: 'FObjectProperty',
      of: 'foam.u2.Element',
      name: 'subMenu',
      documentation: 'Used to store selected submenu element after window reload and scroll into parent view'
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'dao_',
      factory: function() {
        return this.menuDAO;
      }
    },
    {
      class: 'String',
      name: 'menuSearch',
      view: {
        class: 'foam.u2.TextField',
        type: 'search',
        onKey: true
      },
      value: ''
    }
  ],

  methods: [
    function initE() {
      var self = this;
      this
      .addClass(this.myClass())
      .start()
        .addClass('side-nav-view')
        .start()
          .startContext({ data: this })
          .start()
            .add(this.MENU_SEARCH)
            .addClass('foam-u2-search-TextSearchView')
          .end()
          .endContext()
          .start()
            .addClass('tree-view-height-manager')
            .tag({
              class: 'foam.u2.view.TreeView',
              data: self.dao_,
              relationship: foam.nanos.menu.MenuMenuChildrenRelationship,
              startExpanded: true,
              query: self.menuSearch$,
              onClickAddOn: function(data) { self.openMenu(data); },
              selection$: self.currentMenu$,
              formatter: function(data) { this.add({ data : data, clsInfo : data.cls_.LABEL.name, default : data.label }); }
            })
          .end()
        .end()
      .end();
    },

    function openMenu(menu) {
      if ( menu.handler ) {
        this.pushMenu(menu);
        this.menuListener(menu);
      }
    }
  ]
});
<commit_msg>Order Menus by order property in VerticalMenu.<commit_after>/**
 * @license
 * Copyright 2020 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.nanos.menu',
  name: 'VerticalMenu',
  extends: 'foam.u2.View',

  implements: [
    'foam.mlang.Expressions'
  ],

  imports: [
    'currentMenu',
    'menuListener',
    'loginSuccess',
    'menuDAO',
    'pushMenu'
  ],

  requires: [
    'foam.nanos.menu.Menu',
    'foam.nanos.menu.VerticalMenu',
    'foam.dao.ArraySink'
  ],

  css: `
  ^ input[type="search"] {
    width: 210px;
  }

  ^ .side-nav-view {
    font-size: medium!important;
    font-weight: normal;
    display: inline-block;
    position: absolute;
    height: calc(100vh - 80px);
    width: 250px;
    overflow-x: hidden;
    z-index: 100;
    font-size: 26px;
    color: /*%GREY2%*/ #9ba1a6;
    border-right: 1px solid /*%GREY4%*/ #e7eaec;
    background: /*%GREY5%*/ #f5f7fas;
  }

  .foam-u2-search-TextSearchView {
    text-align: center;
    margin: 14px 0 0;
  }

  ^ .foam-u2-view-TreeViewRow-label {
    font-weight: 300;
  }

  ^ .foam-u2-view-TreeViewRow {
    width: 100%;
  }
  ^ .tree-view-height-manager {
    margin-bottom: 40px;
  }
  `,

  properties: [
    {
      class: 'FObjectProperty',
      of: 'foam.u2.Element',
      name: 'subMenu',
      documentation: 'Used to store selected submenu element after window reload and scroll into parent view'
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'dao_',
      factory: function() {
        return this.menuDAO.orderBy(this.Menu.ORDER);
      }
    },
    {
      class: 'String',
      name: 'menuSearch',
      view: {
        class: 'foam.u2.TextField',
        type: 'search',
        onKey: true
      },
      value: ''
    }
  ],

  methods: [
    function initE() {
      var self = this;
      this
      .addClass(this.myClass())
      .start()
        .addClass('side-nav-view')
        .start()
          .startContext({ data: this })
          .start()
            .add(this.MENU_SEARCH)
            .addClass('foam-u2-search-TextSearchView')
          .end()
          .endContext()
          .start()
            .addClass('tree-view-height-manager')
            .tag({
              class: 'foam.u2.view.TreeView',
              data: self.dao_,
              relationship: foam.nanos.menu.MenuMenuChildrenRelationship,
              startExpanded: true,
              query: self.menuSearch$,
              onClickAddOn: function(data) { self.openMenu(data); },
              selection$: self.currentMenu$,
              formatter: function(data) { this.add({ data : data, clsInfo : data.cls_.LABEL.name, default : data.label }); }
            })
          .end()
        .end()
      .end();
    },

    function openMenu(menu) {
      if ( menu.handler ) {
        this.pushMenu(menu);
        this.menuListener(menu);
      }
    }
  ]
});
<|endoftext|>


<commit_before>// @flow

import React, { Component } from 'react';
import Cropper from 'react-cropper';
import 'cropperjs/dist/cropper.css';
import fetch from 'isomorphic-fetch';
import { imageApiUrl } from '../config';
import { getTokenFromLocalCookie } from '../lib/fetch';

type Props = {
  imageUrl?: string
};

type State = {
  ratio: number,
  imageApiBody?: any,
  existingParameters?: any,
  postResult?: string
};

class Crop extends Component<Props, State> {
  state = { ratio: 0.81, imageApiBody: null, existingParameters: null };

  toPercentages = c => {
    var data = c.getData();
    return {
      cropStartX: Math.max(
        0,
        Math.round((data.x / c.getImageData().naturalWidth) * 100)
      ),
      cropEndX: Math.min(
        100,
        Math.round(
          ((data.x + data.width) / c.getImageData().naturalWidth) * 100
        )
      ),
      cropStartY: Math.max(
        0,
        Math.round((data.y / c.getImageData().naturalHeight) * 100)
      ),

      cropEndY: Math.min(
        100,
        Math.round(
          ((data.y + data.height) / c.getImageData().naturalHeight) * 100
        )
      )
    };
  };

  toImageApiBody = pcnt => {
    const existingParametersForCurrentRatio =
      this.state.existingParameters &&
      this.state.existingParameters.find(
        p => p.forRatio === String(this.state.ratio)
      );
    const revision =
      (existingParametersForCurrentRatio &&
        existingParametersForCurrentRatio.revision) ||
      1;

    return {
      forRatio: String(this.state.ratio),
      revision: revision,
      imageUrl:
        this.props.imageUrl &&
        this.props.imageUrl.substr(this.props.imageUrl.lastIndexOf('/')),
      rawImageQueryParameters: {
        cropStartX: pcnt.cropStartX,
        cropEndX: pcnt.cropEndX,
        cropStartY: pcnt.cropStartY,
        cropEndY: pcnt.cropEndY
      }
    };
  };

  crop = () => {
    var pcnt = this.toPercentages(this.refs.cropper);
    this.setState({ imageApiBody: this.toImageApiBody(pcnt) });
  };

  postToImageApi = async () => {
    if (this.state.imageApiBody !== null) {
      this.setState({ postResult: 'Posting…' });
      const response = await fetch(`${imageApiUrl}/images/stored-parameters`, {
        method: 'POST',
        headers: {
          Authorization: 'Bearer ' + getTokenFromLocalCookie(),
          Accept: 'application/json'
        },
        body: JSON.stringify(this.state.imageApiBody)
      });

      if (response.ok) {
        this.setState({ postResult: 'Ok!' });
      } else if (response.status === 401) {
        this.setState({ postResult: 'Unauthorized, please login' });
      } else {
        this.setState({ postResult: `Unknown error: ${response.status}` });
      }
      await this.getExistingParameters();
    }
  };

  getExistingParameters = async () => {
    const imageUrl =
      this.props.imageUrl &&
      this.props.imageUrl.substr(this.props.imageUrl.lastIndexOf('/'));
    const url =
      imageUrl && `${imageApiUrl}/images/stored-parameters${imageUrl}`;
    const response = await fetch(url);
    this.setState({ existingParameters: await response.json() });
  };

  toggleRatio = e => {
    e.preventDefault();
    if (this.state.ratio === 0.81) {
      this.setState({ ratio: 2.63 });
    } else {
      this.setState({ ratio: 0.81 });
    }
  };

  componentDidMount() {
    this.getExistingParameters();
  }

  displayPostResult() {
    if (this.state.postResult !== null) {
      return
{this.state.postResult}
;
    } else {
      return null;
    }
  }

  render() {
    return (

          <button onClick={this.toggleRatio}>Toggle ratio</button>

        <Cropper
          ref="cropper"
          src={this.props.imageUrl}
          aspectRatio={this.state.ratio}
          guides={false}
          viewMode={2}
          zoomable={false}
          dragMode={'move'}
          preview={'.preview'}
          crop={this.crop}
        />

        <button onClick={this.postToImageApi}>
          Save this crop config for ratio={this.state.ratio}
        </button>
        {this.displayPostResult()}

    );
  }
}

const CropPage = ({ imageUrl }: { imageUrl?: string }) => (

Crop

    {imageUrl == null ? (

        You need to specify <tt>imageUrl</tt> in the URL

    ) : (
      <Crop imageUrl={imageUrl} />
    )}

);

CropPage.getInitialProps = context => {
  const imageUrl = context.query.imageUrl;

  return {
    imageUrl
  };
};

export default CropPage;
<commit_msg>#367: Use existing crop data if it exists GlobalDigitalLibraryio/issues#367<commit_after>// @flow

import React, { Component } from 'react';
import Cropper from 'react-cropper';
import 'cropperjs/dist/cropper.css';
import fetch from 'isomorphic-fetch';
import { imageApiUrl } from '../config';
import { getTokenFromLocalCookie } from '../lib/fetch';

type Props = {
  imageUrl?: string
};

type State = {
  ratio: number,
  imageApiBody?: any,
  existingParameters?: any,
  postResult?: string
};

class Crop extends Component<Props, State> {
  state = { ratio: 0.81, imageApiBody: null, existingParameters: null };

  toPercentages = c => {
    var data = c.getData();
    return {
      cropStartX: Math.max(
        0,
        Math.round((data.x / c.getImageData().naturalWidth) * 100)
      ),
      cropEndX: Math.min(
        100,
        Math.round(
          ((data.x + data.width) / c.getImageData().naturalWidth) * 100
        )
      ),
      cropStartY: Math.max(
        0,
        Math.round((data.y / c.getImageData().naturalHeight) * 100)
      ),

      cropEndY: Math.min(
        100,
        Math.round(
          ((data.y + data.height) / c.getImageData().naturalHeight) * 100
        )
      )
    };
  };

  toImageApiBody = pcnt => {
    const existingParametersForCurrentRatio =
      this.state.existingParameters &&
      this.state.existingParameters.find(
        p => p.forRatio === String(this.state.ratio)
      );
    const revision =
      (existingParametersForCurrentRatio &&
        existingParametersForCurrentRatio.revision) ||
      1;

    return {
      forRatio: String(this.state.ratio),
      revision: revision,
      imageUrl:
        this.props.imageUrl &&
        this.props.imageUrl.substr(this.props.imageUrl.lastIndexOf('/')),
      rawImageQueryParameters: {
        cropStartX: pcnt.cropStartX,
        cropEndX: pcnt.cropEndX,
        cropStartY: pcnt.cropStartY,
        cropEndY: pcnt.cropEndY
      }
    };
  };

  crop = () => {
    var pcnt = this.toPercentages(this.refs.cropper);
    this.setState({ imageApiBody: this.toImageApiBody(pcnt) });
  };

  postToImageApi = async () => {
    if (this.state.imageApiBody !== null) {
      this.setState({ postResult: 'Posting…' });
      const response = await fetch(`${imageApiUrl}/images/stored-parameters`, {
        method: 'POST',
        headers: {
          Authorization: 'Bearer ' + getTokenFromLocalCookie(),
          Accept: 'application/json'
        },
        body: JSON.stringify(this.state.imageApiBody)
      });

      if (response.ok) {
        this.setState({ postResult: 'Ok!' });
      } else if (response.status === 401) {
        this.setState({ postResult: 'Unauthorized, please login' });
      } else {
        this.setState({ postResult: `Unknown error: ${response.status}` });
      }
      await this.getExistingParameters();
    }
  };

  getExistingParameters = async () => {
    const imageUrl =
      this.props.imageUrl &&
      this.props.imageUrl.substr(this.props.imageUrl.lastIndexOf('/'));
    const url =
      imageUrl && `${imageApiUrl}/images/stored-parameters${imageUrl}`;
    const response = await fetch(url);
    this.setState({ existingParameters: await response.json() });
  };

  toggleRatio = e => {
    e.preventDefault();
    if (this.state.ratio === 0.81) {
      this.setState({ ratio: 2.63 });
    } else {
      this.setState({ ratio: 0.81 });
    }
    // TODO Set existing crop data here, not only on page load (via onReady)
  };

  componentDidMount() {
    this.getExistingParameters();
  }

  displayPostResult() {
    if (this.state.postResult !== null) {
      return
{this.state.postResult}
;
    } else {
      return null;
    }
  }

  existingParametersToCropData = ps => {
    const p = ps.find(x => x.forRatio === String(this.state.ratio));
    const imageWidth = this.refs.cropper.getImageData().naturalWidth;
    const imageHeight = this.refs.cropper.getImageData().naturalHeight;
    if (p !== null) {
      const r = p.rawImageQueryParameters;
      return {
        x: imageWidth * (r.cropStartX / 100),
        y: imageHeight * (r.cropStartY / 100),
        width: imageWidth * ((r.cropEndX - r.cropStartX) / 100),
        height: imageHeight * ((r.cropEndY - r.cropStartY) / 100)
      };
    }
  };

  onReady = () => {
    this.getExistingParameters();
    const data = this.existingParametersToCropData(
      this.state.existingParameters
    );
    if (data !== null) {
      this.refs.cropper.setData(data);
    }
  };

  render() {
    return (

          <button onClick={this.toggleRatio}>Toggle ratio</button>

        <Cropper
          ref="cropper"
          src={this.props.imageUrl}
          aspectRatio={this.state.ratio}
          guides={false}
          viewMode={2}
          zoomable={false}
          dragMode={'move'}
          preview={'.preview'}
          crop={this.crop}
          ready={this.onReady}
        />

        <button onClick={this.postToImageApi}>
          Save this crop config for ratio={this.state.ratio}
        </button>
        {this.displayPostResult()}

    );
  }
}

const CropPage = ({ imageUrl }: { imageUrl?: string }) => (

Crop

    {imageUrl == null ? (

        You need to specify <tt>imageUrl</tt> in the URL

    ) : (
      <Crop imageUrl={imageUrl} />
    )}

);

CropPage.getInitialProps = context => {
  const imageUrl = context.query.imageUrl;

  return {
    imageUrl
  };
};

export default CropPage;
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2020 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.nanos.menu',
  name: 'VerticalMenu',
  extends: 'foam.u2.View',

  implements: [
    'foam.mlang.Expressions'
  ],

  imports: [
    'currentMenu',
    'menuListener',
    'loginSuccess',
    'menuDAO',
    'pushMenu'
  ],

  requires: [
    'foam.nanos.menu.Menu',
    'foam.nanos.menu.VerticalMenu',
    'foam.dao.ArraySink'
  ],

  css: `
  input {
    width: 220px;
  }

  ^ .side-nav-view {
    font-size: medium!important;
    font-weight: normal;
    display: inline-block;
    position: absolute;
    height: calc(100vh - 80px);
    width: 240px;
    overflow-x: hidden;
    z-index: 100;
    font-size: 26px;
    color: /*%GREY2%*/ #9ba1a6;
    border-right: 1px solid /*%GREY4%*/ #e7eaec;
    background: /*%GREY5%*/ #f5f7fas;
  }

  .foam-u2-search-TextSearchView {
    text-align: center;
    margin: 4px 0;
  }

  ^ .foam-u2-view-TreeViewRow-label {
    font-weight: 300;
  }

  ^ .foam-u2-view-TreeViewRow {
    width: 100%;
  }
  ^ .tree-view-height-manager {
    margin-bottom: 40px;
  }
  `,

  properties: [
    {
      class: 'FObjectProperty',
      of: 'foam.u2.Element',
      name: 'subMenu',
      documentation: 'Used to store selected submenu element after window reload and scroll into parent view'
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'dao_',
      factory: function() {
        return this.menuDAO.orderBy(this.Menu.ORDER);
      }
    },
    {
      name: 'menuSearch',
      class: 'String',
      value: '',
      view: {
        class: 'foam.u2.view.TextField',
        onKey: true
      }
    }
  ],

  methods: [
    function initE() {
      var self = this;

      this
      .addClass(this.myClass())
      .start()
        .addClass('side-nav-view')
        .start()
          .startContext({ data: this })
          .start()
            .add(self.MENU_SEARCH.clone().copyFrom({ view: {
              class: 'foam.u2.view.TextField',
              type: 'search',
              onKey: true
            } }))
            .addClass('foam-u2-search-TextSearchView')
          .end()
          .endContext()
          .start()
            .addClass('tree-view-height-manager')
            .tag({
              class: 'foam.u2.view.TreeView',
              data: self.dao_,
              relationship: foam.nanos.menu.MenuMenuChildrenRelationship,
              startExpanded: true,
              query: self.menuSearch$,
              onClickAddOn: function(data) { self.openMenu(data); },
              formatter: function(data) { this.add(data.label); }
            })
          .end()
        .end()
      .end();
    },

    function openMenu(menu) {
      if ( Object.keys(menu.handler.instance_).length > 0 ) {
        this.pushMenu(menu.id);
        this.menuListener(menu);
      }
    }
  ]
});
<commit_msg>pr fixes<commit_after>/**
 * @license
 * Copyright 2020 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.nanos.menu',
  name: 'VerticalMenu',
  extends: 'foam.u2.View',

  implements: [
    'foam.mlang.Expressions'
  ],

  imports: [
    'currentMenu',
    'menuListener',
    'loginSuccess',
    'menuDAO',
    'pushMenu'
  ],

  requires: [
    'foam.nanos.menu.Menu',
    'foam.nanos.menu.VerticalMenu',
    'foam.dao.ArraySink'
  ],

  css: `
  input {
    width: 220px;
  }

  ^ .side-nav-view {
    font-size: medium!important;
    font-weight: normal;
    display: inline-block;
    position: absolute;
    height: calc(100vh - 80px);
    width: 240px;
    overflow-x: hidden;
    z-index: 100;
    font-size: 26px;
    color: /*%GREY2%*/ #9ba1a6;
    border-right: 1px solid /*%GREY4%*/ #e7eaec;
    background: /*%GREY5%*/ #f5f7fas;
  }

  .foam-u2-search-TextSearchView {
    text-align: center;
    margin: 4px 0;
  }

  ^ .foam-u2-view-TreeViewRow-label {
    font-weight: 300;
  }

  ^ .foam-u2-view-TreeViewRow {
    width: 100%;
  }
  ^ .tree-view-height-manager {
    margin-bottom: 40px;
  }
  `,

  properties: [
    {
      class: 'FObjectProperty',
      of: 'foam.u2.Element',
      name: 'subMenu',
      documentation: 'Used to store selected submenu element after window reload and scroll into parent view'
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'dao_',
      factory: function() {
        return this.menuDAO.orderBy(this.Menu.ORDER);
      }
    },
    {
      name: 'menuSearch',
      class: 'String',
      value: '',
      view: {
        class: 'foam.u2.view.TextField',
        onKey: true
      }
    }
  ],

  methods: [
    function initE() {
      var self = this;

      this
      .addClass(this.myClass())
      .start()
        .addClass('side-nav-view')
        .start()
          .startContext({ data: this })
          .start()
            .add(this.MENU_SEARCH)
            .addClass('foam-u2-search-TextSearchView')
          .end()
          .endContext()
          .start()
            .addClass('tree-view-height-manager')
            .tag({
              class: 'foam.u2.view.TreeView',
              data: self.dao_,
              relationship: foam.nanos.menu.MenuMenuChildrenRelationship,
              startExpanded: true,
              query: self.menuSearch$,
              onClickAddOn: function(data) { self.openMenu(data); },
              formatter: function(data) { this.add(data.label); }
            })
          .end()
        .end()
      .end();
    },

    function openMenu(menu) {
      if ( Object.keys(menu.handler.instance_).length > 0 ) {
        this.pushMenu(menu.id);
        this.menuListener(menu);
      }
    }
  ]
});
<|endoftext|>


<commit_before>/** @license Copyright 2017 The FOAM Authors, All Rights Reserved. */

foam.CLASS({
  package: 'foam.u2',
  name: 'ListCreateController',
  extends: 'foam.u2.stack.StackView',

  classes: [
    {
      name: 'ListController',
      extends: 'foam.u2.Element',

      imports: [ 'controller', 'dao' ],

      methods: [
        function initE() {
          this.tag(this.controller.summaryView, {data: this.dao}).add(this.CREATE);
        }
      ],

      actions: [
        function create(X) {
          X.controller.data.push(X.controller.CreateController.create());
        }
      ]
    },

    {
      name: 'CreateController',
      extends: 'foam.u2.Element',

      imports: [ 'controller', 'dao' ],
      exports: [ 'as data' ],

      properties: [
          {
            name: 'obj',
            factory: function() { return this.dao.of.create(); }
          }
      ],

      methods: [
        function initE() {
          this.tag(this.controller.detailView, {data: this.obj}).add(this.SAVE, this.CANCEL);
        }
      ],

      actions: [
        function save(X) {
          this.dao.put(this.obj);
          this.controller.back();
        },
        function cancel(X) {
          this.controller.back();
        }
      ]
    }
  ],

  requires: [
    'foam.u2.TableView',
    'foam.u2.DetailView'
  ],

  exports: [ 'as controller', 'dao' ],

  properties: [
    'dao',
    {
      name: 'summaryView',
      value: { class: 'foam.u2.TableView' }
    },
    {
      name: 'detailView',
      value: { class: 'foam.u2.DetailView' }
    },
    [ 'showActions', false ]
  ],

  methods: [
    function initE() {
      this.data.push(this.ListController.create());
    },

    function back() {
      this.data.back();
    }
  ],

  actions: [
    {
      name: 'create',
      code: function() {
        this.data.push(this.detailView);
      }
    }
  ]
});
<commit_msg>Add factory support to ListCreateController.<commit_after>/** @license Copyright 2017 The FOAM Authors, All Rights Reserved. */

foam.CLASS({
  package: 'foam.u2',
  name: 'ListCreateController',
  extends: 'foam.u2.stack.StackView',

  classes: [
    {
      name: 'ListController',
      extends: 'foam.u2.Element',

      imports: [ 'controller', 'dao' ],

      methods: [
        function initE() {
          this.tag(this.controller.summaryView, {data: this.dao}).add(this.CREATE);
        }
      ],

      actions: [
        function create(X) {
          X.controller.data.push(X.controller.CreateController.create());
        }
      ]
    },

    {
      name: 'CreateController',
      extends: 'foam.u2.Element',

      imports: [ 'controller', 'dao', 'factory' ],
      exports: [ 'as data' ],

      properties: [
          {
            name: 'obj',
            factory: function() { return this.factory(); }
          }
      ],

      methods: [
        function initE() {
          this.tag(this.controller.detailView, {data: this.obj}).add(this.SAVE, this.CANCEL);
        }
      ],

      actions: [
        function save(X) {
          this.dao.put(this.obj);
          this.controller.back();
        },
        function cancel(X) {
          this.controller.back();
        }
      ]
    }
  ],

  requires: [
    'foam.u2.TableView',
    'foam.u2.DetailView'
  ],

  exports: [ 'as controller', 'dao', 'factory' ],

  properties: [
    'dao',
    {
      name: 'summaryView',
      value: { class: 'foam.u2.TableView' }
    },
    {
      name: 'detailView',
      value: { class: 'foam.u2.DetailView' }
    },
    {
      name: 'factory',
      value: function() { return this.dao.of.create(); }
    },
    [ 'showActions', false ]
  ],

  methods: [
    function initE() {
      this.data.push(this.ListController.create());
    },

    function back() {
      this.data.back();
    }
  ],

  actions: [
    {
      name: 'create',
      code: function() {
        this.data.push(this.detailView);
      }
    }
  ]
});
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2017 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

 foam.CLASS({
  package: 'foam.u2.view',
  name: 'ScrollTableView',
  extends: 'foam.u2.Element',

  imports: [
    'memento',
    'stack'
  ],

  exports: [
    'as summaryView',
    'dblclick',
    'memento'
  ],

  requires: [
    'foam.dao.FnSink',
    'foam.mlang.sink.Count',
    'foam.u2.view.TableView',
    'foam.comics.v2.DAOControllerConfig',
    'foam.nanos.controller.Memento'
  ],

  css: `
    ^ {
      overflow: scroll;
      padding-bottom: 20px;
    }
    ^table {
      position: relative;
    }
    ^table  .foam-u2-view-TableView-thead {
      z-index: 1;
      overflow: visible;
    }
  `,

  constants: [
    {
      type: 'Float',
      name: 'MIN_TOP_PAGE_PROGRESS',
      documentation: `
        If the "top" page isn't scrolled by at least this amount, make
        the top page the previous page.
        i.e. If the page that's currently in view is only scrolled 10% of the way,
        we consider the page that's above it to be the "top" page. If the page
        that's on screen is scrolled 51% of the way, then it is considered the top
        page.
      `,
      value: 0.5
    },
    {
      type: 'Integer',
      name: 'NUM_PAGES_TO_RENDER',
      value: 3
    },
    {
      type: 'Integer',
      name: 'TABLE_HEAD_HEIGHT',
      value: 51
    }
  ],

  properties: [
    {
      class: 'foam.dao.DAOProperty',
      name: 'data'
    },
    'columns',
    {
      class: 'FObjectArray',
      of: 'foam.core.Action',
      name: 'contextMenuActions'
    },
    {
      class: 'Int',
      name: 'daoCount'
    },
    'selection',
    'disableUserSelection',
    {
      class: 'Boolean',
      name: 'editColumnsEnabled',
      documentation: `
        Set to true if users should be allowed to choose which columns to use.
      `,
      value: true
    },
    {
      class: 'Int',
      name: 'rowHeight',
      documentation: 'The height of one row of the table in px.',
      value: 49
    },
    {
      name: 'table_',
      documentation: `
        A reference to the table element we use in various calculations.
      `
    },
    {
      type: 'Int',
      name: 'scrollHeight',
      expression: function(daoCount, rowHeight) {
        return rowHeight * daoCount + this.TABLE_HEAD_HEIGHT;
      }
    },
    {
      type: 'Int',
      name: 'pageSize',
      value: 30,
      documentation: 'The number of items in each "page". There are three pages.'
    },
    {
      type: 'Boolean',
      name: 'enableDynamicTableHeight',
      value: true,
    },
    {
      class: 'Boolean',
      name: 'multiSelectEnabled',
      documentation: 'Pass through to UnstyledTableView.'
    },
    {
      class: 'Map',
      name: 'selectedObjects',
      documentation: `
        The objects selected by the user when multi-select support is enabled.
        It's a map where the key is the object id and the value is the object.
        Here we simply bind it to the selectedObjects property on TableView.
      `
    },
    {
      class: 'Int',
      name: 'scrollPos_'
    },
    {
      class: 'Int',
      name: 'numPages_',
      expression: function(daoCount, pageSize) {
        return Math.ceil(daoCount / pageSize);
      }
    },
    {
      class: 'Int',
      name: 'currentTopPage_',
      expression: function(numPages_, scrollPos_, scrollHeight) {
        var scrollPercent = scrollPos_ / scrollHeight;
        var topPage = Math.floor(scrollPercent * numPages_);
        var topPageProgress = (scrollPercent * numPages_) % 1;
        if ( topPageProgress < this.MIN_TOP_PAGE_PROGRESS ) topPage = topPage - 1;
        return Math.min(Math.max(0, numPages_ - this.NUM_PAGES_TO_RENDER), Math.max(0, topPage));
      }
    },
    {
      class: 'Map',
      name: 'renderedPages_'
    },
    {
      class: 'FObjectProperty',
      of: 'foam.comics.v2.DAOControllerConfig',
      name: 'config',
      factory: function() {
        return this.DAOControllerConfig.create({ dao: this.data });
      }
    },
    {
      name: 'dblClickListenerAction',
      factory: () => {
        return function(obj, id) {
          if ( ! this.stack ) return;

          this.stack.push({
            class: 'foam.comics.v2.DAOSummaryView',
            data: obj,
            config: this.config,
            idOfRecord: id
          }, this);
        }
      }
    },
    'currentMemento',
    {
      class: 'Boolean',
      name: 'isInit'
    }
  ],

  reactions: [
    ['', 'propertyChange.currentTopPage_', 'updateRenderedPages_'],
    ['', 'propertyChange.table_',          'updateRenderedPages_']
  ],

  methods: [
    function init() {
      this.onDetach(this.data$proxy.listen(this.FnSink.create({ fn: this.updateCount })));
      this.updateCount();

      if ( this.memento )
        this.currentMemento$ = this.memento.tail$;
      else
        this.currentMemento$ = this.memento$;
    },

    function initE() {
      if ( this.currentMemento ) {
        var mementoHead = this.currentMemento.head;

        var of = this.data.of || this.config.of;
        if ( mementoHead === 'Create' && of ) {
          this.stack.push({
            class: 'foam.comics.v2.DAOCreateView',
            data: ((this.config.factory && this.config.factory$cls) ||  this.data.of).create({ mode: 'create'}, this),
            config$: this.config$,
            of: of
          }, this.__subContext__);
          return;
        }

        var id = mementoHead;
        if ( of ) {
          id = of.ID.fromString(mementoHead);
        }

        this.stack.push({
          class: 'foam.comics.v2.DAOSummaryView',
          data: null,
          config: this.config,
          idOfRecord: id
        }, this);
        return;
      }

      this.
        addClass(this.myClass()).
        on('scroll', this.onScroll).
        start(this.TableView, {
          data: foam.dao.NullDAO.create({of: this.data.of}),
          columns: this.columns,
          contextMenuActions: this.contextMenuActions,
          selection$: this.selection$,
          editColumnsEnabled: this.editColumnsEnabled,
          disableUserSelection: this.disableUserSelection,
          multiSelectEnabled: this.multiSelectEnabled,
          selectedObjects$: this.selectedObjects$
        }, this.table_$).
          addClass(this.myClass('table')).
          style({
            height: this.scrollHeight$.map(h => h + 'px')
          }).
        end();

      /*
        to be used in cases where we don't want the whole table to
        take the whole page (i.e. we need multiple tables)
        and enableDynamicTableHeight can be switched off
      */
      if ( this.enableDynamicTableHeight ) {
        this.onDetach(this.onload.sub(this.updateTableHeight));
        window.addEventListener('resize', this.updateTableHeight);
        this.onDetach(() => {
          window.removeEventListener('resize', this.updateTableHeight);
        });
      }
    }
  ],

  listeners: [
    {
      name: 'refresh',
      isFramed: true,
      code: function() {
        Object.keys(this.renderedPages_).forEach(i => {
          this.renderedPages_[i].remove();
          delete this.renderedPages_[i];
        });
        this.updateRenderedPages_();
        if ( this.el() && ! this.isInit && this.memento.paramsObj.r ) {
          var scroll = this.memento.paramsObj.r * this.rowHeight;
          scroll = scroll >= this.rowHeight && scroll < this.scrollHeight ? scroll : 0;

          document.getElementById(this.id).scrollTop = scroll;

          this.isInit = true;
        } else if ( this.el() ) this.el().scrollTop = 0;
      }
    },
    {
      name: 'updateCount',
      isFramed: true,
      code: function() {
        return this.data$proxy.select(this.Count.create()).then((s) => {
          this.daoCount = s.value;
          this.refresh();
        });
      }
    },
    {
      name: 'updateRenderedPages_',
      isFramed: true,
      code: function() {
        if ( ! this.table_ ) return;

        // Remove any pages that are no longer on screen to save on
        // the amount of DOM we add to the page.
        Object.keys(this.renderedPages_).forEach(i => {
          if ( i >= this.currentTopPage_ && i < this.currentTopPage_ + this.NUM_PAGES_TO_RENDER ) return;
          this.renderedPages_[i].remove();
          delete this.renderedPages_[i];
        });

        // Add any pages that are not already rendered.
        for ( var i = 0; i < Math.min(this.numPages_, this.NUM_PAGES_TO_RENDER) ; i++) {
          var page = this.currentTopPage_ + i;
          if ( this.renderedPages_[page] ) continue;
          var dao = this.data$proxy.limit(this.pageSize).skip(page * this.pageSize);
          var tbody = this.table_.slotE_(this.table_.rowsFrom(dao));
          tbody.style({
            position: 'absolute',
            width: '100%',
            top: this.TABLE_HEAD_HEIGHT + page * this.pageSize * this.rowHeight + 'px'
          });
          this.table_.add(tbody);
          this.renderedPages_[page] = tbody;
        }
      }
    },
    {
      name: 'onScroll',
      isFramed: true,
      code: function(e) {
        this.scrollPos_ = e.target.scrollTop;
        this.memento.paramsObj.r = this.scrollPos_ >= this.rowHeight && this.scrollPos_ < this.scrollHeight ? Math.floor( this.scrollPos_  / this.rowHeight) : 0;
        this.memento.paramsObj = foam.Object.clone(this.memento.paramsObj);
      }
    },
    {
      name: 'updateTableHeight',
      code: function() {
        // Find the distance from the top of the table to the top of the screen.
        var distanceFromTop = this.el().getBoundingClientRect().y;

        // Calculate the remaining space we have to make use of.
        var remainingSpace = window.innerHeight - distanceFromTop;

        // TODO: Do we want to do this?
        // Leave space for the footer.
        remainingSpace -= 44;

        this.style({ height: `${remainingSpace}px` });
      }
    },
    function dblclick(obj, id) {
      this.dblClickListenerAction(obj, id);
    }
  ]
});
<commit_msg>Make memento import optional so works when not run under NANOS.<commit_after>/**
 * @license
 * Copyright 2017 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

 foam.CLASS({
  package: 'foam.u2.view',
  name: 'ScrollTableView',
  extends: 'foam.u2.Element',

  imports: [
    'memento?',
    'stack'
  ],

  exports: [
    'as summaryView',
    'dblclick',
    'memento'
  ],

  requires: [
    'foam.dao.FnSink',
    'foam.mlang.sink.Count',
    'foam.u2.view.TableView',
    'foam.comics.v2.DAOControllerConfig',
    'foam.nanos.controller.Memento'
  ],

  css: `
    ^ {
      overflow: scroll;
      padding-bottom: 20px;
    }
    ^table {
      position: relative;
    }
    ^table  .foam-u2-view-TableView-thead {
      z-index: 1;
      overflow: visible;
    }
  `,

  constants: [
    {
      type: 'Float',
      name: 'MIN_TOP_PAGE_PROGRESS',
      documentation: `
        If the "top" page isn't scrolled by at least this amount, make
        the top page the previous page.
        i.e. If the page that's currently in view is only scrolled 10% of the way,
        we consider the page that's above it to be the "top" page. If the page
        that's on screen is scrolled 51% of the way, then it is considered the top
        page.
      `,
      value: 0.5
    },
    {
      type: 'Integer',
      name: 'NUM_PAGES_TO_RENDER',
      value: 3
    },
    {
      type: 'Integer',
      name: 'TABLE_HEAD_HEIGHT',
      value: 51
    }
  ],

  properties: [
    {
      class: 'foam.dao.DAOProperty',
      name: 'data'
    },
    'columns',
    {
      class: 'FObjectArray',
      of: 'foam.core.Action',
      name: 'contextMenuActions'
    },
    {
      class: 'Int',
      name: 'daoCount'
    },
    'selection',
    'disableUserSelection',
    {
      class: 'Boolean',
      name: 'editColumnsEnabled',
      documentation: `
        Set to true if users should be allowed to choose which columns to use.
      `,
      value: true
    },
    {
      class: 'Int',
      name: 'rowHeight',
      documentation: 'The height of one row of the table in px.',
      value: 49
    },
    {
      name: 'table_',
      documentation: `
        A reference to the table element we use in various calculations.
      `
    },
    {
      type: 'Int',
      name: 'scrollHeight',
      expression: function(daoCount, rowHeight) {
        return rowHeight * daoCount + this.TABLE_HEAD_HEIGHT;
      }
    },
    {
      type: 'Int',
      name: 'pageSize',
      value: 30,
      documentation: 'The number of items in each "page". There are three pages.'
    },
    {
      type: 'Boolean',
      name: 'enableDynamicTableHeight',
      value: true,
    },
    {
      class: 'Boolean',
      name: 'multiSelectEnabled',
      documentation: 'Pass through to UnstyledTableView.'
    },
    {
      class: 'Map',
      name: 'selectedObjects',
      documentation: `
        The objects selected by the user when multi-select support is enabled.
        It's a map where the key is the object id and the value is the object.
        Here we simply bind it to the selectedObjects property on TableView.
      `
    },
    {
      class: 'Int',
      name: 'scrollPos_'
    },
    {
      class: 'Int',
      name: 'numPages_',
      expression: function(daoCount, pageSize) {
        return Math.ceil(daoCount / pageSize);
      }
    },
    {
      class: 'Int',
      name: 'currentTopPage_',
      expression: function(numPages_, scrollPos_, scrollHeight) {
        var scrollPercent = scrollPos_ / scrollHeight;
        var topPage = Math.floor(scrollPercent * numPages_);
        var topPageProgress = (scrollPercent * numPages_) % 1;
        if ( topPageProgress < this.MIN_TOP_PAGE_PROGRESS ) topPage = topPage - 1;
        return Math.min(Math.max(0, numPages_ - this.NUM_PAGES_TO_RENDER), Math.max(0, topPage));
      }
    },
    {
      class: 'Map',
      name: 'renderedPages_'
    },
    {
      class: 'FObjectProperty',
      of: 'foam.comics.v2.DAOControllerConfig',
      name: 'config',
      factory: function() {
        return this.DAOControllerConfig.create({ dao: this.data });
      }
    },
    {
      name: 'dblClickListenerAction',
      factory: () => {
        return function(obj, id) {
          if ( ! this.stack ) return;

          this.stack.push({
            class: 'foam.comics.v2.DAOSummaryView',
            data: obj,
            config: this.config,
            idOfRecord: id
          }, this);
        }
      }
    },
    'currentMemento',
    {
      class: 'Boolean',
      name: 'isInit'
    }
  ],

  reactions: [
    ['', 'propertyChange.currentTopPage_', 'updateRenderedPages_'],
    ['', 'propertyChange.table_',          'updateRenderedPages_']
  ],

  methods: [
    function init() {
      this.onDetach(this.data$proxy.listen(this.FnSink.create({ fn: this.updateCount })));
      this.updateCount();

      if ( this.memento )
        this.currentMemento$ = this.memento.tail$;
      else if ( this.memento != undefined )
        this.currentMemento$ = this.memento$;
    },

    function initE() {
      if ( this.currentMemento ) {
        var mementoHead = this.currentMemento.head;

        var of = this.data.of || this.config.of;
        if ( mementoHead === 'Create' && of ) {
          this.stack.push({
            class: 'foam.comics.v2.DAOCreateView',
            data: ((this.config.factory && this.config.factory$cls) ||  this.data.of).create({ mode: 'create'}, this),
            config$: this.config$,
            of: of
          }, this.__subContext__);
          return;
        }

        var id = mementoHead;
        if ( of ) {
          id = of.ID.fromString(mementoHead);
        }

        this.stack.push({
          class: 'foam.comics.v2.DAOSummaryView',
          data: null,
          config: this.config,
          idOfRecord: id
        }, this);
        return;
      }

      this.
        addClass(this.myClass()).
        on('scroll', this.onScroll).
        start(this.TableView, {
          data: foam.dao.NullDAO.create({of: this.data.of}),
          columns: this.columns,
          contextMenuActions: this.contextMenuActions,
          selection$: this.selection$,
          editColumnsEnabled: this.editColumnsEnabled,
          disableUserSelection: this.disableUserSelection,
          multiSelectEnabled: this.multiSelectEnabled,
          selectedObjects$: this.selectedObjects$
        }, this.table_$).
          addClass(this.myClass('table')).
          style({
            height: this.scrollHeight$.map(h => h + 'px')
          }).
        end();

      /*
        to be used in cases where we don't want the whole table to
        take the whole page (i.e. we need multiple tables)
        and enableDynamicTableHeight can be switched off
      */
      if ( this.enableDynamicTableHeight ) {
        this.onDetach(this.onload.sub(this.updateTableHeight));
        window.addEventListener('resize', this.updateTableHeight);
        this.onDetach(() => {
          window.removeEventListener('resize', this.updateTableHeight);
        });
      }
    }
  ],

  listeners: [
    {
      name: 'refresh',
      isFramed: true,
      code: function() {
        Object.keys(this.renderedPages_).forEach(i => {
          this.renderedPages_[i].remove();
          delete this.renderedPages_[i];
        });
        this.updateRenderedPages_();
        if ( this.el() && ! this.isInit && this.memento.paramsObj.r ) {
          var scroll = this.memento.paramsObj.r * this.rowHeight;
          scroll = scroll >= this.rowHeight && scroll < this.scrollHeight ? scroll : 0;

          document.getElementById(this.id).scrollTop = scroll;

          this.isInit = true;
        } else if ( this.el() ) this.el().scrollTop = 0;
      }
    },
    {
      name: 'updateCount',
      isFramed: true,
      code: function() {
        return this.data$proxy.select(this.Count.create()).then((s) => {
          this.daoCount = s.value;
          this.refresh();
        });
      }
    },
    {
      name: 'updateRenderedPages_',
      isFramed: true,
      code: function() {
        if ( ! this.table_ ) return;

        // Remove any pages that are no longer on screen to save on
        // the amount of DOM we add to the page.
        Object.keys(this.renderedPages_).forEach(i => {
          if ( i >= this.currentTopPage_ && i < this.currentTopPage_ + this.NUM_PAGES_TO_RENDER ) return;
          this.renderedPages_[i].remove();
          delete this.renderedPages_[i];
        });

        // Add any pages that are not already rendered.
        for ( var i = 0; i < Math.min(this.numPages_, this.NUM_PAGES_TO_RENDER) ; i++) {
          var page = this.currentTopPage_ + i;
          if ( this.renderedPages_[page] ) continue;
          var dao = this.data$proxy.limit(this.pageSize).skip(page * this.pageSize);
          var tbody = this.table_.slotE_(this.table_.rowsFrom(dao));
          tbody.style({
            position: 'absolute',
            width: '100%',
            top: this.TABLE_HEAD_HEIGHT + page * this.pageSize * this.rowHeight + 'px'
          });
          this.table_.add(tbody);
          this.renderedPages_[page] = tbody;
        }
      }
    },
    {
      name: 'onScroll',
      isFramed: true,
      code: function(e) {
        this.scrollPos_ = e.target.scrollTop;
        this.memento.paramsObj.r = this.scrollPos_ >= this.rowHeight && this.scrollPos_ < this.scrollHeight ? Math.floor( this.scrollPos_  / this.rowHeight) : 0;
        this.memento.paramsObj = foam.Object.clone(this.memento.paramsObj);
      }
    },
    {
      name: 'updateTableHeight',
      code: function() {
        // Find the distance from the top of the table to the top of the screen.
        var distanceFromTop = this.el().getBoundingClientRect().y;

        // Calculate the remaining space we have to make use of.
        var remainingSpace = window.innerHeight - distanceFromTop;

        // TODO: Do we want to do this?
        // Leave space for the footer.
        remainingSpace -= 44;

        this.style({ height: `${remainingSpace}px` });
      }
    },
    function dblclick(obj, id) {
      this.dblClickListenerAction(obj, id);
    }
  ]
});
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2017 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

// TODO:
//   [ ] See if I can get it so that the DOM elements aren't removed and
//       re-added when filtering. It would be nicer to just change the
//       underlying DAO so you don't see the flicker.
//   [ ] Fix overlays.
//   [ ] Fix jump in scrollbar when tables are added/removed.
//         * `position: absolute` would solve this perfectly if it weren't for
//           the fact that the conainer wouldn't use the table to calculate its
//           width anymore so it breaks the layout.
//             * Note that using `table-layout: fixed` would solve this.
//         * Note that the jumping only happens when scrolling down, which is
//           surprising. I'm not sure why that's the case, I'd expect it happen
//           while scrolling either way.
//   [ ] Make the table header sticky.
//   [x] Make sure all column widths are consistent.
//   [x] Handle when a user scrolls more than an entire page in one event.
//   [x] Make sure table height isn't broken.
//   [x] Make sure filtering didn't break.
//   [x] Make sure sorting didn't break.
//   [x] Fix bug where table header is gone if you scroll back up after it was
//       removed when previously scrolling down far enough for it to be removed.

 foam.CLASS({
  package: 'foam.u2.view',
  name: 'ScrollTableView',
  extends: 'foam.u2.Element',

  requires: [
    'foam.dao.FnSink',
    'foam.mlang.sink.Count',
    'foam.u2.view.TableView'
  ],

  css: `
    ^ {
      position: relative;
    }

    ^scrollbar {
      box-sizing: border-box;
    }

    ^scrollbarContainer {
      overflow: scroll;
      display: grid;
      grid-template-columns: auto auto;
    }
  `,

  constants: [
    {
      type: 'Integer',
      name: 'TABLE_HEAD_HEIGHT',
      value: 40
    }
  ],

  properties: [
    {
      class: 'foam.dao.DAOProperty',
      name: 'data'
    },
    {
      class: 'Int',
      name: 'limit',
      value: 18,
      // TODO make this a funciton of the height.
    },
    {
      class: 'Int',
      name: 'skip',
    },
    'columns',
    {
      class: 'FObjectArray',
      of: 'foam.core.Action',
      name: 'contextMenuActions'
    },
    {
      class: 'Int',
      name: 'daoCount'
    },
    'selection',
    {
      class: 'Boolean',
      name: 'editColumnsEnabled',
      documentation: `
        Set to true if users should be allowed to choose which columns to use.
      `,
      value: true
    },
    {
      class: 'Int',
      name: 'rowHeight',
      documentation: 'The height of one row of the table in px.',
      value: 48
    },
    {
      name: 'table_',
      documentation: `
        A reference to the table element we use in various calculations.
      `
    },
    {
      class: 'Int',
      name: 'accumulator',
      documentation: 'Used internally to mimic native scrolling speed.',
      adapt: function(_, v) {
        return v % this.rowHeight;
      }
    },
    {
      name: 'scrollbarContainer_',
      documentation: `
        A reference to the scrollbar's container element so we can update the
        height after the view has loaded.
      `
    },
    {
      type: 'String',
      name: 'scrollHeight',
      expression: function(daoCount, limit, rowHeight) {
        this.refresh();
        return rowHeight * daoCount + this.TABLE_HEAD_HEIGHT + 'px';
      }
    },
    {
      type: 'Int',
      name: 'lastScrollTop_',
      value: 0
    },
    {
      type: 'Int',
      name: 'pageSize',
      value: 30,
      documentation: 'The number of items in each "page". There are three pages.'
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'initialPage1DAO_',
      expression: function(data, pageSize) {
        return data && data.limit(pageSize);
      }
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'initialPage2DAO_',
      expression: function(data, pageSize) {
        return data && data.skip(pageSize).limit(pageSize);
      }
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'initialPage3DAO_',
      expression: function(data, pageSize) {
        return data && data.skip(pageSize * 2).limit(pageSize);
      }
    },
    {
      type: 'Int',
      name: 'currentLowerBound',
      value: 0
    },
    {
      type: 'Int',
      name: 'currentUpperBound',
      expression: function(pageSize) {
        return pageSize * 3;
      }
    },
    {
      name: 'spacer_',
      documentation: `
        A named reference to an element so we can update its height
        periodically. Used to put empty space between the top of the container
        and the tables so we create the illusion of the table always being
        visible.
      `
    },
    'topBufferTable_',
    'visibleTable_',
    'bottomBufferTable_',
    'tablesContainer_',
    {
      type: 'Int',
      name: 'tablesRemoved_',
      value: 0,
      documentation: `
        The number of tables that have been removed due to scrolling down.
      `
    },
    {
      type: 'String',
      name: 'spacerHeight_',
      expression: function(tablesRemoved_, pageSize, rowHeight) {
        return tablesRemoved_ * pageSize * rowHeight + 'px';
      }
    }
  ],

  methods: [
    function init() {
      this.onDetach(this.data$proxy.listen(this.FnSink.create({ fn: this.onDAOUpdate })));
      this.onDAOUpdate();
    },

    function initE() {
      this.
        addClass(this.myClass()).
        start('div', undefined, this.scrollbarContainer_$).
          addClass(this.myClass('scrollbarContainer')).
          on('scroll', this.onScroll).
          start().
            show(this.daoCount$.map((count) => count >= this.limit)).
            addClass(this.myClass('scrollbar')).
            style({ height: this.scrollHeight$ }).
          end().
          start('div', undefined, this.tablesContainer_$).
            start('div', undefined, this.spacer_$).
              style({ height: this.spacerHeight_$ }).
            end().
            start(this.TableView, {
              data: this.initialPage1DAO_,
              columns: this.columns,
              contextMenuActions: this.contextMenuActions,
              selection$: this.selection$,
              editColumnsEnabled: this.editColumnsEnabled
            }, this.table_$).
              addClass(this.myClass('table')).
            end().
          end().
        end();

      this.onDetach(this.onload.sub(this.addTbodies));

      this.onDetach(this.onload.sub(this.updateTableHeight));
      window.addEventListener('resize', this.updateTableHeight);
      this.onDetach(() => {
        window.removeEventListener('resize', this.updateTableHeight);
      });
    },
    {
      name: 'refresh',
      code: function() {
        this.lastScrollTop_ = 0;
        this.skip = 0;
        this.currentLowerBound = 0;
        this.currentUpperBound = this.pageSize * 3;
        this.tablesRemoved_ = 0;

        // Remove all of the <tbody> elements.
        if ( ! this.table_ ) return;

        this.table_.childNodes
          .filter((x) => x.nodeName === 'TBODY')
          .forEach((x) => x.remove());

        // Re-add them.
        this.table_.add(this.table_.rowsFrom(this.initialPage1DAO_));
        this.table_.add(this.table_.rowsFrom(this.initialPage2DAO_));
        this.table_.add(this.table_.rowsFrom(this.initialPage3DAO_));

        // Update references.
        var tbodies = this.table_.childNodes.filter((x) => x.nodeName === 'TBODY');
        this.topBufferTable_ = tbodies[0];
        this.visibleTable_ = tbodies[1];
        this.bottomBufferTable_ = tbodies[2];
      }
    },
    {
      name: 'scrollDownOnePage',
      code: function() {
        // Update the bounds.
        if ( this.currentUpperBound > this.daoCount ) return;

        this.currentUpperBound += this.pageSize;
        this.currentLowerBound += this.pageSize;

        // Remove the top buffer table.
        this.topBufferTable_.remove();
        this.tablesRemoved_ += 1;

        // Update the table references.
        this.topBufferTable_ = this.visibleTable_;
        this.visibleTable_   = this.bottomBufferTable_;

        // Add a new bottom buffer table.
        var rows = this.table_.rowsFrom(this.data.skip(this.currentUpperBound - this.pageSize).limit(this.pageSize));
        this.table_.add(rows);
        this.bottomBufferTable_ = this.table_.childNodes
          .filter((x) => x.nodeName === 'TBODY')
          .pop();
      }
    },
    {
      name: 'scrollUpOnePage',
      code: function() {
        // Update the bounds.
        if ( this.currentLowerBound <= 0 ) return;

        this.currentLowerBound -= this.pageSize;
        this.currentUpperBound -= this.pageSize;

        // Remove the bottom buffer table.
        this.bottomBufferTable_.remove();
        this.tablesRemoved_ -= 1;

        // Update the table references.
        this.bottomBufferTable_ = this.visibleTable_;
        this.visibleTable_      = this.topBufferTable_;

        // Add a new top buffer table.
        var rows = this.table_.rowsFrom(this.data.skip(this.currentLowerBound).limit(this.pageSize));
        this.table_.insertBefore(this.table_.slotE_(rows), this.visibleTable_);
        this.topBufferTable_ = this.table_.childNodes
          .filter((x) => x.nodeName === 'TBODY')
          .shift();
      }
    }
  ],

  listeners: [
    {
      name: 'onScroll',
      code: function(e) {
        var deltaY = e.target.scrollTop - this.lastScrollTop_;
        var negative = deltaY < 0;
        var rows = Math.floor(Math.abs(this.accumulator + deltaY) / this.rowHeight);
        this.accumulator += deltaY;
        var oldSkip = this.skip;
        this.skip = Math.min(this.daoCount - this.limit, Math.max(0, this.skip + (negative ? -rows : rows)));
        this.lastScrollTop_ = e.target.scrollTop;

        var uBound = () => this.currentUpperBound - this.pageSize;
        var lBound = () => this.currentLowerBound + this.pageSize;

        if ( oldSkip <= uBound() && this.skip > uBound() ) {
          // The user scrolled down past a page boundary.
          while (
            this.skip > uBound() &&
            this.currentUpperBound < this.daoCount
          ) {
            this.scrollDownOnePage();
          }
        } else if ( oldSkip > lBound() && this.skip <= lBound() ) {
          // The user scrolled up past a page boundary.
          while ( this.skip <= lBound() && this.currentLowerBound > 0 ) {
            this.scrollUpOnePage();
          }
        }
      }
    },
    {
      // TODO: Avoid onDAOUpdate approaches.
      name: 'onDAOUpdate',
      isFramed: true,
      code: function() {
        var self = this;
        this.data$proxy.select(this.Count.create()).then(function(s) {
          self.daoCount = s.value;
          self.refresh();
        });
      }
    },
    {
      name: 'updateTableHeight',
      code: function() {
        // Find the distance from the top of the table to the top of the screen.
        var distanceFromTop = this.table_.el().getBoundingClientRect().y;

        // Calculate the remaining space we have to make use of.
        var remainingSpace = window.innerHeight - distanceFromTop;

        // TODO: Do we want to do this?
        // Leave space for the footer.
        remainingSpace -= 44;

        this.scrollbarContainer_.style({ height: `${remainingSpace}px` });
      }
    },
    {
      name: 'addTbodies',
      code: function() {
        this.table_.add(this.table_.rowsFrom(this.initialPage2DAO_));
        this.table_.add(this.table_.rowsFrom(this.initialPage3DAO_));
        var tbodies = this.table_.childNodes.filter((x) => x.nodeName === 'TBODY');
        this.topBufferTable_ = tbodies[0];
        this.visibleTable_ = tbodies[1];
        this.bottomBufferTable_ = tbodies[2];
      }
    }
  ]
});
<commit_msg>Fix visual flicker when filtering<commit_after>/**
 * @license
 * Copyright 2017 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

// TODO:
//   [ ] Fix overlays.
//   [ ] Fix jump in scrollbar when tables are added/removed.
//         * `position: absolute` would solve this perfectly if it weren't for
//           the fact that the conainer wouldn't use the table to calculate its
//           width anymore so it breaks the layout.
//             * Note that using `table-layout: fixed` would solve this.
//         * Note that the jumping only happens when scrolling down, which is
//           surprising. I'm not sure why that's the case, I'd expect it happen
//           while scrolling either way.
//   [ ] Make the table header sticky.
//   [x] See if I can get it so that the DOM elements aren't removed and
//       re-added when filtering. It would be nicer to just change the
//       underlying DAO so you don't see the flicker.
//   [x] Make sure all column widths are consistent.
//   [x] Handle when a user scrolls more than an entire page in one event.
//   [x] Make sure table height isn't broken.
//   [x] Make sure filtering didn't break.
//   [x] Make sure sorting didn't break.
//   [x] Fix bug where table header is gone if you scroll back up after it was
//       removed when previously scrolling down far enough for it to be removed.

 foam.CLASS({
  package: 'foam.u2.view',
  name: 'ScrollTableView',
  extends: 'foam.u2.Element',

  requires: [
    'foam.dao.FnSink',
    'foam.mlang.sink.Count',
    'foam.u2.view.TableView'
  ],

  css: `
    ^ {
      position: relative;
    }

    ^scrollbar {
      box-sizing: border-box;
    }

    ^scrollbarContainer {
      overflow: scroll;
      display: grid;
      grid-template-columns: auto auto;
    }
  `,

  constants: [
    {
      type: 'Integer',
      name: 'TABLE_HEAD_HEIGHT',
      value: 40
    }
  ],

  properties: [
    {
      class: 'foam.dao.DAOProperty',
      name: 'data'
    },
    {
      class: 'Int',
      name: 'limit',
      value: 18,
      // TODO make this a funciton of the height.
    },
    {
      class: 'Int',
      name: 'skip',
    },
    'columns',
    {
      class: 'FObjectArray',
      of: 'foam.core.Action',
      name: 'contextMenuActions'
    },
    {
      class: 'Int',
      name: 'daoCount'
    },
    'selection',
    {
      class: 'Boolean',
      name: 'editColumnsEnabled',
      documentation: `
        Set to true if users should be allowed to choose which columns to use.
      `,
      value: true
    },
    {
      class: 'Int',
      name: 'rowHeight',
      documentation: 'The height of one row of the table in px.',
      value: 48
    },
    {
      name: 'table_',
      documentation: `
        A reference to the table element we use in various calculations.
      `
    },
    {
      class: 'Int',
      name: 'accumulator',
      documentation: 'Used internally to mimic native scrolling speed.',
      adapt: function(_, v) {
        return v % this.rowHeight;
      }
    },
    {
      name: 'scrollbarContainer_',
      documentation: `
        A reference to the scrollbar's container element so we can update the
        height after the view has loaded.
      `
    },
    {
      type: 'String',
      name: 'scrollHeight',
      expression: function(daoCount, limit, rowHeight) {
        this.refresh();
        return rowHeight * daoCount + this.TABLE_HEAD_HEIGHT + 'px';
      }
    },
    {
      type: 'Int',
      name: 'lastScrollTop_',
      value: 0
    },
    {
      type: 'Int',
      name: 'pageSize',
      value: 30,
      documentation: 'The number of items in each "page". There are three pages.'
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'initialPage1DAO_',
      expression: function(data, pageSize) {
        return data && data.limit(pageSize);
      }
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'initialPage2DAO_',
      expression: function(data, pageSize) {
        return data && data.skip(pageSize).limit(pageSize);
      }
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'initialPage3DAO_',
      expression: function(data, pageSize) {
        return data && data.skip(pageSize * 2).limit(pageSize);
      }
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'page1DAO_',
      factory: function() {
        return this.initialPage1DAO_;
      }
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'page2DAO_',
      factory: function() {
        return this.initialPage2DAO_;
      }
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'page3DAO_',
      factory: function() {
        return this.initialPage3DAO_;
      }
    },
    {
      type: 'Int',
      name: 'currentLowerBound',
      value: 0
    },
    {
      type: 'Int',
      name: 'currentUpperBound',
      expression: function(pageSize) {
        return pageSize * 3;
      }
    },
    {
      name: 'spacer_',
      documentation: `
        A named reference to an element so we can update its height
        periodically. Used to put empty space between the top of the container
        and the tables so we create the illusion of the table always being
        visible.
      `
    },
    'topBufferTable_',
    'visibleTable_',
    'bottomBufferTable_',
    'tablesContainer_',
    {
      type: 'Int',
      name: 'tablesRemoved_',
      value: 0,
      documentation: `
        The number of tables that have been removed due to scrolling down.
      `
    },
    {
      type: 'String',
      name: 'spacerHeight_',
      expression: function(tablesRemoved_, pageSize, rowHeight) {
        return tablesRemoved_ * pageSize * rowHeight + 'px';
      }
    }
  ],

  methods: [
    function init() {
      this.onDetach(this.data$proxy.listen(this.FnSink.create({ fn: this.onDAOUpdate })));
      this.onDAOUpdate();
    },

    function initE() {
      this.
        addClass(this.myClass()).
        start('div', undefined, this.scrollbarContainer_$).
          addClass(this.myClass('scrollbarContainer')).
          on('scroll', this.onScroll).
          start().
            show(this.daoCount$.map((count) => count >= this.limit)).
            addClass(this.myClass('scrollbar')).
            style({ height: this.scrollHeight$ }).
          end().
          start('div', undefined, this.tablesContainer_$).
            start('div', undefined, this.spacer_$).
              style({ height: this.spacerHeight_$ }).
            end().
            start(this.TableView, {
              data: this.page1DAO_$proxy,
              columns: this.columns,
              contextMenuActions: this.contextMenuActions,
              selection$: this.selection$,
              editColumnsEnabled: this.editColumnsEnabled
            }, this.table_$).
              addClass(this.myClass('table')).
            end().
          end().
        end();

      this.onDetach(this.onload.sub(this.addTbodies));

      this.onDetach(this.onload.sub(this.updateTableHeight));
      window.addEventListener('resize', this.updateTableHeight);
      this.onDetach(() => {
        window.removeEventListener('resize', this.updateTableHeight);
      });
    },
    {
      name: 'refresh',
      code: function() {
        this.lastScrollTop_ = 0;
        this.skip = 0;
        this.currentLowerBound = 0;
        this.currentUpperBound = this.pageSize * 3;
        this.tablesRemoved_ = 0;
        this.page1DAO_ = this.initialPage1DAO_;
        this.page2DAO_ = this.initialPage2DAO_;
        this.page3DAO_ = this.initialPage3DAO_;
      }
    },
    {
      name: 'scrollDownOnePage',
      code: function() {
        // Update the bounds.
        if ( this.currentUpperBound > this.daoCount ) return;

        this.currentUpperBound += this.pageSize;
        this.currentLowerBound += this.pageSize;

        // Remove the top buffer table.
        this.topBufferTable_.remove();
        this.tablesRemoved_ += 1;

        // Update the table references.
        this.topBufferTable_ = this.visibleTable_;
        this.visibleTable_   = this.bottomBufferTable_;

        // Add a new bottom buffer table.
        var daoName = ['page1DAO_', 'page2DAO_', 'page3DAO_'][this.tablesRemoved_ % 3];
        this[daoName] = this.data.skip(this.currentUpperBound - this.pageSize).limit(this.pageSize);
        var rows = this.table_.rowsFrom(this[daoName + '$proxy']);
        this.table_.add(rows);
        this.bottomBufferTable_ = this.table_.childNodes
          .filter((x) => x.nodeName === 'TBODY')
          .pop();
      }
    },
    {
      name: 'scrollUpOnePage',
      code: function() {
        // Update the bounds.
        if ( this.currentLowerBound <= 0 ) return;

        this.currentLowerBound -= this.pageSize;
        this.currentUpperBound -= this.pageSize;

        // Remove the bottom buffer table.
        this.bottomBufferTable_.remove();
        this.tablesRemoved_ -= 1;

        // Update the table references.
        this.bottomBufferTable_ = this.visibleTable_;
        this.visibleTable_      = this.topBufferTable_;

        // Add a new top buffer table.
        var daoName = ['page1DAO_', 'page2DAO_', 'page3DAO_'][this.tablesRemoved_ % 3];
        this[daoName] = this.data.skip(this.currentLowerBound).limit(this.pageSize);
        var rows = this.table_.rowsFrom(this[daoName + '$proxy']);
        this.table_.insertBefore(this.table_.slotE_(rows), this.visibleTable_);
        this.topBufferTable_ = this.table_.childNodes
          .filter((x) => x.nodeName === 'TBODY')
          .shift();
      }
    }
  ],

  listeners: [
    {
      name: 'onScroll',
      code: function(e) {
        var deltaY = e.target.scrollTop - this.lastScrollTop_;
        var negative = deltaY < 0;
        var rows = Math.floor(Math.abs(this.accumulator + deltaY) / this.rowHeight);
        this.accumulator += deltaY;
        var oldSkip = this.skip;
        this.skip = Math.min(this.daoCount - this.limit, Math.max(0, this.skip + (negative ? -rows : rows)));
        this.lastScrollTop_ = e.target.scrollTop;

        var uBound = () => this.currentUpperBound - this.pageSize;
        var lBound = () => this.currentLowerBound + this.pageSize;

        if ( oldSkip <= uBound() && this.skip > uBound() ) {
          // The user scrolled down past a page boundary.
          while (
            this.skip > uBound() &&
            this.currentUpperBound < this.daoCount
          ) {
            this.scrollDownOnePage();
          }
        } else if ( oldSkip > lBound() && this.skip <= lBound() ) {
          // The user scrolled up past a page boundary.
          while ( this.skip <= lBound() && this.currentLowerBound > 0 ) {
            this.scrollUpOnePage();
          }
        }
      }
    },
    {
      // TODO: Avoid onDAOUpdate approaches.
      name: 'onDAOUpdate',
      isFramed: true,
      code: function() {
        var self = this;
        this.data$proxy.select(this.Count.create()).then(function(s) {
          self.daoCount = s.value;
          self.refresh();
        });
      }
    },
    {
      name: 'updateTableHeight',
      code: function() {
        // Find the distance from the top of the table to the top of the screen.
        var distanceFromTop = this.table_.el().getBoundingClientRect().y;

        // Calculate the remaining space we have to make use of.
        var remainingSpace = window.innerHeight - distanceFromTop;

        // TODO: Do we want to do this?
        // Leave space for the footer.
        remainingSpace -= 44;

        this.scrollbarContainer_.style({ height: `${remainingSpace}px` });
      }
    },
    {
      name: 'addTbodies',
      code: function() {
        this.table_.add(this.table_.rowsFrom(this.page2DAO_$proxy));
        this.table_.add(this.table_.rowsFrom(this.page3DAO_$proxy));
        var tbodies = this.table_.childNodes.filter((x) => x.nodeName === 'TBODY');
        this.topBufferTable_ = tbodies[0];
        this.visibleTable_ = tbodies[1];
        this.bottomBufferTable_ = tbodies[2];
      }
    }
  ]
});
<|endoftext|>


<commit_before>var GOVUK = GOVUK || {};
GOVUK.Analytics = GOVUK.Analytics || {};
GOVUK.Analytics.Trackers = {};

/*
 * Available methods on control:
 * - trackTimeBasedSuccess(millisecondsUntilSuccess)
 * - trackLinks(linkSelector)
 * - trackSuccess()
 */

/* Guide */
GOVUK.Analytics.Trackers.guide = function (control) {
    control.trackTimeBasedSuccess(7000);
    control.trackLinks("#content a");
};

/* Transaction (services) */
GOVUK.Analytics.Trackers.transaction = function (control) {
    control.trackLinks("#content a");
};

/* Benefit */
GOVUK.Analytics.Trackers.programme = function (control) {
    control.trackTimeBasedSuccess(7000);
    control.trackLinks("#content a");
};

/* Quick answer */
GOVUK.Analytics.Trackers.answer = function (control) {
    control.trackTimeBasedSuccess(7000);
    control.trackLinks("#content a");
};

/* Smart Answer */
/**
 * The Entry event should only be fired on the first page of the smart answer (the one with the get started button).
 * The Success event should only be fired if the user is coming from the first page and the 'smartanswerOutcome' custom
 *   event has been fired.
 *
 */
GOVUK.Analytics.Trackers.smart_answer = function (control) {
    $(document).bind("smartanswerOutcome", control.trackSuccess);
};

GOVUK.Analytics.Trackers.smart_answer.shouldTrackEntry = function() {
    return GOVUK.Analytics.isRootOfArtefact(document.URL);
};

GOVUK.Analytics.Trackers.smart_answer.shouldTrackSuccess = function () {
    return GOVUK.Analytics.isTheSameArtefact(document.URL, document.referrer);
};<commit_msg>Add documentation for custom entry and success.<commit_after>var GOVUK = GOVUK || {};
GOVUK.Analytics = GOVUK.Analytics || {};
GOVUK.Analytics.Trackers = {};

/*
 * Available methods on control:
 * - trackTimeBasedSuccess(millisecondsUntilSuccess)
 * - trackLinks(linkSelector)
 * - trackSuccess()
 *
 * Additional methods:
 * Trackers can optionally add functions to override control over whether we fire
 * entry or success events.
 *
 * - shouldTrackEntry() bool
 * - shouldTrackSuccess() bool
 */

/* Guide */
GOVUK.Analytics.Trackers.guide = function (control) {
    control.trackTimeBasedSuccess(7000);
    control.trackLinks("#content a");
};

/* Transaction (services) */
GOVUK.Analytics.Trackers.transaction = function (control) {
    control.trackLinks("#content a");
};

/* Benefit */
GOVUK.Analytics.Trackers.programme = function (control) {
    control.trackTimeBasedSuccess(7000);
    control.trackLinks("#content a");
};

/* Quick answer */
GOVUK.Analytics.Trackers.answer = function (control) {
    control.trackTimeBasedSuccess(7000);
    control.trackLinks("#content a");
};

/* Smart Answer */
/**
 * The Entry event should only be fired on the first page of the smart answer (the one with the get started button).
 * The Success event should only be fired if the user is coming from the first page and the 'smartanswerOutcome' custom
 *   event has been fired.
 *
 */
GOVUK.Analytics.Trackers.smart_answer = function (control) {
    $(document).bind("smartanswerOutcome", control.trackSuccess);
};

GOVUK.Analytics.Trackers.smart_answer.shouldTrackEntry = function() {
    return GOVUK.Analytics.isRootOfArtefact(document.URL);
};

GOVUK.Analytics.Trackers.smart_answer.shouldTrackSuccess = function () {
    return GOVUK.Analytics.isTheSameArtefact(document.URL, document.referrer);
};<|endoftext|>


<commit_before>App.addChild('Contribution', {
  el: '#new-contribution',

  events: {
    'click .radio' : 'clickReward',
    'click .submit-form' : 'submitForm',
    'keyup .user-reward-value' : 'submitOnReturnKey',
    'click .user-reward-value' : 'clearOnFocus',
    'input #contribution_value' : 'restrictChars'
  },

  restrictChars: function(event){
    var $target = $(event.target);
    $target.val($target.val().replace(/[^\d,]/, ''));
  },

  submitOnReturnKey: function(event) {
    event.preventDefault();
    if(event.keyCode === 13) {
      this.$('.submit-form').trigger('click');
    }
  },

  submitForm: function(event){
    var $target_row = $(event.target).parents('.back-reward-money'),
        user_value = this.$('.selected').find('.user-reward-value').val().replace(/\./g,''),
        minimumValue=this.minimumValue();
    if(user_value === ''){
      this.$value.val(minimumValue);
    }else{
      this.$value.val(user_value);
    }
    if(parseInt(user_value) < parseInt(minimumValue)){
      $target_row.find('.user-reward-value').addClass('error');
      $target_row.find('.text-error').slideDown();
    }else{
      CatarseAnalytics.event({cat:'contribution_create',act:'contribution_continue_click',lbl:minimumValue,val:minimumValue});
      this.$('form').submit();
    }

    return false;
  },

  activate: function(){
    this.$('.user-reward-value').mask('000.000.000', {reverse: true});
    this.$value = this.$('#contribution_value');
    this.$minimum = this.$('#minimum-value');
    if(this.$('input[type=radio]').length > 0) {
      this.clickReward({currentTarget: this.$('input[type=radio]:checked').parents('.radio')});
      this.isOnAutoScroll = false;
      this.activateFloattingHeader();
    }
    // copy default value from rendered contribution
    $('.user-reward-value:first').val($('#contribution_value').val());
    CatarseAnalytics.event({cat:'contribution_create',act:'contribution_started'});
  },

  activateFloattingHeader: function(){
    var that = this,
        top,
        top_title = $('#new-contribution'),
        faq_top = $("#faq-box").offset().top;
    $(window).scroll(function() {
        if(!that.isOnAutoScroll && !app.isMobile()){
            top = $(top_title).offset().top,
            $(window).scrollTop() > top ? $(".reward-floating").addClass("reward-floating-display") : $(".reward-floating").removeClass("reward-floating-display");
            var t = $("#faq-box");
            $(window).scrollTop() > faq_top-130 ? $(t).hasClass("faq-card-fixed") || $(t).addClass("faq-card-fixed") : $(t).hasClass("faq-card-fixed") && $(t).removeClass("faq-card-fixed");
        }
    });
  },

  clearOnFocus: function(event){
    this.$(event.target).val("");
    this.$('.error').removeClass('error');
    this.$('.text-error').slideUp();
    return false;
  },

  customValidation: function(event){
    if(parseInt(this.$value.val()) < this.minimumValue()){
      this.selectReward(this.$('.radio label'));
    }
  },

  minimumValue: function(){
    return this.$('.selected').find('label[data-minimum-value]').data('minimum-value');
  },

  resetSelected: function(){
    this.$('.w-radio').removeClass('selected');
  },

  selectReward: function(reward){
    this.resetSelected();
    reward.find('input[type=radio]').prop('checked', true);
    this.$('.back-reward-money').hide();
    reward.find('.back-reward-money').show();
    reward.parents('.back-reward-radio-reward').addClass('selected');
  },

  clickReward: function(event){
    var $el = $(event.currentTarget).find('label');
    if(!$el.parents('.back-reward-radio-reward').hasClass('selected')) {
      var elOffset = $el.offset().top;
      var elHeight = $el.height();
      var windowHeight = $(window).height();
      var isOnAutoScroll = this.isOnAutoScroll;
      var offset;
      if (elHeight < windowHeight) {
        offset = elOffset - ((windowHeight / 2) - ((elHeight * 2) / 3));
      } else {
        offset = elOffset;
      }
      $.smoothScroll({
        speed: 600,
        offset: -60,
        callback: function(){
          isOnAutoScroll = false;
        }
      }, offset);
      this.selectReward($el);
      var minimum = this.minimumValue();
      var reward_value = $el.find('.user-reward-value');
      if(reward_value.val() === ''){
        reward_value.val(minimum);
      }
      CatarseAnalytics.event({cat:"contribution_create",act:"contribution_reward_change",lbl:this.minimumValue()});
    }
  }
});

App.addChild('FaqBox', {
  el: '#faq-box',

  events: {
    'click li.list-question' : 'clickQuestion'
  },

  clickQuestion: function(event){
    var $question = $(event.currentTarget);
    var $answer = $question.next();
    $question.toggleClass('open').toggleClass('alt-link');
    $answer.slideToggle('slow');

    if($question.hasClass('open')) {//só vamos lançar o evento se abriu.
      CatarseAnalytics.event({cat:"contribution_create",act:"contribution_info_click",lbl:$.trim($question.text())});
    }
  },

  activate: function(){
    this.$('li.list-answer').hide();
  }
});
<commit_msg>Adicionando chat zopim na contribution de alguns projetos. Para testes<commit_after>App.addChild('Contribution', {
  el: '#new-contribution',

  events: {
    'click .radio' : 'clickReward',
    'click .submit-form' : 'submitForm',
    'keyup .user-reward-value' : 'submitOnReturnKey',
    'click .user-reward-value' : 'clearOnFocus',
    'input #contribution_value' : 'restrictChars'
  },

  restrictChars: function(event){
    var $target = $(event.target);
    $target.val($target.val().replace(/[^\d,]/, ''));
  },

  submitOnReturnKey: function(event) {
    event.preventDefault();
    if(event.keyCode === 13) {
      this.$('.submit-form').trigger('click');
    }
  },

  submitForm: function(event){
    var $target_row = $(event.target).parents('.back-reward-money'),
        user_value = this.$('.selected').find('.user-reward-value').val().replace(/\./g,''),
        minimumValue=this.minimumValue();
    if(user_value === ''){
      this.$value.val(minimumValue);
    }else{
      this.$value.val(user_value);
    }
    if(parseInt(user_value) < parseInt(minimumValue)){
      $target_row.find('.user-reward-value').addClass('error');
      $target_row.find('.text-error').slideDown();
    }else{
      CatarseAnalytics.event({cat:'contribution_create',act:'contribution_continue_click',lbl:minimumValue,val:minimumValue});
      this.$('form').submit();
    }

    return false;
  },

  activate: function(){
    this.$('.user-reward-value').mask('000.000.000', {reverse: true});
    this.$value = this.$('#contribution_value');
    this.$minimum = this.$('#minimum-value');
    if(this.$('input[type=radio]').length > 0) {
      this.clickReward({currentTarget: this.$('input[type=radio]:checked').parents('.radio')});
      this.isOnAutoScroll = false;
      this.activateFloattingHeader();
    }
    // copy default value from rendered contribution
    $('.user-reward-value:first').val($('#contribution_value').val());
    var stats=$('[data-stats]').data('stats');
    CatarseAnalytics.event({cat:'contribution_create',act:'contribution_started',lbl:stats && stats.id});
    if(stats && stats.id && _.contains([41679,40191,40271,38768,42815,43002,42129,41867,39655], stats.id)) {
          (window.$zopim && window.$zopim.livechat)||(function(d,s){var z=window.$zopim=function(c){z._.push(c)},$=z.s=d.createElement(s),e=d.getElementsByTagName(s)[0];z.set=function(o){z.set._.push(o)};z._=[];z.set._=[];$.async=!0;$.setAttribute('charset','utf-8');$.src='//v2.zopim.com/?2qPtIfZX0Exh5Szx5JUoUxWKqrTQI5Tm';z.t=+new Date;$.type='text/javascript';e.parentNode.insertBefore($,e)})(document,'script');
    }
  },

  activateFloattingHeader: function(){
    var that = this,
        top,
        top_title = $('#new-contribution'),
        faq_top = $("#faq-box").offset().top;
    $(window).scroll(function() {
        if(!that.isOnAutoScroll && !app.isMobile()){
            top = $(top_title).offset().top,
            $(window).scrollTop() > top ? $(".reward-floating").addClass("reward-floating-display") : $(".reward-floating").removeClass("reward-floating-display");
            var t = $("#faq-box");
            $(window).scrollTop() > faq_top-130 ? $(t).hasClass("faq-card-fixed") || $(t).addClass("faq-card-fixed") : $(t).hasClass("faq-card-fixed") && $(t).removeClass("faq-card-fixed");
        }
    });
  },

  clearOnFocus: function(event){
    this.$(event.target).val("");
    this.$('.error').removeClass('error');
    this.$('.text-error').slideUp();
    return false;
  },

  customValidation: function(event){
    if(parseInt(this.$value.val()) < this.minimumValue()){
      this.selectReward(this.$('.radio label'));
    }
  },

  minimumValue: function(){
    return this.$('.selected').find('label[data-minimum-value]').data('minimum-value');
  },

  resetSelected: function(){
    this.$('.w-radio').removeClass('selected');
  },

  selectReward: function(reward){
    this.resetSelected();
    reward.find('input[type=radio]').prop('checked', true);
    this.$('.back-reward-money').hide();
    reward.find('.back-reward-money').show();
    reward.parents('.back-reward-radio-reward').addClass('selected');
  },

  clickReward: function(event){
    var $el = $(event.currentTarget).find('label');
    if(!$el.parents('.back-reward-radio-reward').hasClass('selected')) {
      var elOffset = $el.offset().top;
      var elHeight = $el.height();
      var windowHeight = $(window).height();
      var isOnAutoScroll = this.isOnAutoScroll;
      var offset;
      if (elHeight < windowHeight) {
        offset = elOffset - ((windowHeight / 2) - ((elHeight * 2) / 3));
      } else {
        offset = elOffset;
      }
      $.smoothScroll({
        speed: 600,
        offset: -60,
        callback: function(){
          isOnAutoScroll = false;
        }
      }, offset);
      this.selectReward($el);
      var minimum = this.minimumValue();
      var reward_value = $el.find('.user-reward-value');
      if(reward_value.val() === ''){
        reward_value.val(minimum);
      }
      CatarseAnalytics.event({cat:"contribution_create",act:"contribution_reward_change",lbl:this.minimumValue()});
    }
  }
});

App.addChild('FaqBox', {
  el: '#faq-box',

  events: {
    'click li.list-question' : 'clickQuestion'
  },

  clickQuestion: function(event){
    var $question = $(event.currentTarget);
    var $answer = $question.next();
    $question.toggleClass('open').toggleClass('alt-link');
    $answer.slideToggle('slow');

    if($question.hasClass('open')) {//só vamos lançar o evento se abriu.
      CatarseAnalytics.event({cat:"contribution_create",act:"contribution_info_click",lbl:$.trim($question.text())});
    }
  },

  activate: function(){
    this.$('li.list-answer').hide();
  }
});
<|endoftext|>