<commit_before>import Ember from 'ember';

// TODO: refactor permissions strings when https://github.com/CenterForOpenScience/ember-osf/pull/23/files#diff-7fd0bf247bef3c257e0fcfd7e544a338R5 is merged

export default Ember.Route.extend({
    model(params) {
        return this.store.findRecord('node', params.node_id);
    },

    setupController(controller, model) {
        controller.set('editedTitle', model.get('title'));
        controller.set('editedTitle', model.get('category'));
        controller.set('editedTitle', model.get('dscription'));
        this._super(...arguments);
    },

    actions: {
        editExisting(title, description, category, isPublic) {
            // TODO: Should test PUT or PATCH
            var node = this.modelFor(this.routeName);
            if (node.get('currentUserPermissions').indexOf('write') !== -1) {
                if (title) {node.set('title', title);}
                if (category) {node.set('category', category);}
                if (description) {node.set('description', description);}
                if (isPublic !== null) {node.set('public', isPublic);}
                node.save();
            } else {
                console.log('You do not have permissions to edit this node');
            }
        },
        affiliateNode(instId) {
            this.store.findRecord('institution', instId).then(inst => {
                var node = this.modelFor(this.routeName);
                node.get('affiliatedInstitutions').pushObject(inst);
                node.save();
            });
        },
        deaffiliateNode(inst) {
            var node = this.modelFor(this.routeName);
            node.get('affiliatedInstitutions').removeObject(inst);
            node.save();
        },
        addContributor(contribId, permission, bibliographic) {
            var node = this.modelFor(this.routeName);
            if (contribId) {
                if (node.get('currentUserPermissions').indexOf('admin') !== -1) {
                    var contributor = this.store.createRecord('contributor', {
                        id: contribId,
                        permission: permission,
                        bibliographic: bibliographic,
                        nodeId: node.id
                    });
                    contributor.save();
                    node.get('contributors').pushObject(contributor);
                    node.save();
                    console.log('Contributor added.');
                } else {
                    console.log('You do not have permissions to add contributors');
                }
            } else {
                console.log('User ID must be specified.');
            }
        },
        updateContributors(editedPermissions, editedBibliographic) {
            var node = this.modelFor(this.routeName);
            var contribMap = this.generateContributorMap(node.get('contributors'));
            let user = this.modelFor('application');

            for (var contrib in editedPermissions) {
                contribMap[contrib].permission = editedPermissions[contrib];
            }

            for (var c in editedBibliographic) {
                contribMap[c].bibliographic = editedBibliographic[c];
            }

            if (node.get('currentUserPermissions').indexOf('admin') !== -1) {
                this.attemptContributorsUpdate(contribMap, node, editedPermissions, editedBibliographic);
            } else {
                // Non-admins can only attempt to remove themselves as contributors
                if (contrib.id === user.id) {
                    this.attemptContributorsUpdate(contribMap, node, editedPermissions,
                      editedBibliographic);
                } else {
                    console.log('Non-admins cannot update other contributors.');
                }
            }

        },
        deleteContributor(contrib) {
            var node = this.modelFor(this.routeName);
            contrib.setProperties({ nodeId: node.id });
            let user = this.modelFor('application');

            var contribMap = this.generateContributorMap(node.get('contributors'));

            if (node.get('currentUserPermissions').indexOf('admin') !== -1) {
                this.attemptContributorRemoval(contrib, contribMap);
            } else {
                // Non-admins can only attempt to remove themselves as contributors
                if (contrib.id === user.id) {
                    this.attemptContributorRemoval(contrib, contribMap);
                } else {
                    console.log('Non-admins cannot delete other contributors.');
                }
            }
        },
        addChildren(title, description, category) {
            var node = this.modelFor(this.routeName);
            if (node.get('currentUserPermissions').indexOf('write') !== -1) {
                var child = this.store.createRecord('node', {
                    title: title,
                    category: category || 'project',
                    description: description || null
                });
                child.one('didCreate', this, function() {
                    this.transitionTo('nodes.detail.children');
                });
                node.get('children').pushObject(child);
                node.save();
            } else {
                console.log('You do not have permissions to create this component');
            }
        },
        deleteNode() {
            var node = this.modelFor(this.routeName);
            if (node.get('currentUserPermissions').indexOf('write') !== -1) {
                node.one('didDelete', this, function () {
                    this.transitionTo('nodes.index');
                });
                node.destroyRecord();
            } else {
                console.log('You do not have permissions to delete this node');
            }
        },
        addNodeLink(targetNodeId) {
            var node = this.modelFor(this.routeName);
            if (node.get('currentUserPermissions').indexOf('write') !== -1) {
                var nodeLink = this.store.createRecord('node-link', {
                    target: targetNodeId
                });
                node.get('nodeLinks').pushObject(nodeLink);
                node.save();
            } else {
                console.log('You do not have permissions to create a node link');
            }
        },
        removeNodeLink(targetNode) {
            var node = this.modelFor(this.routeName);
            if (node.get('currentUserPermissions').indexOf('write') !== -1) {
                targetNode.destroyRecord();
                console.log('Node link removed.');
            } else {
                console.log('You do not have permissions to delete this node link.');
            }
        }
    },
    generateContributorMap(contributors) {
        // Maps all node contributors to format {contribID: {permission: "read|write|admin", bibliographic: "true|false"}}
        var contribMap = contributors.content.currentState.reduce(function(newMap, contrib) {
            newMap[contrib.id] = { permission: contrib._data.permission, bibliographic: contrib._data.bibliographic };
            return newMap;
        }, {});
        return contribMap;
    },
    canModifyContributor(contribRemoving, contribMap) {
        /** Checks to see if contributor(s) can be updated/removed. Contributor can only be updated/removed
        if there is at least one other contributor with admin permissions, and at least one other bibliographic contributor **/
        var bibliographic = false;
        var admin = false;
        for (var contribId in contribMap) {
            if (contribRemoving && contribId === contribRemoving.id) {
                continue;
            } else {
                if (contribMap[contribId].bibliographic) {
                    bibliographic = true;
                }
                if (contribMap[contribId].permission === 'admin') {
                    admin = true;
                }
            }
        }
        if (bibliographic && admin) {
            return true;
        }
        return false;
    },

    attemptContributorsUpdate(contribMap, node, editedPermissions, editedBibliographic) {
        if (this.canModifyContributor(null, contribMap)) {
            for (var contrib in editedPermissions) {
                this.modifyPermissions(contrib, node, editedPermissions);
            }
            for (var c in editedBibliographic) {
                this.modifyBibliographic(c, node, editedBibliographic);
            }
            console.log('Contributor(s) updated.');
        } else {
            console.log('Cannot update contributor(s)');
        }
    },

    modifyPermissions(contrib, node, editedPermissions) {
        this.store.findRecord('contributor', contrib).then(function(contributor) {
            contributor.set('nodeId', node.id);
            contributor.set('permission', editedPermissions[contrib]);
            contributor.save();
        });
    },

    modifyBibliographic(contrib, node, editedBibliographic) {
        this.store.findRecord('contributor', contrib).then(function(contributor) {
            contributor.set('nodeId', node.id);
            contributor.set('bibliographic', editedBibliographic[contrib]);
            contributor.save();
        });
    },

    attemptContributorRemoval(contrib, contribMap) {
        if (this.canModifyContributor(contrib, contribMap)) {
            contrib.deleteRecord();
            contrib.save();
            console.log('Contributor removed.');
        } else {
            console.log('Cannot remove contributor');
        }
    }
});
<commit_msg>Remove contributor.save().<commit_after>import Ember from 'ember';

// TODO: refactor permissions strings when https://github.com/CenterForOpenScience/ember-osf/pull/23/files#diff-7fd0bf247bef3c257e0fcfd7e544a338R5 is merged

