<commit_before>// Native requires.
var fs = require('fs');

// package.json requires.
var mysql = require('mysql');
var _ = require('lodash');

// Local requires.
var Response = require('./../tools/response');
var common = require('./../tools/common');
var mysqlconf = require('./../conf/mysql.json');

// Removes peers from database whose last update is more than 1 hour.
Database.prototype.flushPeers = function() {
    console.log('Flushing peers');
    // Delete all peers whose last update is more than 1 hour ago.
    this.connection.query('SELECT `id`, `peer_id`, `info_hash`, `left` '
        + 'FROM `nodetracker`.`peers` '
        + 'WHERE `last_update` < DATE_SUB(NOW(), INTERVAL 1 HOUR)'
        , function(err, rows, fields) {
            rows.forEach(function(item) {
                var field = '';
                // If peer had 0 bytes left to download, it was a seeder.
                if (item.left == 0) {
                    field = '`complete`' ;
                } else {
                    field = '`incomplete`';
                }
                this.connection.query('UPDATE `nodetracker`.`torrent` '
                    + 'SET ' + field + ' = ' + field + ' - 1 '
                    + 'WHERE `info_hash` = ' + this.connection.escape(item.info_hash) + ' '
                    + 'AND ' + field + ' > 0'
                    , function(err, result) {
                        console.log('Decremented ' + field + ' on ' + item.info_hash);
                });
                this.connection.query('DELETE FROM `nodetracker`.`peers` '
                    + 'WHERE `id` = ' + this.connection.escape(item.id)
                    , function(err, result) {
                        console.log('Removed peer ' + item.peer_id + ' for torrent: ' + item.info_hash);
                });
            }.bind(this));
    }.bind(this));
};

Database.prototype.checkTorrent = function(peer, callback) {
    this.connection.query('SELECT * FROM `nodetracker`.`torrent` '
        + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash)
        , function(err, rows, fields) {
            if (err) {
                callback(err);
            } else if (rows.length == 0) {
                callback(new Error('Torrent not found'));
            } else {
                callback(undefined);
            }
    }.bind(this));
};

// Adds peer to database or returns error if the torrent is not tracked.
// NOTE: failure codes are optional, but included anyway. Can be removed
// for bandwidth saving.
Database.prototype.addPeer = function(peer, callback) {
    this.checkTorrent(peer, function(err) {
        if (err) {
            callback(undefined, common.bencodeFailure(200, 'Torrent not in database.'));
        } else  {
            this.transaction(peer, callback);
        }
    }.bind(this));
};

// Transaction to add a peer.
Database.prototype.transaction = function(peer, callback) {
    this.connection.beginTransaction(function(err) {
        this.connection.query('REPLACE '
            + 'INTO `nodetracker`.`peers` (`id`, `peer_id`, `info_hash`, `ip`, `port`, `uploaded`, `downloaded`, `left`) '
            + 'VALUES (' + peer.toAddPeerString() + ')'
            , function(err, result) {
                if (err) {
                    this.connection.rollback(function() {
                        callback(err, undefined);
                    });
                } else {
                    // TODO: lol fix that it doesn't update every cycle.
                    var field = '';
                    if (peer._left == 0) {
                        field = '`complete`';
                    } else {
                        field = '`incomplete`';
                    }
                    this.connection.query('UPDATE `nodetracker`.`torrent` '
                        + 'SET ' + field + ' = ' + field + ' + 1 '
                        + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash)
                        , function(err, result) {
                            if (err) {
                                this.connection.rollback(function() {
                                    callback(err, undefined);
                                });
                            } else {
                                this.connection.commit(function(err) {
                                    if (err) {
                                        this.connection.rollback(function() {
                                            callback(err, undefined);
                                        })
                                    } else {
                                        this.getPeers(peer, callback);
                                    }
                                }.bind(this));
                            }
                    }.bind(this));
                }
        }.bind(this));
    }.bind(this));
};

// Removes peer from database.
Database.prototype.removePeer = function(peer, callback) {
    this.connection.query('SELECT `id`, `peer_id`, `info_hash`, `left` '
        + 'FROM `nodetracker`.`peers` '
        + 'WHERE `id` = ' + this.connection.escape(peer.peer_id + peer.info_hash)
        , function(err, rows, fields) {
            if (err) {
                callback(err, undefined);
            } else {
                // TODO: Convert this to a function. (Also used by flushPeers)
                rows.forEach(function(item) {
                    var field = item.left == 0 ? '`complete`' : '`incomplete`';
                    this.connection.beginTransaction(function(err) {
                        this.connection.query('UPDATE `nodetracker`.`torrent` '
                            + 'SET ' + field + ' = ' + field + ' - 1 '
                            + 'WHERE `info_hash` = ' + this.connection.escape(item.info_hash) + ' '
                            + 'AND ' + field + ' > 0'
                            , function(err, result) {
                                if (err) {
                                    this.connection.rollback(function() {
                                        callback(err, undefined);
                                    })
                                } else {
                                    console.log('Decremented ' + field + ' on ' + item.info_hash);
                                    this.connection.query('DELETE FROM `nodetracker`.`peers` '
                                        + 'WHERE `id` = ' + this.connection.escape(item.id)
                                        , function(err, result) {
                                            if (err) {
                                                this.connection.rollback(function() {
                                                    callback(err, undefined);
                                                })
                                            } else {
                                                this.connection.commit(function(err) {
                                                    if (err) {
                                                        this.connection.rollback(function() {
                                                            callback(err, undefined);
                                                        });
                                                    } else {
                                                        console.log('Removed peer ' + item.peer_id + ' for torrent: ' + item.info_hash);
                                                        this.getPeers(peer, callback);
                                                    }
                                                }.bind(this));
                                            }
                                    }.bind(this));
                                }
                        }.bind(this));
                    }.bind(this));
                }.bind(this));
            }
    }.bind(this));
};

// Increments the completed count for torrent.
// TODO: Clients can spam complete count and it will keep increasing,
// should add check to prevent clients with left=0 from sending complete
// (or just add completed boolean)
Database.prototype.completePeer = function(peer, callback) {
    this.connection.query('UPDATE `nodetracker`.`torrent` '
        + 'SET `downloaded` = `downloaded` + 1, '
        + '`incomplete` = `incomplete` - 1, '
        + '`complete` = `complete` + 1 '
        + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash)
        , function(err, result) {
            if (err) {
                callback(err, undefined);
            } else {
                this.updatePeer(peer, callback);
            }
    }.bind(this));
};

// Updates the peer in the database.
Database.prototype.updatePeer = function(peer, callback) {
    this.connection.query('UPDATE `nodetracker`.`peers` '
        + 'SET `uploaded` = ' + this.connection.escape(peer.uploaded) + ', '
        + '`downloaded` = ' + this.connection.escape(peer.downloaded) + ', '
        + '`left` = ' + this.connection.escape(peer._left) + ' '
        + 'WHERE `id` = ' + this.connection.escape(peer.peer_id + peer.info_hash)
        , function(err, result) {
            if (err) {
                callback(err, undefined);
            } else {
                this.getPeers(peer, callback);
            }

    }.bind(this));
};

// Returns a randomly ordered set of peers to the client.
// NOTE: Tracker supports compact and non-compact, however non-compact
// requires more bandwidth.
Database.prototype.getPeers = function(peer, callback) {
    var colums = '`ip`, `port` ';
    if (peer._compact === 0) {
        colums = '`peer_id`, ' + colums;
    }

    // TODO: Maybe create a smarter algorithm?
    // Return a certain high download : low download distribution.
    this.connection.query('SELECT ' + colums
        + 'FROM `nodetracker`.`peers` '
        + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash) + ' '
        + 'AND `peer_id` != ' + this.connection.escape(peer.peer_id) + ' '
        + 'ORDER BY RAND() '
        + 'LIMIT ' + this.connection.escape(peer.numwant)
        , function(err, rows, result) {
            if (err) {
                callback(err, undefined);
                return;
            }
            this.scrape(peer.info_hash, function(err, response) {
                if (err) {
                    callback(err, undefined);
                } else {
                    // Add all peers to response!
                    rows.forEach(function(item) {
                        response.addPeer(item);
                    });
                    // Return hex IPv4 if client requested compact.
                    if (peer._compact) {
                        callback(undefined, response.bencodePeersIPv4Compact());
                    } else {
                        callback(undefined, response.bencodePeersIPv4());
                    }
                }
            }.bind(this));
    }.bind(this));
};

Database.prototype.scrape = function(info_hash, callback) {
    if (Array.isArray(info_hash)) {
        info_hash = _.map(info_hash, function(item) {
            return this.connection.escape(item);
        }.bind(this))
        info_hash = info_hash.join(',');
    } else {
        info_hash = this.connection.escape(info_hash);
    }

    this.connection.query('SELECT `info_hash`, `complete`, `incomplete`, `downloaded` '
        + 'FROM `nodetracker`.`torrent` '
        + 'WHERE `info_hash` IN (' + info_hash + ')'
        , function(err, rows, fields) {
            if (err) {
                callback(err, undefined);
                return;
            }
            var response = new Response();
            rows.forEach(function(item) {
                response.addScrape(item);
            });
            callback(undefined, response);
    });
};

function Database() {
    this.connection = mysql.createConnection(mysqlconf);

    this.connection.connect();
    // Clear out expired peers.
    this.flushPeers();
    // Set interval to clear expired peers.
    setInterval(this.flushPeers.bind(this), 1000 * 60 * 4);
}

module.exports = Database;<commit_msg>Fixed issue #3<commit_after>// Native requires.
var fs = require('fs');

// package.json requires.
var mysql = require('mysql');
var _ = require('lodash');

// Local requires.
var Response = require('./../tools/response');
var common = require('./../tools/common');
var mysqlconf = require('./../conf/mysql.json');

