<commit_before>var Events = require('../lib/Events.js');

import {equal} from '../lib/utils.js';

/**
 * Update a component.
 *
 * @param {Element} entity - Entity to modify.
 * @param {string} component - Name of the component.
 * @param {string} property - Property name.
 * @param {string|number} value - New value.
 */
export function updateEntity (entity, propertyName, value) {
  if (propertyName.indexOf('.') !== -1) {
    // Multi-prop
    var splitName = propertyName.split('.');
    if (value === null || value === undefined) {
      // Remove property.
      var parameters = entity.getAttribute(splitName[0]);
      delete parameters[splitName[1]];
      entity.setAttribute(splitName[0], parameters);
    } else {
      // Set property.
      entity.setAttribute(splitName[0], splitName[1], value);
    }
  } else {
    if (value === null || value === undefined) {
      // Remove property.
      entity.removeAttribute(propertyName);
    } else {
      // Set property.
      entity.setAttribute(propertyName, value);
    }
  }
  Events.emit('objectchanged', entity.object3D);
}

/**
 * Remove an entity
 * @param  {Element} entity Entity to remove.
 * @param  {boolean} force (Optional) If true it won't ask for confirmation
 */
export function removeEntity (entity, force) {
  if (entity) {
    if (force === true || confirm('Do you really want to remove entity `' + (entity.id || entity.tagName) + '`?')) {
      var closest = findClosestEntity(entity);
      AFRAME.INSPECTOR.removeObject(entity.object3D);
      entity.parentNode.removeChild(entity);
      AFRAME.INSPECTOR.selectEntity(closest);
    }
  }
}

function findClosestEntity (entity) {
  // First we try to find the after the entity
  var nextEntity = entity.nextElementSibling;
  while (nextEntity && (!nextEntity.isEntity || nextEntity.isInspector)) {
    nextEntity = nextEntity.nextElementSibling;
  }

  // Return if we found it
  if (nextEntity && nextEntity.isEntity && !nextEntity.isInspector) {
    return nextEntity;
  }
  // Otherwise try to find before the entity
  var prevEntity = entity.previousElementSibling;
  while (prevEntity && (!prevEntity.isEntity || prevEntity.isInspector)) {
    prevEntity = prevEntity.previousElementSibling;
  }

  // Return if we found it
  if (prevEntity && prevEntity.isEntity && !prevEntity.isInspector) {
    return prevEntity;
  }

  return null;
}

/**
 * Remove the selected entity
 * @param  {boolean} force (Optional) If true it won't ask for confirmation
 */
export function removeSelectedEntity (force) {
  if (AFRAME.INSPECTOR.selectedEntity) {
    removeEntity(AFRAME.INSPECTOR.selectedEntity, force);
  }
}

/**
 * Insert an node after a referenced node.
 * @param  {Element} newNode       Node to insert.
 * @param  {Element} referenceNode Node used as reference to insert after it.
*/
function insertAfter (newNode, referenceNode) {
  if (!referenceNode.parentNode) {
    referenceNode = AFRAME.INSPECTOR.selectedEntity;
  }

  if (!referenceNode) {
    AFRAME.INSPECTOR.sceneEl.appendChild(newNode);
  } else {
    referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  }
}

/**
 * Clone an entity, inserting it after the cloned one.
 * @param  {Element} entity Entity to clone
 */
export function cloneEntity (entity) {
  entity.flushToDOM();
  var copy = entity.cloneNode(true);
  copy.addEventListener('loaded', function (e) {
    AFRAME.INSPECTOR.selectEntity(copy);
  });

  // Get a valid unique ID for the entity
  if (entity.id) {
    copy.id = getUniqueId(entity.id);
  }
  copy.addEventListener('loaded', function () {
    Events.emit('dommodified');
    AFRAME.INSPECTOR.selectEntity(copy);
  });
  insertAfter(copy, entity);
}

/**
 * Clone the selected entity
 */
export function cloneSelectedEntity () {
  if (AFRAME.INSPECTOR.selectedEntity) {
    cloneEntity(AFRAME.INSPECTOR.selectedEntity);
  }
}

/**
 * Returns the clipboard representation to be used to copy to the clipboard
 * @param  {Element} entity Entity to copy to clipboard
 * @return {string}        Entity clipboard representation
 */
export function getClipboardRepresentation (entity) {
  var clone = prepareForSerialization(entity);
  clone.flushToDOM(true);
  return clone.outerHTML;
}

/**
 * Returns a copy of the DOM hierarchy prepared for serialization.
 * The process optimises component representation to avoid values coming from
 * primitive attributes, mixins and defaults.
 *
 * @param {Element} entity Root of the DOM hierarchy.
 * @return {Elment}        Copy of the DOM hierarchy ready for serialization.
 */
function prepareForSerialization(entity) {
  var clone = entity.cloneNode(false);
  var children = entity.childNodes;
  for (var i = 0, l = children.length; i < l; i++) {
    var child = children[i];
    if (child.nodeType !== Node.ELEMENT_NODE ||
        !child.hasAttribute('aframe-injected') &&
        !child.hasAttribute('data-aframe-inspector') &&
        !child.hasAttribute('data-aframe-canvas')) {
      clone.appendChild(prepareForSerialization(children[i]));
    }
  }
  optimizeComponents(clone, entity);
  return clone;
}

/**
 * Removes from copy those components or components' properties that comes from
 * primitive attributes, mixins, injected default components or schema defaults.
 *
 * @param {Element} copy   Destinatary element for the optimization.
 * @param {Element} source Element to be optimized.
 */
function optimizeComponents(copy, source) {
  var components = source.components || {};
  Object.keys(components).forEach(function (name) {
    var component = components[name];
    var result = getImplicitValue(component, source);
    var isInherited = result[1];
    var implicitValue = result[0];
    var currentValue = source.getAttribute(name);
    var optimalUpdate = getOptimalUpdate(component, implicitValue, currentValue);
    var doesNotNeedUpdate = optimalUpdate === null;
    if (isInherited && doesNotNeedUpdate) {
      copy.removeAttribute(name);
    }
    else {
      copy.setAttribute(name, optimalUpdate, true);
    }
  });
}

/**
 * Computes the value for a component coming from primitive attributes,
 * mixins, primitive defaults, a-frame default components and schema defaults.
 * In this specific order.
 *
 * In other words, it is the value of the component if the author would have not
 * overridden it explicitly.
 *
 * @param {Component} component Component to calculate the value of.
 * @param {Element}   source    Element owning the component.
 * @return                      A pair with the computed value for the component of source and a flag indicating if the component is completely inherited from other sources (`true`) or genuinely owned by the source entity (`false`).
 */
function getImplicitValue(component, source) {
  var isInherited = false;
  var value = (isSingleProperty(component) ? _single : _multi)();
  return [value, isInherited];

  function _single() {
    var value = getMixedValue(component, null, source);
    if (value === undefined) {
      value = getInjectedValue(component, null, source);
    }
    if (value !== undefined) {
      isInherited = true;
    }
    else {
      value = getDefaultValue(component, null, source);
    }
    if (value !== undefined) {
      // XXX: This assumes parse is idempotent
      return component.schema.parse(value);
    }
    return value;
  }

  function _multi() {
    var value;

    Object.keys(component.schema).forEach(function (propertyName) {
      var propertyValue = getFromAttribute(component, propertyName, source);
      if (propertyValue === undefined) {
        propertyValue = getMixedValue(component, propertyName, source);
      }
      if (propertyValue === undefined) {
        propertyValue = getInjectedValue(component, propertyName, source);
      }
      if (propertyValue !== undefined) {
        isInherited = isInherited || true;
      }
      else {
        propertyValue = getDefaultValue(component, propertyName, source);
      }
      if (propertyValue !== undefined) {
        var parse = component.schema[propertyName].parse;
        value = value || {};
        // XXX: This assumes parse is idempotent
        value[propertyName] = parse(propertyValue);
      }
    });

    return value;
  }
}

/**
 * Gets the value for the component's property coming from a primitive
 * attribute.
 *
 * Primitives have mappings from attributes to component's properties.
 * The function looks for a present attribute in the source element which
 * maps to the specified component's property.
 *
 * @param  {Component} component    Component to be found.
 * @param  {string}    propertyName Component's property to be found.
 * @param  {Element}   source       Element owning the component.
 * @return {any}                    The value of the component's property coming
 *                                  from the primitive's attribute if any or
 *                                  `undefined`, otherwise.
 */
function getFromAttribute(component, propertyName, source) {
  var value;
  var mappings = source.mappings || {};
  var route = component.name + '.' + propertyName;
  var primitiveAttribute = findAttribute(mappings, route);
  if (primitiveAttribute && source.hasAttribute(primitiveAttribute)) {
    value = source.getAttribute(primitiveAttribute);
  }
  return value;

  function findAttribute(mappings, route) {
    var attributes = Object.keys(mappings);
    for (var i = 0, l = attributes.length; i < l; i++) {
      var attribute = attributes[i];
      if (mappings[attribute] === route) {
        return attribute;
      }
    }
    return undefined;
  }
}

/**
 * Gets the value for a component or component's property coming from mixins of
 * an element.
 *
 * If the component or component's property is not provided by mixins, the
 * functions will return `undefined`.
 *
 * @param {Component} component      Component to be found.
 * @param {string}    [propertyName] If provided, component's property to be
 *                                   found.
 * @param {Element}   source         Element owning the component.
 * @return                           The value of the component or components'
 *                                   property coming from mixins of the source.
 */
function getMixedValue(component, propertyName, source) {
  var value;
  var reversedMixins = source.mixinEls.reverse();
  for (var i = 0; value === undefined && i < reversedMixins.length; i++) {
    var mixin = reversedMixins[i];
    if (mixin.attributes.hasOwnProperty(component.name)) {
      if (!propertyName) {
        value = mixin.getAttribute(component.name);
      }
      else {
        value = mixin.getAttribute(component.name)[propertyName];
      }
    }
  }
  return value;
}

/**
 * Gets the value for a component or component's property coming from primitive
 * defaults or a-frame defaults. In this specific order.
 *
 * @param {Component} component      Component to be found.
 * @param {string}    [propertyName] If provided, component's property to be
 *                                   found.
 * @param {Element}   source         Element owning the component.
 * @return                           The component value coming from the
 *                                   injected default components of source.
 */
function getInjectedValue(component, propertyName, source) {
  var value;
  var primitiveDefaults = source.defaultComponentsFromPrimitive || {};
  var aFrameDefaults = source.defaultComponents || {};
  var defaultSources = [primitiveDefaults, aFrameDefaults];
  for (var i = 0; value === undefined && i < defaultSources.length; i++) {
    var defaults = defaultSources[i];
    if (defaults.hasOwnProperty(component.name)) {
      if (!propertyName) {
        value = defaults[component.name];
      }
      else {
        value = defaults[component.name][propertyName];
      }
    }
  }
  return value;
}

/**
 * Gets the value for a component or component's property coming from schema
 * defaults.
 *
 * @param {Component} component      Component to be found.
 * @param {string}    [propertyName] If provided, component's property to be
 *                                   found.
 * @param {Element}   source         Element owning the component.
 * @return                           The component value coming from the schema
 *                                   default.
 */
function getDefaultValue(component, propertyName, source) {
  if (!propertyName) {
    return component.schema.default;
  }
  return component.schema[propertyName].default;
}

/**
 * Returns the minimum value for a component with an implicit value to equal a
 * reference value. A `null` optimal value means that there is no need for an
 * update since the implicit value and the reference are equal.
 *
 * @param {Component} component Component of the computed value.
 * @param {any}       implicit  The implicit value of the component.
 * @param {any}       reference The reference value for the component.
 * @return                      the minimum value making the component to equal
 *                              the reference value.
 */