export default Ember.Route.extend({
    model(params) {
        return this.store.findRecord('node', params.node_id);
    },

    setupController(controller, model) {
        controller.set('editedTitle', model.get('title'));
        controller.set('editedTitle', model.get('category'));
        controller.set('editedTitle', model.get('dscription'));
        this._super(...arguments);
    },

    actions: {
        editExisting(title, description, category, isPublic) {
            // TODO: Should test PUT or PATCH
            var node = this.modelFor(this.routeName);
            if (node.get('currentUserPermissions').indexOf('write') !== -1) {
                if (title) {node.set('title', title);}
                if (category) {node.set('category', category);}
                if (description) {node.set('description', description);}
                if (isPublic !== null) {node.set('public', isPublic);}
                node.save();
            } else {
                console.log('You do not have permissions to edit this node');
            }
        },
        affiliateNode(instId) {
            this.store.findRecord('institution', instId).then(inst => {
                var node = this.modelFor(this.routeName);
                node.get('affiliatedInstitutions').pushObject(inst);
                node.save();
            });
        },
        deaffiliateNode(inst) {
            var node = this.modelFor(this.routeName);
            node.get('affiliatedInstitutions').removeObject(inst);
            node.save();
        },
        addContributor(contribId, permission, bibliographic) {
            var node = this.modelFor(this.routeName);
            if (contribId) {
                if (node.get('currentUserPermissions').indexOf('admin') !== -1) {
                    var contributor = this.store.createRecord('contributor', {
                        id: contribId,
                        permission: permission,
                        bibliographic: bibliographic,
                        nodeId: node.id
                    });
                    node.get('contributors').pushObject(contributor);
                    node.save();
                    console.log('Contributor added.');
                } else {
                    console.log('You do not have permissions to add contributors');
                }
            } else {
                console.log('User ID must be specified.');
            }
        },
        updateContributors(editedPermissions, editedBibliographic) {
            var node = this.modelFor(this.routeName);
            var contribMap = this.generateContributorMap(node.get('contributors'));
            let user = this.modelFor('application');

            for (var contrib in editedPermissions) {
                contribMap[contrib].permission = editedPermissions[contrib];
            }

            for (var c in editedBibliographic) {
                contribMap[c].bibliographic = editedBibliographic[c];
            }

            if (node.get('currentUserPermissions').indexOf('admin') !== -1) {
                this.attemptContributorsUpdate(contribMap, node, editedPermissions, editedBibliographic);
            } else {
                // Non-admins can only attempt to remove themselves as contributors
                if (contrib.id === user.id) {
                    this.attemptContributorsUpdate(contribMap, node, editedPermissions,
                      editedBibliographic);
                } else {
                    console.log('Non-admins cannot update other contributors.');
                }
            }

        },
        deleteContributor(contrib) {
            var node = this.modelFor(this.routeName);
            contrib.setProperties({ nodeId: node.id });
            let user = this.modelFor('application');

            var contribMap = this.generateContributorMap(node.get('contributors'));

            if (node.get('currentUserPermissions').indexOf('admin') !== -1) {
                this.attemptContributorRemoval(contrib, contribMap);
            } else {
                // Non-admins can only attempt to remove themselves as contributors
                if (contrib.id === user.id) {
                    this.attemptContributorRemoval(contrib, contribMap);
                } else {
                    console.log('Non-admins cannot delete other contributors.');
                }
            }
        },
        addChildren(title, description, category) {
            var node = this.modelFor(this.routeName);
            if (node.get('currentUserPermissions').indexOf('write') !== -1) {
                var child = this.store.createRecord('node', {
                    title: title,
                    category: category || 'project',
                    description: description || null
                });
                child.one('didCreate', this, function() {
                    this.transitionTo('nodes.detail.children');
                });
                node.get('children').pushObject(child);
                node.save();
            } else {
                console.log('You do not have permissions to create this component');
            }
        },
        deleteNode() {
            var node = this.modelFor(this.routeName);
            if (node.get('currentUserPermissions').indexOf('write') !== -1) {
                node.one('didDelete', this, function () {
                    this.transitionTo('nodes.index');
                });
                node.destroyRecord();
            } else {
                console.log('You do not have permissions to delete this node');
            }
        },
        addNodeLink(targetNodeId) {
            var node = this.modelFor(this.routeName);
            if (node.get('currentUserPermissions').indexOf('write') !== -1) {
                var nodeLink = this.store.createRecord('node-link', {
                    target: targetNodeId
                });
                node.get('nodeLinks').pushObject(nodeLink);
                node.save();
            } else {
                console.log('You do not have permissions to create a node link');
            }
        },
        removeNodeLink(targetNode) {
            var node = this.modelFor(this.routeName);
            if (node.get('currentUserPermissions').indexOf('write') !== -1) {
                targetNode.destroyRecord();
                console.log('Node link removed.');
            } else {
                console.log('You do not have permissions to delete this node link.');
            }
        }
    },
    generateContributorMap(contributors) {
        // Maps all node contributors to format {contribID: {permission: "read|write|admin", bibliographic: "true|false"}}
        var contribMap = contributors.content.currentState.reduce(function(newMap, contrib) {
            newMap[contrib.id] = { permission: contrib._data.permission, bibliographic: contrib._data.bibliographic };
            return newMap;
        }, {});
        return contribMap;
    },
    canModifyContributor(contribRemoving, contribMap) {
        /** Checks to see if contributor(s) can be updated/removed. Contributor can only be updated/removed
        if there is at least one other contributor with admin permissions, and at least one other bibliographic contributor **/
        var bibliographic = false;
        var admin = false;
        for (var contribId in contribMap) {
            if (contribRemoving && contribId === contribRemoving.id) {
                continue;
            } else {
                if (contribMap[contribId].bibliographic) {
                    bibliographic = true;
                }
                if (contribMap[contribId].permission === 'admin') {
                    admin = true;
                }
            }
        }
        if (bibliographic && admin) {
            return true;
        }
        return false;
    },

    attemptContributorsUpdate(contribMap, node, editedPermissions, editedBibliographic) {
        if (this.canModifyContributor(null, contribMap)) {
            for (var contrib in editedPermissions) {
                this.modifyPermissions(contrib, node, editedPermissions);
            }
            for (var c in editedBibliographic) {
                this.modifyBibliographic(c, node, editedBibliographic);
            }
            console.log('Contributor(s) updated.');
        } else {
            console.log('Cannot update contributor(s)');
        }
    },

    modifyPermissions(contrib, node, editedPermissions) {
        this.store.findRecord('contributor', contrib).then(function(contributor) {
            contributor.set('nodeId', node.id);
            contributor.set('permission', editedPermissions[contrib]);
            contributor.save();
        });
    },

    modifyBibliographic(contrib, node, editedBibliographic) {
        this.store.findRecord('contributor', contrib).then(function(contributor) {
            contributor.set('nodeId', node.id);
            contributor.set('bibliographic', editedBibliographic[contrib]);
            contributor.save();
        });
    },

    attemptContributorRemoval(contrib, contribMap) {
        if (this.canModifyContributor(contrib, contribMap)) {
            contrib.deleteRecord();
            contrib.save();
            console.log('Contributor removed.');
        } else {
            console.log('Cannot remove contributor');
        }
    }
});
<|endoftext|>


<commit_before>import Delta from 'rich-text/lib/delta';
import Parchment from 'parchment';
import Quill from '../core/quill';
import logger from '../core/logger';
import Module from '../core/module';

import { AlignStyle } from '../formats/align';
import { BackgroundStyle } from '../formats/background';
import { ColorStyle } from '../formats/color';
import { DirectionStyle } from '../formats/direction';
import { FontStyle } from '../formats/font';
import { SizeStyle } from '../formats/size';

let debug = logger('quill:clipboard');

const CLIPBOARD_CONFIG = [
  [Node.TEXT_NODE, matchText],
  ['br', matchBreak],
  [Node.ELEMENT_NODE, matchNewline],
  [Node.ELEMENT_NODE, matchBlot],
  [Node.ELEMENT_NODE, matchSpacing],
  [Node.ELEMENT_NODE, matchAttributor],
  [Node.ELEMENT_NODE, matchStyles],
  ['b', matchAlias.bind(matchAlias, 'bold')],
  ['i', matchAlias.bind(matchAlias, 'italic')],
  ['style', matchIgnore]
];

const STYLE_ATTRIBUTORS = [
  AlignStyle,
  BackgroundStyle,
  ColorStyle,
  DirectionStyle,
  FontStyle,
  SizeStyle
].reduce(function(memo, attr) {
  memo[attr.keyName] = attr;
  return memo;
}, {});

class Clipboard extends Module {
  constructor(quill, options) {
    super(quill, options);
    this.quill.root.addEventListener('paste', this.onPaste.bind(this));
    this.container = this.quill.addContainer('ql-clipboard');
    this.container.setAttribute('contenteditable', true);
    this.container.setAttribute('tabindex', -1);
    this.matchers = [];
    CLIPBOARD_CONFIG.concat(this.options.matchers).forEach((pair) => {
      this.addMatcher(...pair);
    });
  }

  addMatcher(selector, matcher) {
    this.matchers.push([selector, matcher]);
  }

  convert(html) {
    const DOM_KEY = '__ql-matcher';
    if (typeof html === 'string') {
      this.container.innerHTML = html;
    }
    let textMatchers = [], elementMatchers = [];
    this.matchers.forEach((pair) => {
      let [selector, matcher] = pair;
      switch (selector) {
        case Node.TEXT_NODE:
          textMatchers.push(matcher);
          break;
        case Node.ELEMENT_NODE:
          elementMatchers.push(matcher);
          break;
        default:
          [].forEach.call(this.container.querySelectorAll(selector), (node) => {
            // TODO use weakmap
            node[DOM_KEY] = node[DOM_KEY] || [];
            node[DOM_KEY].push(matcher);
          });
          break;
      }
    });
    let traverse = (node) => {  // Post-order
      if (node.nodeType === node.TEXT_NODE) {
        return textMatchers.reduce(function(delta, matcher) {
          return matcher(node, delta);
        }, new Delta());
      } else if (node.nodeType === node.ELEMENT_NODE) {
        return [].reduce.call(node.childNodes || [], (delta, childNode) => {
          let childrenDelta = traverse(childNode);
          if (childNode.nodeType === node.ELEMENT_NODE) {
            childrenDelta = elementMatchers.reduce(function(childrenDelta, matcher) {
              return matcher(childNode, childrenDelta);
            }, childrenDelta);
            childrenDelta = (childNode[DOM_KEY] || []).reduce(function(childrenDelta, matcher) {
              return matcher(childNode, childrenDelta);
            }, childrenDelta);
          }
          return delta.concat(childrenDelta);
        }, new Delta());
      } else {
        return new Delta();
      }
    };
    let delta = traverse(this.container);
    // Remove trailing newline
    if (deltaEndsWith(delta, '\n') && delta.ops[delta.ops.length - 1].attributes == null) {
      delta = delta.compose(new Delta().retain(delta.length() - 1).delete(1));
    }
    debug.log('convert', this.container.innerHTML, delta);
    this.container.innerHTML = '';
    return delta;
  }