// Removes peers from database whose last update is more than 1 hour.
Database.prototype.flushPeers = function() {
    // Delete all peers whose last update is more than 1 hour ago.
    this.connection.query('SELECT `id`, `peer_id`, `info_hash`, `left` '
        + 'FROM `nodetracker`.`peers` '
        + 'WHERE `last_update` < DATE_SUB(NOW(), INTERVAL 1 HOUR)'
        , function(err, rows, fields) {
            rows.forEach(function(item) {
                var field = '';
                // If peer had 0 bytes left to download, it was a seeder.
                if (item.left == 0) {
                    field = '`complete`' ;
                } else {
                    field = '`incomplete`';
                }
                // TODO: Convert to transaction.
                this.connection.query('UPDATE `nodetracker`.`torrent` '
                    + 'SET ' + field + ' = ' + field + ' - 1 '
                    + 'WHERE `info_hash` = ' + this.connection.escape(item.info_hash) + ' '
                    + 'AND ' + field + ' > 0'
                    , function(err, result) {
                        console.log('Decremented ' + field + ' on ' + item.info_hash);
                });
                this.connection.query('DELETE FROM `nodetracker`.`peers` '
                    + 'WHERE `id` = ' + this.connection.escape(item.id)
                    , function(err, result) {
                        console.log('Removed peer ' + item.peer_id + ' for torrent: ' + item.info_hash);
                });
            }.bind(this));
    }.bind(this));
};

Database.prototype.checkTorrent = function(peer, callback) {
    this.connection.query('SELECT * FROM `nodetracker`.`torrent` '
        + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash)
        , function(err, rows, fields) {
            if (err) {
                callback(err);
            } else if (rows.length == 0) {
                callback(new Error('Torrent not found'));
            } else {
                callback(undefined);
            }
    }.bind(this));
};

// Adds peer to database or returns error if the torrent is not tracked.
// NOTE: failure codes are optional, but included anyway. Can be removed
// for bandwidth saving.
Database.prototype.addPeer = function(peer, callback) {
    this.checkTorrent(peer, function(err) {
        if (err) {
            callback(undefined, common.bencodeFailure(200, 'Torrent not in database.'));
        } else  {
            this.transaction(peer, callback);
        }
    }.bind(this));
};

// Transaction to add a peer.
Database.prototype.transaction = function(peer, callback) {
    this.connection.beginTransaction(function(err) {
        this.connection.query('REPLACE '
            + 'INTO `nodetracker`.`peers` (`id`, `peer_id`, `info_hash`, `ip`, `port`, `uploaded`, `downloaded`, `left`) '
            + 'VALUES (' + peer.toAddPeerString() + ')'
            , function(err, result) {
                if (err) {
                    this.connection.rollback(function() {
                        callback(err, undefined);
                    });
                } else {
                    // TODO: lol fix that it doesn't update every cycle.
                    var field = '';
                    if (peer._left == 0) {
                        field = '`complete`';
                    } else {
                        field = '`incomplete`';
                    }
                    this.connection.query('UPDATE `nodetracker`.`torrent` '
                        + 'SET ' + field + ' = ' + field + ' + 1 '
                        + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash)
                        , function(err, result) {
                            if (err) {
                                this.connection.rollback(function() {
                                    callback(err, undefined);
                                });
                            } else {
                                this.connection.commit(function(err) {
                                    if (err) {
                                        this.connection.rollback(function() {
                                            callback(err, undefined);
                                        })
                                    } else {
                                        this.getPeers(peer, callback);
                                    }
                                }.bind(this));
                            }
                    }.bind(this));
                }
        }.bind(this));
    }.bind(this));
};

// Removes peer from database.
Database.prototype.removePeer = function(peer, callback) {
    // Select correct peer.
    this.connection.query('SELECT `id`, `peer_id`, `info_hash`, `left` '
        + 'FROM `nodetracker`.`peers` '
        + 'WHERE `id` = ' + this.connection.escape(peer.peer_id + peer.info_hash)
        , function(err, rows, fields) {
            if (err) {
                callback(err, undefined);
            } else {
                // TODO: Convert this to a function. (Also used by flushPeers)
                rows.forEach(function(item) {
                    // Peer was a seeder or leecher?
                    var field = item.left == 0 ? '`complete`' : '`incomplete`';
                    this.connection.beginTransaction(function(err) {
                        // Update correct field in torrents.
                        this.connection.query('UPDATE `nodetracker`.`torrent` '
                            + 'SET ' + field + ' = ' + field + ' - 1 '
                            + 'WHERE `info_hash` = ' + this.connection.escape(item.info_hash) + ' '
                            + 'AND ' + field + ' > 0'
                            , function(err, result) {
                                if (err) {
                                    this.connection.rollback(function() {
                                        callback(err, undefined);
                                    });
                                } else {
                                    // Delete peer from database.
                                    this.connection.query('DELETE FROM `nodetracker`.`peers` '
                                        + 'WHERE `id` = ' + this.connection.escape(item.id)
                                        , function(err, result) {
                                            if (err) {
                                                this.connection.rollback(function() {
                                                    callback(err, undefined);
                                                })
                                            } else {
                                                this.connection.commit(function(err) {
                                                    if (err) {
                                                        this.connection.rollback(function() {
                                                            callback(err, undefined);
                                                        });
                                                    } else {
                                                        // Return list of peers.
                                                        // (required by deluge on pause torrents for some reason)
                                                        this.getPeers(peer, callback);
                                                    }
                                                }.bind(this));
                                            }
                                    }.bind(this));
                                }
                        }.bind(this));
                    }.bind(this));
                }.bind(this));
            }
    }.bind(this));
};

// Checks if torrent was already completed by peer.
// To prevent completed spam.
Database.prototype.checkComplete = function(peer, callback) {
    this.connection.query('SELECT `left` '
        + 'FROM `nodetracker`.`peers` '
        + 'WHERE `id` = ' + this.connection.escape(peer.peer_id) + ' '
        + 'AND `info_hash = ' + this.connection.escape(peer.info_hash)
        , function(err, rows, fields) {
            if (err) {
                callback(err);
            } else if (rows.length == 1 && rows[0].left == 0) {
                callback(1);
            } else {
                callback(undefined);
            }
    }.bind(this));
};

// Increments the completed count for torrent.
// TODO: Clients can spam complete count and it will keep increasing,
// should add check to prevent clients with left=0 from sending complete
// (or just add completed boolean)
Database.prototype.completePeer = function(peer, callback) {
    this.checkComplete(peer, function(err) {
        if (err == 1) {
            callback(undefined, common.bencodeFailure(900, 'Torrent already reported as completed'));
        } else if (err) {
            callback(err, undefined);
        } else {
            this.connection.query('UPDATE `nodetracker`.`torrent` '
                + 'SET `downloaded` = `downloaded` + 1, '
                + '`incomplete` = `incomplete` - 1, '
                + '`complete` = `complete` + 1 '
                + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash)
                , function(err, result) {
                    if (err) {
                        callback(err, undefined);
                    } else {
                        this.updatePeer(peer, callback);
                    }
            }.bind(this));
        }
    }.bind(this));
};

// Updates the peer in the database.
Database.prototype.updatePeer = function(peer, callback) {
    this.connection.query('UPDATE `nodetracker`.`peers` '
        + 'SET `uploaded` = ' + this.connection.escape(peer.uploaded) + ', '
        + '`downloaded` = ' + this.connection.escape(peer.downloaded) + ', '
        + '`left` = ' + this.connection.escape(peer._left) + ' '
        + 'WHERE `id` = ' + this.connection.escape(peer.peer_id + peer.info_hash)
        , function(err, result) {
            if (err) {
                callback(err, undefined);
            } else {
                this.getPeers(peer, callback);
            }
    }.bind(this));
};

// Returns a randomly ordered set of peers to the client.
// NOTE: Tracker supports compact and non-compact, however non-compact
// requires more bandwidth.
Database.prototype.getPeers = function(peer, callback) {
    var colums = '`ip`, `port` ';
    if (peer._compact === 0) {
        colums = '`peer_id`, ' + colums;
    }

    // TODO: Maybe create a smarter algorithm?
    // Return a certain high download : low download distribution.
    this.connection.query('SELECT ' + colums
        + 'FROM `nodetracker`.`peers` '
        + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash) + ' '
        + 'AND `peer_id` != ' + this.connection.escape(peer.peer_id) + ' '
        + 'ORDER BY RAND() '
        + 'LIMIT ' + this.connection.escape(peer.numwant)
        , function(err, rows, result) {
            if (err) {
                callback(err, undefined);
                return;
            }
            this.scrape(peer.info_hash, function(err, response) {
                if (err) {
                    callback(err, undefined);
                } else {
                    // Add all peers to response!
                    rows.forEach(function(item) {
                        response.addPeer(item);
                    });
                    // Return hex IPv4 if client requested compact.
                    if (peer._compact) {
                        callback(undefined, response.bencodePeersIPv4Compact());
                    } else {
                        callback(undefined, response.bencodePeersIPv4());
                    }
                }
            }.bind(this));
    }.bind(this));
};

Database.prototype.scrape = function(info_hash, callback) {
    if (Array.isArray(info_hash)) {
        info_hash = _.map(info_hash, function(item) {
            return this.connection.escape(item);
        }.bind(this));
        info_hash = info_hash.join(',');
    } else {
        info_hash = this.connection.escape(info_hash);
    }

    this.connection.query('SELECT `info_hash`, `complete`, `incomplete`, `downloaded` '
        + 'FROM `nodetracker`.`torrent` '
        + 'WHERE `info_hash` IN (' + info_hash + ')'
        , function(err, rows, fields) {
            if (err) {
                callback(err, undefined);
                return;
            }
            var response = new Response();
            rows.forEach(function(item) {
                response.addScrape(item);
            });
            callback(undefined, response);
    });
};

function Database() {
    this.connection = mysql.createConnection(mysqlconf);

    this.connection.connect();
    // Clear out expired peers.
    this.flushPeers();
    // Set interval to clear expired peers.
    setInterval(this.flushPeers.bind(this), 1000 * 60 * 4);
}

module.exports = Database;<|endoftext|>


