<commit_before>const WindowManager = require('./window_manager')
const { autoUpdater } = require('electron-updater')
const { translate } = require('./i18n')
const { dialog } = require('electron')
const path = require('path')

const log = require('../../core/app').logger({
  component: 'GUI:autoupdater'
})

/** The delay starting from the update info request after which it is skipped.
 *
 * Long enough so users with slow connection have chances to start downloading
 * the update before it is skipped (5s was tried but didn't seem sufficient in
 * some cases).
 *
 * App startup could be slower in case GitHub is down exactly at the same
 * time. But the delay still seems acceptable  for an app starting
 * automatically on boot. Even in case it is started by hand.
 *
 * Except for downtimes, users with fast connection should still get a fast
 * available or unavailable update answer anyway.
 */
const UPDATE_CHECK_TIMEOUT = 10000

module.exports = class UpdaterWM extends WindowManager {
  windowOptions() {
    return {
      title: 'UPDATER',
      width: 500,
      height: 400
    }
  }

  humanError(err) {
    switch (err.code) {
      case 'EPERM':
        return translate('Updater Error EPERM')
      case 'ENOSP':
        return translate('Updater Error ENOSPC')
      default:
        return translate('Updater Error Other')
    }
  }

  constructor(...opts) {
    autoUpdater.logger = log
    autoUpdater.autoDownload = false
    autoUpdater.on('update-available', info => {
      this.clearTimeoutIfAny()
      log.info({ update: info, skipped: this.skipped }, 'Update available')
      // Make sure UI don't show up in front of onboarding after timeout
      if (!this.skipped) {
        this.skipped = true
        const shouldUpdate =
          dialog.showMessageBox({
            icon: path.resolve(__dirname, '..', 'images', 'icon.png'),
            title: 'Cozy Drive',
            message: 'Cozy Drive',
            detail: translate(
              'A new version is available, do you want to update?'
            ),
            type: 'question',
            buttons: ['Update', 'Cancel'].map(translate)
          }) === 0
        if (shouldUpdate) {
          autoUpdater.downloadUpdate()
          this.show()
        } else {
          this.skipped = false
        }
      }
    })
    autoUpdater.on('update-not-available', info => {
      log.info({ update: info }, 'No update available')
      this.afterUpToDate()
    })
    autoUpdater.on('error', err => {
      // May also happen in dev because of code signature error. Not an issue.
      log.error({ err }, 'Error in auto-updater! ')
      this.clearTimeoutIfAny()
      this.afterUpToDate()
    })
    autoUpdater.on('download-progress', progressObj => {
      log.trace({ progress: progressObj }, 'Downloading...')
      this.send('update-downloading', progressObj)
    })
    autoUpdater.on('update-downloaded', info => {
      log.info({ update: info }, 'Update downloaded. Exit and install...')
      setImmediate(() =>
        this.desktop
          .stopSync()
          .then(() => this.desktop.pouch.db.close())
          .then(() => autoUpdater.quitAndInstall())
          .then(() => this.app.quit())
          .then(() => this.app.exit(0))
          .catch(err => this.send('error-updating', this.humanError(err)))
      )
    })

    super(...opts)
  }

  clearTimeoutIfAny() {
    if (this.timeout) {
      clearTimeout(this.timeout)
      this.timeout = null
    }
  }

  onUpToDate(handler) {
    this.afterUpToDate = () => {
      this.clearTimeoutIfAny()
      handler()
    }
  }

  checkForUpdates() {
    log.info('Looking for updates...')
    this.timeout = setTimeout(() => {
      log.warn(
        { timeout: UPDATE_CHECK_TIMEOUT },
        'Updates check is taking too long'
      )
      this.skipped = true

      // Disable handler & warn on future calls
      const handler = this.afterUpToDate
      this.afterUpToDate = () => {}

      handler()
    }, UPDATE_CHECK_TIMEOUT)
    autoUpdater.checkForUpdates()
  }

  hash() {
    return '#updater'
  }

  ipcEvents() {
    return {}
  }
}
<commit_msg>gui/js/updater: Improve error handling (#1617)<commit_after>const Promise = require('bluebird')
const WindowManager = require('./window_manager')
const { autoUpdater } = require('electron-updater')
const { translate } = require('./i18n')
const { dialog } = require('electron')
const path = require('path')

const log = require('../../core/app').logger({
  component: 'GUI:autoupdater'
})

/** The delay starting from the update info request after which it is skipped.
 *
 * Long enough so users with slow connection have chances to start downloading
 * the update before it is skipped (5s was tried but didn't seem sufficient in
 * some cases).
 *
 * App startup could be slower in case GitHub is down exactly at the same
 * time. But the delay still seems acceptable  for an app starting
 * automatically on boot. Even in case it is started by hand.
 *
 * Except for downtimes, users with fast connection should still get a fast
 * available or unavailable update answer anyway.
 */
const UPDATE_CHECK_TIMEOUT = 10000

const UPDATE_RETRY_DELAY = 1000

module.exports = class UpdaterWM extends WindowManager {
  windowOptions() {
    return {
      title: 'UPDATER',
      width: 500,
      height: 400
    }
  }

  humanError(err) {
    switch (err.code) {
      case 'EPERM':
        return translate('Updater Error EPERM')
      case 'ENOSP':
        return translate('Updater Error ENOSPC')
      default:
        return translate('Updater Error Other')
    }
  }

  constructor(...opts) {
    autoUpdater.logger = log
    autoUpdater.autoDownload = false
    autoUpdater.on('update-available', info => {
      this.clearTimeoutIfAny()
      log.info({ update: info, skipped: this.skipped }, 'Update available')
      // Make sure UI don't show up in front of onboarding after timeout
      if (!this.skipped) {
        this.skipped = true
        const shouldUpdate =
          dialog.showMessageBox({
            icon: path.resolve(__dirname, '..', 'images', 'icon.png'),
            title: 'Cozy Drive',
            message: 'Cozy Drive',
            detail: translate(
              'A new version is available, do you want to update?'
            ),
            type: 'question',
            buttons: ['Update', 'Cancel'].map(translate)
          }) === 0
        if (shouldUpdate) {
          autoUpdater.downloadUpdate()
          this.show()
        } else {
          this.skipped = false
        }
      }
    })
    autoUpdater.on('update-not-available', info => {
      log.info({ update: info }, 'No update available')
      this.afterUpToDate()
    })
    autoUpdater.on('error', async err => {
      if (this.skipped) {
        return
      } else if (err.code === 'ENOENT') {
        this.skipUpdate('assuming development environment')
      } else {
        await Promise.delay(UPDATE_RETRY_DELAY)
        autoUpdater.checkForUpdates()
      }
    })
    autoUpdater.on('download-progress', progressObj => {
      log.trace({ progress: progressObj }, 'Downloading...')
      this.send('update-downloading', progressObj)
    })
    autoUpdater.on('update-downloaded', info => {
      log.info({ update: info }, 'Update downloaded. Exit and install...')
      setImmediate(() =>
        this.desktop
          .stopSync()
          .then(() => this.desktop.pouch.db.close())
          .then(() => autoUpdater.quitAndInstall())
          .then(() => this.app.quit())
          .then(() => this.app.exit(0))
          .catch(err => this.send('error-updating', this.humanError(err)))
      )
    })

    super(...opts)
  }

  clearTimeoutIfAny() {
    if (this.timeout) {
      clearTimeout(this.timeout)
      this.timeout = null
    }
  }

  onUpToDate(handler) {
    this.afterUpToDate = () => {
      this.clearTimeoutIfAny()
      handler()
    }
  }

  checkForUpdates() {
    this.timeout = setTimeout(() => {
      this.skipUpdate(`check is taking more than ${UPDATE_CHECK_TIMEOUT} ms`)
    }, UPDATE_CHECK_TIMEOUT)
    autoUpdater.checkForUpdates()
  }

  skipUpdate(reason) {
    log.info(`Not updating: ${reason}`)
    this.skipped = true

    // Disable handler & warn on future calls
    const handler = this.afterUpToDate
    this.afterUpToDate = () => {}

    handler()
  }

  hash() {
    return '#updater'
  }

  ipcEvents() {
    return {}
  }
}
<|endoftext|>


<commit_before>/*
 * jQuery UI Depends:
 *        jquery.ui.autocomplete.js
 *        jquery.ui.button.js
 *        jquery.ui.core.js
 *        jquery.ui.widget.js
 *        jquery.ui.position.js
 *        jquery.ui.menu.js
 */

nb.define('select', {
    events: {
        'click': '_onclick'
        //'open' { event, ui}
        //'close' { event, ui}
    },

    /**
     * Init select
     * @fires 'nb-inited'
     */
    oninit: function() {
        this.$control = this.$node.find('select');
        this.$dropdown = this.$node.children('.nb-select__dropdown').appendTo('body');
        this.data = this.nbdata();

        this._updateFromSelect();

        // preparing control depending on configuration and content
        this.controlPrepare();
        this.trigger('nb-inited', this);
    },

    /**
     * preparing control depending on configuration and content
     */
    controlPrepare: function() {
        var that = this;
        // preparing position parameters for popup from direction data
        var position = {};
        position.collision = 'flip';

        if (that.data.within) {
            position.within = that.data.within;
        }

        if (that.data.direction == 'top') {
            position.my = "left bottom";
            position.at = "left top";

        } else {
            position.my = "left top";
            position.at = "left bottom";
        }

        // select JUI control init
        this.$node.autocomplete({
            delay: 0,
            minLength: 0,
            autoFocus: false,
            position: position,
            appendTo: that.$dropdown,
            source: function(request, response) {
                response(that.$control.children(['option', 'optgroup']).map(function() {
                    var returnObj;
                    var $this = $(this);

                    if (this.tagName == 'OPTGROUP') {
                        returnObj = {
                            type: 'group',
                            label: $this.attr('label'),
                            option: this,
                            group: $this.children('option').map(function() {
                                return {
                                    label: $(this).text(),
                                    value: $(this).val(),
                                    option: this
                                };
                            })
                        };
                    } else {
                        var icon = $this.data('icon');
                        returnObj = {
                            label: $this.text(),
                            value: $this.val(),
                            option: this
                        };

                        if (icon) {
                            returnObj['icon'] = icon;
                        }
                    }

                    return returnObj;
                }));
            },
            select: function(event, ui) {

                if (ui.item.type != 'group') {
                    ui.item.option.selected = true;

                    that.$jUI._trigger('selected', event, {
                        item: ui.item.option
                    });
                }
            },
            // delegate handler on 'outer' click on open
            open: function() {
                that.$jUI._on(that.$jUI.document, {
                    // on 'outer' mousedown close control
                    mousedown: function(e) {
                        if (e.which == 1 && !$.contains(that.$jUI.element.get(0), e.target) && !$.contains(that.$dropdown[0], e.target)) {
                            this.close();
                        }
                    }
                });
                that.trigger('nb-opened', that);
            },

            close: function() {
                that.$jUI._off(that.$jUI.document, 'mousedown');
                that.trigger('nb-closed', that);
            }
        }).addClass('ui-widget ui-widget-content');

        that.$jUI = that.$node.data('uiAutocomplete');

        // redefine one menu item rendering method, fires every time, then popup opening
        that.$jUI._renderItem = function(ul, item) {
            var $itemNode = $('

');

            if (item.option.selected) {
                $itemNode.addClass('nb-select__item_selected_yes');
            }

            if (item.type == 'group') {

                $itemNode.addClass('nb-select__item_type_group');
                var $innerUL = $('

');

                item.group.each(function(index, item) {
                    that.$jUI._renderItem($innerUL, item);
                });

                $itemNode.append($innerUL);
            }

            $itemNode.data('ui-autocomplete-item', item);

            var $itemNodeContent = $('');
            var $itemText = $('').html(item.label).appendTo($itemNodeContent);
            if (item.icon) {
                $itemText.prepend('');
            }
            $itemNode
                .append($itemNodeContent)
                .appendTo(ul);

            return $itemNode;
        };

        // redefine valueMethod, extend with button text changing and fallback select value changing
        // if value not provided, return current value of fallback select
        that.$jUI.valueMethod = function(value) {

            if (typeof value === 'string') {
                var text = that.$control.find('option[value="' + value + '"]').text();
                that.setState({
                    value: value,
                    text: text
                });
            }
            return that.$selected.val();
        };

        // safe original function
        that.$jUI.__resizeMenu = that.$jUI._resizeMenu;

        that.$jUI._resizeMenu = function() {
            // set maxHeight before the menu is displayed
            if (that.data.maxheight) {
                that._setMaxHeight(that.data.maxheight);
            }

            this.__resizeMenu();
        };

        that.$jUI.menu.element.on('click', function(evt) {
            evt.stopPropagation();
        });
    },

    /**
     * Save value and text from <select> node.
     * @private
     */
    _updateFromSelect: function() {
        // get selected <option/>
        this.$selected = this.$control.children(':selected');

        this.value = this.$selected.val();
        //   - to prevent button from collapse if no text on <option/>
        this.text = this.$selected.text() || ' ';

        this._setText(this.text);
    },

    _onclick: function(evt) {
        if (this.$node && this.$node.data('uiAutocomplete')) {
            evt.preventDefault();
            // close if already visible
            if (this.$node.data('uiAutocomplete') && this.$node.autocomplete('widget').css('display') == 'block') {
                this.close();
                return;
            }

            if (this.isEnabled()) {
                this.open();
                this.$node.focus();
            }
        }
    },

    _setText: function(text) {
        this.$node.find('.nb-button__text').html(text);
    },

    _setMaxHeight: function(maxheight) {
        var height;
        if (/^\d+$/.test(maxheight)) {
            var item = this.$jUI.menu.element.find('.nb-select__item').first();
            height = parseInt(item.height()) * maxheight;
        } else {
            height = maxheight;
        }

        this.$jUI.menu.element.css({
            'max-height': height,
            'overflow-y': 'auto',
            'overflow-x': 'hidden'
        });
    },

    /**
     * Render dropdown of the select
     * @fires 'nb-rendered'
     * @returns {Object} nb.block
     */
    render: function() {
        if (!this.isEnabled()) {
            return this;
        }

        // pass empty string as value to search for, displaying all results
        this.$node.autocomplete('search', '');

        this.trigger('nb-rendered', this);
        return this;
    },

    /**
     * Open dropdown of the select
     * @fires 'nb-opened'
     * @returns {Object} nb.block
     */
    open: function() {
        if (this.$node && this.$node.data('uiAutocomplete') && this.isEnabled()) {
            this.render();
        }
        return this;
    },

    /**
     * Close dropdown of the select
     * @fires 'nb-closed'
     * @returns {Object} nb.block
     */
    close: function() {
        if (this.$node && this.$node.data('uiAutocomplete')) {
            this.$node.autocomplete('close');
            this.trigger('nb-closed', this);
        }
        return this;
    },

    /**
     * Changes a value of control, text on the button and select value it the fallback
     * @param {Object} params — {
         *     text: '..'
         *     value: '..'
         * }
     * @fires 'nb-changed'
     * @returns {Object} nb.block
     */
    setState: function(params) {
        params = params || {};

        if (this.value !== params.value) {
            var selected;

            if (params.value) {
                selected = this.$control.find('option[value="' + params.value + '"]');
            } else {
                selected = this.$control.find('option:contains(' + params.text + ')');
            }

            if (selected.length !== 0) {
                this.$selected.prop('selected', false);

                this.$selected = selected;

                this.$selected.prop('selected', true);

                this.value = this.$selected.val();

                this.text = this.$selected.html();

                this._setText(this.text);

                this.trigger('nb-changed', this);

                this.$control.val(params.value);

            }
        }
        return this;
    },

    /**
     * Returns state of the select
     *
     * @return {Object} -
     * {
         *     value: '..'
         *     text: '..'
         * }
     */
    getState: function() {
        return {
            value: this.value,
            text: this.text
        };
    },

    /**
     * Get name of the select
     * @returns {String|Object} name
     */
    getName: function() {
        return this.$control.prop('name');
    },

    /**
     * Changes a value of control, text on the button and select value it the fallback
     * @param {string} name
     * @fires 'nb-name-set'
     * @returns {Object} nb.block
     */
    setName: function(name) {
        this.$control.prop('name', name);
        this.trigger('nb-name-set', this);
        return this;
    },

    /**
     * Disables the select
     * @fires 'nb-disabled'
     * @returns {Object} nb.block
     */
    disable: function() {
        if (this.isEnabled()) {
            this.$node
                .addClass('nb-is-disabled')
                .autocomplete('disable');
            this.trigger('nb-disabled', this);
        }
        return this;
    },

    /**
     * Enables the select
     * @fires 'nb-enabled'
     * @returns {Object} nb.block
     */
    enable: function() {
        if (!this.isEnabled()) {
            this.$node
                .removeClass('nb-is-disabled')
                .autocomplete('enable');
            this.trigger('nb-enabled', this);
        }
        return this;
    },

    /**
     * Return state of the select
     * @returns {Boolean}
     */
    isEnabled: function() {
        return !this.$node.hasClass('nb-is-disabled');
    },

    /*
     * Set new items for select
     * @params {Array} source New source
     * @fires 'nb-source-changed'
     * @returns {Object} nb.block
     */
    setSource: function(source) {

        if (!source) {
            return this;
        }

        if (!(source instanceof Array)) {
            source = [source];
        }

        var html = [];
        for (var i = 0, j = source.length; i < j; i++) {
            var item = source[i];
            html.push(
                '<option' +
                    ' label="' + item.text + '"' +
                    ' value="' + item.value + '"' +
                    (item.selected ? ' selected="selected"' : '') +
                    '>' + item.text + '</option>'
            );
        }

        // set new source for select
        this.$control.empty().append(html);

        this._updateFromSelect();
        this.trigger('nb-source-changed', this);
        return this;
    },

    /*
     * Get items from select
     * @returns {Array} source
     */
    getSource: function() {
        return $.map(this.$control.children('option'), function(node) {
            return {
                text: $(node).text(),
                value: $(node).val()
            };
        });
    },

    /*
     * Add items to select
     * @param {Array|Object} items
     * @param {Number} index to insert
     * @fires 'nb-source-changed'
     * @returns {Object} nb.block
     */
    addToSource: function(items, index) {
        var source = this.getSource();

        if (!(items instanceof Array)) {
            items = [items];
        }

        var insertion = items.filter(function(item) {
            return source.indexOf(item) === -1;
        }, this);

        if (isNaN(index)) {
            index = source.length;
        }

        insertion.forEach(function(item, i) {
            source.splice(index + i, 0, item);
        }, this);

        this.setSource(source);
        this.trigger('nb-source-changed', this);
        return this;
    },

    /*
     * Remove items to select
     * @param {Array|Object|number} items or index
     * @fires 'nb-source-changed'
     * @returns {Object} nb.block
     */
    removeFromSource: function(param) {
        var source = this.getSource();
        var index;

        if (typeof param == 'number' || typeof param == 'string') {
            index = parseInt(param);
        } else if (!(param instanceof Array)) {
            param = [param];
        }

        if (index || index === 0) {
            source.splice(index, 1);
        } else {
            param.forEach(function(item) {
                source = source.filter(function(obj) {
                    return obj.text != item.text && obj.value != item.value;
                });
            }, this);
        }

        this.setSource(source);
        this.trigger('nb-source-changed', this);
        return this;
    },

    /**
     * Focus the select
     * @fires 'nb-focused'
     * @returns {Object} nb.block
     */
    focus: function() {
        if (this.isEnabled()) {
            this.$node.focus();
        }
        this.trigger('nb-focused', this);
        return this;
    },

    /**
     * Blur the select
     * @fires 'nb-blured'
     * @returns {Object} nb.block
     */
    blur: function() {
        if (this.isEnabled()) {
            this.$node.blur();
        }
        this.trigger('nb-blured', this);
        return this;
    },

    /**
     * Sets option to the jUI widget
     * http://api.jqueryui.com/autocomplete/#method-option
     * @param  {Object.<string, number>} option — {
           *      name: value —  имя и значение опцииопции
           * }
     * @fires 'nb-option-set'
     * @returns {Object} nb.block
     */
    setOption: function(option) {
        this.$node.autocomplete('option', option);
        this.trigger('nb-option-set', this);
        return this;
    },

    /**
     * Gets option of the jUI widget
     * http://api.jqueryui.com/autocomplete/#method-option
     * @param {String} option
     * @returns {String} option value
     */
    getOption: function(option) {
        return this.$node.autocomplete('option', option);
    },

    /**
     * Destroy the select
     * @fires 'nb-destroyed'
     */
    destroy: function() {
        if (this.$node && this.$node.data('uiAutocomplete')) {
            this.$node.autocomplete('destroy');
            this.$dropdown.empty().appendTo(this.$node);
        }
        this.trigger('nb-destroyed', this);
        this.nbdestroy();
    }
}, 'base');
<commit_msg>fix XSS in jUI menu rendering<commit_after>/*
 * jQuery UI Depends:
 *        jquery.ui.autocomplete.js
 *        jquery.ui.button.js
 *        jquery.ui.core.js
 *        jquery.ui.widget.js
 *        jquery.ui.position.js
 *        jquery.ui.menu.js
 */

nb.define('select', {
    events: {
        'click': '_onclick'
        //'open' { event, ui}
        //'close' { event, ui}
    },

    /**
     * Init select
     * @fires 'nb-inited'
     */
    oninit: function() {
        this.$control = this.$node.find('select');
        this.$dropdown = this.$node.children('.nb-select__dropdown').appendTo('body');
        this.data = this.nbdata();

        this._updateFromSelect();

        // preparing control depending on configuration and content
        this.controlPrepare();
        this.trigger('nb-inited', this);
    },

    /**
     * preparing control depending on configuration and content
     */
    controlPrepare: function() {
        var that = this;
        // preparing position parameters for popup from direction data
        var position = {};
        position.collision = 'flip';

        if (that.data.within) {
            position.within = that.data.within;
        }

        if (that.data.direction == 'top') {
            position.my = "left bottom";
            position.at = "left top";

        } else {
            position.my = "left top";
            position.at = "left bottom";
        }

        // select JUI control init
        this.$node.autocomplete({
            delay: 0,
            minLength: 0,
            autoFocus: false,
            position: position,
            appendTo: that.$dropdown,
            source: function(request, response) {
                response(that.$control.children(['option', 'optgroup']).map(function() {
                    var returnObj;
                    var $this = $(this);

                    if (this.tagName == 'OPTGROUP') {
                        returnObj = {
                            type: 'group',
                            label: $this.attr('label'),
                            option: this,
                            group: $this.children('option').map(function() {
                                return {
                                    label: $(this).text(),
                                    value: $(this).val(),
                                    option: this
                                };
                            })
                        };
                    } else {
                        var icon = $this.data('icon');
                        returnObj = {
                            label: $this.text(),
                            value: $this.val(),
                            option: this
                        };

                        if (icon) {
                            returnObj['icon'] = icon;
                        }
                    }

                    return returnObj;
                }));
            },
            select: function(event, ui) {

                if (ui.item.type != 'group') {
                    ui.item.option.selected = true;

                    that.$jUI._trigger('selected', event, {
                        item: ui.item.option
                    });
                }
            },
            // delegate handler on 'outer' click on open
            open: function() {
                that.$jUI._on(that.$jUI.document, {
                    // on 'outer' mousedown close control
                    mousedown: function(e) {
                        if (e.which == 1 && !$.contains(that.$jUI.element.get(0), e.target) && !$.contains(that.$dropdown[0], e.target)) {
                            this.close();
                        }
                    }
                });
                that.trigger('nb-opened', that);
            },

            close: function() {
                that.$jUI._off(that.$jUI.document, 'mousedown');
                that.trigger('nb-closed', that);
            }
        }).addClass('ui-widget ui-widget-content');

        that.$jUI = that.$node.data('uiAutocomplete');

        // redefine one menu item rendering method, fires every time, then popup opening
        that.$jUI._renderItem = function(ul, item) {
            var $itemNode = $('

');

            if (item.option.selected) {
                $itemNode.addClass('nb-select__item_selected_yes');
            }

            if (item.type == 'group') {

                $itemNode.addClass('nb-select__item_type_group');
                var $innerUL = $('

');

                item.group.each(function(index, item) {
                    that.$jUI._renderItem($innerUL, item);
                });

                $itemNode.append($innerUL);
            }

            $itemNode.data('ui-autocomplete-item', item);

            var $itemNodeContent = $('');
            var $itemText = $('').text(item.label).appendTo($itemNodeContent);
            if (item.icon) {
                $itemText.prepend('');
            }
            $itemNode
                .append($itemNodeContent)
                .appendTo(ul);

            return $itemNode;
        };

        // redefine valueMethod, extend with button text changing and fallback select value changing
        // if value not provided, return current value of fallback select
        that.$jUI.valueMethod = function(value) {

            if (typeof value === 'string') {
                var text = that.$control.find('option[value="' + value + '"]').text();
                that.setState({
                    value: value,
                    text: text
                });
            }
            return that.$selected.val();
        };

        // safe original function
        that.$jUI.__resizeMenu = that.$jUI._resizeMenu;

        that.$jUI._resizeMenu = function() {
            // set maxHeight before the menu is displayed
            if (that.data.maxheight) {
                that._setMaxHeight(that.data.maxheight);
            }

            this.__resizeMenu();
        };

        that.$jUI.menu.element.on('click', function(evt) {
            evt.stopPropagation();
        });
    },

    /**
     * Save value and text from <select> node.
     * @private
     */
    _updateFromSelect: function() {
        // get selected <option/>
        this.$selected = this.$control.children(':selected');

        this.value = this.$selected.val();
        //   - to prevent button from collapse if no text on <option/>
        this.text = this.$selected.text() || ' ';

        this._setText(this.text);
    },

    _onclick: function(evt) {
        if (this.$node && this.$node.data('uiAutocomplete')) {
            evt.preventDefault();
            // close if already visible
            if (this.$node.data('uiAutocomplete') && this.$node.autocomplete('widget').css('display') == 'block') {
                this.close();
                return;
            }

            if (this.isEnabled()) {
                this.open();
                this.$node.focus();
            }
        }
    },

    _setText: function(text) {
        this.$node.find('.nb-button__text').html(text);
    },

    _setMaxHeight: function(maxheight) {
        var height;
        if (/^\d+$/.test(maxheight)) {
            var item = this.$jUI.menu.element.find('.nb-select__item').first();
            height = parseInt(item.height()) * maxheight;
        } else {
            height = maxheight;
        }

        this.$jUI.menu.element.css({
            'max-height': height,
            'overflow-y': 'auto',
            'overflow-x': 'hidden'
        });
    },

    /**
     * Render dropdown of the select
     * @fires 'nb-rendered'
     * @returns {Object} nb.block
     */
    render: function() {
        if (!this.isEnabled()) {
            return this;
        }

        // pass empty string as value to search for, displaying all results
        this.$node.autocomplete('search', '');

        this.trigger('nb-rendered', this);
        return this;
    },

    /**
     * Open dropdown of the select
     * @fires 'nb-opened'
     * @returns {Object} nb.block
     */
    open: function() {
        if (this.$node && this.$node.data('uiAutocomplete') && this.isEnabled()) {
            this.render();
        }
        return this;
    },

    /**
     * Close dropdown of the select
     * @fires 'nb-closed'
     * @returns {Object} nb.block
     */
    close: function() {
        if (this.$node && this.$node.data('uiAutocomplete')) {
            this.$node.autocomplete('close');
            this.trigger('nb-closed', this);
        }
        return this;
    },

    /**
     * Changes a value of control, text on the button and select value it the fallback
     * @param {Object} params — {
         *     text: '..'
         *     value: '..'
         * }
     * @fires 'nb-changed'
     * @returns {Object} nb.block
     */
    setState: function(params) {
        params = params || {};

        if (this.value !== params.value) {
            var selected;

            if (params.value) {
                selected = this.$control.find('option[value="' + params.value + '"]');
            } else {
                selected = this.$control.find('option:contains(' + params.text + ')');
            }

            if (selected.length !== 0) {
                this.$selected.prop('selected', false);

                this.$selected = selected;

                this.$selected.prop('selected', true);

                this.value = this.$selected.val();

                this.text = this.$selected.html();

                this._setText(this.text);

                this.trigger('nb-changed', this);

                this.$control.val(params.value);

            }
        }
        return this;
    },

    /**
     * Returns state of the select
     *
     * @return {Object} -
     * {
         *     value: '..'
         *     text: '..'
         * }
     */
    getState: function() {
        return {
            value: this.value,
            text: this.text
        };
    },

    /**
     * Get name of the select
     * @returns {String|Object} name
     */
    getName: function() {
        return this.$control.prop('name');
    },

    /**
     * Changes a value of control, text on the button and select value it the fallback
     * @param {string} name
     * @fires 'nb-name-set'
     * @returns {Object} nb.block
     */
    setName: function(name) {
        this.$control.prop('name', name);
        this.trigger('nb-name-set', this);
        return this;
    },

    /**
     * Disables the select
     * @fires 'nb-disabled'
     * @returns {Object} nb.block
     */
    disable: function() {
        if (this.isEnabled()) {
            this.$node
                .addClass('nb-is-disabled')
                .autocomplete('disable');
            this.trigger('nb-disabled', this);
        }
        return this;
    },

    /**
     * Enables the select
     * @fires 'nb-enabled'
     * @returns {Object} nb.block
     */
    enable: function() {
        if (!this.isEnabled()) {
            this.$node
                .removeClass('nb-is-disabled')
                .autocomplete('enable');
            this.trigger('nb-enabled', this);
        }
        return this;
    },

    /**
     * Return state of the select
     * @returns {Boolean}
     */
    isEnabled: function() {
        return !this.$node.hasClass('nb-is-disabled');
    },

    /*
     * Set new items for select
     * @params {Array} source New source
     * @fires 'nb-source-changed'
     * @returns {Object} nb.block
     */
    setSource: function(source) {

        if (!source) {
            return this;
        }

        if (!(source instanceof Array)) {
            source = [source];
        }

        var html = [];
        for (var i = 0, j = source.length; i < j; i++) {
            var item = source[i];
            html.push(
                '<option' +
                    ' label="' + item.text + '"' +
                    ' value="' + item.value + '"' +
                    (item.selected ? ' selected="selected"' : '') +
                    '>' + item.text + '</option>'
            );
        }

        // set new source for select
        this.$control.empty().append(html);

        this._updateFromSelect();
        this.trigger('nb-source-changed', this);
        return this;
    },

    /*
     * Get items from select
     * @returns {Array} source
     */
    getSource: function() {
        return $.map(this.$control.children('option'), function(node) {
            return {
                text: $(node).text(),
                value: $(node).val()
            };
        });
    },

    /*
     * Add items to select
     * @param {Array|Object} items
     * @param {Number} index to insert
     * @fires 'nb-source-changed'
     * @returns {Object} nb.block
     */
    addToSource: function(items, index) {
        var source = this.getSource();

        if (!(items instanceof Array)) {
            items = [items];
        }

        var insertion = items.filter(function(item) {
            return source.indexOf(item) === -1;
        }, this);

        if (isNaN(index)) {
            index = source.length;
        }

        insertion.forEach(function(item, i) {
            source.splice(index + i, 0, item);
        }, this);

        this.setSource(source);
        this.trigger('nb-source-changed', this);
        return this;
    },

    /*
     * Remove items to select
     * @param {Array|Object|number} items or index
     * @fires 'nb-source-changed'
     * @returns {Object} nb.block
     */
    removeFromSource: function(param) {
        var source = this.getSource();
        var index;

        if (typeof param == 'number' || typeof param == 'string') {
            index = parseInt(param);
        } else if (!(param instanceof Array)) {
            param = [param];
        }

        if (index || index === 0) {
            source.splice(index, 1);
        } else {
            param.forEach(function(item) {
                source = source.filter(function(obj) {
                    return obj.text != item.text && obj.value != item.value;
                });
            }, this);
        }

        this.setSource(source);
        this.trigger('nb-source-changed', this);
        return this;
    },

    /**
     * Focus the select
     * @fires 'nb-focused'
     * @returns {Object} nb.block
     */
    focus: function() {
        if (this.isEnabled()) {
            this.$node.focus();
        }
        this.trigger('nb-focused', this);
        return this;
    },

    /**
     * Blur the select
     * @fires 'nb-blured'
     * @returns {Object} nb.block
     */
    blur: function() {
        if (this.isEnabled()) {
            this.$node.blur();
        }
        this.trigger('nb-blured', this);
        return this;
    },

    /**
     * Sets option to the jUI widget
     * http://api.jqueryui.com/autocomplete/#method-option
     * @param  {Object.<string, number>} option — {
           *      name: value —  имя и значение опцииопции
           * }
     * @fires 'nb-option-set'
     * @returns {Object} nb.block
     */
    setOption: function(option) {
        this.$node.autocomplete('option', option);
        this.trigger('nb-option-set', this);
        return this;
    },

    /**
     * Gets option of the jUI widget
     * http://api.jqueryui.com/autocomplete/#method-option
     * @param {String} option
     * @returns {String} option value
     */
    getOption: function(option) {
        return this.$node.autocomplete('option', option);
    },

    /**
     * Destroy the select
     * @fires 'nb-destroyed'
     */
    destroy: function() {
        if (this.$node && this.$node.data('uiAutocomplete')) {
            this.$node.autocomplete('destroy');
            this.$dropdown.empty().appendTo(this.$node);
        }
        this.trigger('nb-destroyed', this);
        this.nbdestroy();
    }
}, 'base');
<|endoftext|>


<commit_before>/* jshint latedef:nofunc */
'use strict';

var $ = require('../util/jquery');
var oo = require('../util/oo');
var Coordinate = require('../model/Coordinate');
var Range = require('../model/Range');
var TextPropertyComponent = require('./TextPropertyComponent');

/*
 * A class that maps DOM selections to model selections.
 *
 * There are some difficulties with mapping model selections:
 * 1. DOM selections can not model discontinuous selections.
 * 2. Not all positions reachable via ContentEditable can be mapped to model selections. For instance,
 *    there are extra positions before and after non-editable child elements.
 * 3. Some native cursor behaviors need to be overidden.
 *
 * @class DOMSelection
 * @constructor
 * @param {Element} rootElement
 */
function DOMSelection(surface) {
  this.surface = surface;
}

function isElementInViewport (el) {
    if (typeof $ === "function" && el instanceof $) {
        el = el[0];
    }
    var rect = el.getBoundingClientRect();
    return (
        rect.top >= 70 && // 70 is adjustment for top navbar etc
        rect.bottom <= el.offsetParent.getBoundingClientRect().bottom - 20 // 20 is adjustment for bottom padding etc
    );
}

DOMSelection.Prototype = function() {

  /**
    Create a model selection by mapping the current DOM selection
    to model coordinates.

    @param {object} options
      - `direction`: `left` or `right`; a hint for disambiguations, used by Surface during cursor navigation.
    @returns {model/Selection}
  */
  this.getSelection = function(options) {
    var range = this.mapDOMSelection(options);
    var doc = this.surface.getDocument();
    return doc.createSelection(range);
  };

  /**
    Transfer a given model selection into the DOM.

    @param {model/Selection} sel
  */
  this.setSelection = function(sel) {
    // console.log('### renderSelection', sel.toString());
    var wSel = window.getSelection();
    if (sel.isNull() || sel.isTableSelection()) {
      this.clear();
      return;
    }
    var startComp = this.surface._getTextPropertyComponent(sel.startPath);
    if (!startComp) {
      console.warn('FIXME: selection seems to be invalid.');
      this.clear();
      return;
    }
    var start = startComp.getDOMCoordinate(sel.startOffset);
    var end;
    if (sel.isCollapsed()) {
      end = start;
    } else {
      var endComp = this.surface._getTextPropertyComponent(sel.endPath);
      if (!endComp) {
        console.warn('FIXME: selection seems to be invalid.');
        this.clear();
        return;
      }
      end = endComp.getDOMCoordinate(sel.endOffset);
    }
    // console.log('mapped to DOM coordinates', start.container, start.offset, end.container, end.offset, 'isReverse?', sel.isReverse());
    // if there is a range then set replace the window selection accordingly
    wSel.removeAllRanges();
    var wRange = window.document.createRange();
    if (sel.isCollapsed()) {
      wRange.setStart(start.container, start.offset);
      wSel.addRange(wRange);

      // If the selection is not visible, scroll the parent to bring it in view
      var el = document.createElement("span");
      el.id = 'curr-pos-span';
      wRange.insertNode(el);
      if(!isElementInViewport(el)){
        var offsetParent = $("#curr-pos-span").offsetParent()[0];
        var scrollAdjustment = 200;
        if ($('#curr-pos-span')[0].offsetTop < offsetParent.scrollTop){
          // scrolling up
          offsetParent.scrollTop = $('#curr-pos-span')[0].offsetTop - scrollAdjustment;
        } else {
          // scrolling down
          offsetParent.scrollTop = $('#curr-pos-span')[0].offsetTop - parseInt($(window).height()) + scrollAdjustment;
        }
      }
      wRange.deleteContents();
    } else {
      if (sel.isReverse()) {
        // console.log('DOMSelection: rendering a reverse selection.');
        var tmp = start;
        start = end;
        end = tmp;
      }
      // HACK: using wRange setEnd does not work reliably
      // so we set just the start anchor
      // and then use window.Selection.extend()
      // unfortunately we are not able to test this behavior as it needs
      // triggering native keyboard events
      wRange.setStart(start.container, start.offset);
      wSel.addRange(wRange);
      wSel.extend(end.container, end.offset);
    }
    // console.log('DOMSelection.setSelection()', 'anchorNode:', wSel.anchorNode, 'anchorOffset:', wSel.anchorOffset, 'focusNode:', wSel.focusNode, 'focusOffset:', wSel.focusOffset, 'collapsed:', wSel.collapsed);
  };

  /*
    Map a DOM range to a model range.

    @param {Range} range
    @returns {model/Range}
  */
  this.mapDOMRange = function(wRange) {
    return this._getRange(wRange.startContainer, wRange.startOffset,
      wRange.endContainer, wRange.endOffset);
  };

  /*
    Maps the current DOM selection to a model range.

    @param {object} [options]
      - `direction`: `left` or `right`; a hint for disambiguations, used by Surface during cursor navigation.
    @returns {model/Range}
  */
  this.mapDOMSelection = function(options) {
    var range;
    var wSel = window.getSelection();
    // Use this log whenever the mapping goes wrong to analyze what
    // is actually being provided by the browser
    // console.log('DOMSelection.getSelection()', 'anchorNode:', wSel.anchorNode, 'anchorOffset:', wSel.anchorOffset, 'focusNode:', wSel.focusNode, 'focusOffset:', wSel.focusOffset, 'collapsed:', wSel.collapsed);
    if (wSel.rangeCount === 0) {
      return null;
    }
    if (wSel.isCollapsed) {
      var coor = this._getCoordinate(wSel.anchorNode, wSel.anchorOffset, options);
      range = _createRange(coor, coor, false, this.getContainerId());
      return range;
    }
    // HACK: special treatment for edge cases as addressed by #354.
    // Sometimes anchorNode and focusNodes are the surface
    if ($(wSel.anchorNode).is('.surface')) {
      range = this._getEnclosingRange(wSel.getRangeAt(0));
    } else {
      range = this._getRange(wSel.anchorNode, wSel.anchorOffset, wSel.focusNode, wSel.focusOffset);
    }
    // console.log('### extracted range from DOM', range.toString());
    return range;
  };

  /*
    Clear the DOM selection.
  */
  this.clear = function() {
    window.getSelection().removeAllRanges();
  };

  this.getContainerId = function() {
    if (this.surface.isContainerEditor()) {
      return this.surface.getContainerId();
    } else {
      return null;
    }
  };

  /*
    Extract a model range from given DOM elements.

    @param {Node} anchorNode
    @param {number} anchorOffset
    @param {Node} focusNode
    @param {number} focusOffset
    @returns {model/Range}
  */
  this._getRange = function(anchorNode, anchorOffset, focusNode, focusOffset) {
    var start = this._getCoordinate(anchorNode, anchorOffset);
    var end;
    if (anchorNode === focusNode && anchorOffset === focusOffset) {
      end = start;
    } else {
      end = this._getCoordinate(focusNode, focusOffset);
    }
    var isReverse = _isReverse(anchorNode, anchorOffset, focusNode, focusOffset);
    if (start && end) {
      return _createRange(start, end, isReverse, this.getContainerId());
    } else {
      return null;
    }
  };

  /*
    Map a DOM coordinate to a model coordinate.

    @param {Node} node
    @param {number} offset
    @param {object} options
    @param {object} [options]
      - `direction`: `left` or `right`; a hint for disambiguation.
    @returns {model/Coordinate}

    @info

    `options.direction` can be used to control the result when this function is called
    after cursor navigation. The root problem is that we are using ContentEditable on
    Container level (as opposed to TextProperty level). The native ContentEditable allows
    cursor positions which do not make sense in the model sense.

    For example,

    ```
    <div contenteditable=true>
      <p data-path="p1.content">foo</p>
      <img>
      <p data-path="p1.content">bar</p>
    </div>
    ```
    would allow to set the cursor directly before or after the image, which
    we want to prevent, as it is not a valid insert position for text.
    Instead, if we find the DOM selection in such a situation, then we map it to the
    closest valid model address. And this depends on the direction of movement.
    Moving `left` would provide the previous address, `right` would provide the next address.
    The default direction is `right`.
  */
  this._getCoordinate = function(node, offset, options) {
    // Trying to apply the most common situation first
    // and after that covering known edge cases
    var coor = TextPropertyComponent.getCoordinate(this.surface.el, node, offset);
    if (!coor) {
      coor = this._searchForCoordinate(node, offset, options);
    }
    return coor;
  };

  /*
    Map a DOM coordinate to a model coordinate via a brute-force search
    on all properties.

    This is used as a backup strategy for delicate DOM selections.

    @param {Node} node
    @param {number} offset
    @param {object} options
    @param {'left'|'right'} options.direction
    @returns {model/Coordinate} the coordinate
  */
  this._searchForCoordinate = function(node, offset, options) {
    options = options || {};
    var elements = this.surface.el.querySelectorAll('*[data-path]');
    var idx, idx1, idx2, cmp1, cmp2;
    idx1 = 0;
    idx2 = elements.length-1;
    cmp1 = _compareNodes(elements[idx1], node);
    cmp2 = _compareNodes(elements[idx2], node);
    while(true) {
      var l = idx2-idx1+1;
      if (cmp2 < 0) {
        idx = idx2;
        break;
      } else if (cmp1 > 0) {
        idx = idx1;
        break;
      } else if (l<=2) {
        idx = idx2;
        break;
      }
      var pivotIdx = idx1 + Math.floor(l/2);
      var pivotCmp = _compareNodes(elements[pivotIdx], node);
      if (pivotCmp < 0) {
        idx1 = pivotIdx;
        cmp1 = pivotCmp;
      } else {
        idx2 = pivotIdx;
        cmp2 = pivotCmp;
      }
    }
    var charPos;
    var doc = this.surface.getDocument();
    var path = _getPath(elements[idx]);
    var text = doc.get(path);
    if (options.direction === "left") {
      if (idx === 0) {
        charPos = 0;
      } else {
        path = _getPath(elements[idx-1]);
        text = doc.get(path);
        charPos = text.length;
      }
    } else if (cmp2<0) {
      charPos = text.length;
    } else {
      charPos = 0;
    }
    return new Coordinate(path, charPos);
  };

  /*
    Computes a model range that encloses all properties
    spanned by a given DOM range.

    This is used in edge cases, where DOM selection anchors are not
    within TextProperties.

    @param {Range} range
    @returns {model/Range}
  */
  this._getEnclosingRange = function(wRange) {
    var frag = wRange.cloneContents();
    var props = frag.querySelectorAll('*[data-path]');
    if (props.length === 0) {
      return null;
    } else {
      var doc = this.surface.getDocument();
      var first = props[0];
      var last = props[props.length-1];
      var startPath = _getPath(first);
      var text;
      if (first === last) {
        text = doc.get(startPath);
        return new Range(
          new Coordinate(startPath, 0),
          new Coordinate(startPath, text.length),
          false
        );
      } else {
        var endPath = _getPath(last);
        text = doc.get(endPath);
        return new Range(
          new Coordinate(startPath, 0),
          new Coordinate(endPath, text.length),
          false
        );
      }
    }
  };

  function _compareNodes(node1, node2) {
    var cmp = node1.compareDocumentPosition(node2);
    // Note: the first two cases are necessary because POSITION_FOLLOWING
    // has strange semantics when the relationship is actually hierarchical
    if (cmp & window.document.DOCUMENT_POSITION_FOLLOWING) {
      return -1;
    } else if (cmp&window.document.DOCUMENT_POSITION_PRECEDING) {
      return 1;
    } else {
      return 0;
    }
  }

  var _r1, _r2;

  function _isReverse(anchorNode, anchorOffset, focusNode, focusOffset) {
    // the selection is reversed when the focus propertyEl is before
    // the anchor el or the computed charPos is in reverse order
    if (focusNode && anchorNode) {
      if (!_r1) {
        _r1 = window.document.createRange();
        _r2 = window.document.createRange();
      }
      _r1.setStart(anchorNode, anchorOffset);
      _r2.setStart(focusNode, focusOffset);
      var cmp = _r1.compareBoundaryPoints(window.Range.START_TO_START, _r2);
      if (cmp === 1) {
        return true;
      }
    }
    return false;
  }

  function _getPath(el) {
    if (el && el.dataset && el.dataset.path) {
      return el.dataset.path.split('.');
    }
  }

  /*
   Helper for creating a model range correctly
   as for model/Range start should be before end.

   In contrast to that, DOM selections are described with anchor and focus coordinates,
   i.e. bearing the information of direction implicitly.
   To simplify the implementation we treat anchor and focus equally
   and only at the end exploit the fact deriving an isReverse flag
   and bringing start and end in the correct order.
  */
  function _createRange(start, end, isReverse, containerId) {
    if (isReverse) {
      var tmp = start;
      start = end;
      end = tmp;
    }
    return new Range(start, end, isReverse, containerId);
  }

};

oo.initClass(DOMSelection);

module.exports = DOMSelection;
<commit_msg>Handle the case when selection falls inside caret span when mapping from DOM Selection to model selection<commit_after>/* jshint latedef:nofunc */
'use strict';

var $ = require('../util/jquery');
var oo = require('../util/oo');
var Coordinate = require('../model/Coordinate');
var Range = require('../model/Range');
var TextPropertyComponent = require('./TextPropertyComponent');

/*
 * A class that maps DOM selections to model selections.
 *
 * There are some difficulties with mapping model selections:
 * 1. DOM selections can not model discontinuous selections.
 * 2. Not all positions reachable via ContentEditable can be mapped to model selections. For instance,
 *    there are extra positions before and after non-editable child elements.
 * 3. Some native cursor behaviors need to be overidden.
 *
 * @class DOMSelection
 * @constructor
 * @param {Element} rootElement
 */
function DOMSelection(surface) {
  this.surface = surface;
}

function isElementInViewport (el) {
    if (typeof $ === "function" && el instanceof $) {
        el = el[0];
    }
    var rect = el.getBoundingClientRect();
    return (
        rect.top >= 70 && // 70 is adjustment for top navbar etc
        rect.bottom <= el.offsetParent.getBoundingClientRect().bottom - 20 // 20 is adjustment for bottom padding etc
    );
}

DOMSelection.Prototype = function() {

  /**
    Create a model selection by mapping the current DOM selection
    to model coordinates.

    @param {object} options
      - `direction`: `left` or `right`; a hint for disambiguations, used by Surface during cursor navigation.
    @returns {model/Selection}
  */
  this.getSelection = function(options) {
    var range = this.mapDOMSelection(options);
    var doc = this.surface.getDocument();
    return doc.createSelection(range);
  };

  /**
    Transfer a given model selection into the DOM.

    @param {model/Selection} sel
  */
  this.setSelection = function(sel) {
    // console.log('### renderSelection', sel.toString());
    var wSel = window.getSelection();
    if (sel.isNull() || sel.isTableSelection()) {
      this.clear();
      return;
    }
    var startComp = this.surface._getTextPropertyComponent(sel.startPath);
    if (!startComp) {
      console.warn('FIXME: selection seems to be invalid.');
      this.clear();
      return;
    }
    var start = startComp.getDOMCoordinate(sel.startOffset);
    var end;
    if (sel.isCollapsed()) {
      end = start;
    } else {
      var endComp = this.surface._getTextPropertyComponent(sel.endPath);
      if (!endComp) {
        console.warn('FIXME: selection seems to be invalid.');
        this.clear();
        return;
      }
      end = endComp.getDOMCoordinate(sel.endOffset);
    }
    // console.log('mapped to DOM coordinates', start.container, start.offset, end.container, end.offset, 'isReverse?', sel.isReverse());
    // if there is a range then set replace the window selection accordingly
    wSel.removeAllRanges();
    var wRange = window.document.createRange();
    if (sel.isCollapsed()) {
      wRange.setStart(start.container, start.offset);
      wSel.addRange(wRange);

      // If the selection is not visible, scroll the parent to bring it in view
      var el = document.createElement("span");
      el.id = 'curr-pos-span';
      wRange.insertNode(el);
      if(!isElementInViewport(el)){
        var offsetParent = $("#curr-pos-span").offsetParent()[0];
        var scrollAdjustment = 200;
        if ($('#curr-pos-span')[0].offsetTop < offsetParent.scrollTop){
          // scrolling up
          offsetParent.scrollTop = $('#curr-pos-span')[0].offsetTop - scrollAdjustment;
        } else {
          // scrolling down
          offsetParent.scrollTop = $('#curr-pos-span')[0].offsetTop - parseInt($(window).height()) + scrollAdjustment;
        }
      }
      wRange.deleteContents();
    } else {
      if (sel.isReverse()) {
        // console.log('DOMSelection: rendering a reverse selection.');
        var tmp = start;
        start = end;
        end = tmp;
      }
      // HACK: using wRange setEnd does not work reliably
      // so we set just the start anchor
      // and then use window.Selection.extend()
      // unfortunately we are not able to test this behavior as it needs
      // triggering native keyboard events
      wRange.setStart(start.container, start.offset);
      wSel.addRange(wRange);
      wSel.extend(end.container, end.offset);
    }
    // console.log('DOMSelection.setSelection()', 'anchorNode:', wSel.anchorNode, 'anchorOffset:', wSel.anchorOffset, 'focusNode:', wSel.focusNode, 'focusOffset:', wSel.focusOffset, 'collapsed:', wSel.collapsed);
  };

  /*
    Map a DOM range to a model range.

    @param {Range} range
    @returns {model/Range}
  */
  this.mapDOMRange = function(wRange) {
    return this._getRange(wRange.startContainer, wRange.startOffset,
      wRange.endContainer, wRange.endOffset);
  };

  /*
    Maps the current DOM selection to a model range.

    @param {object} [options]
      - `direction`: `left` or `right`; a hint for disambiguations, used by Surface during cursor navigation.
    @returns {model/Range}
  */
  this.mapDOMSelection = function(options) {
    var range;
    var wSel = window.getSelection();
    // Use this log whenever the mapping goes wrong to analyze what
    // is actually being provided by the browser
    // console.log('DOMSelection.getSelection()', 'anchorNode:', wSel.anchorNode, 'anchorOffset:', wSel.anchorOffset, 'focusNode:', wSel.focusNode, 'focusOffset:', wSel.focusOffset, 'collapsed:', wSel.collapsed);
    if (wSel.rangeCount === 0) {
      return null;
    }
    if (wSel.isCollapsed) {
      var anchorNode = wSel.anchorNode;
      var anchorOffset = wSel.anchorOffset;

      if($(wSel.anchorNode.parentElement).is('.sm-local-user')) {
        if(wSel.anchorNode.parentElement.previousSibling === null) {
          anchorNode = wSel.anchorNode.parentElement.nextSibling;
          anchorOffset = 0;
        }
        else {
          anchorNode = wSel.anchorNode.parentElement.previousSibling;
          anchorOffset = anchorNode.length;
        }
      }
      var coor = this._getCoordinate(anchorNode, anchorOffset, options);
      range = _createRange(coor, coor, false, this.getContainerId());
      return range;
    }
    // HACK: special treatment for edge cases as addressed by #354.
    // Sometimes anchorNode and focusNodes are the surface
    if ($(wSel.anchorNode).is('.surface')) {
      range = this._getEnclosingRange(wSel.getRangeAt(0));
    } else {
      range = this._getRange(wSel.anchorNode, wSel.anchorOffset, wSel.focusNode, wSel.focusOffset);
    }
    // console.log('### extracted range from DOM', range.toString());
    return range;
  };

  /*
    Clear the DOM selection.
  */
  this.clear = function() {
    window.getSelection().removeAllRanges();
  };

  this.getContainerId = function() {
    if (this.surface.isContainerEditor()) {
      return this.surface.getContainerId();
    } else {
      return null;
    }
  };

  /*
    Extract a model range from given DOM elements.

    @param {Node} anchorNode
    @param {number} anchorOffset
    @param {Node} focusNode
    @param {number} focusOffset
    @returns {model/Range}
  */
  this._getRange = function(anchorNode, anchorOffset, focusNode, focusOffset) {
    var start = this._getCoordinate(anchorNode, anchorOffset);
    var end;
    if (anchorNode === focusNode && anchorOffset === focusOffset) {
      end = start;
    } else {
      end = this._getCoordinate(focusNode, focusOffset);
    }
    var isReverse = _isReverse(anchorNode, anchorOffset, focusNode, focusOffset);
    if (start && end) {
      return _createRange(start, end, isReverse, this.getContainerId());
    } else {
      return null;
    }
  };

  /*
    Map a DOM coordinate to a model coordinate.

    @param {Node} node
    @param {number} offset
    @param {object} options
    @param {object} [options]
      - `direction`: `left` or `right`; a hint for disambiguation.
    @returns {model/Coordinate}

    @info

    `options.direction` can be used to control the result when this function is called
    after cursor navigation. The root problem is that we are using ContentEditable on
    Container level (as opposed to TextProperty level). The native ContentEditable allows
    cursor positions which do not make sense in the model sense.

    For example,

    ```
    <div contenteditable=true>
      <p data-path="p1.content">foo</p>
      <img>
      <p data-path="p1.content">bar</p>
    </div>
    ```
    would allow to set the cursor directly before or after the image, which
    we want to prevent, as it is not a valid insert position for text.
    Instead, if we find the DOM selection in such a situation, then we map it to the
    closest valid model address. And this depends on the direction of movement.
    Moving `left` would provide the previous address, `right` would provide the next address.
    The default direction is `right`.
  */
  this._getCoordinate = function(node, offset, options) {
    // Trying to apply the most common situation first
    // and after that covering known edge cases
    var coor = TextPropertyComponent.getCoordinate(this.surface.el, node, offset);
    if (!coor) {
      coor = this._searchForCoordinate(node, offset, options);
    }
    return coor;
  };

  /*
    Map a DOM coordinate to a model coordinate via a brute-force search
    on all properties.

    This is used as a backup strategy for delicate DOM selections.

    @param {Node} node
    @param {number} offset
    @param {object} options
    @param {'left'|'right'} options.direction
    @returns {model/Coordinate} the coordinate
  */
  this._searchForCoordinate = function(node, offset, options) {
    options = options || {};
    var elements = this.surface.el.querySelectorAll('*[data-path]');
    var idx, idx1, idx2, cmp1, cmp2;
    idx1 = 0;
    idx2 = elements.length-1;
    cmp1 = _compareNodes(elements[idx1], node);
    cmp2 = _compareNodes(elements[idx2], node);
    while(true) {
      var l = idx2-idx1+1;
      if (cmp2 < 0) {
        idx = idx2;
        break;
      } else if (cmp1 > 0) {
        idx = idx1;
        break;
      } else if (l<=2) {
        idx = idx2;
        break;
      }
      var pivotIdx = idx1 + Math.floor(l/2);
      var pivotCmp = _compareNodes(elements[pivotIdx], node);
      if (pivotCmp < 0) {
        idx1 = pivotIdx;
        cmp1 = pivotCmp;
      } else {
        idx2 = pivotIdx;
        cmp2 = pivotCmp;
      }
    }
    var charPos;
    var doc = this.surface.getDocument();
    var path = _getPath(elements[idx]);
    var text = doc.get(path);
    if (options.direction === "left") {
      if (idx === 0) {
        charPos = 0;
      } else {
        path = _getPath(elements[idx-1]);
        text = doc.get(path);
        charPos = text.length;
      }
    } else if (cmp2<0) {
      charPos = text.length;
    } else {
      charPos = 0;
    }
    return new Coordinate(path, charPos);
  };

  /*
    Computes a model range that encloses all properties
    spanned by a given DOM range.

    This is used in edge cases, where DOM selection anchors are not
    within TextProperties.

    @param {Range} range
    @returns {model/Range}
  */
  this._getEnclosingRange = function(wRange) {
    var frag = wRange.cloneContents();
    var props = frag.querySelectorAll('*[data-path]');
    if (props.length === 0) {
      return null;
    } else {
      var doc = this.surface.getDocument();
      var first = props[0];
      var last = props[props.length-1];
      var startPath = _getPath(first);
      var text;
      if (first === last) {
        text = doc.get(startPath);
        return new Range(
          new Coordinate(startPath, 0),
          new Coordinate(startPath, text.length),
          false
        );
      } else {
        var endPath = _getPath(last);
        text = doc.get(endPath);
        return new Range(
          new Coordinate(startPath, 0),
          new Coordinate(endPath, text.length),
          false
        );
      }
    }
  };

  function _compareNodes(node1, node2) {
    var cmp = node1.compareDocumentPosition(node2);
    // Note: the first two cases are necessary because POSITION_FOLLOWING
    // has strange semantics when the relationship is actually hierarchical
    if (cmp & window.document.DOCUMENT_POSITION_FOLLOWING) {
      return -1;
    } else if (cmp&window.document.DOCUMENT_POSITION_PRECEDING) {
      return 1;
    } else {
      return 0;
    }
  }

  var _r1, _r2;

  function _isReverse(anchorNode, anchorOffset, focusNode, focusOffset) {
    // the selection is reversed when the focus propertyEl is before
    // the anchor el or the computed charPos is in reverse order
    if (focusNode && anchorNode) {
      if (!_r1) {
        _r1 = window.document.createRange();
        _r2 = window.document.createRange();
      }
      _r1.setStart(anchorNode, anchorOffset);
      _r2.setStart(focusNode, focusOffset);
      var cmp = _r1.compareBoundaryPoints(window.Range.START_TO_START, _r2);
      if (cmp === 1) {
        return true;
      }
    }
    return false;
  }

  function _getPath(el) {
    if (el && el.dataset && el.dataset.path) {
      return el.dataset.path.split('.');
    }
  }

  /*
   Helper for creating a model range correctly
   as for model/Range start should be before end.

   In contrast to that, DOM selections are described with anchor and focus coordinates,
   i.e. bearing the information of direction implicitly.
   To simplify the implementation we treat anchor and focus equally
   and only at the end exploit the fact deriving an isReverse flag
   and bringing start and end in the correct order.
  */
  function _createRange(start, end, isReverse, containerId) {
    if (isReverse) {
      var tmp = start;
      start = end;
      end = tmp;
    }
    return new Range(start, end, isReverse, containerId);
  }

};

oo.initClass(DOMSelection);

module.exports = DOMSelection;
<|endoftext|>


<commit_before>import React from 'react';
import lbry from '../lbry.js';
import lighthouse from '../lighthouse.js';
import uri from '../uri.js';
import {Video} from '../page/watch.js'
import {TruncatedText, Thumbnail, FilePrice, BusyMessage} from '../component/common.js';
import {FileActions} from '../component/file-actions.js';
import {Link} from '../component/link.js';
import UriIndicator from '../component/channel-indicator.js';

var FormatItem = React.createClass({
  propTypes: {
    metadata: React.PropTypes.object,
    contentType: React.PropTypes.string,
    uri: React.PropTypes.string,
    outpoint: React.PropTypes.string,
  },
  render: function() {
    const {thumbnail, author, title, description, language, license} = this.props.metadata;
    const mediaType = lbry.getMediaType(this.props.contentType);

    return (

            Content-Type	{this.props.contentType}

            Author	{author}

            Language	{language}

            License	{license}

    );
  }
});

let ShowPage = React.createClass({
  _uri: null,

  propTypes: {
    uri: React.PropTypes.string,
  },
  getInitialState: function() {
    return {
      metadata: null,
      contentType: null,
      hasSignature: false,
      signatureIsValid: false,
      cost: null,
      costIncludesData: null,
      uriLookupComplete: null,
    };
  },
  componentWillMount: function() {
    this._uri = uri.normalizeLbryUri(this.props.uri);
    document.title = this._uri;

    lbry.resolve({uri: this._uri}).then(({ claim: {txid, nout, has_signature, signature_is_valid, value: {stream: {metadata, source: {contentType}}}}}) => {
      this.setState({
        outpoint: txid + ':' + nout,
        metadata: metadata,
        hasSignature: has_signature,
        signatureIsValid: signature_is_valid,
        contentType: contentType,
        uriLookupComplete: true,
      });
    });

    lbry.getCostInfo(this._uri).then(({cost, includesData}) => {
      this.setState({
        cost: cost,
        costIncludesData: includesData,
      });
    });
  },
  render: function() {
    const
      metadata = this.state.uriLookupComplete ? this.state.metadata : null,
      title = this.state.uriLookupComplete ? metadata.title : this._uri;

    return (

          { this.state.contentType && this.state.contentType.startsWith('video/') ?
              <Video className="video-embedded" uri={this._uri} metadata={metadata} /> :
              (metadata ? <Thumbnail src={metadata.thumbnail} /> : <Thumbnail />) }

              <FilePrice uri={this._uri} metadata={metadata} />

{title}

              { this.state.uriLookupComplete ?

                    <UriIndicator uri={this._uri} hasSignature={this.state.hasSignature} signatureIsValid={this.state.signatureIsValid} />

                    <FileActions uri={this._uri} outpoint={this.state.outpoint} metadata={metadata} contentType={this.state.contentType} />

 : '' }

            { this.state.uriLookupComplete ?

                    {metadata.description}

              :
<BusyMessage message="Loading magic decentralized data..." />
 }

          { metadata ?

                <FormatItem metadata={metadata} contentType={this.state.contentType} cost={this.state.cost} uri={this._uri} outpoint={this.state.outpoint} costIncludesData={this.state.costIncludesData}  />

 : '' }

            <Link href="https://lbry.io/dmca" label="report" className="button-text-help" />

    );
  }
});

export default ShowPage;
<commit_msg>Show: don't display price if file is already downloaded<commit_after>import React from 'react';
import lbry from '../lbry.js';
import lighthouse from '../lighthouse.js';
import uri from '../uri.js';
import {Video} from '../page/watch.js'
import {TruncatedText, Thumbnail, FilePrice, BusyMessage} from '../component/common.js';
import {FileActions} from '../component/file-actions.js';
import {Link} from '../component/link.js';
import UriIndicator from '../component/channel-indicator.js';

var FormatItem = React.createClass({
  propTypes: {
    metadata: React.PropTypes.object,
    contentType: React.PropTypes.string,
    uri: React.PropTypes.string,
    outpoint: React.PropTypes.string,
  },
  render: function() {
    const {thumbnail, author, title, description, language, license} = this.props.metadata;
    const mediaType = lbry.getMediaType(this.props.contentType);

    return (

            Content-Type	{this.props.contentType}

            Author	{author}

            Language	{language}

            License	{license}

    );
  }
});

let ShowPage = React.createClass({
  _uri: null,

  propTypes: {
    uri: React.PropTypes.string,
  },
  getInitialState: function() {
    return {
      metadata: null,
      contentType: null,
      hasSignature: false,
      signatureIsValid: false,
      cost: null,
      costIncludesData: null,
      uriLookupComplete: null,
      isDownloaded: null,
    };
  },
  componentWillMount: function() {
    this._uri = uri.normalizeLbryUri(this.props.uri);
    document.title = this._uri;

    lbry.resolve({uri: this._uri}).then(({ claim: {txid, nout, has_signature, signature_is_valid, value: {stream: {metadata, source: {contentType}}}}}) => {
      const outpoint = txid + ':' + nout;

      lbry.file_list({outpoint}).then((fileInfo) => {
        this.setState({
          isDownloaded: fileInfo.length > 0,
        });
      });

      this.setState({
        outpoint: outpoint,
        metadata: metadata,
        hasSignature: has_signature,
        signatureIsValid: signature_is_valid,
        contentType: contentType,
        uriLookupComplete: true,
      });
    });

    lbry.getCostInfo(this._uri).then(({cost, includesData}) => {
      this.setState({
        cost: cost,
        costIncludesData: includesData,
      });
    });
  },
  render: function() {
    const metadata = this.state.metadata;
    const title = metadata ? this.state.metadata.title : this._uri;
    return (

          { this.state.contentType && this.state.contentType.startsWith('video/') ?
              <Video className="video-embedded" uri={this._uri} metadata={metadata} outpoint={this.state.outpoint} /> :
              (metadata ? <Thumbnail src={metadata.thumbnail} /> : <Thumbnail />) }

              {this.state.isDownloaded === false
                ? <FilePrice uri={this._uri} metadata={this.state.metadata} />
                : null}

{title}

              { this.state.uriLookupComplete ?

                    <UriIndicator uri={this._uri} hasSignature={this.state.hasSignature} signatureIsValid={this.state.signatureIsValid} />

                    <FileActions uri={this._uri} outpoint={this.state.outpoint} metadata={metadata} contentType={this.state.contentType} />

 : '' }

            { this.state.uriLookupComplete ?

                    {metadata.description}

              :
<BusyMessage message="Loading magic decentralized data..." />
 }

          { metadata ?

                <FormatItem metadata={metadata} contentType={this.state.contentType} cost={this.state.cost} uri={this._uri} outpoint={this.state.outpoint} costIncludesData={this.state.costIncludesData}  />

 : '' }

            <Link href="https://lbry.io/dmca" label="report" className="button-text-help" />

    );
  }
});

export default ShowPage;
<|endoftext|>


<commit_before>var EventEmitter = require('events').EventEmitter,
    doc = require('doc-js'),
    predator = require('predator'),
    venfix = require('venfix'),
    interact = require('interact-js'),
    translate = require('css-translate'),
    cloneWithStyles = require('clone-with-styles'),
    droppables = [];

// grabetha needs to share droppables between instances.
if(typeof window !== 'undefined'){
    droppables = window._grabethaDroppables = window._grabethaDroppables || droppables;
}

function checkElementLocation(element, position){
    var boundingRect = predator(element);

    return boundingRect.left < position.x && boundingRect.left + boundingRect.width > position.x &&
        boundingRect.top < position.y && boundingRect.top + boundingRect.height > position.y;
}

function emitDroppableEvent(event, grabbable, position){
    var droppable,
        targets;
    for(var i = 0; i < droppables.length; i++) {
        droppable = droppables[i];
        targets;

        if(typeof droppable.selector === 'string'){
            targets = doc(droppable.selector);
        }else{
            targets = [droppable.selector];
        }

        if(!targets){
            continue;
        }

        for(var j = 0; j < targets.length; j++) {
            if(checkElementLocation(targets[j], position)){
                droppable._emit(event, {
                    target: targets[j],
                    grabbable: grabbable,
                    position: position
                });
            }
        }
    }
}

function getterSetter(get, set){
    return function(){
        if(arguments.length === 0){
            return get.call(this);
        }else{
            set.apply(this, arguments);
        }
    };
}

function Grab(grabbable, target, interaction){
    var targetRects = target.getBoundingClientRect();
    this.target = target;
    this.grabbable = grabbable;
    this.interaction = interaction;

    this.targetOffset = {
        x: targetRects.left - (interaction.pageX - window.scrollX),
        y: targetRects.top - (interaction.pageY - window.scrollY)
    }

    this._position = {
        x:0,
        y:0
    };
    this.position({
        x: interaction.pageX - window.scrollX,
        y: interaction.pageY - window.scrollY
    });
}
Grab.prototype = Object.create(EventEmitter.prototype);
Grab.prototype.constructor = Grab;
Grab.prototype.position = getterSetter(
    function(){
        return {
            x: this._position.x,
            y: this._position.y
        };
    },
    function(position){
        this._position.x = position.x;
        this._position.y = position.y;
        emitDroppableEvent('hover', this.grabbable, this.position());
        this.emit('move', this.position());
    }
);

var inited,
    grabbables = [];

function initEvents(grabbable){
    grabbables.push(grabbable);

    if(inited){
        return;
    }
    inited = true;

    function dragHandler(interaction){
        if(interaction._noGrabs){
            return;
        }
        if(interaction._grabHandler){
            interaction._grabHandler._drag(interaction);
            return;
        }

        var handlers = [];

        for(var i = 0; i < grabbables.length && !interaction._grabHandler; i++){
            var handler = grabbables[i]._getGrabTarget(interaction);
            if(handler){
                handlers.push(handler);
            }
        }

        var handler = handlers.sort(function(handler1, handler2){
            return doc(handler1.target).closest(handler2.target) ? 1 : -1;
        }).pop();

        if(!handler){
            interaction._noGrabs = true;
            return;
        }

        interaction._grabHandler = handler;
        handler.currentGrab = new Grab(handler, handler.target, interaction);
        handler.emit('grab', handler.currentGrab);
    }
    function endHandler(interaction){
        if(interaction._grabHandler){
            interaction._grabHandler._end(interaction);
        }
        interaction._noGrabs = false;
    }

    interact.on('drag', document, dragHandler);
    interact.on('end', document, endHandler);
    interact.on('cancel', document, endHandler);
}

function Grabbable(selector){
    this.selector = selector;

    doc.ready(this.init.bind(this));
}
Grabbable.prototype = Object.create(EventEmitter.prototype);
Grabbable.prototype.constructor = Grabbable;
Grabbable.prototype.init = function(){
    initEvents(this);
};
Grabbable.prototype._getGrabTarget = function(interaction){
    this.target = doc(interaction.target).closest(this.selector);

    if(!this.target){
        return;
    }

    return this;
};
Grabbable.prototype._drag = function(interaction){
    var grabbable = this,
        target = this.target;

    interaction.stopPropagation();
    interaction.preventDefault();

    var moveDelta = interaction.getMoveDelta(),
        oldPosition = this.currentGrab.position();

    this.currentGrab.position({
        x: oldPosition.x + moveDelta.x,
        y: oldPosition.y + moveDelta.y
    });

    return this;
};
Grabbable.prototype._end = function(interaction){
    var grabbable = this,
        target = this.target;

    delete this._grabethaInvalid;

    if(!target){
        return;
    }

    emitDroppableEvent('drop', this, this.currentGrab.position());
    this.emit('drop', this.currentGrab.position());

    delete this.target;
    delete this.currentGrab;

    return this;
};
Grabbable.prototype.createGhost = function(element){
    element = element || this.target;
    var ghost = cloneWithStyles(element),
        grab = this.currentGrab;

    ghost.style.position = 'fixed';
    ghost.style.opacity = '0.5';
    ghost.style.top = '0';
    ghost.style.left = '0';
    ghost.style.transform = null;
    ghost.style['-webkit-transform'] = null;
    ghost.style['-moz-transform'] = null;
    ghost.style['-ms-transform'] = null;
    ghost.style['-o-transform'] = null;

    grab.on('move', function(position){
        ghost.style[venfix('transform')] = translate('3d',  grab.targetOffset.x + position.x + window.scrollX, grab.targetOffset.y + position.y + window.scrollY, 0);
    });

    ghost.destroy = function(){
        ghost.parentNode.removeChild(ghost);
    };

    document.body.appendChild(ghost);

    return ghost;
};
Grabbable.prototype.destroy = function(){
    var grabbableIndex = grabbables.indexOf(this);

    grabbables.splice(grabbableIndex, 1);
};

function grabbable(selector){
    var instance = Object.create(Grabbable.prototype);
    Grabbable.apply(instance, arguments);

    return instance;
}

function Droppable(selector){
    droppables.push(this);

    this.selector = selector;

    doc.ready(this.init.bind(this));
}
Droppable.prototype = Object.create(EventEmitter.prototype);
Droppable.prototype.constructor = Droppable;
Droppable.prototype.init = function(){

};
Droppable.prototype.destroy = function(){
    var dropableIndex = droppables.indexOf(this);

    droppables.splice(dropableIndex, 1);
};
Droppable.prototype._emit = function(event, details){
    this.emit(event, details);
};

function droppable(selector){
    var instance = Object.create(Droppable.prototype);
    Droppable.apply(instance, arguments);

    return instance;
}

module.exports.grabbable = grabbable;
module.exports.droppable = droppable;<commit_msg>improved ghost options<commit_after>var EventEmitter = require('events').EventEmitter,
    doc = require('doc-js'),
    predator = require('predator'),
    venfix = require('venfix'),
    interact = require('interact-js'),
    translate = require('css-translate'),
    cloneWithStyles = require('clone-with-styles'),
    droppables = [];

// grabetha needs to share droppables between instances.
if(typeof window !== 'undefined'){
    droppables = window._grabethaDroppables = window._grabethaDroppables || droppables;
}

function checkElementLocation(element, position){
    var boundingRect = predator(element);

    return boundingRect.left < position.x && boundingRect.left + boundingRect.width > position.x &&
        boundingRect.top < position.y && boundingRect.top + boundingRect.height > position.y;
}

function emitDroppableEvent(event, grabbable, position){
    var droppable,
        targets;
    for(var i = 0; i < droppables.length; i++) {
        droppable = droppables[i];
        targets;

        if(typeof droppable.selector === 'string'){
            targets = doc(droppable.selector);
        }else{
            targets = [droppable.selector];
        }

        if(!targets){
            continue;
        }

        for(var j = 0; j < targets.length; j++) {
            if(checkElementLocation(targets[j], position)){
                droppable._emit(event, {
                    target: targets[j],
                    grabbable: grabbable,
                    position: position
                });
            }
        }
    }
}

function getterSetter(get, set){
    return function(){
        if(arguments.length === 0){
            return get.call(this);
        }else{
            set.apply(this, arguments);
        }
    };
}

function Grab(grabbable, target, interaction){
    var targetRects = target.getBoundingClientRect();
    this.target = target;
    this.grabbable = grabbable;
    this.interaction = interaction;

    this.targetOffset = {
        x: targetRects.left - (interaction.pageX - window.scrollX),
        y: targetRects.top - (interaction.pageY - window.scrollY)
    }

    this._position = {
        x:0,
        y:0
    };
    this.position({
        x: interaction.pageX - window.scrollX,
        y: interaction.pageY - window.scrollY
    });
}
Grab.prototype = Object.create(EventEmitter.prototype);
Grab.prototype.constructor = Grab;
Grab.prototype.position = getterSetter(
    function(){
        return {
            x: this._position.x,
            y: this._position.y
        };
    },
    function(position){
        this._position.x = position.x;
        this._position.y = position.y;
        emitDroppableEvent('hover', this.grabbable, this.position());
        this.emit('move', this.position());
    }
);

var inited,
    grabbables = [];

function initEvents(grabbable){
    grabbables.push(grabbable);

    if(inited){
        return;
    }
    inited = true;

    function dragHandler(interaction){
        if(interaction._noGrabs){
            return;
        }
        if(interaction._grabHandler){
            interaction._grabHandler._drag(interaction);
            return;
        }

        var handlers = [];

        for(var i = 0; i < grabbables.length && !interaction._grabHandler; i++){
            var handler = grabbables[i]._getGrabTarget(interaction);
            if(handler){
                handlers.push(handler);
            }
        }

        var handler = handlers.sort(function(handler1, handler2){
            return doc(handler1.target).closest(handler2.target) ? 1 : -1;
        }).pop();

        if(!handler){
            interaction._noGrabs = true;
            return;
        }

        interaction._grabHandler = handler;
        handler.currentGrab = new Grab(handler, handler.target, interaction);
        handler.emit('grab', handler.currentGrab);
    }
    function endHandler(interaction){
        if(interaction._grabHandler){
            interaction._grabHandler._end(interaction);
        }
        interaction._noGrabs = false;
    }

    interact.on('drag', document, dragHandler);
    interact.on('end', document, endHandler);
    interact.on('cancel', document, endHandler);
}

function Grabbable(selector){
    this.selector = selector;

    doc.ready(this.init.bind(this));
}
Grabbable.prototype = Object.create(EventEmitter.prototype);
Grabbable.prototype.constructor = Grabbable;
Grabbable.prototype.init = function(){
    initEvents(this);
};
Grabbable.prototype._getGrabTarget = function(interaction){
    this.target = doc(interaction.target).closest(this.selector);

    if(!this.target){
        return;
    }

    return this;
};
Grabbable.prototype._drag = function(interaction){
    var grabbable = this,
        target = this.target;

    interaction.stopPropagation();
    interaction.preventDefault();

    var moveDelta = interaction.getMoveDelta(),
        oldPosition = this.currentGrab.position();

    this.currentGrab.position({
        x: oldPosition.x + moveDelta.x,
        y: oldPosition.y + moveDelta.y
    });

    return this;
};
Grabbable.prototype._end = function(interaction){
    var grabbable = this,
        target = this.target;

    delete this._grabethaInvalid;

    if(!target){
        return;
    }

    emitDroppableEvent('drop', this, this.currentGrab.position());
    this.emit('drop', this.currentGrab.position());

    delete this.target;
    delete this.currentGrab;

    return this;
};
Grabbable.prototype.createGhost = function(element, options){
    element = element || this.target;
    options = options || {};
    var ghost = options.cloneStyles === false ? element.cloneNode(true) : cloneWithStyles(element),
        grab = this.currentGrab;

    ghost.style.position = 'fixed';
    ghost.style.opacity = '0.5';
    ghost.style.top = '0';
    ghost.style.left = '0';
    ghost.style.transform = null;
    ghost.style['-webkit-transform'] = null;
    ghost.style['-moz-transform'] = null;
    ghost.style['-ms-transform'] = null;
    ghost.style['-o-transform'] = null;

    grab.on('move', function(position){
        var left = options.lockX ? 0 : grab.targetOffset.x + position.x + window.scrollX;
        var top = options.lockY ? 0 : grab.targetOffset.y + position.y + window.scrollY;

        ghost.style[venfix('transform')] = translate('3d',left ,top , 0);
    });

    ghost.destroy = function(){
        ghost.parentNode.removeChild(ghost);
    };

    (options.insertInto || document.body).appendChild(ghost);

    return ghost;
};
Grabbable.prototype.destroy = function(){
    var grabbableIndex = grabbables.indexOf(this);

    grabbables.splice(grabbableIndex, 1);
};

function grabbable(selector){
    var instance = Object.create(Grabbable.prototype);
    Grabbable.apply(instance, arguments);

    return instance;
}

function Droppable(selector){
    droppables.push(this);

    this.selector = selector;

    doc.ready(this.init.bind(this));
}
Droppable.prototype = Object.create(EventEmitter.prototype);
Droppable.prototype.constructor = Droppable;
Droppable.prototype.init = function(){

};
Droppable.prototype.destroy = function(){
    var dropableIndex = droppables.indexOf(this);

    droppables.splice(dropableIndex, 1);
};
Droppable.prototype._emit = function(event, details){
    this.emit(event, details);
};

function droppable(selector){
    var instance = Object.create(Droppable.prototype);
    Droppable.apply(instance, arguments);

    return instance;
}

module.exports.grabbable = grabbable;
module.exports.droppable = droppable;<|endoftext|>