  onPaste(e) {
    if (e.defaultPrevented) return;
    let range = this.quill.getSelection();
    let delta = new Delta().retain(range.index).delete(range.length);
    let types = e.clipboardData ? e.clipboardData.types : null;
    if ((types instanceof DOMStringList && types.contains("text/html")) ||
        (types && types.indexOf && types.indexOf('text/html') !== -1)) {
      this.container.innerHTML = e.clipboardData.getData('text/html');
      paste.call(this);
      e.preventDefault();
    } else {
      let bodyTop = document.body.scrollTop;
      this.container.focus();
      setTimeout(() => {
        paste.call(this);
        document.body.scrollTop = bodyTop;
        this.quill.selection.scrollIntoView();
      }, 1);
    }
    function paste() {
      delta = delta.concat(this.convert());
      this.quill.updateContents(delta, Quill.sources.USER);
      // range.length contributes to delta.length()
      this.quill.setSelection(delta.length() - range.length, Quill.sources.SILENT);
    }
  }
}
Clipboard.DEFAULTS = {
  matchers: []
};

function computeStyle(node) {
  if (node.nodeType !== Node.ELEMENT_NODE) return {};
  const DOM_KEY = '__ql-computed-style';
  return node[DOM_KEY] || (node[DOM_KEY] = window.getComputedStyle(node));
}

function deltaEndsWith(delta, text) {
  let endText = "";
  for (let i = delta.ops.length - 1; i >= 0 && endText.length < text.length; --i) {
    let op  = delta.ops[i];
    if (typeof op.insert !== 'string') break;
    endText = op.insert + endText;
  }
  return endText.slice(-1*text.length) === text;
}

function isLine(node) {
  if (node.childNodes.length === 0) return false;   // Exclude embed blocks
  let style = computeStyle(node);
  return ['block', 'list-item'].indexOf(style.display) > -1;
}

function matchAlias(format, node, delta) {
  return delta.compose(new Delta().retain(delta.length(), { [format]: true }));
}

function matchAttributor(node, delta) {
  let attributes = Parchment.Attributor.Attribute.keys(node);
  let classes = Parchment.Attributor.Class.keys(node);
  let styles = Parchment.Attributor.Style.keys(node);
  let formats = {};
  attributes.concat(classes).concat(styles).forEach((name) => {
    let attr = Parchment.query(name, Parchment.Scope.ATTRIBUTE) || STYLE_ATTRIBUTORS[name];
    if (attr != null) {
      formats[attr.attrName] = attr.value(node);
    }
  });
  if (Object.keys(formats).length > 0) {
    delta = delta.compose(new Delta().retain(delta.length(), formats));
  }
  return delta;
}

function matchBlot(node, delta) {
  let match = Parchment.query(node);
  if (match == null) return delta;
  if (match.prototype instanceof Parchment.Embed) {
    let embed = {};
    let value = match.value(node);
    if (value != null) {
      embed[match.blotName] = value;
      delta = new Delta().insert(embed, match.formats(node));
    }
  } else if (typeof match.formats === 'function') {
    let formats = { [match.blotName]: match.formats(node) };
    delta = delta.compose(new Delta().retain(delta.length(), formats));
  }
  return delta;
}

function matchBreak(node, delta) {
  if (!deltaEndsWith(delta, '\n')) {
    delta.insert('\n');
  }
  return delta;
}

function matchIgnore(node, delta) {
  return new Delta();
}

function matchNewline(node, delta) {
  if (isLine(node) && !deltaEndsWith(delta, '\n')) {
    delta.insert('\n');
  }
  return delta;
}

function matchSpacing(node, delta) {
  if (isLine(node) && node.nextElementSibling != null && !deltaEndsWith(delta, '\n\n')) {
    let nodeHeight = node.offsetHeight + parseFloat(computeStyle(node).marginTop) + parseFloat(computeStyle(node).marginBottom);
    if (node.nextElementSibling.offsetTop > node.offsetTop + nodeHeight*1.5) {
      delta.insert('\n');
    }
  }
  return delta;
}

function matchStyles(node, delta) {
  let formats = {};
  let style = node.style || {};
  if (style.fontWeight && computeStyle(node).fontWeight === 'bold') {
    formats.bold = true;
  }
  if (Object.keys(formats).length > 0) {
    delta = delta.compose(new Delta().retain(delta.length(), formats));
  }
  if (parseFloat(style.textIndent || 0) > 0) {  // Could be 0.5in
    delta = new Delta().insert('\t').concat(delta);
  }
  return delta;
}

function matchText(node, delta) {
  let text = node.data;
  // Word represents empty line with <o:p> </o:p>
  if (node.parentNode.tagName === 'O:P') {
    return delta.insert(text.trim());
  }
  if (!computeStyle(node.parentNode).whiteSpace.startsWith('pre')) {
    function replacer(collapse, match) {
      match = match.replace(/[^\u00a0]/g, '');    // \u00a0 is nbsp;
      return match.length < 1 && collapse ? ' ' : match;
    }
    text = text.replace(/\r\n/g, ' ').replace(/\n/g, ' ');
    text = text.replace(/\s\s+/g, replacer.bind(replacer, true));  // collapse whitespace
    if ((node.previousSibling == null && isLine(node.parentNode)) ||
        (node.previousSibling != null && isLine(node.previousSibling))) {
      text = text.replace(/^\s+/, replacer.bind(replacer, false));
    }
    if ((node.nextSibling == null && isLine(node.parentNode)) ||
        (node.nextSibling != null && isLine(node.nextSibling))) {
      text = text.replace(/\s+$/, replacer.bind(replacer, false));
    }
  }
  return delta.insert(text);
}

export { Clipboard as default, matchAttributor, matchBlot, matchNewline, matchSpacing, matchText };
<commit_msg>Revert relying on clipboardData html<commit_after>import Delta from 'rich-text/lib/delta';
import Parchment from 'parchment';
import Quill from '../core/quill';
import logger from '../core/logger';
import Module from '../core/module';

import { AlignStyle } from '../formats/align';
import { BackgroundStyle } from '../formats/background';
import { ColorStyle } from '../formats/color';
import { DirectionStyle } from '../formats/direction';
import { FontStyle } from '../formats/font';
import { SizeStyle } from '../formats/size';

let debug = logger('quill:clipboard');

const CLIPBOARD_CONFIG = [
  [Node.TEXT_NODE, matchText],
  ['br', matchBreak],
  [Node.ELEMENT_NODE, matchNewline],
  [Node.ELEMENT_NODE, matchBlot],
  [Node.ELEMENT_NODE, matchSpacing],
  [Node.ELEMENT_NODE, matchAttributor],
  [Node.ELEMENT_NODE, matchStyles],
  ['b', matchAlias.bind(matchAlias, 'bold')],
  ['i', matchAlias.bind(matchAlias, 'italic')],
  ['style', matchIgnore]
];

const STYLE_ATTRIBUTORS = [
  AlignStyle,
  BackgroundStyle,
  ColorStyle,
  DirectionStyle,
  FontStyle,
  SizeStyle
].reduce(function(memo, attr) {
  memo[attr.keyName] = attr;
  return memo;
}, {});

class Clipboard extends Module {
  constructor(quill, options) {
    super(quill, options);
    this.quill.root.addEventListener('paste', this.onPaste.bind(this));
    this.container = this.quill.addContainer('ql-clipboard');
    this.container.setAttribute('contenteditable', true);
    this.container.setAttribute('tabindex', -1);
    this.matchers = [];
    CLIPBOARD_CONFIG.concat(this.options.matchers).forEach((pair) => {
      this.addMatcher(...pair);
    });
  }

  addMatcher(selector, matcher) {
    this.matchers.push([selector, matcher]);
  }