<commit_before>import Ember from 'ember';
import layout from './template';
import Editor from 'mobiledoc-kit/editor/editor';
import Range from 'mobiledoc-kit/utils/cursor/range';
import { MOBILEDOC_VERSION } from 'mobiledoc-kit/renderers/mobiledoc';
let { computed, Component } = Ember;
let { capitalize, camelize } = Ember.String;

export const ADD_HOOK = 'addComponent';
export const REMOVE_HOOK = 'removeComponent';
const EDITOR_CARD_SUFFIX = '-editor';
const EMPTY_MOBILEDOC = {
  version: MOBILEDOC_VERSION,
  markups: [],
  atoms: [],
  cards: [],
  sections: []
};

function arrayToMap(array, propertyName) {
  let map = Object.create(null);
  array.forEach(item => {
    if (item[propertyName]) {
      item = `is${capitalize(camelize(item[propertyName]))}`;
      map[item] = true;
    }
  });
  return map;
}

export default Component.extend({
  layout,
  tagName: 'article',
  classNames: ['mobiledoc-editor'],

  placeholder: 'Write here...',
  spellcheck: true,
  autofocus: true,
  serializeVersion: MOBILEDOC_VERSION,

  options: {},

  // merge in named options with the `options` property data-bag
  editorOptions: computed(function() {
    let options = this.get('options');

    return Ember.merge({
      placeholder: this.get('placeholder'),
      spellcheck:  this.get('spellcheck'),
      autofocus:   this.get('autofocus'),
      cards:       this.get('cards') || [],
      atoms:       this.get('atoms') || []
    }, options);
  }),

  init() {
    this._super(...arguments);
    let mobiledoc = this.get('mobiledoc');
    if (!mobiledoc) {
      mobiledoc = EMPTY_MOBILEDOC;
      this.set('mobiledoc', mobiledoc);
    }
    this.set('componentCards', Ember.A([]));
    this.set('linkOffsets', null);
    this.set('activeMarkupTagNames', {});
    this.set('activeSectionTagNames', {});
    this._ignoreCursorDidChange = false;
    this._startedRunLoop  = false;
  },

  actions: {
    toggleMarkup(markupTagName) {
      let editor = this.get('editor');
      editor.run(postEditor => postEditor.toggleMarkup(markupTagName));
    },

    toggleSection(newTagName) {
      let editor = this.get('editor');
      editor.run(postEditor => postEditor.toggleSection(newTagName));
    },

    // Deprecated
    toggleSectionTagName(newTagName) {
      Ember.warn('toggleSectionTagName is deprecated. Use toggleSection instead',
                 false,
                 {id: 'mobiledoc-editor-toggleSectionTagName'});
      this.send('toggleSection', newTagName);
    },

    createListSection(tagName) {
      const editor = this.get('editor');
      const section = editor.activeSections[0];
      if (!section) { return; }

      // can only convert a section *into* an li, do nothing to
      // sections that are already li sections
      if (section.tagName === 'li') { return; }

      const listItem = editor.run(postEditor => {
        const { builder } = postEditor;
        const listItem = builder.createListItem();
        const listSection = builder.createListSection(tagName, [listItem]);
        section.markers.forEach(m => listItem.markers.append(m.clone()));
        postEditor.replaceSection(section, listSection);
        return listItem;
      });

      editor.selectSections([listItem]);
    },

    addCard(cardName, payload={}) {
      this._addCard(cardName, payload);
    },

    addCardInEditMode(cardName, payload={}) {
      let editMode = true;
      this._addCard(cardName, payload, editMode);
    },

    toggleLink() {
      let editor = this.get('editor');
      let range = editor.range;
      let headSection = range.head.section,
          tailSection = range.tail.section;
      if (!(headSection.isMarkerable && tailSection.isMarkerable)) {
        return;
      }
      let hasMarkup = editor.detectMarkupInRange(range, 'a');
      if (hasMarkup) {
        editor.run(postEditor => {
          postEditor.removeMarkupFromRange(range, hasMarkup);
        });
      } else {
        this._ignoreCursorDidChange = true;
        this.set('linkOffsets', range);
      }
    },

    completeLink(href) {
      let offsets = this.get('linkOffsets');
      this.set('linkOffsets', null);
      let editor = this.get('editor');
      editor.run(postEditor => {
        let markup = postEditor.builder.createMarkup('a', {
          href: href
        });
        postEditor.addMarkupToRange(offsets, markup);
      });
    },

    cancelLink() {
      this.set('linkOffsets', null);
    }
  },

  editingContexts: computed(function() {
    return Ember.A([]);
  }),

  willRender() {
    // Use a default mobiledoc. If there are no changes, then return
    // early.
    let mobiledoc = this.get('mobiledoc') || EMPTY_MOBILEDOC;
    if (
      (
        (this._localMobiledoc && this._localMobiledoc === mobiledoc) ||
        (this._upstreamMobiledoc && this._upstreamMobiledoc === mobiledoc)
      ) && (this._lastIsEditingDisabled === this.get('isEditingDisabled'))
    ) {
      // No change to mobiledoc, no need to recreate the editor
      return;
    }
    this._lastIsEditingDisabled = this.get('isEditingDisabled');
    this._upstreamMobiledoc = mobiledoc;
    this._localMobiledoc = null;

    // Teardown any old editor that might be around.
    let editor = this.get('editor');
    if (editor) {
      editor.destroy();
    }

    // Create a new editor.
    let editorOptions = this.get('editorOptions');
    editorOptions.mobiledoc = mobiledoc;
    editorOptions.cardOptions = {
      [ADD_HOOK]: ({env, options, payload}, isEditing=false) => {
        let cardId = Ember.uuid();
        let cardName = env.name;
        if (isEditing) {
          cardName = cardName + EDITOR_CARD_SUFFIX;
        }
        let destinationElementId = `mobiledoc-editor-card-${cardId}`;
        let element = document.createElement('div');
        element.id = destinationElementId;

        // The data must be copied to avoid sharing the reference
        payload = Ember.copy(payload, true);

        let card = Ember.Object.create({
          destinationElementId,
          cardName,
          payload,
          callbacks: env,
          editor,
          postModel: env.postModel
        });
        Ember.run.schedule('afterRender', () => {
          this.get('componentCards').pushObject(card);
        });
        return { card, element };
      },
      [REMOVE_HOOK]: (card) => {
        this.get('componentCards').removeObject(card);
      }
    };
    editor = new Editor(editorOptions);
    editor.willRender(() => {
      // The editor's render/rerender will happen after this `editor.willRender`,
      // so we explicitly start a runloop here if there is none, so that the
      // add/remove card hooks happen inside a runloop.
      // When pasting text that gets turned into a card, for example,
      // the add card hook would run outside the runloop if we didn't begin a new
      // one now.
      if (!Ember.run.currentRunLoop) {
        this._startedRunLoop = true;
        Ember.run.begin();
      }
    });
    editor.didRender(() => {
      // If we had explicitly started a run loop in `editor.willRender`,
      // we must explicitly end it here.
      if (this._startedRunLoop) {
        this._startedRunLoop = false;
        Ember.run.end();
      }
    });
    editor.on('update', () => {
      Ember.run.join(() => {
        let serializeVersion = this.get('serializeVersion');
        let updatedMobileDoc = editor.serialize(serializeVersion);
        this._localMobiledoc = updatedMobileDoc;
        this.sendAction('on-change', updatedMobileDoc);
      });
    });
    editor.cursorDidChange(() => {
      if (this.isDestroyed) { return; }

      Ember.run.join(() => {
        const markupTags = arrayToMap(editor.markupsInSelection, 'tagName');
        const sectionTags = arrayToMap(editor.activeSections, 'tagName');

        this.set('activeMarkupTagNames', markupTags);
        this.set('activeSectionTagNames', sectionTags);

        let isCursorOffEditor = !this.get('editor').cursor.offsets.head.section;
        if (!isCursorOffEditor && !this._ignoreCursorDidChange) {
          this.set('linkOffsets', null);
        } else {
          this._ignoreCursorDidChange = false;
        }
      });
    });
    if (this.get('isEditingDisabled')) {
      editor.disableEditing();
    }
    this.set('editor', editor);
  },

  didRender() {
    let editor = this.get('editor');
    if (!editor.hasRendered) {
      let editorElement = this.$('.mobiledoc-editor__editor')[0];
      editor.render(editorElement);
    }
  },

  willDestroyElement() {
    let editor = this.get('editor');
    editor.destroy();
  },

  _addCard(cardName, payload, editMode=false) {
    let editor = this.get('editor');
    let section = editor.activeSection;
    editor.run(postEditor => {
      let card = editor.builder.createCardSection(cardName, payload);
      if (editMode) {
        editor.editCard(card);
      }
      let nextSection = section && section.next;
      postEditor.insertSectionBefore(editor.post.sections, card, nextSection);
      if (section && section.isBlank) {
        postEditor.removeSection(section);
      }

      // Explicitly put the cursor at the end of the card
      // This prevents problems with the editor element being out-of-focus
      // but the window's selection still in the editor element.
      // See https://github.com/bustlelabs/mobiledoc-kit/issues/286
      let range = new Range(card.tailPosition());
      postEditor.setRange(range);
    });
  }
});
<commit_msg>Extensibility hooks<commit_after>import Ember from 'ember';
import layout from './template';
import Editor from 'mobiledoc-kit/editor/editor';
import Range from 'mobiledoc-kit/utils/cursor/range';
import { MOBILEDOC_VERSION } from 'mobiledoc-kit/renderers/mobiledoc';
let { computed, Component } = Ember;
let { capitalize, camelize } = Ember.String;

export const ADD_HOOK = 'addComponent';
export const REMOVE_HOOK = 'removeComponent';
const EDITOR_CARD_SUFFIX = '-editor';
const EMPTY_MOBILEDOC = {
  version: MOBILEDOC_VERSION,
  markups: [],
  atoms: [],
  cards: [],
  sections: []
};

function arrayToMap(array, propertyName) {
  let map = Object.create(null);
  array.forEach(item => {
    if (item[propertyName]) {
      item = `is${capitalize(camelize(item[propertyName]))}`;
      map[item] = true;
    }
  });
  return map;
}