function getOptimalUpdate(component, implicit, reference) {
  if (equal(implicit, reference)) {
    return null;
  }
  if (isSingleProperty(component)) {
    return reference;
  }
  var optimal = {};
  Object.keys(reference).forEach(function (key) {
    var needsUpdate = !equal(reference[key], implicit[key]);
    if (needsUpdate) {
      optimal[key] = reference[key];
    }
  });
  return optimal;
}

/**
 * @param {Component} component Component to test if it is single property.
 * @return                      `true` if component is single property.
 */
function isSingleProperty(component) {
  return AFRAME.schema.isSingleProperty(component.schema);
}

/**
 * Detect element's Id collision and returns a valid one
 * @param  {string} baseId Proposed Id
 * @return {string}        Valid Id based on the proposed Id
 */
function getUniqueId (baseId) {
  if (!document.getElementById(baseId)) {
    return baseId;
  }

  var i = 2;
  // If the baseId ends with _#, it extracts the baseId removing the suffix
  var groups = baseId.match(/(\w+)-(\d+)/);
  if (groups) {
    baseId = groups[1];
    i = groups[2];
  }

  while (document.getElementById(baseId + '-' + i)) { i++; }

  return baseId + '-' + i;
}
<commit_msg>Avoiding setAttribute() side effects.<commit_after>var Events = require('../lib/Events.js');

import {equal} from '../lib/utils.js';

/**
 * Update a component.
 *
 * @param {Element} entity - Entity to modify.
 * @param {string} component - Name of the component.
 * @param {string} property - Property name.
 * @param {string|number} value - New value.
 */
export function updateEntity (entity, propertyName, value) {
  if (propertyName.indexOf('.') !== -1) {
    // Multi-prop
    var splitName = propertyName.split('.');
    if (value === null || value === undefined) {
      // Remove property.
      var parameters = entity.getAttribute(splitName[0]);
      delete parameters[splitName[1]];
      entity.setAttribute(splitName[0], parameters);
    } else {
      // Set property.
      entity.setAttribute(splitName[0], splitName[1], value);
    }
  } else {
    if (value === null || value === undefined) {
      // Remove property.
      entity.removeAttribute(propertyName);
    } else {
      // Set property.
      entity.setAttribute(propertyName, value);
    }
  }
  Events.emit('objectchanged', entity.object3D);
}

/**
 * Remove an entity
 * @param  {Element} entity Entity to remove.
 * @param  {boolean} force (Optional) If true it won't ask for confirmation
 */
export function removeEntity (entity, force) {
  if (entity) {
    if (force === true || confirm('Do you really want to remove entity `' + (entity.id || entity.tagName) + '`?')) {
      var closest = findClosestEntity(entity);
      AFRAME.INSPECTOR.removeObject(entity.object3D);
      entity.parentNode.removeChild(entity);
      AFRAME.INSPECTOR.selectEntity(closest);
    }
  }
}

function findClosestEntity (entity) {
  // First we try to find the after the entity
  var nextEntity = entity.nextElementSibling;
  while (nextEntity && (!nextEntity.isEntity || nextEntity.isInspector)) {
    nextEntity = nextEntity.nextElementSibling;
  }

  // Return if we found it
  if (nextEntity && nextEntity.isEntity && !nextEntity.isInspector) {
    return nextEntity;
  }
  // Otherwise try to find before the entity
  var prevEntity = entity.previousElementSibling;
  while (prevEntity && (!prevEntity.isEntity || prevEntity.isInspector)) {
    prevEntity = prevEntity.previousElementSibling;
  }

  // Return if we found it
  if (prevEntity && prevEntity.isEntity && !prevEntity.isInspector) {
    return prevEntity;
  }

  return null;
}

/**
 * Remove the selected entity
 * @param  {boolean} force (Optional) If true it won't ask for confirmation
 */
export function removeSelectedEntity (force) {
  if (AFRAME.INSPECTOR.selectedEntity) {
    removeEntity(AFRAME.INSPECTOR.selectedEntity, force);
  }
}

/**
 * Insert an node after a referenced node.
 * @param  {Element} newNode       Node to insert.
 * @param  {Element} referenceNode Node used as reference to insert after it.
*/
function insertAfter (newNode, referenceNode) {
  if (!referenceNode.parentNode) {
    referenceNode = AFRAME.INSPECTOR.selectedEntity;
  }

  if (!referenceNode) {
    AFRAME.INSPECTOR.sceneEl.appendChild(newNode);
  } else {
    referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  }
}

/**
 * Clone an entity, inserting it after the cloned one.
 * @param  {Element} entity Entity to clone
 */
export function cloneEntity (entity) {
  entity.flushToDOM();
  var copy = entity.cloneNode(true);
  copy.addEventListener('loaded', function (e) {
    AFRAME.INSPECTOR.selectEntity(copy);
  });

  // Get a valid unique ID for the entity
  if (entity.id) {
    copy.id = getUniqueId(entity.id);
  }
  copy.addEventListener('loaded', function () {
    Events.emit('dommodified');
    AFRAME.INSPECTOR.selectEntity(copy);
  });
  insertAfter(copy, entity);
}

/**
 * Clone the selected entity
 */
export function cloneSelectedEntity () {
  if (AFRAME.INSPECTOR.selectedEntity) {
    cloneEntity(AFRAME.INSPECTOR.selectedEntity);
  }
}

/**
 * Returns the clipboard representation to be used to copy to the clipboard
 * @param  {Element} entity Entity to copy to clipboard
 * @return {string}        Entity clipboard representation
 */
export function getClipboardRepresentation (entity) {
  var clone = prepareForSerialization(entity);
  return clone.outerHTML;
}

/**
 * Returns a copy of the DOM hierarchy prepared for serialization.
 * The process optimises component representation to avoid values coming from
 * primitive attributes, mixins and defaults.
 *
 * @param {Element} entity Root of the DOM hierarchy.
 * @return {Elment}        Copy of the DOM hierarchy ready for serialization.
 */
function prepareForSerialization(entity) {
  var clone = entity.cloneNode(false);
  var children = entity.childNodes;
  for (var i = 0, l = children.length; i < l; i++) {
    var child = children[i];
    if (child.nodeType !== Node.ELEMENT_NODE ||
        !child.hasAttribute('aframe-injected') &&
        !child.hasAttribute('data-aframe-inspector') &&
        !child.hasAttribute('data-aframe-canvas')) {
      clone.appendChild(prepareForSerialization(children[i]));
    }
  }
  optimizeComponents(clone, entity);
  return clone;
}

/**
 * Removes from copy those components or components' properties that comes from
 * primitive attributes, mixins, injected default components or schema defaults.
 *
 * @param {Element} copy   Destinatary element for the optimization.
 * @param {Element} source Element to be optimized.
 */
function optimizeComponents(copy, source) {
  var removeAttribute = HTMLElement.prototype.removeAttribute;
  var setAttribute = HTMLElement.prototype.setAttribute;
  var components = source.components || {};
  Object.keys(components).forEach(function (name) {
    var component = components[name];
    var result = getImplicitValue(component, source);
    var isInherited = result[1];
    var implicitValue = result[0];
    var currentValue = source.getAttribute(name);
    var optimalUpdate = getOptimalUpdate(component, implicitValue, currentValue);
    var doesNotNeedUpdate = optimalUpdate === null;
    if (isInherited && doesNotNeedUpdate) {
      removeAttribute.call(copy, name);
    }
    else {
      var schema = component.schema;
      var value = stringifyComponentValue(schema, optimalUpdate);
      setAttribute.call(copy, name, value);
    }
  });
}

/**
 * @param  {Schema} schema The component schema.
 * @param  {any}    data   The component value.
 * @return {string}        The string representation of data according to the
 *                         passed component's schema.
 */
function stringifyComponentValue(schema, data) {
  data = typeof data === 'undefined' ? {} : data;
  if (data === null) {
    return '';
  }
  return (isSingleProperty(schema) ? _single : _multi)();

  function _single() {
    return schema.stringify(data);
  }

  function _multi() {
    var propertyBag = {};
    Object.keys(data).forEach(function (name) {
      propertyBag[name] = schema[name].stringify(data[name]);
    });
    return AFRAME.utils.styleParser.stringify(propertyBag);
  }
}

/**
 * Computes the value for a component coming from primitive attributes,
 * mixins, primitive defaults, a-frame default components and schema defaults.
 * In this specific order.
 *
 * In other words, it is the value of the component if the author would have not
 * overridden it explicitly.
 *
 * @param {Component} component Component to calculate the value of.
 * @param {Element}   source    Element owning the component.
 * @return                      A pair with the computed value for the component of source and a flag indicating if the component is completely inherited from other sources (`true`) or genuinely owned by the source entity (`false`).
 */
function getImplicitValue(component, source) {
  var isInherited = false;
  var value = (isSingleProperty(component.schema) ? _single : _multi)();
  return [value, isInherited];

  function _single() {
    var value = getMixedValue(component, null, source);
    if (value === undefined) {
      value = getInjectedValue(component, null, source);
    }
    if (value !== undefined) {
      isInherited = true;
    }
    else {
      value = getDefaultValue(component, null, source);
    }
    if (value !== undefined) {
      // XXX: This assumes parse is idempotent
      return component.schema.parse(value);
    }
    return value;
  }

  function _multi() {
    var value;

    Object.keys(component.schema).forEach(function (propertyName) {
      var propertyValue = getFromAttribute(component, propertyName, source);
      if (propertyValue === undefined) {
        propertyValue = getMixedValue(component, propertyName, source);
      }
      if (propertyValue === undefined) {
        propertyValue = getInjectedValue(component, propertyName, source);
      }
      if (propertyValue !== undefined) {
        isInherited = isInherited || true;
      }
      else {
        propertyValue = getDefaultValue(component, propertyName, source);
      }
      if (propertyValue !== undefined) {
        var parse = component.schema[propertyName].parse;
        value = value || {};
        // XXX: This assumes parse is idempotent
        value[propertyName] = parse(propertyValue);
      }
    });

    return value;
  }
}

/**
 * Gets the value for the component's property coming from a primitive
 * attribute.
 *
 * Primitives have mappings from attributes to component's properties.
 * The function looks for a present attribute in the source element which
 * maps to the specified component's property.
 *
 * @param  {Component} component    Component to be found.
 * @param  {string}    propertyName Component's property to be found.
 * @param  {Element}   source       Element owning the component.
 * @return {any}                    The value of the component's property coming
 *                                  from the primitive's attribute if any or
 *                                  `undefined`, otherwise.
 */
function getFromAttribute(component, propertyName, source) {
  var value;
  var mappings = source.mappings || {};
  var route = component.name + '.' + propertyName;
  var primitiveAttribute = findAttribute(mappings, route);
  if (primitiveAttribute && source.hasAttribute(primitiveAttribute)) {
    value = source.getAttribute(primitiveAttribute);
  }
  return value;

  function findAttribute(mappings, route) {
    var attributes = Object.keys(mappings);
    for (var i = 0, l = attributes.length; i < l; i++) {
      var attribute = attributes[i];
      if (mappings[attribute] === route) {
        return attribute;
      }
    }
    return undefined;
  }
}

/**
 * Gets the value for a component or component's property coming from mixins of
 * an element.
 *
 * If the component or component's property is not provided by mixins, the
 * functions will return `undefined`.
 *
 * @param {Component} component      Component to be found.
 * @param {string}    [propertyName] If provided, component's property to be
 *                                   found.
 * @param {Element}   source         Element owning the component.
 * @return                           The value of the component or components'
 *                                   property coming from mixins of the source.
 */