  convert(html) {
    const DOM_KEY = '__ql-matcher';
    if (typeof html === 'string') {
      this.container.innerHTML = html;
    }
    let textMatchers = [], elementMatchers = [];
    this.matchers.forEach((pair) => {
      let [selector, matcher] = pair;
      switch (selector) {
        case Node.TEXT_NODE:
          textMatchers.push(matcher);
          break;
        case Node.ELEMENT_NODE:
          elementMatchers.push(matcher);
          break;
        default:
          [].forEach.call(this.container.querySelectorAll(selector), (node) => {
            // TODO use weakmap
            node[DOM_KEY] = node[DOM_KEY] || [];
            node[DOM_KEY].push(matcher);
          });
          break;
      }
    });
    let traverse = (node) => {  // Post-order
      if (node.nodeType === node.TEXT_NODE) {
        return textMatchers.reduce(function(delta, matcher) {
          return matcher(node, delta);
        }, new Delta());
      } else if (node.nodeType === node.ELEMENT_NODE) {
        return [].reduce.call(node.childNodes || [], (delta, childNode) => {
          let childrenDelta = traverse(childNode);
          if (childNode.nodeType === node.ELEMENT_NODE) {
            childrenDelta = elementMatchers.reduce(function(childrenDelta, matcher) {
              return matcher(childNode, childrenDelta);
            }, childrenDelta);
            childrenDelta = (childNode[DOM_KEY] || []).reduce(function(childrenDelta, matcher) {
              return matcher(childNode, childrenDelta);
            }, childrenDelta);
          }
          return delta.concat(childrenDelta);
        }, new Delta());
      } else {
        return new Delta();
      }
    };
    let delta = traverse(this.container);
    // Remove trailing newline
    if (deltaEndsWith(delta, '\n') && delta.ops[delta.ops.length - 1].attributes == null) {
      delta = delta.compose(new Delta().retain(delta.length() - 1).delete(1));
    }
    debug.log('convert', this.container.innerHTML, delta);
    this.container.innerHTML = '';
    return delta;
  }

  onPaste(e) {
    if (e.defaultPrevented) return;
    let range = this.quill.getSelection();
    let delta = new Delta().retain(range.index).delete(range.length);
    let types = e.clipboardData ? e.clipboardData.types : null;
    let bodyTop = document.body.scrollTop;
    this.container.focus();
    setTimeout(() => {
      delta = delta.concat(this.convert());
      this.quill.updateContents(delta, Quill.sources.USER);
      // range.length contributes to delta.length()
      this.quill.setSelection(delta.length() - range.length, Quill.sources.SILENT);
      document.body.scrollTop = bodyTop;
      this.quill.selection.scrollIntoView();
    }, 1);
  }
}
Clipboard.DEFAULTS = {
  matchers: []
};

function computeStyle(node) {
  if (node.nodeType !== Node.ELEMENT_NODE) return {};
  const DOM_KEY = '__ql-computed-style';
  return node[DOM_KEY] || (node[DOM_KEY] = window.getComputedStyle(node));
}

function deltaEndsWith(delta, text) {
  let endText = "";
  for (let i = delta.ops.length - 1; i >= 0 && endText.length < text.length; --i) {
    let op  = delta.ops[i];
    if (typeof op.insert !== 'string') break;
    endText = op.insert + endText;
  }
  return endText.slice(-1*text.length) === text;
}

function isLine(node) {
  if (node.childNodes.length === 0) return false;   // Exclude embed blocks
  let style = computeStyle(node);
  return ['block', 'list-item'].indexOf(style.display) > -1;
}

function matchAlias(format, node, delta) {
  return delta.compose(new Delta().retain(delta.length(), { [format]: true }));
}

function matchAttributor(node, delta) {
  let attributes = Parchment.Attributor.Attribute.keys(node);
  let classes = Parchment.Attributor.Class.keys(node);
  let styles = Parchment.Attributor.Style.keys(node);
  let formats = {};
  attributes.concat(classes).concat(styles).forEach((name) => {
    let attr = Parchment.query(name, Parchment.Scope.ATTRIBUTE) || STYLE_ATTRIBUTORS[name];
    if (attr != null) {
      formats[attr.attrName] = attr.value(node);
    }
  });
  if (Object.keys(formats).length > 0) {
    delta = delta.compose(new Delta().retain(delta.length(), formats));
  }
  return delta;
}

function matchBlot(node, delta) {
  let match = Parchment.query(node);
  if (match == null) return delta;
  if (match.prototype instanceof Parchment.Embed) {
    let embed = {};
    let value = match.value(node);
    if (value != null) {
      embed[match.blotName] = value;
      delta = new Delta().insert(embed, match.formats(node));
    }
  } else if (typeof match.formats === 'function') {
    let formats = { [match.blotName]: match.formats(node) };
    delta = delta.compose(new Delta().retain(delta.length(), formats));
  }
  return delta;
}

function matchBreak(node, delta) {
  if (!deltaEndsWith(delta, '\n')) {
    delta.insert('\n');
  }
  return delta;
}

function matchIgnore(node, delta) {
  return new Delta();
}

function matchNewline(node, delta) {
  if (isLine(node) && !deltaEndsWith(delta, '\n')) {
    delta.insert('\n');
  }
  return delta;
}

function matchSpacing(node, delta) {
  if (isLine(node) && node.nextElementSibling != null && !deltaEndsWith(delta, '\n\n')) {
    let nodeHeight = node.offsetHeight + parseFloat(computeStyle(node).marginTop) + parseFloat(computeStyle(node).marginBottom);
    if (node.nextElementSibling.offsetTop > node.offsetTop + nodeHeight*1.5) {
      delta.insert('\n');
    }
  }
  return delta;
}

function matchStyles(node, delta) {
  let formats = {};
  let style = node.style || {};
  if (style.fontWeight && computeStyle(node).fontWeight === 'bold') {
    formats.bold = true;
  }
  if (Object.keys(formats).length > 0) {
    delta = delta.compose(new Delta().retain(delta.length(), formats));
  }
  if (parseFloat(style.textIndent || 0) > 0) {  // Could be 0.5in
    delta = new Delta().insert('\t').concat(delta);
  }
  return delta;
}

function matchText(node, delta) {
  let text = node.data;
  // Word represents empty line with <o:p> </o:p>
  if (node.parentNode.tagName === 'O:P') {
    return delta.insert(text.trim());
  }
  if (!computeStyle(node.parentNode).whiteSpace.startsWith('pre')) {
    function replacer(collapse, match) {
      match = match.replace(/[^\u00a0]/g, '');    // \u00a0 is nbsp;
      return match.length < 1 && collapse ? ' ' : match;
    }
    text = text.replace(/\r\n/g, ' ').replace(/\n/g, ' ');
    text = text.replace(/\s\s+/g, replacer.bind(replacer, true));  // collapse whitespace
    if ((node.previousSibling == null && isLine(node.parentNode)) ||
        (node.previousSibling != null && isLine(node.previousSibling))) {
      text = text.replace(/^\s+/, replacer.bind(replacer, false));
    }
    if ((node.nextSibling == null && isLine(node.parentNode)) ||
        (node.nextSibling != null && isLine(node.nextSibling))) {
      text = text.replace(/\s+$/, replacer.bind(replacer, false));
    }
  }
  return delta.insert(text);
}

export { Clipboard as default, matchAttributor, matchBlot, matchNewline, matchSpacing, matchText };
<|endoftext|>


<commit_before>import { module } from 'qunit';
import startApp from '../helpers/start-app';
import destroyApp from '../helpers/destroy-app';

export default function(name, options = {}) {
  module(name, {
    beforeEach() {
      this.application = startApp();

      if (options.beforeEach) {
        options.beforeEach.apply(this, arguments);
      }
    },

    afterEach() {
      destroyApp(this.application);

      if (options.afterEach) {
        options.afterEach.apply(this, arguments);
      }
    }
  });
}
<commit_msg>fix "Illegal use of apply method. Use the spread operator instead"<commit_after>import { module } from 'qunit';
import startApp from '../helpers/start-app';
import destroyApp from '../helpers/destroy-app';

export default function(name, options = {}) {
  module(name, {
    beforeEach() {
      this.application = startApp();

      if (options.beforeEach) {
        options.beforeEach.apply(...arguments);
      }
    },

    afterEach() {
      destroyApp(this.application);

      if (options.afterEach) {
        options.afterEach.apply(...arguments);
      }
    }
  });
}
<|endoftext|>