export default Component.extend({
  layout,
  tagName: 'article',
  classNames: ['mobiledoc-editor'],

  placeholder: 'Write here...',
  spellcheck: true,
  autofocus: true,
  serializeVersion: MOBILEDOC_VERSION,

  options: {},

  // merge in named options with the `options` property data-bag
  editorOptions: computed(function() {
    let options = this.get('options');

    return Ember.merge({
      placeholder: this.get('placeholder'),
      spellcheck:  this.get('spellcheck'),
      autofocus:   this.get('autofocus'),
      cards:       this.get('cards') || [],
      atoms:       this.get('atoms') || []
    }, options);
  }),

  init() {
    this._super(...arguments);
    let mobiledoc = this.get('mobiledoc');
    if (!mobiledoc) {
      mobiledoc = EMPTY_MOBILEDOC;
      this.set('mobiledoc', mobiledoc);
    }
    this.set('componentCards', Ember.A([]));
    this.set('linkOffsets', null);
    this.set('activeMarkupTagNames', {});
    this.set('activeSectionTagNames', {});
    this._ignoreCursorDidChange = false;
    this._startedRunLoop  = false;
  },

  actions: {
    toggleMarkup(markupTagName) {
      let editor = this.get('editor');
      editor.run(postEditor => postEditor.toggleMarkup(markupTagName));
    },

    toggleSection(newTagName) {
      let editor = this.get('editor');
      editor.run(postEditor => postEditor.toggleSection(newTagName));
    },

    // Deprecated
    toggleSectionTagName(newTagName) {
      Ember.warn('toggleSectionTagName is deprecated. Use toggleSection instead',
                 false,
                 {id: 'mobiledoc-editor-toggleSectionTagName'});
      this.send('toggleSection', newTagName);
    },

    createListSection(tagName) {
      const editor = this.get('editor');
      const section = editor.activeSections[0];
      if (!section) { return; }

      // can only convert a section *into* an li, do nothing to
      // sections that are already li sections
      if (section.tagName === 'li') { return; }

      const listItem = editor.run(postEditor => {
        const { builder } = postEditor;
        const listItem = builder.createListItem();
        const listSection = builder.createListSection(tagName, [listItem]);
        section.markers.forEach(m => listItem.markers.append(m.clone()));
        postEditor.replaceSection(section, listSection);
        return listItem;
      });

      editor.selectSections([listItem]);
    },

    addCard(cardName, payload={}) {
      this._addCard(cardName, payload);
    },

    addCardInEditMode(cardName, payload={}) {
      let editMode = true;
      this._addCard(cardName, payload, editMode);
    },

    toggleLink() {
      let editor = this.get('editor');
      let range = editor.range;
      let headSection = range.head.section,
          tailSection = range.tail.section;
      if (!(headSection.isMarkerable && tailSection.isMarkerable)) {
        return;
      }
      let hasMarkup = editor.detectMarkupInRange(range, 'a');
      if (hasMarkup) {
        editor.run(postEditor => {
          postEditor.removeMarkupFromRange(range, hasMarkup);
        });
      } else {
        this._ignoreCursorDidChange = true;
        this.set('linkOffsets', range);
      }
    },

    completeLink(href) {
      let offsets = this.get('linkOffsets');
      this.set('linkOffsets', null);
      let editor = this.get('editor');
      editor.run(postEditor => {
        let markup = postEditor.builder.createMarkup('a', {
          href: href
        });
        postEditor.addMarkupToRange(offsets, markup);
      });
    },

    cancelLink() {
      this.set('linkOffsets', null);
    }
  },

  editingContexts: computed(function() {
    return Ember.A([]);
  }),

  willRender() {
    // Use a default mobiledoc. If there are no changes, then return
    // early.
    let mobiledoc = this.get('mobiledoc') || EMPTY_MOBILEDOC;
    if (
      (
        (this._localMobiledoc && this._localMobiledoc === mobiledoc) ||
        (this._upstreamMobiledoc && this._upstreamMobiledoc === mobiledoc)
      ) && (this._lastIsEditingDisabled === this.get('isEditingDisabled'))
    ) {
      // No change to mobiledoc, no need to recreate the editor
      return;
    }
    this._lastIsEditingDisabled = this.get('isEditingDisabled');
    this._upstreamMobiledoc = mobiledoc;
    this._localMobiledoc = null;

    this.willCreateEditor();

    // Teardown any old editor that might be around.
    let editor = this.get('editor');
    if (editor) {
      editor.destroy();
    }

    // Create a new editor.
    let editorOptions = this.get('editorOptions');
    editorOptions.mobiledoc = mobiledoc;
    editorOptions.cardOptions = {
      [ADD_HOOK]: ({env, options, payload}, isEditing=false) => {
        let cardId = Ember.uuid();
        let cardName = env.name;
        if (isEditing) {
          cardName = cardName + EDITOR_CARD_SUFFIX;
        }
        let destinationElementId = `mobiledoc-editor-card-${cardId}`;
        let element = document.createElement('div');
        element.id = destinationElementId;

        // The data must be copied to avoid sharing the reference
        payload = Ember.copy(payload, true);

        let card = Ember.Object.create({
          destinationElementId,
          cardName,
          payload,
          callbacks: env,
          editor,
          postModel: env.postModel
        });
        Ember.run.schedule('afterRender', () => {
          this.get('componentCards').pushObject(card);
        });
        return { card, element };
      },
      [REMOVE_HOOK]: (card) => {
        this.get('componentCards').removeObject(card);
      }
    };
    editor = new Editor(editorOptions);
    editor.willRender(() => {
      // The editor's render/rerender will happen after this `editor.willRender`,
      // so we explicitly start a runloop here if there is none, so that the
      // add/remove card hooks happen inside a runloop.
      // When pasting text that gets turned into a card, for example,
      // the add card hook would run outside the runloop if we didn't begin a new
      // one now.
      if (!Ember.run.currentRunLoop) {
        this._startedRunLoop = true;
        Ember.run.begin();
      }
    });
    editor.didRender(() => {
      // If we had explicitly started a run loop in `editor.willRender`,
      // we must explicitly end it here.
      if (this._startedRunLoop) {
        this._startedRunLoop = false;
        Ember.run.end();
      }
    });
    editor.on('update', () => {
      Ember.run.join(() => {
        this.editorUpdated(editor);
      });
    });
    editor.cursorDidChange(() => {
      if (this.isDestroyed) { return; }
      Ember.run.join(() => {
        this.cursorDidChange(editor);
      });
    });
    if (this.get('isEditingDisabled')) {
      editor.disableEditing();
    }
    this.set('editor', editor);
    this.didCreateEditor();
  },

  didRender() {
    let editor = this.get('editor');
    if (!editor.hasRendered) {
      let editorElement = this.$('.mobiledoc-editor__editor')[0];
      editor.render(editorElement);
    }
  },

  willDestroyElement() {
    let editor = this.get('editor');
    editor.destroy();
  },

  editorUpdated(editor) {
    let serializeVersion = this.get('serializeVersion');
    let updatedMobileDoc = editor.serialize(serializeVersion);
    this._localMobiledoc = updatedMobileDoc;
    this.sendAction('on-change', updatedMobileDoc);
  },

  cursorDidChange(editor) {
    const markupTags = arrayToMap(editor.markupsInSelection, 'tagName');
    const sectionTags = arrayToMap(editor.activeSections, 'tagName');

    this.set('activeMarkupTagNames', markupTags);
    this.set('activeSectionTagNames', sectionTags);

    let isCursorOffEditor = !this.get('editor').cursor.offsets.head.section;
    if (!isCursorOffEditor && !this._ignoreCursorDidChange) {
      this.set('linkOffsets', null);
    } else {
      this._ignoreCursorDidChange = false;
    }
  },

  willCreateEditor: Ember.K,
  didCreateEditor: Ember.K,

  _addCard(cardName, payload, editMode=false) {
    let editor = this.get('editor');
    let section = editor.activeSection;
    editor.run(postEditor => {
      let card = editor.builder.createCardSection(cardName, payload);
      if (editMode) {
        editor.editCard(card);
      }
      let nextSection = section && section.next;
      postEditor.insertSectionBefore(editor.post.sections, card, nextSection);
      if (section && section.isBlank) {
        postEditor.removeSection(section);
      }

      // Explicitly put the cursor at the end of the card
      // This prevents problems with the editor element being out-of-focus
      // but the window's selection still in the editor element.
      // See https://github.com/bustlelabs/mobiledoc-kit/issues/286
      let range = new Range(card.tailPosition());
      postEditor.setRange(range);
    });
  }
});
<|endoftext|>


<commit_before>import Ember from 'ember';
import layout from '../../templates/components/search-flow/input-dropdown';