function getMixedValue(component, propertyName, source) {
  var value;
  var reversedMixins = source.mixinEls.reverse();
  for (var i = 0; value === undefined && i < reversedMixins.length; i++) {
    var mixin = reversedMixins[i];
    if (mixin.attributes.hasOwnProperty(component.name)) {
      if (!propertyName) {
        value = mixin.getAttribute(component.name);
      }
      else {
        value = mixin.getAttribute(component.name)[propertyName];
      }
    }
  }
  return value;
}

/**
 * Gets the value for a component or component's property coming from primitive
 * defaults or a-frame defaults. In this specific order.
 *
 * @param {Component} component      Component to be found.
 * @param {string}    [propertyName] If provided, component's property to be
 *                                   found.
 * @param {Element}   source         Element owning the component.
 * @return                           The component value coming from the
 *                                   injected default components of source.
 */
function getInjectedValue(component, propertyName, source) {
  var value;
  var primitiveDefaults = source.defaultComponentsFromPrimitive || {};
  var aFrameDefaults = source.defaultComponents || {};
  var defaultSources = [primitiveDefaults, aFrameDefaults];
  for (var i = 0; value === undefined && i < defaultSources.length; i++) {
    var defaults = defaultSources[i];
    if (defaults.hasOwnProperty(component.name)) {
      if (!propertyName) {
        value = defaults[component.name];
      }
      else {
        value = defaults[component.name][propertyName];
      }
    }
  }
  return value;
}

/**
 * Gets the value for a component or component's property coming from schema
 * defaults.
 *
 * @param {Component} component      Component to be found.
 * @param {string}    [propertyName] If provided, component's property to be
 *                                   found.
 * @param {Element}   source         Element owning the component.
 * @return                           The component value coming from the schema
 *                                   default.
 */
function getDefaultValue(component, propertyName, source) {
  if (!propertyName) {
    return component.schema.default;
  }
  return component.schema[propertyName].default;
}

/**
 * Returns the minimum value for a component with an implicit value to equal a
 * reference value. A `null` optimal value means that there is no need for an
 * update since the implicit value and the reference are equal.
 *
 * @param {Component} component Component of the computed value.
 * @param {any}       implicit  The implicit value of the component.
 * @param {any}       reference The reference value for the component.
 * @return                      the minimum value making the component to equal
 *                              the reference value.
 */
function getOptimalUpdate(component, implicit, reference) {
  if (equal(implicit, reference)) {
    return null;
  }
  if (isSingleProperty(component.schema)) {
    return reference;
  }
  var optimal = {};
  Object.keys(reference).forEach(function (key) {
    var needsUpdate = !equal(reference[key], implicit[key]);
    if (needsUpdate) {
      optimal[key] = reference[key];
    }
  });
  return optimal;
}

/**
 * @param {Schema} schema Component's schema to test if it is single property.
 * @return                `true` if component is single property.
 */
function isSingleProperty(schema) {
  return AFRAME.schema.isSingleProperty(schema);
}

/**
 * Detect element's Id collision and returns a valid one
 * @param  {string} baseId Proposed Id
 * @return {string}        Valid Id based on the proposed Id
 */
function getUniqueId (baseId) {
  if (!document.getElementById(baseId)) {
    return baseId;
  }

  var i = 2;
  // If the baseId ends with _#, it extracts the baseId removing the suffix
  var groups = baseId.match(/(\w+)-(\d+)/);
  if (groups) {
    baseId = groups[1];
    i = groups[2];
  }

  while (document.getElementById(baseId + '-' + i)) { i++; }

  return baseId + '-' + i;
}
<|endoftext|>


<commit_before>function sampleAction() {
  console.log(arguments);
  // {'0':
  //   { projects: { ios: null, android: null },
  //     actions: { update: [Object], sample: [Object] } },
  //  '1': { folderName: 'd', rnVersion: undefined },
  //  '2': { force: undefined } }
}

module.exports = {
  description: 'This is sample demonstration with what is available',
  options: [{
    name: '-f --force',
    description: 'Force linking of all dependencies'
  }],
  args: [{
    name: 'folder',
    required: true,
  }, {
    name: 'reactNativeVersion',
    property: 'rnVersion'
  }],
  run: sampleAction
};
<commit_msg>Add in some trailing commas, cc: Kureev<commit_after>function sampleAction() {
  console.log(arguments);
  // {'0':
  //   { projects: { ios: null, android: null },
  //     actions: { update: [Object], sample: [Object] } },
  //  '1': { folderName: 'd', rnVersion: undefined },
  //  '2': { force: undefined } }
}

module.exports = {
  description: 'This is sample demonstration with what is available',
  options: [{
    name: '-f --force',
    description: 'Force linking of all dependencies',
  }],
  args: [{
    name: 'folder',
    required: true,
  }, {
    name: 'reactNativeVersion',
    property: 'rnVersion',
  }],
  run: sampleAction,
};
<|endoftext|>


<commit_before>module( 'plugins.rowspacing' );

/*trace 1029*/
test( '设置段前距查看状态反射', function () {
        var editor = te.obj[0];
        var range = te.obj[1];
        editor.setContent( '
hello1

hello2
' );
        range.setStart( editor.body.firstChild, 0 ).setEnd( editor.body.lastChild, 1 ).select();
        editor.execCommand( 'rowspacing', 15 ,'top');
//stop()
//    range.selectNode( editor.body ).select();
        equal( editor.queryCommandValue( 'rowspacing' ,'top'), 15, '查看段前距' );
//        /*光标闭合时查看状态反射*/
        range.setStart( editor.body.firstChild.firstChild, 1 ).collapse( true ).select();
        equal( editor.queryCommandValue( 'rowspacing','top' ), 15, '查看段前距' );
} );

/*trace 1035*/
test( '非闭合清除段前距等样式，查看状态反射', function () {
        var editor = te.obj[0];
        var range = te.obj[1];
        editor.setContent( '
hello1

hello2
' );
        range.setStart( editor.body.firstChild, 0 ).setEnd( editor.body.lastChild, 1 ).select();
        editor.execCommand( 'rowspacing', 20,'top' );
        equal( editor.queryCommandValue( 'rowspacing' ,'top'), 20, '段前距为2.0' );

} );

test( '闭合清除段前距等样式，查看状态反射', function () {
        var editor = te.obj[0];
        var range = te.obj[1];
        editor.setContent( '
hello1

hello2
' );
        range.setStart( editor.body.firstChild, 0 ).collapse( true ).select();
        editor.execCommand( 'rowspacing', 20 ,'top');
        equal( editor.queryCommandValue( 'rowspacing','top' ), 20, '段前距为2.0' );
        range.setStart( editor.body.firstChild, 0 ).collapse( true ).select();
        editor.execCommand( 'removeformat' );
        //1.2后改
        equal( editor.queryCommandValue( 'rowspacing' ,'top'), 5, '闭合清除格式后，段前距为5' );
} );

/*trace 1026*/
test( '设置段后距后设置字体颜色', function () {
        var editor = te.obj[0];
        var range = te.obj[1];
        editor.setContent( '
hello1hellohello2
' );
        range.setStart( editor.body.firstChild, 0 ).setEnd( editor.body.lastChild, 1 ).select();
        editor.execCommand( 'rowspacing', 15 ,'bottom');
        editor.execCommand( 'forecolor', 'rgb(255,0,0)' );
        equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 15, '查看段后距' );
        /*闭合的方式去查看行距的状态反射*/
        range.setStart( editor.body.firstChild.firstChild, 1 ).collapse( true ).select();
        equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 15, '查看段后距' );
} );

test( '设置段后距后设置加粗等多种样式', function () {
        var editor = te.obj[0];
        var range = te.obj[1];
        editor.setContent( '
hello1

hello2
' );
        range.setStart( editor.body.firstChild, 0 ).setEnd( editor.body.lastChild, 1 ).select();
        editor.execCommand( 'rowspacing', 15 ,'bottom');
        editor.execCommand( 'bold' );
        editor.execCommand( 'underline' );
        equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 15, '查看段后距' );
} );

test( '非闭合去除加粗等样式', function () {
        var editor = te.obj[0];
        var range = te.obj[1];
        editor.setContent( '
hello1

hello2
' );
        range.setStart( editor.body.firstChild, 0 ).setEnd( editor.body.lastChild, 1 ).select();
        editor.execCommand( 'rowspacing', 15 ,'bottom');
        editor.execCommand( 'bold' );
        editor.execCommand( 'underline' );
        equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 15, '查看段后距' );
        editor.execCommand( 'removeformat' );
        //1.2后改
        equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 5, '去除样式后查看段后距' );
} );

test( '闭合去除样式', function () {
        var editor = te.obj[0];
        var range = te.obj[1];
        editor.setContent( '
hello1

hello2
' );
        range.setStart( editor.body.firstChild, 0 ).setEnd( editor.body.lastChild, 1 ).select();
        editor.execCommand( 'rowspacing', 15 ,'bottom');
        editor.execCommand( 'bold' );
        editor.execCommand( 'underline' );
        /*采用闭合的方式查询段后距，
         介个好像用手选不太能选的出来，总是会选到里面去*/
        range.setStart( editor.body.firstChild, 0 ).collapse( true ).select();
        equal( editor.queryCommandValue( 'rowspacing','bottom' ), 15, '查看段后距' );
        /*闭合方式鼠标放在第二个p中*/
        range.setStart( editor.body.lastChild, 0 ).collapse( true ).select();
        equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 15, '查看段后距' );
        editor.execCommand( 'removeformat' );
        //1.2后改
        equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 5, '去除样式后查看段后距' );
        /*第一行的样式应当仍然在*/
        range.setStart( editor.body.firstChild, 0 ).collapse( true ).select();
        equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 15, '查看段后距' );
} );