<commit_before>Backbone.Factory = (function() {
  var Factory, factories, sequences;

  function initialize() {
    factories = {};
    sequences = {};
  }

  Factory = function(options, callback) {
    var self = {},
      count = 0,
      builder = {};

    options = options || {};

    builder.sequence = function(callback) {
      return callback ? callback(count) : count;
    };

    self.options = function() {
      if (options.parent) {
        return _(factories[options.parent].options()).extend(options);
      } else {
        return _(options).clone();
      }
    };

    self.attributes = function(overrides) {
      var attributes;

      attributes = callback.call(builder);

      if (options.parent) {
        attributes = factories[options.parent].attributes(attributes);
      }

      return _(attributes).extend(overrides);
    };

    self.create = function(overrides) {
      var Model;

      count += 1;

      Model = self.options().model || Backbone.Model;

      return new Model(self.attributes(overrides));
    };

    return self;
  };

  Factory.define = function(name) {
    var callback,
      options = {};

    if (arguments.length == 2) {
      callback = arguments[1];
    } else {
      options = arguments[1];
      callback = arguments[2];
    }

    factories[name] = new Factory(options, callback);
  };

  Factory.create = function(name, overrides) {
    var factory = factories[name];

    return factory.create(overrides);
  };

  Factory.sequence = function(name, sequence) {
    sequences[name] = {
      count: 0,
      callback: sequence
    };
  };

  Factory.next = function(name) {
    var sequence = sequences[name];

    sequence.count += 1;

    return sequence.callback ? sequence.callback(sequence.count) : sequence.count;
  };

  Factory.reset = function() {
    initialize();
  };

  initialize();

  return Factory;
}());
<commit_msg>Moved sequence functionality into its own class<commit_after>Backbone.Factory = (function() {
  var Factory, Sequence, factories, sequences;

  function initialize() {
    factories = {};
    sequences = {};
  }

  Sequence = function(callback) {
    var self = {},
      count = 0;

    self.next = function() {
      count += 1;

      return callback ? callback(count) : count;
    };

    return self;
  };

  Factory = function(options, callback) {
    var self = {},
      count = 0,
      builder = {};

    options = options || {};

    builder.sequence = function(callback) {
      return callback ? callback(count) : count;
    };

    self.options = function() {
      if (options.parent) {
        return _(factories[options.parent].options()).extend(options);
      } else {
        return _(options).clone();
      }
    };

    self.attributes = function(overrides) {
      var attributes;

      attributes = callback.call(builder);

      if (options.parent) {
        attributes = factories[options.parent].attributes(attributes);
      }

      return _(attributes).extend(overrides);
    };

    self.create = function(overrides) {
      var Model;

      count += 1;

      Model = self.options().model || Backbone.Model;

      return new Model(self.attributes(overrides));
    };

    return self;
  };

  Factory.define = function(name) {
    var callback,
      options = {};

    if (arguments.length == 2) {
      callback = arguments[1];
    } else {
      options = arguments[1];
      callback = arguments[2];
    }

    factories[name] = new Factory(options, callback);
  };

  Factory.create = function(name, overrides) {
    var factory = factories[name];

    return factory.create(overrides);
  };

  Factory.sequence = function(name, callback) {
    sequences[name] = new Sequence(callback);
  };

  Factory.next = function(name) {
    var sequence = sequences[name];

    return sequence.next();
  };

  Factory.reset = function() {
    initialize();
  };

  initialize();

  return Factory;
}());
<|endoftext|>


<commit_before>const _ = require('lodash');
const moment = require('frozen-moment');
require('moment-timezone');

const bg = require('./common.js');
const spData = require('../common/sp-data.js');

module.exports = function(domain, entityId) {
    const latestCampaignData = {
        timestamp: 0,
        data: [],
    };

    const currencyCode = guessCurrencyCode();

    function formatId(id) {
        if (id.match(/^A/)) {
            throw new Error('Preformatted ID? : ' + id);
        }
        return 'A' + id;
    }

    function guessCurrencyCode() {
        // We assume (possibly wrongly) that we can predict the currency from
        // the domain.
        switch (domain) {
            case 'advertising.amazon.co.uk':
                return 'GBP';
            case 'advertising.amazon.ca':
                return 'CAD';
            case 'advertising.amazon.co.de':
            case 'advertising.amazon.co.fr':
                return 'EUR';
            case 'advertising.amazon.com':
            default:
                return 'USD';
        }
    }

    async function requestDataPaged(reqfn) {
        const pageSize = 100;
        let pageOffset = 0;
        let accum = [];

        let data = null;
        do {
            data = await reqfn(pageOffset, pageSize);
            accum = accum.concat(data.campaigns || data.keywords);
            pageOffset++;
        } while (pageOffset < data.summary.maxPageNumber);

        return accum;
    }

    async function probe() {
        const campaigns = await getLifetimeCampaignData();
        if (!campaigns.length)
            return true; // I guess?

        const campaign = campaigns.find(x => x.id.slice(0, 2) != 'AC');
        if (campaign) {
            await bg.ajax(`https://${domain}/cm/sp/campaigns/${campaign.id}`, {
                method: 'GET',
                queryData: { entityId },
            });
        }
        return true;
    }

    async function probeKeywordUpdate({ operation, keyword, dataValues }) {
        const result = await updateKeywords({ operation, dataValues, keywords: [keyword] });
        return result.ok.length == 1;
    }

    async function getDailyCampaignData(date) {
        const utcDay = moment(date).tz('UTC');
        const allData = await requestDataPaged((pageOffset, pageSize) => bg.ajax(`https://${domain}/cm/api/campaigns`, {
            method: 'POST',
            queryData: { entityId },
            jsonData: {
                pageOffset,
                pageSize,
                sort: { order: "DESC", field: "CAMPAIGN_NAME" },
                period: "CUSTOM",
                startDateUTC: utcDay.startOf('day').valueOf(),
                endDateUTC: utcDay.endOf('day').valueOf(),
                filters: [{ field: "CAMPAIGN_STATE", operator: "EXACT", values: ["ENABLED", "PAUSED"], not: false }],
                interval: "SUMMARY",
                programType: "SP",
                fields: ["CAMPAIGN_NAME", "CAMPAIGN_ELIGIBILITY_STATUS", "IMPRESSIONS", "CLICKS", "SPEND", "CTR", "CPC", "ORDERS", "SALES", "ACOS"],
                queries: []
            },
            responseType: 'json'
        }));

        allData.forEach(x => x.campaignId = x.id);

        if (date > latestCampaignData.timestamp) {
            latestCampaignData.timestamp = date;
            latestCampaignData.data = allData;
        }

        return allData;
    }

    async function getLifetimeCampaignData() {
        const allData = await requestDataPaged((pageOffset, pageSize) => bg.ajax(`https://${domain}/cm/api/campaigns`, {
            method: 'POST',
            queryData: { entityId },
            jsonData: {
                pageOffset,
                pageSize,
                sort: { order: "DESC", field: "CAMPAIGN_NAME" },
                period: "LIFETIME",
                startDateUTC: 1,
                endDateUTC: moment().valueOf(),
                filters: [{ field: "CAMPAIGN_STATE", operator: "EXACT", values: ["ENABLED", "PAUSED"], not: false }],
                interval: "SUMMARY",
                programType: "SP",
                fields: ["CAMPAIGN_NAME", "CAMPAIGN_ELIGIBILITY_STATUS", "IMPRESSIONS", "CLICKS", "SPEND", "CTR", "CPC", "ORDERS", "SALES", "ACOS"],
                queries: []
            },
            responseType: 'json'
        }));

        allData.forEach(x => x.campaignId = x.id);
        return allData;
    }

    function getCampaignStatus() {
        const obj = {};
        for (const item of latestCampaignData.data) {
            obj[item.campaignId] = { stateName: item.state, statusName: item.status };
        }
        return obj;
    }

    async function getAdGroupId(campaignId) {
        const page = await bg.ajax(`https://${domain}/cm/sp/campaigns/${formatId(campaignId)}`, {
            method: 'GET',
            queryData: { entityId },
        });

        const match = page.match(/adGroupId *: *"(\w+)"/i);
        if (!match)
            return null;
        return match[1];
    }

    async function getCampaignAsin(campaignId, adGroupId) {
        const response = await bg.ajax(`https://${domain}/cm/api/sp/adgroups/${formatId(adGroupId)}/ads`, {
            method: 'POST',
            responseType: 'json',
            queryData: { entityId },
            jsonData: {
                pageOffset: 0,
                pageSize: 10,
                sort: null,
                period: "LIFETIME",
                startDateUTC: 0,
                endDateUTC: moment().valueOf(),
                filters: [],
                programType: "SP",
                fields: ["AD_ASIN", "AD_SKU"]
            },
        });

        return _.get(response, 'ads[0].asin');
    }

    async function getKeywordData(campaignId, adGroupId) {
        const allData = await requestDataPaged((pageOffset, pageSize) => bg.ajax(`https://${domain}/cm/api/sp/campaigns/${formatId(campaignId)}/adgroups/${formatId(adGroupId)}/keywords`, {
            method: 'POST',
            responseType: 'json',
            queryData: { entityId },
            jsonData: {
                startDateUTC: 1,
                endDateUTC: moment().valueOf(),
                pageOffset,
                pageSize,
                sort: null,
                period: "LIFETIME",
                filters: [{ field: "KEYWORD_STATE", operator: "EXACT", values: ["ENABLED", "PAUSED"], not: false}],
                interval: "SUMMARY",
                programType: "SP",
                fields: ["KEYWORD_STATE", "KEYWORD", "KEYWORD_MATCH_TYPE", "KEYWORD_ELIGIBILITY_STATUS", "IMPRESSIONS", "CLICKS", "SPEND", "CTR", "CPC", "ORDERS", "SALES", "ACOS", "KEYWORD_BID"],
                queries: []
            },
        }));

        return allData;
    }

    async function updateKeywords({ keywords, operation, dataValues }) {
        const result = { ok: [], fail: [] };

        keywords = disaggregateKeywords(keywords);
        const keywordsByAdGroup = _.groupBy(keywords, 'adGroupId');
        await bg.parallelQueue(Object.keys(keywordsByAdGroup), async adGroupId => {
            const list = keywordsByAdGroup[adGroupId];
            try {
                const response = await bg.ajax(`https://${domain}/cm/api/sp/adgroups/${formatId(adGroupId)}/keywords`, {
                    method: 'PATCH',
                    queryData: { entityId },
                    jsonData: list.map(kw => {
                        const item = { id: formatId(kw.id), programType: "SP" };
                        if (operation == 'PAUSE')
                            item.state = 'PAUSED';
                        if (operation == 'ENABLE')
                            item.state = 'ENABLED';
                        if (operation == 'UPDATE')
                            item.bid = { millicents: dataValues.bid * 100000, currencyCode };
                        return item;
                    }),
                    responseType: 'json',
                });

                result.ok.push(...response.updatedKeywords.map(kw => spData.stripPrefix(kw.id)));
                result.fail.push(...response.failedKeywordIds.map(spData.stripPrefix));
            }
            catch (ex) {
                console.error(ex);
                result.fail.push(...list);
            }
        });

        return result;
    }

    async function addKeywords({ keywords, adGroupId, bid }) {
        const response = await bg.ajax(`https://${domain}/cm/api/sp/adgroups/${formatId(adGroupId)}/keyword`, {
            method: 'POST',
            queryData: { entityId },
            jsonData: { keywords: keywords.map(kw => ({ keyword: kw, matchType: "BROAD", bid: { millicents: bid * 100000, currencyCode } })) },
            responseType: 'json',
        });

        return {
            ok: response.succeededKeywords.map(kw => spData.stripPrefix(kw.id)),
            fail: response.failedKeywords.map(kw => spData.stripPrefix(kw.id)),
        };
    }

    return {
        name: 'cm',
        domain,
        entityId,
        probe,
        probeKeywordUpdate,
        getDailyCampaignData,
        getLifetimeCampaignData,
        getCampaignStatus,
        getAdGroupId,
        getCampaignAsin,
        getKeywordData,
        updateKeywords,
        addKeywords
    };
};
<commit_msg>remove old reference to disaggregate keywords<commit_after>const _ = require('lodash');
const moment = require('frozen-moment');
require('moment-timezone');