export default Ember.Component.extend({
  layout,
  classNames: ['search-flow_input-dropdown'],
  router: Ember.inject.service('-routing'),
  init() {
    this._super(...arguments);
    if (this.get('filter.value')) {
      this.set('value', `${this.get('filter.isContains') ? 'Contains: ' : ''}${this.get('filter.value')}`);
    }
    else {
      this.set('value', '');
    }
    Ember.run.schedule('afterRender', this, function () {
      if (!this.get('value')) {
        if (!this.get('filter')) {
          this.set('filter', Ember.Object.create({}));
        }
        this.set('filter.isFocused', true);
        this.focusInput();
      }
    });
  },
  didInsertElement() {
    this.setInputWidth();
  },
  getCurrentRoute() {
    return Ember.getOwner(this).lookup(`route:${this.get('router.currentRouteName')}`);
  },
  sendActionToRoute(action, arg) {
    try {
      this.getCurrentRoute().send(action, arg);
    }
    catch (error) {
      // The action does not exist
      return;
    }
  },
  fetchOptions() {
    if (this.get('remoteFiltering')) {
      this.sendActionToRoute('searchFlowValueUpdated', this.get('filter'));
    }
  },
  valueObserver: Ember.observer('value', function () {
    if (this.get('filter.isFocused')) {
      this.set('filter.value', this.get('value'));
      this.fetchOptions();
    }
    this.setInputWidth();
  }),
  setInputWidth() {
    let tempDiv = this.$(`<div class="search-flow_temp-div" style="display:none">${this.get('value') || this.get('placeholder')}</div>`).appendTo('body');
    this.$('.search-flow_input').css('width', tempDiv.width() + 3);
    tempDiv.remove();
  },
  availableOptions: Ember.computed('options.[],value', function () {
    let options = this.get('options');
    if (!options || !this.get('filter.isFocused')) {
      return Ember.A([]);
    }

    // Convert options to array of objects if it is an array of strings
    options = options.map(option => {
      if (this.get('isParameterSelection')) {
        option = Ember.Object.create({ title: option.title, value: option });
      }
      else {
        option = Ember.Object.create({ title: option, value: option });
      }
      return option;
    });

    if (!this.get('remoteFiltering')) {
      options = options.filter(option => {
        return option.title.toLowerCase().includes(this.get('value').toLowerCase());
      });
    }

    // Options should always be in alphabetical order
    options = options.sortBy('title');

    // Insert contains option into list
    if (this.get('filter.parameter.contains') && this.get('value') && options.length) {
      options.unshift(Ember.Object.create({ title: `Contains: ${this.get('value')}`, value: this.get('value'), isContains: true }));
    }

    // Set the index on each item for easy access later on
    options.forEach((option, index) => {
      option.index = index;
    });

    // Ensure first option is selected
    options.setEach('isActive', false);
    if (options.get('firstObject')) {
      options.set('firstObject.isActive', true);
    }

    return options;
  }),
  activeOption: Ember.computed('availableOptions.@each.isActive', function () {
    return this.get('availableOptions').findBy('isActive');
  }),
  blurInput() {
    let input = this.$('.search-flow_input');
    if (input.is(':focus')) {
      input.blur();
    }
  },
  focusInput() {
    let input = this.$('.search-flow_input');
    if (!input.is(':focus')) {
      input.focus();
    }
  },
  actions: {
    selectOption() {
      let activeOption = this.get('activeOption');
      this.set('filter.isFocused', false);
      if (this.get('isParameterSelection')) {
        this.get('newFilter')(activeOption.get('value'));
      }
      else {
        this.set('value', activeOption.get('value'));
        this.set('filter.value', activeOption.get('value'));
        if (activeOption.get('isContains')) {
          this.set('filter.isContains', true);
          this.set('value', `Contains: ${activeOption.get('value')}`);
        }
      }
      this.blurInput();
      this.get('inputBlurred')(this.get('isParameterSelection'), this.get('filter'));
    },
    inputEntered() {
      // Ensure item is not selected from preivous enter key hit from 'Add Filter' button
      if (!this.get('didHitEnter') && !!this.get('activeOption')) {
        this.set('didHitEnter', true);
        this.send('selectOption', this.get('activeOption'));
      }
    },
    inputKeyDown() {
      if (event.which === 38) { // Up
        event.preventDefault();
        let previousItem = this.get(`availableOptions.${this.get('activeOption.index') - 1}`);
        if (previousItem) {
          this.set('activeOption.isActive', false);
          Ember.set(previousItem, 'isActive', true);
        }
      }
      else if (event.which === 40) { // Down
        event.preventDefault();
        let nextItem = this.get(`availableOptions.${this.get('activeOption.index') + 1}`);
        if (nextItem) {
          this.set('activeOption.isActive', false);
          Ember.set(nextItem, 'isActive', true);
        }
      }
      else if (event.which === 8 && !this.get('value')) { // Backspace
        // If backspace is hit with no value typed in, blur
        this.set('shouldRemoveFilter', true);
        this.blurInput();
      }
    },
    inputKeyUp() {
      // Prevent the up or down key from moving the cursor when releasing the key
      if (event.which === 38 || event.which === 40) { // Up or Down
        event.preventDefault();
      }
      else if (event.which === 13) { // Enter key
        // Make sure user can hit enter after key released
        this.set('didHitEnter', false);
      }
    },
    inputFocused() {
      this.fetchOptions();
      this.set('filter.isFocused', true);
      if (this.get('filter.isContains')) {
        this.set('value', this.get('filter.value'));
      }
    },
    inputBlurred() {
      // Ensure the filter is not removed with clicking on an option
      if (this.$('.search-flow_dropdown-option:hover').length || this.get('didHitEnter')) {
        return;
      }

      if (this.get('filter.isFocused') && !this.get('shouldRemoveFilter')) {
        this.set('filter.isFocused', false);
        this.set('filter.value', this.get('activeOption.value'));
        if (this.get('activeOption.isContains')) {
          this.set('filter.isContains', true);
          this.set('value', `Contains: ${this.get('activeOption.value')}`);
        }
        else {
          this.set('value', this.get('activeOption.value'));
        }
      }
      this.get('inputBlurred')(this.get('isParameterSelection'), this.get('filter'));
    }
  }
});
<commit_msg>fixed bug<commit_after>import Ember from 'ember';
import layout from '../../templates/components/search-flow/input-dropdown';

export default Ember.Component.extend({
  layout,
  classNames: ['search-flow_input-dropdown'],
  router: Ember.inject.service('-routing'),
  init() {
    this._super(...arguments);
    if (this.get('filter.value')) {
      this.set('value', `${this.get('filter.isContains') ? 'Contains: ' : ''}${this.get('filter.value')}`);
    }
    else {
      this.set('value', '');
    }
    Ember.run.schedule('afterRender', this, function () {
      if (!this.get('value')) {
        if (!this.get('filter')) {
          this.set('filter', Ember.Object.create({}));
        }
        this.set('filter.isFocused', true);
        this.focusInput();
      }
    });
  },
  didInsertElement() {
    this.setInputWidth();
  },
  getCurrentRoute() {
    return Ember.getOwner(this).lookup(`route:${this.get('router.currentRouteName')}`);
  },
  sendActionToRoute(action, arg) {
    try {
      this.getCurrentRoute().send(action, arg);
    }
    catch (error) {
      // The action does not exist
      return;
    }
  },
  fetchOptions() {
    if (this.get('remoteFiltering')) {
      this.sendActionToRoute('searchFlowValueUpdated', this.get('filter'));
    }
  },
  valueObserver: Ember.observer('value', function () {
    if (this.get('filter.isFocused')) {
      this.set('filter.value', this.get('value'));
      this.fetchOptions();
    }
    this.setInputWidth();
  }),
  setInputWidth() {
    let tempDiv = this.$(`<div class="search-flow_temp-div" style="display:none">${this.get('value') || this.get('placeholder')}</div>`).appendTo('body');
    this.$('.search-flow_input').css('width', tempDiv.width() + 3);
    tempDiv.remove();
  },
  availableOptions: Ember.computed('options.[],value', function () {
    let options = this.get('options');
    if (!options || !this.get('filter.isFocused')) {
      return Ember.A([]);
    }

    // Convert options to array of objects if it is an array of strings
    options = options.map(option => {
      if (this.get('isParameterSelection')) {
        option = Ember.Object.create({ title: option.title, value: option });
      }
      else {
        option = Ember.Object.create({ title: option, value: option });
      }
      return option;
    });

    if (!this.get('remoteFiltering')) {
      options = options.filter(option => {
        return option.title.toLowerCase().includes(this.get('value').toLowerCase());
      });
    }

    // Options should always be in alphabetical order
    options = options.sortBy('title');

    // Insert contains option into list
    if (this.get('filter.parameter.contains') && this.get('value') && options.length) {
      options.unshift(Ember.Object.create({ title: `Contains: ${this.get('value')}`, value: this.get('value'), isContains: true }));
    }

    // Set the index on each item for easy access later on
    options.forEach((option, index) => {
      option.index = index;
    });

    // Ensure first option is selected
    options.setEach('isActive', false);
    if (options.get('firstObject')) {
      options.set('firstObject.isActive', true);
    }

    return options;
  }),
  activeOption: Ember.computed('availableOptions.@each.isActive', function () {
    return this.get('availableOptions').findBy('isActive');
  }),
  blurInput() {
    let input = this.$('.search-flow_input');
    if (input.is(':focus')) {
      input.blur();
    }
  },
  focusInput() {
    let input = this.$('.search-flow_input');
    if (!input.is(':focus')) {
      input.focus();
    }
  },
  actions: {
    selectOption() {
      let activeOption = this.get('activeOption');
      this.set('filter.isFocused', false);
      if (this.get('isParameterSelection')) {
        this.get('newFilter')(activeOption.get('value'));
      }
      else {

        this.set('filter.value', activeOption.get('value'));
        if (activeOption.get('isContains')) {
          this.set('filter.isContains', true);
          this.set('value', `Contains: ${activeOption.get('value')}`);
        }
        else {
          this.set('filter.isContains', false);
          this.set('value', activeOption.get('value'));
        }
      }
      this.blurInput();
      this.get('inputBlurred')(this.get('isParameterSelection'), this.get('filter'));
    },
    inputEntered() {
      // Ensure item is not selected from preivous enter key hit from 'Add Filter' button
      if (!this.get('didHitEnter') && !!this.get('activeOption')) {
        this.set('didHitEnter', true);
        this.send('selectOption', this.get('activeOption'));
      }
    },
    inputKeyDown() {
      if (event.which === 38) { // Up
        event.preventDefault();
        let previousItem = this.get(`availableOptions.${this.get('activeOption.index') - 1}`);
        if (previousItem) {
          this.set('activeOption.isActive', false);
          Ember.set(previousItem, 'isActive', true);
        }
      }
      else if (event.which === 40) { // Down
        event.preventDefault();
        let nextItem = this.get(`availableOptions.${this.get('activeOption.index') + 1}`);
        if (nextItem) {
          this.set('activeOption.isActive', false);
          Ember.set(nextItem, 'isActive', true);
        }
      }
      else if (event.which === 8 && !this.get('value')) { // Backspace
        // If backspace is hit with no value typed in, blur
        this.set('shouldRemoveFilter', true);
        this.blurInput();
      }
    },
    inputKeyUp() {
      // Prevent the up or down key from moving the cursor when releasing the key
      if (event.which === 38 || event.which === 40) { // Up or Down
        event.preventDefault();
      }
      else if (event.which === 13) { // Enter key
        // Make sure user can hit enter after key released
        this.set('didHitEnter', false);
      }
    },
    inputFocused() {
      this.fetchOptions();
      this.set('filter.isFocused', true);
      if (this.get('filter.isContains')) {
        this.set('value', this.get('filter.value'));
      }
    },
    inputBlurred() {
      // Ensure the filter is not removed with clicking on an option
      if (this.$('.search-flow_dropdown-option:hover').length || this.get('didHitEnter')) {
        return;
      }

      if (this.get('filter.isFocused') && !this.get('shouldRemoveFilter')) {
        this.set('filter.isFocused', false);
        this.set('filter.value', this.get('activeOption.value'));
        if (this.get('activeOption.isContains')) {
          this.set('filter.isContains', true);
          this.set('value', `Contains: ${this.get('activeOption.value')}`);
        }
        else {
          this.set('filter.isContains', false);
          this.set('value', this.get('activeOption.value'));
        }
      }
      this.get('inputBlurred')(this.get('isParameterSelection'), this.get('filter'));
    }
  }
});
<|endoftext|>