test( '表格中设置段距', function () {
        var editor = te.obj[0];
        var range = te.obj[1];
        editor.setContent( '

hello1	hello2

' );
        var tds = editor.body.firstChild.getElementsByTagName( 'td' );
        /*选中表格中的文本设置段前距*/
        range.selectNode( tds[0].firstChild ).select();
        editor.execCommand( 'rowspacing', 20 ,'top');
        equal( editor.queryCommandValue( 'rowspacing','top' ), 20, '设置表格中文本段前距为2' );
        /*采用闭合的方式查询段前距*/
        range.setStart( tds[0].firstChild.firstChild, 1 ).collapse( true ).select();
        equal( editor.queryCommandValue( 'rowspacing','top' ), 20, '设置表格中文本段前距为2' );

        /*闭合在表格中的文本设置段后距*/
        range.setStart( tds[1].firstChild, 1 ).collapse( true ).select();
        editor.execCommand( 'rowspacing', 15 ,'bottom');
        /*选中整个单元格查询段后距*/
        range.selectNode( tds[1] ).select();
        equal( editor.queryCommandValue( 'rowspacing','bottom'), 15, '设置表格中文本段后距为1.5' );
        /*闭合在空白单元格中设置段后距*/
        range.setStart( tds[2], 0 ).collapse( true ).select();
        editor.execCommand( 'rowspacing', 25,'bottom' );
        equal( editor.queryCommandValue( 'rowspacing','bottom' ), 25, '设置表格中文本段后距为2.5' );
} );

test( '跨多个单元格设置段前距', function () {
        var editor = te.obj[0];
        var range = te.obj[1];
        editor.setContent( '

hello1	hello2

hello3

hello4

' );
        var tds = editor.body.firstChild.getElementsByTagName( 'td' );
        var p = editor.body.getElementsByTagName( 'p' );
        range.selectNode( editor.body.firstChild ).select();
        editor.execCommand( 'rowspacing', 15 ,'top');
        for ( var index = 0; index < tds.length; index++ ) {
                range.selectNode( tds[index] ).select();
                equal( editor.queryCommandValue( 'rowspacing','top' ), 15, '设置表格中文本段前距为1.5' );
                /*会自动在非block元素外面套p*/
                //1.2版本，加在p上
                equal( p[index].style['marginTop'], '15px', '段前距属性都是加在第一个孩子节点上' );

        }

} );

/*trace 1052*/
test( '对插入的代码设置多倍段前距', function () {
        var editor = te.obj[0];
        var range = te.obj[1];
        editor.setContent( '

' );
        range.setStart( editor.body.firstChild, 0 ).collapse( true ).select();
        var stylecode = "var tds = editor.body.firstChild.getElementsByTagName( 'td' );\n range.selectNode( editor.body.firstChild ).select();";
        editor.execCommand( 'inserthtml', "
" + stylecode + "
" );
        equal( editor.body.firstChild.tagName.toLowerCase(), 'pre', '第一个孩子节点为pre' );
        range.selectNode( editor.body.firstChild ).select();
        editor.execCommand( 'rowspacing', 20,'top' );
        var pre = editor.body.firstChild;
        equal( pre.tagName.toLowerCase(), 'pre', '不允许将p换成pre' );

        equal( pre.style['borderWidth'], '1px', '宽度' );
        ok( pre.style['borderColor'].toUpperCase() == '#CCCCCC' || pre.style['borderColor'] == 'rgb(204, 204, 204)', '颜色' );
} );

test( '在合并单元格中设置多倍段前距', function () {
        var editor = new baidu.editor.Editor({'autoFloatEnabled':false});
    stop();

        setTimeout( function () {
        var div = document.body.appendChild( document.createElement( 'div' ) );
        te.dom.push( div );
        editor.render( div );
        editor.setContent( '

' );
        var range = new baidu.editor.dom.Range( editor.document );
        range.setStart( editor.body.firstChild, 0 ).collapse( true ).select();
        editor.execCommand( 'inserttable', {numCols:3, numRows:3} );
        stop();
        /*insertHTML有一个200ms的超时函数*/
        setTimeout( function () {
                ua.manualDeleteFillData( editor.body );
                var trs = editor.body.getElementsByTagName( 'tr' );
                range.setStart( trs[0].firstChild, 0 ).setEnd( trs[1].firstChild, 0 ).select();
                editor.currentSelectedArr = [trs[0].firstChild, trs[1].firstChild];
                editor.execCommand( 'mergecells' );
                /*合并单元格后设置这个单元格多倍段前距*/
                ua.manualDeleteFillData( editor.body );
                range.setStart( trs[0].firstChild, 0 ).collapse( true ).select();
                editor.execCommand( 'rowspacing', 20 ,'top' );
                ua.manualDeleteFillData( editor.body );
                equal( trs[0].firstChild.firstChild.tagName.toLowerCase(), 'p', 'td下面创建了一个p' );
                equal( trs[0].firstChild.firstChild.style['marginTop'], '20px', 'p设置了2倍行距' );
                trs = editor.body.firstChild.getElementsByTagName( 'tr' );
                equal( trs.length, 3, '3行' );
                var tbodyChild = editor.body.getElementsByTagName( 'tbody' )[0].childNodes;
                for ( var index = 0; index < tbodyChild.length; index++ ) {
                        equal( tbodyChild[index].tagName.toLowerCase(), 'tr', 'tbody下面都是tr' );
                }
                start();
        }, 300 );
        },50);

} );

/*trace 1079*/
test( '合并单元格后设置多个单元格多倍段前距', function () {
        var editor = new baidu.editor.Editor( {'plugins':['table'],'autoFloatEnabled':false} );
    stop();
    setTimeout(function(){
                var div = document.body.appendChild( document.createElement( 'div' ) );
        te.dom.push( div );
        editor.render( div );
        editor.setContent( '

' );
        var range = new baidu.editor.dom.Range( editor.document );
        range.setStart( editor.body.firstChild, 0 ).collapse( true ).select();
        editor.execCommand( 'inserttable', {numCols:3, numRows:3} );

        setTimeout( function () {
                ua.manualDeleteFillData( editor.body );
                var trs = editor.body.firstChild.getElementsByTagName( 'tr' );
                /*合并第一列前2个单元格*/
                range.setStart( trs[0].firstChild, 0 ).setEnd( trs[1].firstChild, 0 ).select();
                editor.currentSelectedArr = [trs[0].firstChild, trs[1].firstChild];
                editor.execCommand( 'mergecells' );
                /*设置多倍段前距*/
                range.setStart( trs[0].firstChild, 0 ).setEnd( trs[2].firstChild, 0 ).select();
                editor.currentSelectedArr = [trs[0].firstChild, trs[1].firstChild, trs[2].firstChild];
                editor.execCommand( 'rowspacing', 20 ,'top' );
                trs = editor.body.firstChild.getElementsByTagName( 'tr' );
                equal( trs.length, 3, '3行' );
                var tbodyChild = editor.body.getElementsByTagName( 'tbody' )[0].childNodes;
                for ( var index = 0; index < tbodyChild.length; index++ ) {
                        equal( tbodyChild[index].tagName.toLowerCase(), 'tr', 'tbody下面都是tr' );
                }
                start();
        }, 300 );
    },50);

} );
<commit_msg>【test】整理用例plugins/rowspacing<commit_after>module( 'plugins.rowspacing' );

/*trace 1029*/
test( '设置段前距查看状态反射', function () {
        var editor = te.obj[0];
        var range = te.obj[1];
        editor.setContent( '
hello1

hello2
' );
        range.setStart( editor.body.firstChild, 0 ).setEnd( editor.body.lastChild, 1 ).select();
        editor.execCommand( 'rowspacing', 15 ,'top');
        equal( editor.queryCommandValue( 'rowspacing' ,'top'), 15, '查看段前距' );
        /*光标闭合时查看状态反射*/
        range.setStart( editor.body.firstChild.firstChild, 1 ).collapse( true ).select();
        equal( editor.queryCommandValue( 'rowspacing','top' ), 15, '查看段前距' );
} );

/*trace 1035*/
test( '非闭合清除段前距等样式，查看状态反射', function () {
    var editor = te.obj[0];
    var range = te.obj[1];
    editor.setContent( '
hello1

hello2
' );
    range.setStart( editor.body.firstChild, 0 ).setEnd( editor.body.lastChild, 1 ).select();
    editor.execCommand( 'rowspacing', 20,'top' );
    equal( editor.queryCommandValue( 'rowspacing' ,'top'), 20, '段前距为2.0' );
} );

test( '闭合清除段前距等样式，查看状态反射', function () {
    var editor = te.obj[0];
    var range = te.obj[1];
    editor.setContent( '
hello1

hello2
' );
    range.setStart( editor.body.firstChild, 0 ).collapse( true ).select();
    editor.execCommand( 'rowspacing', 20 ,'top');
    equal( editor.queryCommandValue( 'rowspacing','top' ), 20, '段前距为2.0' );
    range.setStart( editor.body.firstChild, 0 ).collapse( true ).select();
    editor.execCommand( 'removeformat' );
    equal( editor.queryCommandValue( 'rowspacing' ,'top'), 5, '闭合清除格式后，段前距为5' );
} );

/*trace 1026*/
test( '设置段后距后设置字体颜色', function () {
    var editor = te.obj[2];
    var div = document.body.appendChild( document.createElement( 'div' ) );
    $( div ).css( 'width', '500px' ).css( 'height', '500px' ).css( 'border', '1px solid #ccc' );
    editor.render(div);
    var range = new baidu.editor.dom.Range( editor.document );
    stop();
    setTimeout(function(){
        editor.setContent( '
hello1hello

hello2
' );
        range.setStart( editor.body.firstChild, 0 ).setEnd( editor.body.lastChild, 1 ).select();
        editor.execCommand( 'rowspacing', 15 ,'bottom');
        setTimeout(function(){
            editor.execCommand( 'forecolor', 'rgb(255,0,0)' );
            equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 15, '查看段后距' );
            /*闭合的方式去查看行距的状态反射*/
            range.setStart( editor.body.firstChild.firstChild, 1 ).collapse( true ).select();
            equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 15, '查看段后距' );
            div.parentNode.removeChild(div);
            start();
        },50);
    },50);
} );

test( '设置段后距后设置加粗等多种样式', function () {
    var editor = te.obj[2];
    var div = document.body.appendChild( document.createElement( 'div' ) );
    $( div ).css( 'width', '500px' ).css( 'height', '500px' ).css( 'border', '1px solid #ccc' );
    editor.render(div);
    var range = new baidu.editor.dom.Range( editor.document );
    stop();
    setTimeout(function(){
        editor.setContent( '
hello1

hello2
' );
        range.setStart( editor.body.firstChild, 0 ).setEnd( editor.body.lastChild, 1 ).select();
        editor.execCommand( 'rowspacing', 15 ,'bottom');
        setTimeout(function(){
            editor.execCommand( 'bold' );
            editor.execCommand( 'underline' );
            equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 15, '查看段后距' );
            div.parentNode.removeChild(div);
            start();
        },50);
    },50);
} );

test( '非闭合去除加粗等样式', function () {
    var editor = te.obj[2];
    var div = document.body.appendChild( document.createElement( 'div' ) );
    $( div ).css( 'width', '500px' ).css( 'height', '500px' ).css( 'border', '1px solid #ccc' );
    editor.render(div);
    var range = new baidu.editor.dom.Range( editor.document );
    stop();
    setTimeout(function(){
        editor.setContent( '
hello1

hello2
' );
        range.setStart( editor.body.firstChild, 0 ).setEnd( editor.body.lastChild, 1 ).select();
        editor.execCommand( 'rowspacing', 15 ,'bottom');
        setTimeout(function(){
            editor.execCommand( 'bold' );
            editor.execCommand( 'underline' );
            equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 15, '查看段后距' );
            editor.execCommand( 'removeformat' );
            equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 5, '去除样式后查看段后距' );
            div.parentNode.removeChild(div);
            start();
        },50);
    },50);
} );

test( '闭合去除样式', function () {
    var editor = te.obj[2];
    var div = document.body.appendChild( document.createElement( 'div' ) );
    $( div ).css( 'width', '500px' ).css( 'height', '500px' ).css( 'border', '1px solid #ccc' );
    editor.render(div);
    var range = new baidu.editor.dom.Range( editor.document );
    stop();
    setTimeout(function(){
        editor.setContent( '
hello1

hello2
' );
        range.setStart( editor.body.firstChild, 0 ).setEnd( editor.body.lastChild, 1 ).select();
        editor.execCommand( 'rowspacing', 15 ,'bottom');
        setTimeout(function(){
            editor.execCommand( 'bold' );
            editor.execCommand( 'underline' );
            /*采用闭合的方式查询段后距，
             介个好像用手选不太能选的出来，总是会选到里面去*/
            range.setStart( editor.body.firstChild, 0 ).collapse( true ).select();
            equal( editor.queryCommandValue( 'rowspacing','bottom' ), 15, '查看段后距' );
            /*闭合方式鼠标放在第二个p中*/
            range.setStart( editor.body.lastChild, 0 ).collapse( true ).select();
            equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 15, '查看段后距' );
            editor.execCommand( 'removeformat' );
            //1.2后改
            equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 5, '去除样式后查看段后距' );
            /*第一行的样式应当仍然在*/
            range.setStart( editor.body.firstChild, 0 ).collapse( true ).select();
            equal( editor.queryCommandValue( 'rowspacing' ,'bottom'), 15, '查看段后距' );
            div.parentNode.removeChild(div);
            start();
        },50);
    },50);
} );

test( '表格中设置段距', function () {
    var editor = te.obj[0];
    var range = te.obj[1];
    editor.setContent( '

hello1	hello2

' );
    var tds = editor.body.firstChild.getElementsByTagName( 'td' );
    /*选中表格中的文本设置段前距*/
    range.selectNode( tds[0].firstChild ).select();
    editor.execCommand( 'rowspacing', 20 ,'top');
    equal( editor.queryCommandValue( 'rowspacing','top' ), 20, '设置表格中文本段前距为2' );
    /*采用闭合的方式查询段前距*/
    range.setStart( tds[0].firstChild.firstChild, 1 ).collapse( true ).select();
    equal( editor.queryCommandValue( 'rowspacing','top' ), 20, '设置表格中文本段前距为2' );

    /*闭合在表格中的文本设置段后距*/
    range.setStart( tds[1].firstChild, 1 ).collapse( true ).select();
    editor.execCommand( 'rowspacing', 15 ,'bottom');
    /*选中整个单元格查询段后距*/
    range.selectNode( tds[1] ).select();
    equal( editor.queryCommandValue( 'rowspacing','bottom'), 15, '设置表格中文本段后距为1.5' );
    /*闭合在空白单元格中设置段后距*/
    range.setStart( tds[2], 0 ).collapse( true ).select();
    editor.execCommand( 'rowspacing', 25,'bottom' );
    equal( editor.queryCommandValue( 'rowspacing','bottom' ), 25, '设置表格中文本段后距为2.5' );
} );

test( '跨多个单元格设置段前距', function () {
    var editor = te.obj[0];
    var range = te.obj[1];
    editor.setContent( '

hello1	hello2

hello3

hello4

' );
    var tds = editor.body.firstChild.getElementsByTagName( 'td' );
    var p = editor.body.getElementsByTagName( 'p' );
    range.selectNode( editor.body.firstChild ).select();
    editor.execCommand( 'rowspacing', 15 ,'top');
    for ( var index = 0; index < tds.length; index++ ) {
        range.selectNode( tds[index] ).select();
        equal( editor.queryCommandValue( 'rowspacing','top' ), 15, '设置表格中文本段前距为1.5' );
        /*会自动在非block元素外面套p*/
        //1.2版本，加在p上
        equal( p[index].style['marginTop'], '15px', '段前距属性都是加在第一个孩子节点上' );
    }
} );

/*trace 1052*/
test( '对插入的代码设置多倍段前距', function () {
    var editor = te.obj[0];
    var range = te.obj[1];
    editor.setContent( '

' );
    range.setStart( editor.body.firstChild, 0 ).collapse( true ).select();
    var stylecode = "var tds = editor.body.firstChild.getElementsByTagName( 'td' );\n range.selectNode( editor.body.firstChild ).select();";
    editor.execCommand( 'inserthtml', "
" + stylecode + "
" );
    equal( editor.body.firstChild.tagName.toLowerCase(), 'pre', '第一个孩子节点为pre' );
    range.selectNode( editor.body.firstChild ).select();
    editor.execCommand( 'rowspacing', 20,'top' );
    var pre = editor.body.firstChild;
    equal( pre.tagName.toLowerCase(), 'pre', '不允许将p换成pre' );
    equal( pre.style['borderWidth'], '1px', '宽度' );
    ok( pre.style['borderColor'].toUpperCase() == '#CCCCCC' || pre.style['borderColor'] == 'rgb(204, 204, 204)', '颜色' );
} );

test( '在合并单元格中设置多倍段前距', function () {
    var editor = new baidu.editor.Editor({'autoFloatEnabled':false});
    stop();
    setTimeout( function () {
        var div = document.body.appendChild( document.createElement( 'div' ) );
        te.dom.push( div );
        editor.render( div );
        editor.setContent( '

' );
        var range = new baidu.editor.dom.Range( editor.document );
        range.setStart( editor.body.firstChild, 0 ).collapse( true ).select();
        editor.execCommand( 'inserttable', {numCols:3, numRows:3} );
        stop();
        /*insertHTML有一个200ms的超时函数*/
        setTimeout( function () {
            ua.manualDeleteFillData( editor.body );
            var trs = editor.body.getElementsByTagName( 'tr' );
            range.setStart( trs[0].firstChild, 0 ).setEnd( trs[1].firstChild, 0 ).select();
            editor.currentSelectedArr = [trs[0].firstChild, trs[1].firstChild];
            editor.execCommand( 'mergecells' );
            /*合并单元格后设置这个单元格多倍段前距*/
            ua.manualDeleteFillData( editor.body );
            range.setStart( trs[0].firstChild, 0 ).collapse( true ).select();
            editor.execCommand( 'rowspacing', 20 ,'top' );
            ua.manualDeleteFillData( editor.body );
            equal( trs[0].firstChild.firstChild.tagName.toLowerCase(), 'p', 'td下面创建了一个p' );
            equal( trs[0].firstChild.firstChild.style['marginTop'], '20px', 'p设置了2倍行距' );
            trs = editor.body.firstChild.getElementsByTagName( 'tr' );
            equal( trs.length, 3, '3行' );
            var tbodyChild = editor.body.getElementsByTagName( 'tbody' )[0].childNodes;
            for ( var index = 0; index < tbodyChild.length; index++ ) {
                equal( tbodyChild[index].tagName.toLowerCase(), 'tr', 'tbody下面都是tr' );
            }
            start();
        }, 300 );
    },50);
} );

/*trace 1079*/
test( '合并单元格后设置多个单元格多倍段前距', function () {
    var editor = new baidu.editor.Editor( {'plugins':['table'],'autoFloatEnabled':false} );
    stop();
    setTimeout(function(){
        var div = document.body.appendChild( document.createElement( 'div' ) );
        te.dom.push( div );
        editor.render( div );
        editor.setContent( '

' );
        var range = new baidu.editor.dom.Range( editor.document );
        range.setStart( editor.body.firstChild, 0 ).collapse( true ).select();
        editor.execCommand( 'inserttable', {numCols:3, numRows:3} );

        setTimeout( function () {
            ua.manualDeleteFillData( editor.body );
            var trs = editor.body.firstChild.getElementsByTagName( 'tr' );
            /*合并第一列前2个单元格*/
            range.setStart( trs[0].firstChild, 0 ).setEnd( trs[1].firstChild, 0 ).select();
            editor.currentSelectedArr = [trs[0].firstChild, trs[1].firstChild];
            editor.execCommand( 'mergecells' );
            /*设置多倍段前距*/
            range.setStart( trs[0].firstChild, 0 ).setEnd( trs[2].firstChild, 0 ).select();
            editor.currentSelectedArr = [trs[0].firstChild, trs[1].firstChild, trs[2].firstChild];
            editor.execCommand( 'rowspacing', 20 ,'top' );
            trs = editor.body.firstChild.getElementsByTagName( 'tr' );
            equal( trs.length, 3, '3行' );
            var tbodyChild = editor.body.getElementsByTagName( 'tbody' )[0].childNodes;
            for ( var index = 0; index < tbodyChild.length; index++ ) {
                equal( tbodyChild[index].tagName.toLowerCase(), 'tr', 'tbody下面都是tr' );
            }
            start();
        }, 300 );
    },50);
} );
<|endoftext|>


<commit_before>/*
 *  Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */

/* More information about these options at jshint.com/docs/options */

/* globals computeBitrate, computeE2EDelay, computeRate, endTime, errorMessages,
   extractStatAsInt, gatheredIceCandidateTypes, getStatsReport,
   getStatsTimer:true, infoDiv, pc,
   prevStats:true, remoteVideo, startTime, stats:true */
/* exported toggleInfoDiv, updateInfoDiv */

'use strict';

function showInfoDiv() {
  getStatsTimer = setInterval(refreshStats, 1000);
  infoDiv.classList.add('active');
}

function toggleInfoDiv() {
  if (infoDiv.classList.contains('active')) {
    clearInterval(getStatsTimer);
    infoDiv.classList.remove('active');
  } else {
    showInfoDiv();
  }
}

function refreshStats() {
  if (pc) {
    pc.getStats(function(response) {
      prevStats = stats;
      stats = response.result();
      updateInfoDiv();
    });
  }
}

function updateInfoDiv() {
  var contents = '
';

  if (stats) {
    // Build the display.
    contents += buildLine('States');
    contents += buildLine('Signaling', pc.signalingState);
    contents += buildLine('Gathering', pc.iceGatheringState);
    contents += buildLine('Connection', pc.iceConnectionState);
    for (var endpoint in gatheredIceCandidateTypes) {
      var types = [];
      for (var type in gatheredIceCandidateTypes[endpoint]) {
        types.push(type + ':' + gatheredIceCandidateTypes[endpoint][type]);
      }
      contents += buildLine(endpoint, types.join(' '));
    }

    var activeCandPair = getStatsReport(stats, 'googCandidatePair',
        'googActiveConnection', 'true');
    var localAddr, remoteAddr, localAddrType, remoteAddrType;
    if (activeCandPair) {
      localAddr = activeCandPair.stat('googLocalAddress');
      remoteAddr = activeCandPair.stat('googRemoteAddress');
      localAddrType = activeCandPair.stat('googLocalCandidateType');
      remoteAddrType = activeCandPair.stat('googRemoteCandidateType');
    }
    if (localAddr && remoteAddr) {
      contents += buildLine('LocalAddr', localAddr +
          ' (' + localAddrType + ')');
      contents += buildLine('RemoteAddr', remoteAddr +
          ' (' + remoteAddrType + ')');
    }
    contents += buildLine();

    contents += buildStatsSection();
  }

  if (errorMessages.length) {
    infoDiv.classList.add('warning');
    for (var i = 0; i !== errorMessages.length; ++i) {
      contents += errorMessages[i] + '\n';
    }
  } else {
    infoDiv.classList.remove('warning');
  }

  contents += '
';

  if (infoDiv.innerHTML !== contents) {
    infoDiv.innerHTML = contents;
  }
}

function buildStatsSection() {
  var contents = buildLine('Stats');

  // Obtain setup and latency stats.
  var rtt = extractStatAsInt(stats, 'ssrc', 'googRtt');
  var captureStart = extractStatAsInt(stats, 'ssrc',
      'googCaptureStartNtpTimeMs');
  var e2eDelay = computeE2EDelay(captureStart, remoteVideo.currentTime);
  if (endTime !== null) {
    contents += buildLine('Setup time',
        (endTime - startTime).toFixed(0).toString() + 'ms');
  }
  if (rtt !== null) {
    contents += buildLine('RTT', rtt.toString() + 'ms');
  }
  if (e2eDelay !== null) {
    contents += buildLine('End to end', e2eDelay.toString() + 'ms');
  }

  // Obtain resolution, framerate, and bitrate stats.
  // TODO(juberti): find a better way to tell these apart.
  var txAudio = getStatsReport(stats, 'ssrc', 'audioInputLevel');
  var rxAudio = getStatsReport(stats, 'ssrc', 'audioOutputLevel');
  var txVideo = getStatsReport(stats, 'ssrc', 'googFirsReceived');
  var rxVideo = getStatsReport(stats, 'ssrc', 'googFirsSent');
  var txPrevAudio = getStatsReport(prevStats, 'ssrc', 'audioInputLevel');
  var rxPrevAudio = getStatsReport(prevStats, 'ssrc', 'audioOutputLevel');
  var txPrevVideo = getStatsReport(prevStats, 'ssrc', 'googFirsReceived');
  var rxPrevVideo = getStatsReport(prevStats, 'ssrc', 'googFirsSent');
  var txAudioCodec, txAudioBitrate, txAudioPacketRate;
  var rxAudioCodec, rxAudioBitrate, rxAudioPacketRate;
  var txVideoHeight, txVideoFps, txVideoCodec;
  var txVideoBitrate, txVideoPacketRate;
  var rxVideoHeight, rxVideoFps, rxVideoCodec;
  var rxVideoBitrate, rxVideoPacketRate;
  if (txAudio) {
    txAudioCodec = txAudio.stat('googCodecName');
    txAudioBitrate = computeBitrate(txAudio, txPrevAudio, 'bytesSent');
    txAudioPacketRate = computeRate(txAudio, txPrevAudio, 'packetsSent');
  }
  if (rxAudio) {
    rxAudioCodec = rxAudio.stat('googCodecName');
    rxAudioBitrate = computeBitrate(rxAudio, rxPrevAudio, 'bytesReceived');
    rxAudioPacketRate = computeRate(rxAudio, rxPrevAudio, 'packetsReceived');
  }
  if (txVideo) {
    txVideoCodec = txVideo.stat('googCodecName');
    txVideoHeight = txVideo.stat('googFrameHeightSent');
    txVideoFps = txVideo.stat('googFrameRateSent');
    txVideoBitrate = computeBitrate(txVideo, txPrevVideo, 'bytesSent');
    txVideoPacketRate = computeRate(txVideo, txPrevVideo, 'packetsSent');
  }
  if (rxVideo) {
    rxVideoCodec = 'TODO';  // rxVideo.stat('googCodecName');
    rxVideoHeight = remoteVideo.videoHeight;
    // TODO(juberti): this should ideally be obtained from the video element.
    rxVideoFps = rxVideo.stat('googFrameRateDecoded');
    rxVideoBitrate = computeBitrate(rxVideo, rxPrevVideo, 'bytesReceived');
    rxVideoPacketRate = computeRate(rxVideo, rxPrevVideo, 'packetsReceived');
  }
  contents += buildLine('Audio Tx', txAudioCodec + ', ' +
      formatBitrate(txAudioBitrate) + ', ' +
      formatPacketRate(txAudioPacketRate));
  contents += buildLine('Audio Rx', rxAudioCodec + ', ' +
      formatBitrate(rxAudioBitrate)  + ', ' +
      formatPacketRate(rxAudioPacketRate));
  contents += buildLine('Video Tx',
      txVideoCodec + ', ' + txVideoHeight.toString() + 'p' +
      txVideoFps.toString() + ', ' + formatBitrate(txVideoBitrate) + ', ' +
      formatPacketRate(txVideoPacketRate));
  contents += buildLine('Video Rx',
      rxVideoCodec + ', ' + rxVideoHeight.toString() + 'p' +
      rxVideoFps.toString() + ', ' + formatBitrate(rxVideoBitrate) + ', ' +
      formatPacketRate(rxVideoPacketRate));
  return contents;
}

function buildLine(label, value) {
  var columnWidth = 12;
  var line = '';
  if (label) {
    line += label + ':';
    while (line.length < columnWidth) {
      line += ' ';
    }

    if (value) {
      line += value;
    }
  }
  line += '\n';
  return line;
}

function formatBitrate(value) {
  var suffix;
  if (value < 1000) {
    suffix = 'bps';
  } else if (value < 1000000) {
    suffix = 'kbps';
    value /= 1000;
  } else {
    suffix = 'Mbps';
    value /= 1000000;
  }

  var str = value.toPrecision(3) + ' ' + suffix;
  return str;
}

function formatPacketRate(value) {
  return value.toPrecision(3) + ' ' + 'pps';
}
<commit_msg>Don't display distracting errors when WSS comm fails.<commit_after>/*
 *  Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */

/* More information about these options at jshint.com/docs/options */

/* globals computeBitrate, computeE2EDelay, computeRate, endTime, errorMessages,
   extractStatAsInt, gatheredIceCandidateTypes, getStatsReport,
   getStatsTimer:true, infoDiv, pc,
   prevStats:true, remoteVideo, startTime, stats:true */
/* exported toggleInfoDiv, updateInfoDiv */

'use strict';

function showInfoDiv() {
  getStatsTimer = setInterval(refreshStats, 1000);
  infoDiv.classList.add('active');
}

function toggleInfoDiv() {
  if (infoDiv.classList.contains('active')) {
    clearInterval(getStatsTimer);
    infoDiv.classList.remove('active');
  } else {
    showInfoDiv();
  }
}

function refreshStats() {
  if (pc) {
    pc.getStats(function(response) {
      prevStats = stats;
      stats = response.result();
      updateInfoDiv();
    });
  }
}

function updateInfoDiv() {
  var contents = '
';

  if (stats) {
    // Build the display.
    contents += buildLine('States');
    contents += buildLine('Signaling', pc.signalingState);
    contents += buildLine('Gathering', pc.iceGatheringState);
    contents += buildLine('Connection', pc.iceConnectionState);
    for (var endpoint in gatheredIceCandidateTypes) {
      var types = [];
      for (var type in gatheredIceCandidateTypes[endpoint]) {
        types.push(type + ':' + gatheredIceCandidateTypes[endpoint][type]);
      }
      contents += buildLine(endpoint, types.join(' '));
    }

    var activeCandPair = getStatsReport(stats, 'googCandidatePair',
        'googActiveConnection', 'true');
    var localAddr, remoteAddr, localAddrType, remoteAddrType;
    if (activeCandPair) {
      localAddr = activeCandPair.stat('googLocalAddress');
      remoteAddr = activeCandPair.stat('googRemoteAddress');
      localAddrType = activeCandPair.stat('googLocalCandidateType');
      remoteAddrType = activeCandPair.stat('googRemoteCandidateType');
    }
    if (localAddr && remoteAddr) {
      contents += buildLine('LocalAddr', localAddr +
          ' (' + localAddrType + ')');
      contents += buildLine('RemoteAddr', remoteAddr +
          ' (' + remoteAddrType + ')');
    }
    contents += buildLine();

    contents += buildStatsSection();
  }

  if (errorMessages.length) {
    infoDiv.classList.add('warning');
    for (var i = 0; i !== errorMessages.length; ++i) {
      contents += errorMessages[i] + '\n';
    }
  } else {
    infoDiv.classList.remove('warning');
  }

  contents += '
';

  if (infoDiv.innerHTML !== contents) {
    infoDiv.innerHTML = contents;
  }
}

function buildStatsSection() {
  var contents = buildLine('Stats');

  // Obtain setup and latency stats.
  var rtt = extractStatAsInt(stats, 'ssrc', 'googRtt');
  var captureStart = extractStatAsInt(stats, 'ssrc',
      'googCaptureStartNtpTimeMs');
  var e2eDelay = computeE2EDelay(captureStart, remoteVideo.currentTime);
  if (endTime !== null) {
    contents += buildLine('Setup time',
        (endTime - startTime).toFixed(0).toString() + 'ms');
  }
  if (rtt !== null) {
    contents += buildLine('RTT', rtt.toString() + 'ms');
  }
  if (e2eDelay !== null) {
    contents += buildLine('End to end', e2eDelay.toString() + 'ms');
  }

  // Obtain resolution, framerate, and bitrate stats.
  // TODO(juberti): find a better way to tell these apart.
  var txAudio = getStatsReport(stats, 'ssrc', 'audioInputLevel');
  var rxAudio = getStatsReport(stats, 'ssrc', 'audioOutputLevel');
  var txVideo = getStatsReport(stats, 'ssrc', 'googFirsReceived');
  var rxVideo = getStatsReport(stats, 'ssrc', 'googFirsSent');
  var txPrevAudio = getStatsReport(prevStats, 'ssrc', 'audioInputLevel');
  var rxPrevAudio = getStatsReport(prevStats, 'ssrc', 'audioOutputLevel');
  var txPrevVideo = getStatsReport(prevStats, 'ssrc', 'googFirsReceived');
  var rxPrevVideo = getStatsReport(prevStats, 'ssrc', 'googFirsSent');
  var txAudioCodec, txAudioBitrate, txAudioPacketRate;
  var rxAudioCodec, rxAudioBitrate, rxAudioPacketRate;
  var txVideoHeight, txVideoFps, txVideoCodec;
  var txVideoBitrate, txVideoPacketRate;
  var rxVideoHeight, rxVideoFps, rxVideoCodec;
  var rxVideoBitrate, rxVideoPacketRate;
  if (txAudio) {
    txAudioCodec = txAudio.stat('googCodecName');
    txAudioBitrate = computeBitrate(txAudio, txPrevAudio, 'bytesSent');
    txAudioPacketRate = computeRate(txAudio, txPrevAudio, 'packetsSent');
    contents += buildLine('Audio Tx', txAudioCodec + ', ' +
        formatBitrate(txAudioBitrate) + ', ' +
        formatPacketRate(txAudioPacketRate));
  }
  if (rxAudio) {
    rxAudioCodec = rxAudio.stat('googCodecName');
    rxAudioBitrate = computeBitrate(rxAudio, rxPrevAudio, 'bytesReceived');
    rxAudioPacketRate = computeRate(rxAudio, rxPrevAudio, 'packetsReceived');
    contents += buildLine('Audio Rx', rxAudioCodec + ', ' +
        formatBitrate(rxAudioBitrate)  + ', ' +
        formatPacketRate(rxAudioPacketRate));
  }
  if (txVideo) {
    txVideoCodec = txVideo.stat('googCodecName');
    txVideoHeight = txVideo.stat('googFrameHeightSent');
    txVideoFps = txVideo.stat('googFrameRateSent');
    txVideoBitrate = computeBitrate(txVideo, txPrevVideo, 'bytesSent');
    txVideoPacketRate = computeRate(txVideo, txPrevVideo, 'packetsSent');
    contents += buildLine('Video Tx',
        txVideoCodec + ', ' + txVideoHeight.toString() + 'p' +
        txVideoFps.toString() + ', ' + formatBitrate(txVideoBitrate) + ', ' +
        formatPacketRate(txVideoPacketRate));
  }
  if (rxVideo) {
    rxVideoCodec = 'TODO';  // rxVideo.stat('googCodecName');
    rxVideoHeight = remoteVideo.videoHeight;
    // TODO(juberti): this should ideally be obtained from the video element.
    rxVideoFps = rxVideo.stat('googFrameRateDecoded');
    rxVideoBitrate = computeBitrate(rxVideo, rxPrevVideo, 'bytesReceived');
    rxVideoPacketRate = computeRate(rxVideo, rxPrevVideo, 'packetsReceived');
    contents += buildLine('Video Rx',
        rxVideoCodec + ', ' + rxVideoHeight.toString() + 'p' +
        rxVideoFps.toString() + ', ' + formatBitrate(rxVideoBitrate) + ', ' +
        formatPacketRate(rxVideoPacketRate));
  }
  return contents;
}

function buildLine(label, value) {
  var columnWidth = 12;
  var line = '';
  if (label) {
    line += label + ':';
    while (line.length < columnWidth) {
      line += ' ';
    }

    if (value) {
      line += value;
    }
  }
  line += '\n';
  return line;
}

function formatBitrate(value) {
  var suffix;
  if (value < 1000) {
    suffix = 'bps';
  } else if (value < 1000000) {
    suffix = 'kbps';
    value /= 1000;
  } else {
    suffix = 'Mbps';
    value /= 1000000;
  }

  var str = value.toPrecision(3) + ' ' + suffix;
  return str;
}

function formatPacketRate(value) {
  return value.toPrecision(3) + ' ' + 'pps';
}
<|endoftext|>


<commit_before>/// ===========================================================================================
/// This currently serves as both the blog admin, user admin, and content admin Javascript area
/// ===========================================================================================

blog_class = function() {
    // Track whether the blog has been saved or not on the add Blog page
    this.blogIsSaved = false;
    this.blogId = -1;
    this.blogSpaceReplacementChar = '-';
};

blog_class.prototype.initPageEvents = function() {
    this.editBlogEvents();
    this.addBlogEvents();
    this.initPermaLinkEvents();
    this.manageBlogEvents();
    //this.initTinyMCEEvents();
    this.initPreviewEvent();
    this.initPublishDateEvents();

    if ($('.editBlog').length > 0) {
        this.initTagEvents();
    }

    // Allow sorting on Blog Admin Modules
    if ($("#Sortable1").length > 0) {
        this.initBlogAdminModuleEvents();
    }

    // Ckeditor instances
    if ($('#CKEDITBLOG').length > 0) {
        this.CKPageEditor = CKEDITOR.replace('CKEDITBLOG', {
            // options here
            height:412
        });
    }

    if ($('#ShortDescription').length > 0) {
        this.CKShortDescEditor = CKEDITOR.replace('ShortDescription', {
            toolbar: [['Source', '-', 'Bold', 'Italic']]
        });
    }
};

blog_class.prototype.initBlogAdminModuleEvents = function () {
    var self = this;

    // Sortable widgets
    $("#Sortable1, #Sortable2").sortable({
        placeholder: "ui-state-highlight",
        connectWith: ".connectedSortable",
        items: ".sortable", // Don't Sort editor
        handle: "h4",
        helper: 'clone', // Prevents click event on h4 after drag
        stop: function(event, ui) {
            // fire off save event after drop is complete
            self.saveModulePositions();
        }
    });

    // Collapse widgets
    $("div.editBlog div.connectedSortable div.panel h4").click(function() {
        $(this).parent().toggleClass("collapsed");

        // fire off save event
        self.saveModulePositions();
    });
};

blog_class.prototype.saveModulePositions = function() {
    var self = this;

    // Cancel any previos ajax requests
    if (self.xhr != null) {
        self.xhr.abort();
    }
    var AdminModulesColumn1 = [];
    var AdminModulesColumn2 = [];

    var count = 0;
    $("#Sortable1 div.panel.sortable").each(function () {

        AdminModulesColumn1.push({
            ModuleName : $(this).attr("id"),
            IsCollapsed: $(this).hasClass("collapsed"),
            OrderNumber: count,
            ColumnNumber : 1
        });

        count++;
    });

    var count2 = 0;
    $("#Sortable2 div.panel.sortable").each(function() {

        AdminModulesColumn2.push({
            ModuleName : $(this).attr("id"),
            IsCollapsed: $(this).hasClass("collapsed"),
            OrderNumber: count2,
            ColumnNumber: 2
        });

        count2++;
    });

    var data = {
        entity: {
            AdminModulesColumn1: AdminModulesColumn1,
            AdminModulesColumn2: AdminModulesColumn2
        }
    };

    self.xhr = $.ajax({
        url: "/admin/blog/savemodules/",
        type: "POST",
        dataType: 'json',
        contentType: 'application/json; charset=utf-8',
        data: JSON.stringify(data, null, 2),
        success: function (data) {
            // Nothing to do yet
        }
    });

};

blog_class.prototype.initPermaLinkEvents = function () {
    // Edit Permalink on edit blog page
    var self = this;

    $("#EditPermaLink, #PermaLinkEnd").click(function () {
        var textVal = $("#PermaLinkEnd").text();
        $("#PermaLinkEditPane").removeClass("hide").show().attr("value", textVal);
        $(this).hide();
        $("#PermaLinkEnd").hide();

        $("#PermaLinkEditPane").focus();
    });

    // Key up format the permalink
    $("#PermaLinkEditPane").keyup(function () {
        $(this).attr("value", self.formatBlogLink($(this).val()));
    });

    // Hide the edit pane and update permalink
    $("#PermaLinkEditPane").blur(function () {

        // Clean up permalink by removing spaces and replacing with dashes
        // Remove all illegal characters
        var val = self.formatBlogLink($(this).val());

        $(this).attr("value", val);

        // Set the text with the new value
        $("#PermaLinkEnd").text($(this).attr("value"));
        $(this).hide();
        $("#EditPermaLink , #PermaLinkEnd").show();

        // Let the editor know we've made a modification
        $("#PermaLinkEnd").attr("data-modified", "true");
    });
};

// Replace spaces with appropriate character (such as dash)
// Remove illegal characters
blog_class.prototype.formatBlogLink = function (value) {
    // replace spaces with whatever (currently dashes)
    value = value.replace(/ /g, this.blogSpaceReplacementChar);

    // Strip bad characters
    return value.replace(/[^a-zA-Z0-9-_]/g, '');
};

blog_class.prototype.initPublishDateEvents = function() {
    $("#PublishDate").datepicker();
};

blog_class.prototype.initPreviewEvent = function() {
    $("#PreviewBlogButton").click(function() {
        var title = $("#PermaLinkEnd").text().toLowerCase();
        var host = 'http://' + window.location.host + '/blog/' + $('.category-permalink').text();

        // Open the blog in a new window
        window.open(host + title + '/' + '?debug=true');
    });
};

blog_class.prototype.manageBlogEvents = function() {
    var self = this;

    // Delete blog pop up
    $("div.manageBlogs").on("click", "a.btn.delete", function() {
        self.manageBlogId = $(this).attr("data-id");
        self.$manageBlogRow = $(this).parent().parent();
        var title = '"' + self.$manageBlogRow.find("td.title a").text() + '"';
        $("#popTitle").text(title);
        $("#DeleteModal").modal();
    });

    // Confirm Delete Blog
    $(document).on("click", "#ConfirmBlogDelete", function() {
        var id = self.manageBlogId;
        var $container = $("#DeleteModal > div.content");
        common.showAjaxLoader($container);
        $.ajax({
            url: "/admin/blog/deleteblog",
            type: "POST",
            data: {
                id: self.manageBlogId
            },
            success: function(data) {
                noty({ text: 'Blog Successfully Deleted.', type: 'success', timeout: 2000 });
                self.$manageBlogRow.remove();
                common.hideAjaxLoader($container);
                $('#DeleteModal').modal('hide');
            },
            error: function (data) {
                common.hideAjaxLoader($container);
                $('#DeleteModal').modal('hide');
                noty({ text: 'There was an error processing your request.', type: 'error' });
            }
        });
    });
};

blog_class.prototype.editBlogEvents = function() {
    var self = this;

    // Set startup information
    if ($("div.editBlog").length > 0) {
        self.blogId = $("div.editBlog").attr("data-id");
        self.blogIsSaved = true;
    }
};

blog_class.prototype.addBlogEvents = function() {
    var self = this;

    // Set blog title permalink hint on keypress
    $("#BlogTitle").keyup(function() {
        // Only update permalink if it hasn't been touched yet
        var permLinkModified = $("#PermaLinkEnd").attr("data-modified").toLowerCase() == "true";
        if (!permLinkModified) {
            var val = $(this).val();
            val = val.replace(/ /g, self.blogSpaceReplacementChar);
            $("#PermaLinkEditPane").text(val);
        }
    });

    // Fix spacing issues on page load
    $("#BlogTitle").trigger("keyup");

    // set permalink prefix
    $("#CategoriesModule ul li.catListing input").change(function () {
        $('.category-permalink').text($(this).val().replace(/ /g, self.blogSpaceReplacementChar).replace(/[^a-zA-Z ]/g, "").toLowerCase() + '/');
    });

    // Save Blog from edit / add blog page
    $("#SaveBlog").click(function() {
        var mainCategory = $("#CategoriesModule ul li.catListing input:checked").val();
        var content = CKEDITOR.instances.CKEDITBLOG.getData();
        var featText = CKEDITOR.instances.ShortDescription.getData();
        var data = {
            entity: {
                Title: $("#BlogTitle").val(),
                HtmlContent: content,
                ImageUrl: $("#FeaturedImage").val(),
                IsActive: $("#IsPublicCheck").is(":checked"),
                IsFeatured: $("#IsFeaturedCheck").is(":checked"),
                Author: $("#Author option:selected").text(),
                AuthorId: $("#Author option:selected").attr("data-id"),
                BlogID: self.blogId,
                MainCategory: mainCategory,
                Tags: $("#BlogTags").val(),
                ShortDesc: featText,
                MetaDescription: $("#MetaDescription").val(),
                OGTitle: $("#OGTitle").val(),
                OGImage: $("#OGImage").val(),
                OGType: $("#OGType").val(),
                OGUrl: $("#OGUrl").val(),
                Canonical: $("#Canonical").val(),
                Date: $("#PublishDate").val(),
                PermaLink: self.formatBlogLink($("#PermaLinkEnd").text())
            }
        };

        // Currently all we really need is a title for validation
        if ($("#BlogTitle").attr("value").length < 1) {
            alert("Please enter a title.");
            return false;
        }

        $("#SaveSpinner").show();
        var postUrl = self.blogIsSaved ? "/admin/blog/modifyblog" : "/admin/blog/addblog";
        $.ajax({
            url: postUrl,
            type: "POST",
            dataType: 'json',
            contentType: 'application/json; charset=utf-8',
            data: JSON.stringify(data, null, 2),
            success: function(data) {
                self.blogIsSaved = true;
                self.blogId = data.id;

                var noty_id = noty({ text: 'Changes saved successfully.', type: 'success', timeout: 3000 });
                $("#SaveSpinner").hide();
            },
            error: function(data) {
                var noty_id = noty({ text: 'There was an error processing your request.', type: 'error' });
                $("#SaveSpinner").hide();
            }
        });
    });
};

blog_class.prototype.initTinyMCEEvents = function() {
    //tinymce on blog editor
    $('div.editArea textarea').tinymce({
        // Location of TinyMCE script
        script_url: '/Scripts/tinyMCE/tiny_mce.js',
        height: "450px",
        // General options
        theme: "advanced",
        skin: "o2k7",
        skin_variant: "silver",
        plugins: "autolink,spellchecker,lists,save,advimage,advlink,iespell,inlinepopups,insertdatetime,media,searchreplace,contextmenu,directionality,noneditable,template,safari,xhtmlxtras",

        // Theme options
        theme_advanced_buttons1: "bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,bullist,numlist,|,outdent,indent,|,forecolor,backcolor,|,fontselect,fontsizeselect,link,|,code,|,blockquote,|,image,|,insertdate,inserttime,|,sub,sup,spellchecker",
        theme_advanced_buttons2: "",
        theme_advanced_buttons3: "",

        theme_advanced_toolbar_location: "top",
        theme_advanced_toolbar_align: "left",
        theme_advanced_statusbar_location: "bottom",
        theme_advanced_resizing: false,

        // Example content CSS (should be your site CSS)
        content_css: "/CSS/site.css",

        // Drop lists for link/image/media/template dialogs
        template_external_list_url: "lists/template_list.js",
        external_link_list_url: "lists/link_list.js",
        external_image_list_url: "lists/image_list.js",
        media_external_list_url: "lists/media_list.js",
    });

};

blog_class.prototype.initTagEvents = function () {

    // Tag Manager
    $("#BlogTags").tagit({
        allowSpaces: true
    });

    $('.commonTags a').click(function () {
        var tag = $(this).attr("data-tag");

        $('#BlogTags').tagit("createTag", tag);

        return false;
    });
};

// Keep at the bottom
$(document).ready(function() {
    blog = new blog_class();
    blog.initPageEvents();
});<commit_msg>Slightly nicer permalink<commit_after>/// ===========================================================================================
/// This currently serves as both the blog admin, user admin, and content admin Javascript area
/// ===========================================================================================

blog_class = function() {
    // Track whether the blog has been saved or not on the add Blog page
    this.blogIsSaved = false;
    this.blogId = -1;
    this.blogSpaceReplacementChar = '-';
};

blog_class.prototype.initPageEvents = function() {
    this.editBlogEvents();
    this.addBlogEvents();
    this.initPermaLinkEvents();
    this.manageBlogEvents();
    //this.initTinyMCEEvents();
    this.initPreviewEvent();
    this.initPublishDateEvents();

    if ($('.editBlog').length > 0) {
        this.initTagEvents();
    }

    // Allow sorting on Blog Admin Modules
    if ($("#Sortable1").length > 0) {
        this.initBlogAdminModuleEvents();
    }

    // Ckeditor instances
    if ($('#CKEDITBLOG').length > 0) {
        this.CKPageEditor = CKEDITOR.replace('CKEDITBLOG', {
            // options here
            height:412
        });
    }

    if ($('#ShortDescription').length > 0) {
        this.CKShortDescEditor = CKEDITOR.replace('ShortDescription', {
            toolbar: [['Source', '-', 'Bold', 'Italic']]
        });
    }
};

blog_class.prototype.initBlogAdminModuleEvents = function () {
    var self = this;

    // Sortable widgets
    $("#Sortable1, #Sortable2").sortable({
        placeholder: "ui-state-highlight",
        connectWith: ".connectedSortable",
        items: ".sortable", // Don't Sort editor
        handle: "h4",
        helper: 'clone', // Prevents click event on h4 after drag
        stop: function(event, ui) {
            // fire off save event after drop is complete
            self.saveModulePositions();
        }
    });

    // Collapse widgets
    $("div.editBlog div.connectedSortable div.panel h4").click(function() {
        $(this).parent().toggleClass("collapsed");

        // fire off save event
        self.saveModulePositions();
    });
};

blog_class.prototype.saveModulePositions = function() {
    var self = this;

    // Cancel any previos ajax requests
    if (self.xhr != null) {
        self.xhr.abort();
    }
    var AdminModulesColumn1 = [];
    var AdminModulesColumn2 = [];

    var count = 0;
    $("#Sortable1 div.panel.sortable").each(function () {

        AdminModulesColumn1.push({
            ModuleName : $(this).attr("id"),
            IsCollapsed: $(this).hasClass("collapsed"),
            OrderNumber: count,
            ColumnNumber : 1
        });

        count++;
    });

    var count2 = 0;
    $("#Sortable2 div.panel.sortable").each(function() {

        AdminModulesColumn2.push({
            ModuleName : $(this).attr("id"),
            IsCollapsed: $(this).hasClass("collapsed"),
            OrderNumber: count2,
            ColumnNumber: 2
        });

        count2++;
    });

    var data = {
        entity: {
            AdminModulesColumn1: AdminModulesColumn1,
            AdminModulesColumn2: AdminModulesColumn2
        }
    };

    self.xhr = $.ajax({
        url: "/admin/blog/savemodules/",
        type: "POST",
        dataType: 'json',
        contentType: 'application/json; charset=utf-8',
        data: JSON.stringify(data, null, 2),
        success: function (data) {
            // Nothing to do yet
        }
    });

};

blog_class.prototype.initPermaLinkEvents = function () {
    // Edit Permalink on edit blog page
    var self = this;

    $("#EditPermaLink, #PermaLinkEnd").click(function () {
        var textVal = $("#PermaLinkEnd").text();
        $("#PermaLinkEditPane").removeClass("hide").show().attr("value", textVal);
        $(this).hide();
        $("#PermaLinkEnd").hide();

        $("#PermaLinkEditPane").focus();
    });

    // Key up format the permalink
    $("#PermaLinkEditPane").keyup(function () {
        $(this).attr("value", self.formatBlogLink($(this).val()));
    });

    // Hide the edit pane and update permalink
    $("#PermaLinkEditPane").blur(function () {

        // Clean up permalink by removing spaces and replacing with dashes
        // Remove all illegal characters
        var val = self.formatBlogLink($(this).val());

        $(this).attr("value", val);

        // Set the text with the new value
        $("#PermaLinkEnd").text($(this).attr("value"));
        $(this).hide();
        $("#EditPermaLink , #PermaLinkEnd").show();

        // Let the editor know we've made a modification
        $("#PermaLinkEnd").attr("data-modified", "true");
    });
};

// Replace spaces with appropriate character (such as dash)
// Remove illegal characters
blog_class.prototype.formatBlogLink = function (value) {
    // replace spaces with whatever (currently dashes)
    value = value.replace(/ /g, this.blogSpaceReplacementChar);

    // Strip bad characters
    return value.replace(/[^a-zA-Z0-9-_]/g, '');
};

blog_class.prototype.initPublishDateEvents = function() {
    $("#PublishDate").datepicker();
};

blog_class.prototype.initPreviewEvent = function() {
    $("#PreviewBlogButton").click(function() {
        var title = $("#PermaLinkEnd").text().toLowerCase();
        var host = 'http://' + window.location.host + '/blog/' + $('.category-permalink').text();

        // Open the blog in a new window
        window.open(host + title + '/' + '?debug=true');
    });
};

blog_class.prototype.manageBlogEvents = function() {
    var self = this;

    // Delete blog pop up
    $("div.manageBlogs").on("click", "a.btn.delete", function() {
        self.manageBlogId = $(this).attr("data-id");
        self.$manageBlogRow = $(this).parent().parent();
        var title = '"' + self.$manageBlogRow.find("td.title a").text() + '"';
        $("#popTitle").text(title);
        $("#DeleteModal").modal();
    });

    // Confirm Delete Blog
    $(document).on("click", "#ConfirmBlogDelete", function() {
        var id = self.manageBlogId;
        var $container = $("#DeleteModal > div.content");
        common.showAjaxLoader($container);
        $.ajax({
            url: "/admin/blog/deleteblog",
            type: "POST",
            data: {
                id: self.manageBlogId
            },
            success: function(data) {
                noty({ text: 'Blog Successfully Deleted.', type: 'success', timeout: 2000 });
                self.$manageBlogRow.remove();
                common.hideAjaxLoader($container);
                $('#DeleteModal').modal('hide');
            },
            error: function (data) {
                common.hideAjaxLoader($container);
                $('#DeleteModal').modal('hide');
                noty({ text: 'There was an error processing your request.', type: 'error' });
            }
        });
    });
};

blog_class.prototype.editBlogEvents = function() {
    var self = this;

    // Set startup information
    if ($("div.editBlog").length > 0) {
        self.blogId = $("div.editBlog").attr("data-id");
        self.blogIsSaved = true;
    }
};

blog_class.prototype.addBlogEvents = function() {
    var self = this;

    // Set blog title permalink hint on keypress
    $("#BlogTitle").keyup(function() {
        // Only update permalink if it hasn't been touched yet
        var permLinkModified = $("#PermaLinkEnd").attr("data-modified").toLowerCase() == "true";
        if (!permLinkModified) {
            var val = $(this).val();
            val = val.replace(/ /g, self.blogSpaceReplacementChar);
            $("#PermaLinkEditPane").text(val);
        }
    });

    // Fix spacing issues on page load
    $("#BlogTitle").trigger("keyup");

    // set permalink prefix
    $("#CategoriesModule ul li.catListing input").change(function () {
        var catName = $(this).val()
            .replace("&", "and")
             .replace("+", "and")
            .replace(/[^a-zA-Z ]/g, "")
            .replace(/ /g, self.blogSpaceReplacementChar).toLowerCase();
        $('.category-permalink').text(encodeURI(catName) + '/');
    });

    // Save Blog from edit / add blog page
    $("#SaveBlog").click(function() {
        var mainCategory = $("#CategoriesModule ul li.catListing input:checked").val();
        var content = CKEDITOR.instances.CKEDITBLOG.getData();
        var featText = CKEDITOR.instances.ShortDescription.getData();
        var data = {
            entity: {
                Title: $("#BlogTitle").val(),
                HtmlContent: content,
                ImageUrl: $("#FeaturedImage").val(),
                IsActive: $("#IsPublicCheck").is(":checked"),
                IsFeatured: $("#IsFeaturedCheck").is(":checked"),
                Author: $("#Author option:selected").text(),
                AuthorId: $("#Author option:selected").attr("data-id"),
                BlogID: self.blogId,
                MainCategory: mainCategory,
                Tags: $("#BlogTags").val(),
                ShortDesc: featText,
                MetaDescription: $("#MetaDescription").val(),
                OGTitle: $("#OGTitle").val(),
                OGImage: $("#OGImage").val(),
                OGType: $("#OGType").val(),
                OGUrl: $("#OGUrl").val(),
                Canonical: $("#Canonical").val(),
                Date: $("#PublishDate").val(),
                PermaLink: self.formatBlogLink($("#PermaLinkEnd").text())
            }
        };

        // Currently all we really need is a title for validation
        if ($("#BlogTitle").attr("value").length < 1) {
            alert("Please enter a title.");
            return false;
        }

        $("#SaveSpinner").show();
        var postUrl = self.blogIsSaved ? "/admin/blog/modifyblog" : "/admin/blog/addblog";
        $.ajax({
            url: postUrl,
            type: "POST",
            dataType: 'json',
            contentType: 'application/json; charset=utf-8',
            data: JSON.stringify(data, null, 2),
            success: function(data) {
                self.blogIsSaved = true;
                self.blogId = data.id;

                var noty_id = noty({ text: 'Changes saved successfully.', type: 'success', timeout: 3000 });
                $("#SaveSpinner").hide();
            },
            error: function(data) {
                var noty_id = noty({ text: 'There was an error processing your request.', type: 'error' });
                $("#SaveSpinner").hide();
            }
        });
    });
};

blog_class.prototype.initTinyMCEEvents = function() {
    //tinymce on blog editor
    $('div.editArea textarea').tinymce({
        // Location of TinyMCE script
        script_url: '/Scripts/tinyMCE/tiny_mce.js',
        height: "450px",
        // General options
        theme: "advanced",
        skin: "o2k7",
        skin_variant: "silver",
        plugins: "autolink,spellchecker,lists,save,advimage,advlink,iespell,inlinepopups,insertdatetime,media,searchreplace,contextmenu,directionality,noneditable,template,safari,xhtmlxtras",

        // Theme options
        theme_advanced_buttons1: "bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,bullist,numlist,|,outdent,indent,|,forecolor,backcolor,|,fontselect,fontsizeselect,link,|,code,|,blockquote,|,image,|,insertdate,inserttime,|,sub,sup,spellchecker",
        theme_advanced_buttons2: "",
        theme_advanced_buttons3: "",

        theme_advanced_toolbar_location: "top",
        theme_advanced_toolbar_align: "left",
        theme_advanced_statusbar_location: "bottom",
        theme_advanced_resizing: false,

        // Example content CSS (should be your site CSS)
        content_css: "/CSS/site.css",

        // Drop lists for link/image/media/template dialogs
        template_external_list_url: "lists/template_list.js",
        external_link_list_url: "lists/link_list.js",
        external_image_list_url: "lists/image_list.js",
        media_external_list_url: "lists/media_list.js",
    });

};

blog_class.prototype.initTagEvents = function () {

    // Tag Manager
    $("#BlogTags").tagit({
        allowSpaces: true
    });

    $('.commonTags a').click(function () {
        var tag = $(this).attr("data-tag");

        $('#BlogTags').tagit("createTag", tag);

        return false;
    });
};

// Keep at the bottom
$(document).ready(function() {
    blog = new blog_class();
    blog.initPageEvents();
});<|endoftext|>