const bg = require('./common.js');
const spData = require('../common/sp-data.js');

module.exports = function(domain, entityId) {
    const latestCampaignData = {
        timestamp: 0,
        data: [],
    };

    const currencyCode = guessCurrencyCode();

    function formatId(id) {
        if (id.match(/^A/)) {
            throw new Error('Preformatted ID? : ' + id);
        }
        return 'A' + id;
    }

    function guessCurrencyCode() {
        // We assume (possibly wrongly) that we can predict the currency from
        // the domain.
        switch (domain) {
            case 'advertising.amazon.co.uk':
                return 'GBP';
            case 'advertising.amazon.ca':
                return 'CAD';
            case 'advertising.amazon.co.de':
            case 'advertising.amazon.co.fr':
                return 'EUR';
            case 'advertising.amazon.com':
            default:
                return 'USD';
        }
    }

    async function requestDataPaged(reqfn) {
        const pageSize = 100;
        let pageOffset = 0;
        let accum = [];

        let data = null;
        do {
            data = await reqfn(pageOffset, pageSize);
            accum = accum.concat(data.campaigns || data.keywords);
            pageOffset++;
        } while (pageOffset < data.summary.maxPageNumber);

        return accum;
    }

    async function probe() {
        const campaigns = await getLifetimeCampaignData();
        if (!campaigns.length)
            return true; // I guess?

        const campaign = campaigns.find(x => x.id.slice(0, 2) != 'AC');
        if (campaign) {
            await bg.ajax(`https://${domain}/cm/sp/campaigns/${campaign.id}`, {
                method: 'GET',
                queryData: { entityId },
            });
        }
        return true;
    }

    async function probeKeywordUpdate({ operation, keyword, dataValues }) {
        const result = await updateKeywords({ operation, dataValues, keywords: [keyword] });
        return result.ok.length == 1;
    }

    async function getDailyCampaignData(date) {
        const utcDay = moment(date).tz('UTC');
        const allData = await requestDataPaged((pageOffset, pageSize) => bg.ajax(`https://${domain}/cm/api/campaigns`, {
            method: 'POST',
            queryData: { entityId },
            jsonData: {
                pageOffset,
                pageSize,
                sort: { order: "DESC", field: "CAMPAIGN_NAME" },
                period: "CUSTOM",
                startDateUTC: utcDay.startOf('day').valueOf(),
                endDateUTC: utcDay.endOf('day').valueOf(),
                filters: [{ field: "CAMPAIGN_STATE", operator: "EXACT", values: ["ENABLED", "PAUSED"], not: false }],
                interval: "SUMMARY",
                programType: "SP",
                fields: ["CAMPAIGN_NAME", "CAMPAIGN_ELIGIBILITY_STATUS", "IMPRESSIONS", "CLICKS", "SPEND", "CTR", "CPC", "ORDERS", "SALES", "ACOS"],
                queries: []
            },
            responseType: 'json'
        }));

        allData.forEach(x => x.campaignId = x.id);

        if (date > latestCampaignData.timestamp) {
            latestCampaignData.timestamp = date;
            latestCampaignData.data = allData;
        }

        return allData;
    }

    async function getLifetimeCampaignData() {
        const allData = await requestDataPaged((pageOffset, pageSize) => bg.ajax(`https://${domain}/cm/api/campaigns`, {
            method: 'POST',
            queryData: { entityId },
            jsonData: {
                pageOffset,
                pageSize,
                sort: { order: "DESC", field: "CAMPAIGN_NAME" },
                period: "LIFETIME",
                startDateUTC: 1,
                endDateUTC: moment().valueOf(),
                filters: [{ field: "CAMPAIGN_STATE", operator: "EXACT", values: ["ENABLED", "PAUSED"], not: false }],
                interval: "SUMMARY",
                programType: "SP",
                fields: ["CAMPAIGN_NAME", "CAMPAIGN_ELIGIBILITY_STATUS", "IMPRESSIONS", "CLICKS", "SPEND", "CTR", "CPC", "ORDERS", "SALES", "ACOS"],
                queries: []
            },
            responseType: 'json'
        }));

        allData.forEach(x => x.campaignId = x.id);
        return allData;
    }

    function getCampaignStatus() {
        const obj = {};
        for (const item of latestCampaignData.data) {
            obj[item.campaignId] = { stateName: item.state, statusName: item.status };
        }
        return obj;
    }

    async function getAdGroupId(campaignId) {
        const page = await bg.ajax(`https://${domain}/cm/sp/campaigns/${formatId(campaignId)}`, {
            method: 'GET',
            queryData: { entityId },
        });

        const match = page.match(/adGroupId *: *"(\w+)"/i);
        if (!match)
            return null;
        return match[1];
    }

    async function getCampaignAsin(campaignId, adGroupId) {
        const response = await bg.ajax(`https://${domain}/cm/api/sp/adgroups/${formatId(adGroupId)}/ads`, {
            method: 'POST',
            responseType: 'json',
            queryData: { entityId },
            jsonData: {
                pageOffset: 0,
                pageSize: 10,
                sort: null,
                period: "LIFETIME",
                startDateUTC: 0,
                endDateUTC: moment().valueOf(),
                filters: [],
                programType: "SP",
                fields: ["AD_ASIN", "AD_SKU"]
            },
        });

        return _.get(response, 'ads[0].asin');
    }

    async function getKeywordData(campaignId, adGroupId) {
        const allData = await requestDataPaged((pageOffset, pageSize) => bg.ajax(`https://${domain}/cm/api/sp/campaigns/${formatId(campaignId)}/adgroups/${formatId(adGroupId)}/keywords`, {
            method: 'POST',
            responseType: 'json',
            queryData: { entityId },
            jsonData: {
                startDateUTC: 1,
                endDateUTC: moment().valueOf(),
                pageOffset,
                pageSize,
                sort: null,
                period: "LIFETIME",
                filters: [{ field: "KEYWORD_STATE", operator: "EXACT", values: ["ENABLED", "PAUSED"], not: false}],
                interval: "SUMMARY",
                programType: "SP",
                fields: ["KEYWORD_STATE", "KEYWORD", "KEYWORD_MATCH_TYPE", "KEYWORD_ELIGIBILITY_STATUS", "IMPRESSIONS", "CLICKS", "SPEND", "CTR", "CPC", "ORDERS", "SALES", "ACOS", "KEYWORD_BID"],
                queries: []
            },
        }));

        return allData;
    }

    async function updateKeywords({ keywords, operation, dataValues }) {
        const result = { ok: [], fail: [] };

        const keywordsByAdGroup = _.groupBy(keywords, 'adGroupId');
        await bg.parallelQueue(Object.keys(keywordsByAdGroup), async adGroupId => {
            const list = keywordsByAdGroup[adGroupId];
            try {
                const response = await bg.ajax(`https://${domain}/cm/api/sp/adgroups/${formatId(adGroupId)}/keywords`, {
                    method: 'PATCH',
                    queryData: { entityId },
                    jsonData: list.map(kw => {
                        const item = { id: formatId(kw.id), programType: "SP" };
                        if (operation == 'PAUSE')
                            item.state = 'PAUSED';
                        if (operation == 'ENABLE')
                            item.state = 'ENABLED';
                        if (operation == 'UPDATE')
                            item.bid = { millicents: dataValues.bid * 100000, currencyCode };
                        return item;
                    }),
                    responseType: 'json',
                });

                result.ok.push(...response.updatedKeywords.map(kw => spData.stripPrefix(kw.id)));
                result.fail.push(...response.failedKeywordIds.map(spData.stripPrefix));
            }
            catch (ex) {
                console.error(ex);
                result.fail.push(...list);
            }
        });

        return result;
    }

    async function addKeywords({ keywords, adGroupId, bid }) {
        const response = await bg.ajax(`https://${domain}/cm/api/sp/adgroups/${formatId(adGroupId)}/keyword`, {
            method: 'POST',
            queryData: { entityId },
            jsonData: { keywords: keywords.map(kw => ({ keyword: kw, matchType: "BROAD", bid: { millicents: bid * 100000, currencyCode } })) },
            responseType: 'json',
        });

        return {
            ok: response.succeededKeywords.map(kw => spData.stripPrefix(kw.id)),
            fail: response.failedKeywords.map(kw => spData.stripPrefix(kw.id)),
        };
    }

    return {
        name: 'cm',
        domain,
        entityId,
        probe,
        probeKeywordUpdate,
        getDailyCampaignData,
        getLifetimeCampaignData,
        getCampaignStatus,
        getAdGroupId,
        getCampaignAsin,
        getKeywordData,
        updateKeywords,
        addKeywords
    };
};
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import { View, Animated, PanResponder } from 'react-native';
import clamp from 'clamp';
import { connectStyle } from '@shoutem/theme';
import mapPropsToStyleNames from '../Utils/mapPropsToStyleNames';