<commit_before>/**
 * handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out
 * @namespace
 */
openerp.web.list_editable = function (openerp) {
    var KEY_RETURN = 13,
        KEY_ESCAPE = 27;
    var QWeb = openerp.web.qweb;

    // editability status of list rows
    openerp.web.ListView.prototype.defaults.editable = null;

    // TODO: not sure second @lends on existing item is correct, to check
    openerp.web.ListView.include(/** @lends openerp.web.ListView# */{
        init: function () {
            var self = this;
            this._super.apply(this, arguments);
            $(this.groups).bind({
                'edit': function (e, id, dataset) {
                    self.do_edit(dataset.index, id, dataset);
                },
                'saved': function () {
                    if (self.groups.get_selection().length) {
                        return;
                    }
                    self.configure_pager(self.dataset);
                    self.compute_aggregates();
                }
            })
        },
        /**
         * Handles the activation of a record in editable mode (making a record
         * editable), called *after* the record has become editable.
         *
         * The default behavior is to setup the listview's dataset to match
         * whatever dataset was provided by the editing List
         *
         * @param {Number} index index of the record in the dataset
         * @param {Object} id identifier of the record being edited
         * @param {openerp.web.DataSet} dataset dataset in which the record is available
         */
        do_edit: function (index, id, dataset) {
            _.extend(this.dataset, dataset);
        },
        /**
         * Sets editability status for the list, based on defaults, view
         * architecture and the provided flag, if any.
         *
         * @param {Boolean} [force] forces the list to editability. Sets new row edition status to "bottom".
         */
        set_editable: function (force) {
            // If ``force``, set editability to bottom
            // otherwise rely on view default
            // view' @editable is handled separately as we have not yet
            // fetched and processed the view at this point.
            this.options.editable = (
                    (force && "bottom")
                    || this.defaults.editable);
        },
        /**
         * Replace do_search to handle editability process
         */
        do_search: function(domain, context, group_by) {
            this.set_editable(context['set_editable']);
            this._super.apply(this, arguments);
        },
        /**
         * Replace do_add_record to handle editability (and adding new record
         * as an editable row at the top or bottom of the list)
         */
        do_add_record: function () {
            if (this.options.editable) {
                this.groups.new_record();
            } else {
                this._super();
            }
        },
        on_loaded: function (data, grouped) {
            // tree/@editable takes priority on everything else if present.
            this.options.editable = data.arch.attrs.editable || this.options.editable;
            return this._super(data, grouped);
        },
        /**
         * Ensures the editable list is saved (saves any pending edition if
         * needed, or tries to)
         *
         * Returns a deferred to the end of the saving.
         *
         * @returns {$.Deferred}
         */
        ensure_saved: function () {
            return this.groups.ensure_saved();
        }
    });

    openerp.web.ListView.Groups.include(/** @lends openerp.web.ListView.Groups# */{
        passtrough_events: openerp.web.ListView.Groups.prototype.passtrough_events + " edit saved",
        new_record: function () {
            // TODO: handle multiple children
            this.children[null].new_record();
        },
        /**
         * Ensures descendant editable List instances are all saved if they have
         * pending editions.
         *
         * @returns {$.Deferred}
         */
        ensure_saved: function () {
            return $.when.apply(null,
                _.invoke(
                    _.values(this.children),
                    'ensure_saved'));
        }
    });

    openerp.web.ListView.List.include(/** @lends openerp.web.ListView.List# */{
        row_clicked: function (event) {
            if (!this.options.editable) {
                return this._super.apply(this, arguments);
            }
            this.edit_record($(event.currentTarget).data('id'));
        },
        /**
         * Checks if a record is being edited, and if so cancels it
         */
        cancel_pending_edition: function () {
            var self = this, cancelled = $.Deferred();
            if (!this.edition) {
                cancelled.resolve();
                return cancelled.promise();
            }

            if (this.edition_id != null) {
                this.reload_record(self.records.get(this.edition_id)).then(function () {
                    cancelled.resolve();
                });
            } else {
                cancelled.resolve();
            }
            cancelled.then(function () {
                self.view.unpad_columns();
                self.edition_form.stop();
                self.edition_form.$element.remove();
                delete self.edition_form;
                delete self.edition_id;
                delete self.edition;
            });
            this.pad_table_to(5);
            return cancelled.promise();
        },
        /**
         * Adapts this list's view description to be suitable to the inner form
         * view of a row being edited.
         *
         * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows.
         */
        get_form_fields_view: function () {
            // deep copy of view
            var view = $.extend(true, {}, this.group.view.fields_view);
            _(view.arch.children).each(function (widget) {
                widget.attrs.nolabel = true;
                if (widget.tag === 'button') {
                    delete widget.attrs.string;
                }
            });
            view.arch.attrs.col = 2 * view.arch.children.length;
            return view;
        },
        on_row_keyup: function (e) {
            var self = this;
            switch (e.which) {
            case KEY_RETURN:
                this.save_row().then(function (result) {
                    if (result.created) {
                        self.new_record();
                        return;
                    }

                    var next_record_id,
                        next_record = self.records.at(
                                self.records.indexOf(result.edited_record) + 1);
                    if (next_record) {
                        next_record_id = next_record.get('id');
                        self.dataset.index = _(self.dataset.ids)
                                .indexOf(next_record_id);
                    } else {
                        self.dataset.index = 0;
                        next_record_id = self.records.at(0).get('id');
                    }
                    self.edit_record(next_record_id);
                });
                break;
            case KEY_ESCAPE:
                this.cancel_edition();
                break;
            }
        },
        render_row_as_form: function (row) {
            var self = this;
            this.cancel_pending_edition().then(function () {
                var record_id = $(row).data('id');
                var $new_row = $('<tr>', {
                        id: _.uniqueId('oe-editable-row-'),
                        'data-id': record_id,
                        'class': (row ? $(row).attr('class') : '') + ' oe_forms',
                        click: function (e) {e.stopPropagation();}
                    })
                    .delegate('button.oe-edit-row-save', 'click', function () {
                        self.save_row();
                    })
                    .delegate('button', 'keyup', function (e) {
                        e.stopImmediatePropagation();
                    })
                    .keyup(function () {
                        return self.on_row_keyup.apply(self, arguments); })
                    .keydown(function (e) { e.stopPropagation(); });
                if (row) {
                    $new_row.replaceAll(row);
                } else if (self.options.editable) {
                    var $last_child = self.$current.children('tr:last');
                    if (self.records.length) {
                        if (self.options.editable === 'top') {
                            $new_row.insertBefore(
                                self.$current.children('[data-id]:first'));
                        } else {
                            $new_row.insertAfter(
                                self.$current.children('[data-id]:last'));
                        }
                    } else {
                        $new_row.prependTo(self.$current);
                    }
                    if ($last_child.is(':not([data-id])')) {
                        $last_child.remove();
                    }
                }
                self.edition = true;
                self.edition_id = record_id;
                self.edition_form = _.extend(new openerp.web.ListEditableFormView(self.view, self.dataset, false), {
                    form_template: 'ListView.row.form',
                    registry: openerp.web.list.form.widgets,
                    $element: $new_row
                });
                // HA HA
                self.edition_form.appendTo();
                $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () {
                    // put in $.when just in case  FormView.on_loaded becomes asynchronous
                    $new_row.find('> td')
                          .addClass('oe-field-cell')
                          .removeAttr('width')
                      .end()
                      .find('td:last').removeClass('oe-field-cell').end();
                    if (self.options.selectable) {
                        $new_row.prepend('<th>');
                    }
                    if (self.options.isClarkGable) {
                        $new_row.prepend('<th>');
                    }
                    // pad in case of groupby
                    _(self.columns).each(function (column) {
                        if (column.meta) {
                            $new_row.prepend('<td>');
                        }
                    });
                    // Add column for the save, if
                    // there is none in the list
                    if (!self.options.deletable) {
                        self.view.pad_columns(
                            1, {except: $new_row});
                    }

                    self.edition_form.do_show();
                });
            });
        },
        handle_onwrite: function (source_record_id) {
            var self = this;
            var on_write_callback = self.view.fields_view.arch.attrs.on_write;
            if (!on_write_callback) { return; }
            this.dataset.call(on_write_callback, [source_record_id], function (ids) {
                _(ids).each(function (id) {
                    var record = self.records.get(id);
                    if (!record) {
                        // insert after the source record
                        var index = self.records.indexOf(
                            self.records.get(source_record_id)) + 1;
                        record = new openerp.web.list.Record({id: id});
                        self.records.add(record, {at: index});
                        self.dataset.ids.splice(index, 0, id);
                    }
                    self.reload_record(record);
                });
            });
        },
        /**
         * Saves the current row, and returns a Deferred resolving to an object
         * with the following properties:
         *
         * ``created``
         *   Boolean flag indicating whether the record saved was being created
         *   (``true`` or edited (``false``)
         * ``edited_record``
         *   The result of saving the record (either the newly created record,
         *   or the post-edition record), after insertion in the Collection if
         *   needs be.
         *
         * @returns {$.Deferred<{created: Boolean, edited_record: Record}>}
         */
        save_row: function () {
            //noinspection JSPotentiallyInvalidConstructorUsage
            var self = this, done = $.Deferred();
            return this.edition_form
                .do_save(null, this.options.editable === 'top')
                .pipe(function (result) {
                    if (result.created && !self.edition_id) {
                        self.records.add({id: result.result},
                            {at: self.options.editable === 'top' ? 0 : null});
                        self.edition_id = result.result;
                    }
                    var edited_record = self.records.get(self.edition_id);

                    return $.when(
                        self.handle_onwrite(self.edition_id),
                        self.cancel_pending_edition().then(function () {
                            $(self).trigger('saved', [self.dataset]);
                        })).pipe(function () {
                            return {
                                created: result.created || false,
                                edited_record: edited_record
                            };
                        }, null);
                }, null);
        },
        /**
         * If the current list is being edited, ensures it's saved
         */
        ensure_saved: function () {
            if (this.edition) {
                return this.save_row();
            }
            //noinspection JSPotentiallyInvalidConstructorUsage
            return $.Deferred().resolve().promise();
        },
        /**
         * Cancels the edition of the row for the current dataset index
         */
        cancel_edition: function () {
            this.cancel_pending_edition();
        },
        /**
         * Edits record currently selected via dataset
         */
        edit_record: function (record_id) {
            this.render_row_as_form(
                this.$current.find('[data-id=' + record_id + ']'));
            $(this).trigger(
                'edit',
                [record_id, this.dataset]);
        },
        new_record: function () {
            this.dataset.index = null;
            this.render_row_as_form();
        },
        render_record: function (record) {
            var index = this.records.indexOf(record),
                 self = this;
            // FIXME: context dict should probably be extracted cleanly
            return QWeb.render('ListView.row', {
                columns: this.columns,
                options: this.options,
                record: record,
                row_parity: (index % 2 === 0) ? 'even' : 'odd',
                view: this.view,
                render_cell: function () {
                    return self.render_cell.apply(self, arguments); },
                edited: !!this.edition_form
            });
        }
    });
    if (!openerp.web.list) {
        openerp.web.list = {};
    }
    if (!openerp.web.list.form) {
        openerp.web.list.form = {};
    }
    openerp.web.list.form.WidgetFrame = openerp.web.form.WidgetFrame.extend({
        template: 'ListView.row.frame'
    });
    var form_widgets = openerp.web.form.widgets;
    openerp.web.list.form.widgets = form_widgets.extend({
        'frame': 'openerp.web.list.form.WidgetFrame'
    });
    // All form widgets inherit a problematic behavior from
    // openerp.web.form.WidgetFrame: the cell itself is removed when invisible
    // whether it's @invisible or @attrs[invisible]. In list view, only the
    // former should completely remove the cell. We need to override update_dom
    // on all widgets since we can't just hit on widget itself (I think)
    var list_form_widgets = openerp.web.list.form.widgets;
    _(form_widgets.map).each(function (widget_path, key) {
        if (key === 'frame') { return; }
        var new_path = 'openerp.web.list.form.' + key;

        openerp.web.list.form[key] = (form_widgets.get_object(key)).extend({
            update_dom: function () {
                this.$element.children().css('visibility', '');
                if (this.modifiers.tree_invisible) {
                    var old_invisible = this.invisible;
                    this.invisible = true;
                    this._super();
                    this.invisible = old_invisible;
                } else if (this.invisible) {
                    this.$element.children().css('visibility', 'hidden');
                } else {
                    this._super();
                }
            }
        });
        list_form_widgets.add(key, new_path);
    });

    openerp.web.ListEditableFormView = openerp.web.FormView.extend({
        init_view: function() {},
        _render_and_insert: function () {
            return this.start();
        }
    });
};
<commit_msg>[IMP] Deferred usage in editable listview<commit_after>/**
 * handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out
 * @namespace
 */
openerp.web.list_editable = function (openerp) {
    var KEY_RETURN = 13,
        KEY_ESCAPE = 27;
    var QWeb = openerp.web.qweb;

    // editability status of list rows
    openerp.web.ListView.prototype.defaults.editable = null;

    // TODO: not sure second @lends on existing item is correct, to check
    openerp.web.ListView.include(/** @lends openerp.web.ListView# */{
        init: function () {
            var self = this;
            this._super.apply(this, arguments);
            $(this.groups).bind({
                'edit': function (e, id, dataset) {
                    self.do_edit(dataset.index, id, dataset);
                },
                'saved': function () {
                    if (self.groups.get_selection().length) {
                        return;
                    }
                    self.configure_pager(self.dataset);
                    self.compute_aggregates();
                }
            })
        },
        /**
         * Handles the activation of a record in editable mode (making a record
         * editable), called *after* the record has become editable.
         *
         * The default behavior is to setup the listview's dataset to match
         * whatever dataset was provided by the editing List
         *
         * @param {Number} index index of the record in the dataset
         * @param {Object} id identifier of the record being edited
         * @param {openerp.web.DataSet} dataset dataset in which the record is available
         */
        do_edit: function (index, id, dataset) {
            _.extend(this.dataset, dataset);
        },
        /**
         * Sets editability status for the list, based on defaults, view
         * architecture and the provided flag, if any.
         *
         * @param {Boolean} [force] forces the list to editability. Sets new row edition status to "bottom".
         */
        set_editable: function (force) {
            // If ``force``, set editability to bottom
            // otherwise rely on view default
            // view' @editable is handled separately as we have not yet
            // fetched and processed the view at this point.
            this.options.editable = (
                    (force && "bottom")
                    || this.defaults.editable);
        },
        /**
         * Replace do_search to handle editability process
         */
        do_search: function(domain, context, group_by) {
            this.set_editable(context['set_editable']);
            this._super.apply(this, arguments);
        },
        /**
         * Replace do_add_record to handle editability (and adding new record
         * as an editable row at the top or bottom of the list)
         */
        do_add_record: function () {
            if (this.options.editable) {
                this.groups.new_record();
            } else {
                this._super();
            }
        },
        on_loaded: function (data, grouped) {
            // tree/@editable takes priority on everything else if present.
            this.options.editable = data.arch.attrs.editable || this.options.editable;
            return this._super(data, grouped);
        },
        /**
         * Ensures the editable list is saved (saves any pending edition if
         * needed, or tries to)
         *
         * Returns a deferred to the end of the saving.
         *
         * @returns {$.Deferred}
         */
        ensure_saved: function () {
            return this.groups.ensure_saved();
        }
    });

    openerp.web.ListView.Groups.include(/** @lends openerp.web.ListView.Groups# */{
        passtrough_events: openerp.web.ListView.Groups.prototype.passtrough_events + " edit saved",
        new_record: function () {
            // TODO: handle multiple children
            this.children[null].new_record();
        },
        /**
         * Ensures descendant editable List instances are all saved if they have
         * pending editions.
         *
         * @returns {$.Deferred}
         */
        ensure_saved: function () {
            return $.when.apply(null,
                _.invoke(
                    _.values(this.children),
                    'ensure_saved'));
        }
    });

    openerp.web.ListView.List.include(/** @lends openerp.web.ListView.List# */{
        row_clicked: function (event) {
            if (!this.options.editable) {
                return this._super.apply(this, arguments);
            }
            this.edit_record($(event.currentTarget).data('id'));
        },
        /**
         * Checks if a record is being edited, and if so cancels it
         */
        cancel_pending_edition: function () {
            var self = this, cancelled;
            if (!this.edition) {
                return $.when();
            }

            if (this.edition_id) {
                cancelled = this.reload_record(this.records.get(this.edition_id));
            } else {
                cancelled = $.when();
            }
            cancelled.then(function () {
                self.view.unpad_columns();
                self.edition_form.stop();
                self.edition_form.$element.remove();
                delete self.edition_form;
                delete self.edition_id;
                delete self.edition;
            });
            this.pad_table_to(5);
            return cancelled;
        },
        /**
         * Adapts this list's view description to be suitable to the inner form
         * view of a row being edited.
         *
         * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows.
         */
        get_form_fields_view: function () {
            // deep copy of view
            var view = $.extend(true, {}, this.group.view.fields_view);
            _(view.arch.children).each(function (widget) {
                widget.attrs.nolabel = true;
                if (widget.tag === 'button') {
                    delete widget.attrs.string;
                }
            });
            view.arch.attrs.col = 2 * view.arch.children.length;
            return view;
        },
        on_row_keyup: function (e) {
            var self = this;
            switch (e.which) {
            case KEY_RETURN:
                this.save_row().then(function (result) {
                    if (result.created) {
                        self.new_record();
                        return;
                    }

                    var next_record_id,
                        next_record = self.records.at(
                                self.records.indexOf(result.edited_record) + 1);
                    if (next_record) {
                        next_record_id = next_record.get('id');
                        self.dataset.index = _(self.dataset.ids)
                                .indexOf(next_record_id);
                    } else {
                        self.dataset.index = 0;
                        next_record_id = self.records.at(0).get('id');
                    }
                    self.edit_record(next_record_id);
                });
                break;
            case KEY_ESCAPE:
                this.cancel_edition();
                break;
            }
        },
        render_row_as_form: function (row) {
            var self = this;
            this.cancel_pending_edition().then(function () {
                var record_id = $(row).data('id');
                var $new_row = $('<tr>', {
                        id: _.uniqueId('oe-editable-row-'),
                        'data-id': record_id,
                        'class': (row ? $(row).attr('class') : '') + ' oe_forms',
                        click: function (e) {e.stopPropagation();}
                    })
                    .delegate('button.oe-edit-row-save', 'click', function () {
                        self.save_row();
                    })
                    .delegate('button', 'keyup', function (e) {
                        e.stopImmediatePropagation();
                    })
                    .keyup(function () {
                        return self.on_row_keyup.apply(self, arguments); })
                    .keydown(function (e) { e.stopPropagation(); });
                if (row) {
                    $new_row.replaceAll(row);
                } else if (self.options.editable) {
                    var $last_child = self.$current.children('tr:last');
                    if (self.records.length) {
                        if (self.options.editable === 'top') {
                            $new_row.insertBefore(
                                self.$current.children('[data-id]:first'));
                        } else {
                            $new_row.insertAfter(
                                self.$current.children('[data-id]:last'));
                        }
                    } else {
                        $new_row.prependTo(self.$current);
                    }
                    if ($last_child.is(':not([data-id])')) {
                        $last_child.remove();
                    }
                }
                self.edition = true;
                self.edition_id = record_id;
                self.edition_form = _.extend(new openerp.web.ListEditableFormView(self.view, self.dataset, false), {
                    form_template: 'ListView.row.form',
                    registry: openerp.web.list.form.widgets,
                    $element: $new_row
                });
                // HA HA
                self.edition_form.appendTo();
                // put in $.when just in case  FormView.on_loaded becomes asynchronous
                $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () {
                    $new_row.find('> td')
                          .addClass('oe-field-cell')
                          .removeAttr('width')
                      .end()
                      .find('td:last').removeClass('oe-field-cell').end();
                    if (self.options.selectable) {
                        $new_row.prepend('<th>');
                    }
                    if (self.options.isClarkGable) {
                        $new_row.prepend('<th>');
                    }
                    // pad in case of groupby
                    _(self.columns).each(function (column) {
                        if (column.meta) {
                            $new_row.prepend('<td>');
                        }
                    });
                    // Add column for the save, if
                    // there is none in the list
                    if (!self.options.deletable) {
                        self.view.pad_columns(
                            1, {except: $new_row});
                    }

                    self.edition_form.do_show();
                });
            });
        },
        handle_onwrite: function (source_record_id) {
            var self = this;
            var on_write_callback = self.view.fields_view.arch.attrs.on_write;
            if (!on_write_callback) { return; }
            this.dataset.call(on_write_callback, [source_record_id], function (ids) {
                _(ids).each(function (id) {
                    var record = self.records.get(id);
                    if (!record) {
                        // insert after the source record
                        var index = self.records.indexOf(
                            self.records.get(source_record_id)) + 1;
                        record = new openerp.web.list.Record({id: id});
                        self.records.add(record, {at: index});
                        self.dataset.ids.splice(index, 0, id);
                    }
                    self.reload_record(record);
                });
            });
        },
        /**
         * Saves the current row, and returns a Deferred resolving to an object
         * with the following properties:
         *
         * ``created``
         *   Boolean flag indicating whether the record saved was being created
         *   (``true`` or edited (``false``)
         * ``edited_record``
         *   The result of saving the record (either the newly created record,
         *   or the post-edition record), after insertion in the Collection if
         *   needs be.
         *
         * @returns {$.Deferred<{created: Boolean, edited_record: Record}>}
         */
        save_row: function () {
            //noinspection JSPotentiallyInvalidConstructorUsage
            var self = this;
            return this.edition_form
                .do_save(null, this.options.editable === 'top')
                .pipe(function (result) {
                    if (result.created && !self.edition_id) {
                        self.records.add({id: result.result},
                            {at: self.options.editable === 'top' ? 0 : null});
                        self.edition_id = result.result;
                    }
                    var edited_record = self.records.get(self.edition_id);

                    return $.when(
                        self.handle_onwrite(self.edition_id),
                        self.cancel_pending_edition().then(function () {
                            $(self).trigger('saved', [self.dataset]);
                        })).pipe(function () {
                            return {
                                created: result.created || false,
                                edited_record: edited_record
                            };
                        });
                });
        },
        /**
         * If the current list is being edited, ensures it's saved
         */
        ensure_saved: function () {
            if (this.edition) {
                return this.save_row();
            }
            //noinspection JSPotentiallyInvalidConstructorUsage
            return $.when();
        },
        /**
         * Cancels the edition of the row for the current dataset index
         */
        cancel_edition: function () {
            this.cancel_pending_edition();
        },
        /**
         * Edits record currently selected via dataset
         */
        edit_record: function (record_id) {
            this.render_row_as_form(
                this.$current.find('[data-id=' + record_id + ']'));
            $(this).trigger(
                'edit',
                [record_id, this.dataset]);
        },
        new_record: function () {
            this.dataset.index = null;
            this.render_row_as_form();
        },
        render_record: function (record) {
            var index = this.records.indexOf(record),
                 self = this;
            // FIXME: context dict should probably be extracted cleanly
            return QWeb.render('ListView.row', {
                columns: this.columns,
                options: this.options,
                record: record,
                row_parity: (index % 2 === 0) ? 'even' : 'odd',
                view: this.view,
                render_cell: function () {
                    return self.render_cell.apply(self, arguments); },
                edited: !!this.edition_form
            });
        }
    });
    if (!openerp.web.list) {
        openerp.web.list = {};
    }
    if (!openerp.web.list.form) {
        openerp.web.list.form = {};
    }
    openerp.web.list.form.WidgetFrame = openerp.web.form.WidgetFrame.extend({
        template: 'ListView.row.frame'
    });
    var form_widgets = openerp.web.form.widgets;
    openerp.web.list.form.widgets = form_widgets.extend({
        'frame': 'openerp.web.list.form.WidgetFrame'
    });
    // All form widgets inherit a problematic behavior from
    // openerp.web.form.WidgetFrame: the cell itself is removed when invisible
    // whether it's @invisible or @attrs[invisible]. In list view, only the
    // former should completely remove the cell. We need to override update_dom
    // on all widgets since we can't just hit on widget itself (I think)
    var list_form_widgets = openerp.web.list.form.widgets;
    _(form_widgets.map).each(function (widget_path, key) {
        if (key === 'frame') { return; }
        var new_path = 'openerp.web.list.form.' + key;

        openerp.web.list.form[key] = (form_widgets.get_object(key)).extend({
            update_dom: function () {
                this.$element.children().css('visibility', '');
                if (this.modifiers.tree_invisible) {
                    var old_invisible = this.invisible;
                    this.invisible = true;
                    this._super();
                    this.invisible = old_invisible;
                } else if (this.invisible) {
                    this.$element.children().css('visibility', 'hidden');
                } else {
                    this._super();
                }
            }
        });
        list_form_widgets.add(key, new_path);
    });

    openerp.web.ListEditableFormView = openerp.web.FormView.extend({
        init_view: function() {},
        _render_and_insert: function () {
            return this.start();
        }
    });
};
<|endoftext|>