const SWIPE_THRESHOLD = 120;

class DeckSwiper extends Component {
constructor(props) {
        super(props);
        this.state = {
            pan: new Animated.ValueXY(),
            pan2: new Animated.ValueXY(),
            enter: new Animated.Value(0.8),
            selectedItem : this.props.dataSource[0],
            selectedItem2 : this.props.dataSource[1],
            card1Top: true,
            card2Top: false,
            fadeAnim: new Animated.Value(0.8)
        }
    }

    getInitialStyle() {
        return {
            topCard: {
                position: 'absolute',
                top: 0,
                right: 0,
                left: 0
            }
        }
    }

    findNextIndexes() {
        let currentIndex = this.props.dataSource.indexOf(this.state.selectedItem);
        let newIdx = currentIndex + 1;
        let newIdx2 = currentIndex + 2;

        if(newIdx2 > this.props.dataSource.length - 1 && newIdx === this.props.dataSource.length - 1) {
            return [newIdx, 0];
        } else if (newIdx > this.props.dataSource.length - 1) {
            return [0, 1];
        } else {
            return [newIdx, newIdx2];
        }
    }

    selectNext() {
        let nextIndexes = this.findNextIndexes();
        setTimeout( () => {
            this.setState({
                selectedItem: this.props.dataSource[nextIndexes[0]]
            });
            setTimeout( () => {
                this.setState({
                    selectedItem2: this.props.dataSource[nextIndexes[1]]
                });
            }, 350);
        }, 50);
    }

    swipeRight() {
      if(this.props.onSwiping)
        this.props.onSwiping('right');
      setTimeout( () => {
        Animated.timing(
          this.state.fadeAnim,
          {toValue: 1}
        ).start();
        Animated.spring(
          this.state.enter,
          { toValue: 1, friction: 7 }
        ).start();
        this.selectNext();
        Animated.decay(this.state.pan, {
            velocity: {x: 8, y: 1},
            deceleration: 0.98
        }).start(this._resetState.bind(this))
      }, 300);
    }

    swipeLeft() {
      if(this.props.onSwiping)
        this.props.onSwiping('left');
      setTimeout( () => {
        Animated.timing(
          this.state.fadeAnim,
          {toValue: 1}
        ).start();
        Animated.spring(
          this.state.enter,
          { toValue: 1, friction: 7 }
        ).start();
        this.selectNext();
        Animated.decay(this.state.pan, {
            velocity: {x: -8, y: 1},
            deceleration: 0.98
        }).start(this._resetState.bind(this))
      }, 300);
    }

    componentWillMount() {
        this._panResponder = PanResponder.create({
            onMoveShouldSetResponderCapture: () => true,
            onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
                return Math.abs(gestureState.dx) > 5;
            },

            onPanResponderGrant: (e, gestureState) => {
                this.state.pan.setOffset({x: this.state.pan.x._value, y: this.state.pan.y._value});
                this.state.pan.setValue({x: 0, y: 0});
            },

            onPanResponderMove: (e, gestureState) => {
                if (gestureState.dx > 20){
                  if(this.props.onSwiping)
                    this.props.onSwiping('right',gestureState.dx);
                }
                else if (gestureState.dx < -20){
                  if(this.props.onSwiping)
                    this.props.onSwiping('left',gestureState.dx);
                }
                let val = Math.abs((gestureState.dx*.0013));
                let opa = Math.abs((gestureState.dx*.0022));
                if (val>0.2) {
                    val = 0.2;
                }
                Animated.timing(
                    this.state.fadeAnim,
                    {toValue: 0.8+val}
                ).start();
                Animated.spring(
                    this.state.enter,
                    { toValue: 0.8+val, friction: 7 }
                ).start();
                Animated.event([
                    null, {dx: this.state.pan.x},
                ])(e, gestureState)
            },

            onPanResponderRelease: (e, {vx, vy}) => {
                if(this.props.onSwiping)
                  this.props.onSwiping(null);
                var velocity;

                if (vx >= 0) {
                    velocity = clamp(vx, 4.5, 10);
                } else if (vx < 0) {
                    velocity = clamp(vx * -1, 4.5, 10) * -1;
                }

                if (Math.abs(this.state.pan.x._value) > SWIPE_THRESHOLD) {

                    if (velocity>0) {
                        (this.props.onSwipeRight) ? this.props.onSwipeRight() : undefined;
                        this.selectNext();
                    } else {
                        (this.props.onSwipeLeft) ? this.props.onSwipeLeft() : undefined;
                        this.selectNext();
                    }

                    Animated.decay(this.state.pan, {
                        velocity: {x: velocity, y: vy},
                        deceleration: 0.98
                    }).start(this._resetState.bind(this))
                } else {
                    Animated.spring(this.state.pan, {
                        toValue: {x: 0, y: 0},
                        friction: 4
                    }).start()
                }
            }
        })
    }

    _resetState() {
        this.state.pan.setValue({x: 0, y: 0});
        this.state.enter.setValue(0.8);
        this.state.fadeAnim.setValue(0.8);
        this.setState({
            card1Top: !this.state.card1Top,
            card2Top: !this.state.card2Top
        });
        if(this.props.onSwiping)
          this.props.onSwiping(null);

    }

    getCardStyles() {

        let { pan, pan2, enter } = this.state;

        let [translateX, translateY] = [pan.x, pan.y];
        // let [translateX, translateY] = [pan2.x, pan2.y];

        let rotate = pan.x.interpolate({inputRange: [-700, 0, 700], outputRange: ['-10deg', '0deg', '10deg']});

        let opacity = pan.x.interpolate({inputRange: [-320, 0, 320], outputRange: [0.9, 1, 0.9]})
        let scale = enter;

        let animatedCardStyles = {transform: [{translateX}, {translateY}, {rotate}], opacity};
        let animatedCardStyles2 = {transform: [{scale}]};

        return [animatedCardStyles, animatedCardStyles2]
    }

    render() {

        return(
            <View ref={c => this._root = c} style={{position: 'relative', flexDirection: 'column'}}>{(this.state.selectedItem)===undefined ? (<View />) :
                (<View>
                    <Animated.View style={[this.getCardStyles()[1],this.getInitialStyle().topCard,{opacity: this.state.fadeAnim}]} {...this._panResponder.panHandlers}>
                        {(this.props.renderBottom)  ?
                          this.props.renderBottom(this.state.selectedItem2)
                        :
                          this.props.renderItem(this.state.selectedItem2)
                        }
                    </Animated.View>
                    <Animated.View style={[ this.getCardStyles()[0], this.getInitialStyle().topCard] } {...this._panResponder.panHandlers} >
                        {(this.props.renderTop) ?
                          this.props.renderTop(this.state.selectedItem)
                        :
                          this.props.renderItem(this.state.selectedItem)
                        }
                    </Animated.View>
                    </View>
                )
            }
            </View>
        );
    }

}

DeckSwiper.propTypes = {
  ...View.propTypes,
  style: React.PropTypes.object,
  dataSource: React.PropTypes.array,
};

const StyledDeckSwiper = connectStyle('NativeBase.DeckSwiper', {}, mapPropsToStyleNames)(DeckSwiper);

export {
  StyledDeckSwiper as DeckSwiper,
};
<commit_msg>add renderEmpty, looping<commit_after>import React, { Component } from 'react';
import { View, Animated, PanResponder } from 'react-native';
import clamp from 'clamp';
import { connectStyle } from '@shoutem/theme';
import mapPropsToStyleNames from '../Utils/mapPropsToStyleNames';

const SWIPE_THRESHOLD = 120;

class DeckSwiper extends Component {
    constructor(props) {
        super(props);
        this.state = {
            pan: new Animated.ValueXY(),
            pan2: new Animated.ValueXY(),
            enter: new Animated.Value(0.8),
            selectedItem : this.props.dataSource[0],
            selectedItem2 : this.props.dataSource[1],
            card1Top: true,
            card2Top: false,
            fadeAnim: new Animated.Value(0.8),
            looping: this.props.looping || true,
            index: this.props.index || 0,
            disabled: this.props.dataSource.length === 0,
            lastCard: this.props.dataSource.length === 1
        }
    }

    getInitialStyle() {
        return {
            topCard: {
                position: 'absolute',
                top: 0,
                right: 0,
                left: 0
            }
        }
    }

    findNextIndexes(currentIndex) {
        let newIdx = currentIndex + 1;
        let newIdx2 = currentIndex + 2;

        if(newIdx2 > this.props.dataSource.length - 1 && newIdx === this.props.dataSource.length - 1) {
            return [newIdx, 0];
        } else if (newIdx > this.props.dataSource.length - 1) {
            return [0, 1];
        } else {
            return [newIdx, newIdx2];
        }
    }

    selectNext() {

        let currentIndex = this.state.index;
        this.setState({
            index: currentIndex + 1;
        })

        // if not looping, check for these conditionals and return from function
        if(!looping) {
            // reached end -> only display static renderEmpty() -> no swiping
            if(currentIndex === dataSource.length - 1) {
                return this.setState({
                  disabled: true
                });
            } else if(currentIndex === dataSource.length - 2) {
                // show last card with renderEmpty() component behind it
                return setTimeout(() => {
                    this.setState({
                        selectedItem: dataSource[index + 1]
                    });
                  setTimeout(() => {
                    this.setState({
                      lastCard: true
                    });
                  }, 350);
                }, 50);
            }
        }

        let nextIndexes = this.findNextIndexes(currentIndex);
        setTimeout( () => {
            this.setState({
                selectedItem: this.props.dataSource[nextIndexes[0]]
            });
            setTimeout( () => {
                this.setState({
                    selectedItem2: this.props.dataSource[nextIndexes[1]]
                });
            }, 350);
        }, 50);

    }

    swipeRight() {
      if(this.props.onSwiping)
        this.props.onSwiping('right');
      setTimeout( () => {
        Animated.timing(
          this.state.fadeAnim,
          {toValue: 1}
        ).start();
        Animated.spring(
          this.state.enter,
          { toValue: 1, friction: 7 }
        ).start();
        this.selectNext();
        Animated.decay(this.state.pan, {
            velocity: {x: 8, y: 1},
            deceleration: 0.98
        }).start(this._resetState.bind(this))
      }, 300);
    }

    swipeLeft() {
      if(this.props.onSwiping)
        this.props.onSwiping('left');
      setTimeout( () => {
        Animated.timing(
          this.state.fadeAnim,
          {toValue: 1}
        ).start();
        Animated.spring(
          this.state.enter,
          { toValue: 1, friction: 7 }
        ).start();
        this.selectNext();
        Animated.decay(this.state.pan, {
            velocity: {x: -8, y: 1},
            deceleration: 0.98
        }).start(this._resetState.bind(this))
      }, 300);
    }

    componentWillMount() {
        this._panResponder = PanResponder.create({
            onMoveShouldSetResponderCapture: () => true,
            onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
                return Math.abs(gestureState.dx) > 5;
            },

            onPanResponderGrant: (e, gestureState) => {
                this.state.pan.setOffset({x: this.state.pan.x._value, y: this.state.pan.y._value});
                this.state.pan.setValue({x: 0, y: 0});
            },

            onPanResponderMove: (e, gestureState) => {
                if (gestureState.dx > 20){
                  if(this.props.onSwiping)
                    this.props.onSwiping('right',gestureState.dx);
                }
                else if (gestureState.dx < -20){
                  if(this.props.onSwiping)
                    this.props.onSwiping('left',gestureState.dx);
                }
                let val = Math.abs((gestureState.dx*.0013));
                let opa = Math.abs((gestureState.dx*.0022));
                if (val>0.2) {
                    val = 0.2;
                }
                Animated.timing(
                    this.state.fadeAnim,
                    {toValue: 0.8+val}
                ).start();
                Animated.spring(
                    this.state.enter,
                    { toValue: 0.8+val, friction: 7 }
                ).start();
                Animated.event([
                    null, {dx: this.state.pan.x},
                ])(e, gestureState)
            },

            onPanResponderRelease: (e, {vx, vy}) => {
                if(this.props.onSwiping)
                  this.props.onSwiping(null);
                var velocity;

                if (vx >= 0) {
                    velocity = clamp(vx, 4.5, 10);
                } else if (vx < 0) {
                    velocity = clamp(vx * -1, 4.5, 10) * -1;
                }

                if (Math.abs(this.state.pan.x._value) > SWIPE_THRESHOLD) {

                    if (velocity>0) {
                        (this.props.onSwipeRight) ? this.props.onSwipeRight(this.selectedItem) : undefined;
                        this.selectNext();
                    } else {
                        (this.props.onSwipeLeft) ? this.props.onSwipeLeft(this.selectedItem) : undefined;
                        this.selectNext();
                    }

                    Animated.decay(this.state.pan, {
                        velocity: {x: velocity, y: vy},
                        deceleration: 0.98
                    }).start(this._resetState.bind(this))
                } else {
                    Animated.spring(this.state.pan, {
                        toValue: {x: 0, y: 0},
                        friction: 4
                    }).start()
                }
            }
        })
    }

    _resetState() {
        this.state.pan.setValue({x: 0, y: 0});
        this.state.enter.setValue(0.8);
        this.state.fadeAnim.setValue(0.8);
        this.setState({
            card1Top: !this.state.card1Top,
            card2Top: !this.state.card2Top
        });
        if(this.props.onSwiping)
          this.props.onSwiping(null);

    }

    getCardStyles() {

        let { pan, pan2, enter } = this.state;

        let [translateX, translateY] = [pan.x, pan.y];
        // let [translateX, translateY] = [pan2.x, pan2.y];

        let rotate = pan.x.interpolate({inputRange: [-700, 0, 700], outputRange: ['-10deg', '0deg', '10deg']});

        let opacity = pan.x.interpolate({inputRange: [-320, 0, 320], outputRange: [0.9, 1, 0.9]})
        let scale = enter;

        let animatedCardStyles = {transform: [{translateX}, {translateY}, {rotate}], opacity};
        let animatedCardStyles2 = {transform: [{scale}]};

        return [animatedCardStyles, animatedCardStyles2]
    }

    render() {

        if(this.state.disabled) {
            // disable swiping and renderEmpty
            return(
                <View ref={c => this._root = c} style={{position: 'relative', flexDirection: 'column'}}>
                    {
                        <View>
                        {
                          this.props.renderEmpty && this.props.renderEmpty()
                        }
                        </View>
                    }
                </View>
                );
        } else if(this.state.lastCard) {
            // display renderEmpty underneath last viable card
            return(
              <View ref={c => this._root = c} style={{position: 'relative', flexDirection: 'column'}}>{(this.state.selectedItem)===undefined ? (<View />) :
                    (
                                <View>
                                    <Animated.View style={[this.getCardStyles()[1], this.getInitialStyle().topCard, {opacity: this.state.fadeAnim}]} {...this._panResponder.panHandlers}>
                                      {
                                        this.props.renderEmpty && this.props.renderEmpty()
                                      }
                                    </Animated.View>
                                  <Animated.View style={[ this.getCardStyles()[0], this.getInitialStyle().topCard] } {...this._panResponder.panHandlers} >
                                      {
                                        this.props.renderItem(this.state.selectedItem)
                                      }
                                  </Animated.View>
                                </View>
                            )
                }
              </View>
            );
        } else {
            return(
                <View ref={c => this._root = c} style={{position: 'relative', flexDirection: 'column'}}>{(this.state.selectedItem)===undefined ? (<View />) :
                    (<View>
                        <Animated.View style={[this.getCardStyles()[1],this.getInitialStyle().topCard,{opacity: this.state.fadeAnim}]} {...this._panResponder.panHandlers}>
                            {(this.props.renderBottom)  ?
                              this.props.renderBottom(this.state.selectedItem2)
                            :
                              this.props.renderItem(this.state.selectedItem2)
                            }
                        </Animated.View>
                        <Animated.View style={[ this.getCardStyles()[0], this.getInitialStyle().topCard] } {...this._panResponder.panHandlers} >
                            {(this.props.renderTop) ?
                              this.props.renderTop(this.state.selectedItem)
                            :
                              this.props.renderItem(this.state.selectedItem)
                            }
                        </Animated.View>
                        </View>
                    )
                }
                </View>
            );
        }

    }

}

DeckSwiper.propTypes = {
  ...View.propTypes,
  style: React.PropTypes.object,
  dataSource: React.PropTypes.array,
};

const StyledDeckSwiper = connectStyle('NativeBase.DeckSwiper', {}, mapPropsToStyleNames)(DeckSwiper);

export {
  StyledDeckSwiper as DeckSwiper,
};
<|endoftext|>