<commit_before>automulti_remove_button_update = function (element) {
	list = document.getElementsByName(element+"__display")[0];
	i = 0;
	while (i!=list.options.length) {
		if (list.options[i].selected) {
			$("automulti_button_style_"+element).setAttribute("class","button enabled");
			break;
		}
		i++;
	}
	if (i==list.options.length) $("automulti_button_style_"+element).setAttribute("class","button disabled");
}

automulti_remove_button_action = function (element, list_sep) {
	list = document.getElementsByName(element+"__display")[0];
	val_holder = $(element+'__var_holder');
	i = 0;
	val_holder.value = "";
	while (i!=list.options.length) {
		if (list.options[i].selected) {
			list.options[i] = null;
		} else {
			val_holder.value += list_sep;
			val_holder.value += list.options[i].value;
			i++;
		}
	}
	automulti_remove_button_update(element);
}

automulti_on_hide = function (element, list_sep) {
	var new_value=$("__autocomplete_id_"+element+"__search").value.split("__");
	if (new_value && typeof(new_value[1])!="undefined") {
		$("__autocomplete_id_"+element+"__search").value="";
		automulti_add_value(element, list_sep, new_value[0], new_value[1]);
	}
}

automulti_add_value = function (element, list_sep, value, label) {
	list = document.getElementsByName(element+"__display")[0];
	alert(value);
	i = 0;
	while (i!=list.options.length) {
		if (list.options[i].value==value) {
			value=null;
			break;
		}
		i++;
	}
	alert(value);
	if (value!=null) {
		list.options[i] = new Option();
		list.options[i].value = value;
		list.options[i].text = label;
		if(typeof iphone != "undefined") list.options[i].selected = true;
		val_holder = $(element+'__var_holder');
		alert(element);
		val_holder.value = val_holder.value + list_sep + value;
		alert(val_holder.value);
	}
}
<commit_msg>automulti remove alerts<commit_after>automulti_remove_button_update = function (element) {
	list = document.getElementsByName(element+"__display")[0];
	i = 0;
	while (i!=list.options.length) {
		if (list.options[i].selected) {
			$("automulti_button_style_"+element).setAttribute("class","button enabled");
			break;
		}
		i++;
	}
	if (i==list.options.length) $("automulti_button_style_"+element).setAttribute("class","button disabled");
}

automulti_remove_button_action = function (element, list_sep) {
	list = document.getElementsByName(element+"__display")[0];
	val_holder = $(element+'__var_holder');
	i = 0;
	val_holder.value = "";
	while (i!=list.options.length) {
		if (list.options[i].selected) {
			list.options[i] = null;
		} else {
			val_holder.value += list_sep;
			val_holder.value += list.options[i].value;
			i++;
		}
	}
	automulti_remove_button_update(element);
}

automulti_on_hide = function (element, list_sep) {
	var new_value=$("__autocomplete_id_"+element+"__search").value.split("__");
	if (new_value && typeof(new_value[1])!="undefined") {
		$("__autocomplete_id_"+element+"__search").value="";
		automulti_add_value(element, list_sep, new_value[0], new_value[1]);
	}
}

automulti_add_value = function (element, list_sep, value, label) {
	list = document.getElementsByName(element+"__display")[0];
	i = 0;
	while (i!=list.options.length) {
		if (list.options[i].value==value) {
			value=null;
			break;
		}
		i++;
	}
	if (value!=null) {
		list.options[i] = new Option();
		list.options[i].value = value;
		list.options[i].text = label;
		if(typeof iphone != "undefined") list.options[i].selected = true;
		val_holder = $(element+'__var_holder');
		val_holder.value = val_holder.value + list_sep + value;
	}
}
<|endoftext|>