{"version":3,"sources":["reorderable-repeat.js"],"names":[],"mappings":"AAoEA;IA8BE,4MAoCC;IAzDD,WAAgB;IAMhB,WAAgB;IAEhB,eAA6B;IAC7B,kBAAyB;IACzB,UAAwB;IAiBtB,mBAAsB;IAItB,QAAY;IACZ,eAA0B;IAC1B,mBAAkC;IAClC,gBAA4B;IAC5B,iBAA8B;IAC9B,iBAA8B;IAC9B,cAAwB;IACxB,qBAAoD;IACpD,qBAAsC;IACtC,qBAAsC;IACtC,cAAwB;IAExB,wBAA2B;IAC3B,sBAA4F;IAU5F,2BAA+D;IAE/D,WAAqC;IAGvC,uCAEC;IAOD,sDAiFC;IAhFC;;;MAAgD;IAChD,oBAA4D;IAC5D,mBAA+G;IAC/G,mBAkEC;IAMC,kBAAkD;IAUtD,eAkBC;IAED,0CAwBC;IAGD,qBAkBC;IAGD,sBAMC;IAED,4BAEC;IAED,oBAEC;IAED,gBA6BC;IAnBC,cAAmE;IAqBrE,uCAmBC;IAGD,iBAAqD;IACrD,aAA0C;IAC1C,sBAAqD;IACrD,eAAiJ;IAEjJ,yDAIC;IAED,wEAIC;IAED,mDAEC;IAED,4DAGC;IAED,6EAGC;IAED,oEAGC;IAED,gCAaC;IAED,qDAMC;IAED,mCAQC;IAED,yBAkBC;IAED,yCAUC;IAED,uCASC;IAED,gCAqBC;IAED,uCAkBC;IAED,2DAsCC;IAED,8BAyDC;IAED,gCAKC;IAED,4DAsEC;CACF","file":"reorderable-repeat.d.ts","sourcesContent":["/*eslint no-loop-func:0, no-unused-vars:0*/\nimport {inject} from 'aurelia-dependency-injection';\nimport {\n  ObserverLocator,\n  observable,\n  BindingEngine,\n  BindingBehavior,\n  ValueConverter,\n  getContextFor\n} from 'aurelia-binding';\nimport {\n  BoundViewFactory,\n  TargetInstruction,\n  ViewSlot,\n  ViewResources,\n  customAttribute,\n  bindable,\n  templateController\n} from 'aurelia-templating';\nimport {\n  getItemsSourceExpression,\n  isOneTime,\n  updateOneTimeBinding,\n  viewsRequireLifecycle,\n  AbstractRepeater\n} from 'aurelia-templating-resources';\nimport {ReorderableRepeatStrategyLocator} from './reorderable-repeat-strategy-locator';\nimport {DndService} from 'bcx-aurelia-dnd';\nimport {EventAggregator} from 'aurelia-event-aggregator';\nimport {TaskQueue} from 'aurelia-task-queue';\nimport repeaterDndType from './repeater-dnd-type';\nimport {ReorderableGroupMap} from './reorderable-group-map';\n\nlet seed = 0;\n\nconst classes = (function() {\n  let cache = {};\n  let start = '(?:^|\\\\s)';\n  let end = '(?:\\\\s|$)';\n\n  function lookupClass(className) {\n    let cached = cache[className];\n    if (cached) {\n      cached.lastIndex = 0;\n    } else {\n      cache[className] = cached = new RegExp(start + className + end, 'g');\n    }\n    return cached;\n  }\n\n  function addClass(el, className) {\n    let current = el.className;\n    if (!current.length) {\n      el.className = className;\n    } else if (!lookupClass(className).test(current)) {\n      el.className += ' ' + className;\n    }\n  }\n\n  function rmClass(el, className) {\n    el.className = el.className.replace(lookupClass(className), ' ').trim();\n  }\n  return {add: addClass, rm: rmClass};\n}());\n\n/**\n* Binding to iterate over Array to genereate a template for each iteration.\n*/\n@customAttribute('reorderable-repeat')\n@templateController\n@inject(EventAggregator, TaskQueue, BindingEngine, DndService, BoundViewFactory, TargetInstruction, ViewSlot, ViewResources, ObserverLocator, ReorderableRepeatStrategyLocator, ReorderableGroupMap)\nexport class ReorderableRepeat extends AbstractRepeater {\n  /**\n  * List of items to bind the repeater to.\n  *\n  * @property items\n  */\n  @bindable items;\n  /**\n  * Local variable which gets assigned on each iteration.\n  *\n  * @property local\n  */\n  @bindable local;\n\n  @observable intention = null;\n  @observable patchedItems;\n  @observable type = null;\n\n  /**\n * Creates an instance of Repeat.\n * @param viewFactory The factory generating the view\n * @param instruction The instructions for how the element should be enhanced.\n * @param viewResources Collection of resources used to compile the the views.\n * @param viewSlot The slot the view is injected in to.\n * @param observerLocator The observer locator instance.\n * @param collectionStrategyLocator The strategy locator to locate best strategy to iterate the collection.\n */\n  constructor(ea, taskQueue, bindingEngine, dndService, viewFactory, instruction, viewSlot, viewResources, observerLocator, strategyLocator, groupMap) {\n    super({\n      local: 'item',\n      viewsRequireLifecycle: viewsRequireLifecycle(viewFactory)\n    });\n\n    this.repeaterId = seed;\n    this.type = this.repeaterId;\n    seed += 1;\n\n    this.ea = ea;\n    this.taskQueue = taskQueue;\n    this.bindingEngine = bindingEngine;\n    this.dndService = dndService;\n    this.viewFactory = viewFactory;\n    this.instruction = instruction;\n    this.viewSlot = viewSlot;\n    this.lookupFunctions = viewResources.lookupFunctions;\n    this.observerLocator = observerLocator;\n    this.strategyLocator = strategyLocator;\n    this.groupMap = groupMap;\n\n    this.ignoreMutation = false;\n    this.sourceExpression = getItemsSourceExpression(this.instruction, 'reorderable-repeat.for');\n    if (this.sourceExpression instanceof BindingBehavior) {\n      throw new Error('BindingBehavior is not supported in reorderable-repeat');\n    }\n    if (this.sourceExpression instanceof ValueConverter) {\n      throw new Error('ValueConverter is not supported in reorderable-repeat');\n    }\n    if (isOneTime(this.sourceExpression)) {\n      throw new Error('oneTime binding is not supported in reorderable-repeat');\n    }\n    this.viewsRequireLifecycle = viewsRequireLifecycle(viewFactory);\n\n    this.group = this._reorderableGroup();\n  }\n\n  call(context, changes) {\n    this[context](this.items, changes);\n  }\n\n  /**\n  * Binds the repeat to the binding context and override context.\n  * @param bindingContext The binding context.\n  * @param overrideContext An override context for binding.\n  */\n  bind(bindingContext, overrideContext) {\n    this.scope = { bindingContext, overrideContext };\n    this.matcherBinding = this._captureAndRemoveMatcherBinding();\n    this.arrayObserver = this.bindingEngine.collectionObserver(this.items).subscribe(this._itemsMutated.bind(this));\n    this._subsribers = [\n      this.ea.subscribe('dnd:willStart', () => {\n        this.intention = null;\n        this.views().forEach(v => {\n          classes.rm(v.firstChild, 'reorderable-repeat-reordering');\n          classes.rm(v.firstChild, 'reorderable-repeat-dragging-me');\n        });\n      }),\n      this.ea.subscribe('dnd:didCancel', () => {\n        this.intention = null;\n        this.views().forEach(v => {\n          classes.rm(v.firstChild, 'reorderable-repeat-reordering');\n          classes.rm(v.firstChild, 'reorderable-repeat-dragging-me');\n        });\n      }),\n      this.ea.subscribe('dnd:didEnd', () => {\n        this.views().forEach(v => {\n          classes.rm(v.firstChild, 'reorderable-repeat-reordering');\n          classes.rm(v.firstChild, 'reorderable-repeat-dragging-me');\n        });\n\n        if (!this.intention) return;\n        const {item, fromIndex, fromRepeaterId, toIndex, toRepeaterId} = this.intention;\n        this.intention = null;\n\n        const repeaterId = repeaterDndType(this.repeaterId);\n        if (repeaterId !== fromRepeaterId && repeaterId !== toRepeaterId) return;\n\n        // no change\n        if (fromRepeaterId === toRepeaterId && fromIndex === toIndex) return;\n\n        const change = {item, fromIndex, toIndex};\n\n        if (repeaterId === fromRepeaterId) {\n          this.items.splice(fromIndex, 1);\n          change.removedFromThisList = true;\n        }\n\n        if (repeaterId === toRepeaterId) {\n          this.items.splice(toIndex, 0, item);\n          change.insertedToThisList = true;\n        }\n\n        const afterReordering = this._reorderableAfterReorderingFunc();\n        if (afterReordering) {\n          // in group mode, wait for all models updated\n          // before hitting callback\n          this.taskQueue.queueMicroTask(() => {\n            afterReordering(this.items, change);\n          });\n        }\n      }),\n      this.ea.subscribe('reorderable-group:intention-changed', intention => {\n        if (intention.type !== repeaterDndType(this.type)) return;\n\n        // avoid double trigger of intentionChanged;\n        if (this.intention &&\n            intention.type === this.intention.type &&\n            intention.fromIndex === this.intention.fromIndex &&\n            intention.fromRepeaterId === this.intention.fromRepeaterId &&\n            intention.toIndex === this.intention.toIndex &&\n            intention.toRepeaterId === this.intention.toRepeaterId) return;\n\n        // sync intention from other repeater\n        this.intention = intention;\n      })\n    ];\n    if (typeof this.group === 'string') {\n      this.type = this.group;\n    } else if (this.group) {\n      // group is a binding expression or interpolation binding expression.\n      this.group.targetProperty = 'type',\n      this._typeBinding = this.group.createBinding(this);\n      this._typeBinding.bind(this.scope);\n    }\n    this.patchedItems = [...this.items];\n    this.patchedItemsChanged();\n  }\n\n  /**\n  * Unbinds the repeat\n  */\n  unbind() {\n    this.groupMap.remove(this);\n    if (this._typeBinding) {\n      this._typeBinding.unbind();\n      delete this._typeBinding;\n    }\n\n    if (this.arrayObserver) {\n      this.arrayObserver.dispose();\n      this.arrayObserver = null;\n    }\n    this._subsribers.forEach(s => s.dispose());\n    this._subsribers = [];\n\n    this.removeAllViews(true, true);\n    this.scope = null;\n    this.items = null;\n    this.matcherBinding = null;\n  }\n\n  intentionChanged(newIntention) {\n    if (newIntention) {\n      const repeaterId = repeaterDndType(this.repeaterId);\n\n      const {item, fromIndex, fromRepeaterId, toIndex, toRepeaterId} = newIntention;\n\n      let patched = [...this.items];\n\n      if (repeaterId === fromRepeaterId) {\n        patched.splice(fromIndex, 1);\n      }\n\n      if (repeaterId === toRepeaterId) {\n        patched.splice(toIndex, 0, item);\n      }\n\n      this.patchedItems = patched;\n    } else {\n      if (this.items) {\n        this.patchedItems = [...this.items];\n      } else {\n        this.patchedItems = null;\n      }\n    }\n  }\n\n  // every time the items property changes.\n  itemsChanged() {\n    // still bound?\n    if (!this.scope) {\n      return;\n    }\n\n    if (this.arrayObserver) {\n      this.arrayObserver.dispose();\n      this.arrayObserver = null;\n    }\n\n    this.arrayObserver = this.bindingEngine.collectionObserver(this.items).subscribe(this._itemsMutated.bind(this));\n\n    if (this.intention === null) {\n      this.patchedItems = [...this.items];\n    } else {\n      this.intention = null;\n    }\n  }\n\n  // every time the items array add/delete.\n  _itemsMutated() {\n    if (this.intention === null) {\n      this.patchedItems = [...this.items];\n    } else {\n      this.intention = null;\n    }\n  }\n\n  patchedItemsChanged() {\n    this._reload();\n  }\n\n  typeChanged() {\n    this._reload();\n  }\n\n  _reload() {\n    // still bound?\n    if (!this.scope) {\n      return;\n    }\n\n    if (!this.patchedItems) {\n      return;\n    }\n\n    this.strategy = this.strategyLocator.getStrategy(this.patchedItems);\n    if (!this.strategy) {\n      throw new Error(`Value for '${this.sourceExpression}' is non-repeatable`);\n    }\n\n    this.strategy.instanceChanged(this, this.patchedItems);\n    this.taskQueue.queueMicroTask(() => {\n      // avoid this async task after unbind\n      if (!this.scope) {\n        return;\n      }\n\n      this.groupMap.remove(this);\n      this.views().forEach(view => {\n        this._unRegisterDnd(view);\n        this._registerDnd(view);\n      });\n      this.groupMap.add(this);\n    });\n  }\n\n  _captureAndRemoveMatcherBinding() {\n    if (this.viewFactory.viewFactory) {\n      const instructions = this.viewFactory.viewFactory.instructions;\n      const instructionIds = Object.keys(instructions);\n      for (let i = 0; i < instructionIds.length; i++) {\n        const expressions = instructions[instructionIds[i]].expressions;\n        if (expressions) {\n          for (let ii = 0; i < expressions.length; i++) {\n            if (expressions[ii].targetProperty === 'matcher') {\n              const matcherBinding = expressions[ii];\n              expressions.splice(ii, 1);\n              return matcherBinding;\n            }\n          }\n        }\n      }\n    }\n\n    return undefined;\n  }\n\n  // @override AbstractRepeater\n  viewCount() { return this.viewSlot.children.length; }\n  views() { return this.viewSlot.children; }\n  view(index) { return this.viewSlot.children[index]; }\n  matcher() { return this.matcherBinding ? this.matcherBinding.sourceExpression.evaluate(this.scope, this.matcherBinding.lookupFunctions) : null; }\n\n  addView(bindingContext, overrideContext) {\n    let view = this.viewFactory.create();\n    view.bind(bindingContext, overrideContext);\n    this.viewSlot.add(view);\n  }\n\n  insertView(index, bindingContext, overrideContext) {\n    let view = this.viewFactory.create();\n    view.bind(bindingContext, overrideContext);\n    this.viewSlot.insert(index, view);\n  }\n\n  moveView(sourceIndex, targetIndex) {\n    this.viewSlot.move(sourceIndex, targetIndex);\n  }\n\n  removeAllViews(returnToCache, skipAnimation) {\n    this.views().forEach(view => this._unRegisterDnd(view));\n    return this.viewSlot.removeAll(returnToCache, skipAnimation);\n  }\n\n  removeViews(viewsToRemove, returnToCache, skipAnimation) {\n    viewsToRemove.forEach(view => this._unRegisterDnd(view));\n    return this.viewSlot.removeMany(viewsToRemove, returnToCache, skipAnimation);\n  }\n\n  removeView(index, returnToCache, skipAnimation) {\n    this._unRegisterDnd(this.view(index));\n    return this.viewSlot.removeAt(index, returnToCache, skipAnimation);\n  }\n\n  updateBindings(view) {\n    let j = view.bindings.length;\n    while (j--) {\n      updateOneTimeBinding(view.bindings[j]);\n    }\n    j = view.controllers.length;\n    while (j--) {\n      let k = view.controllers[j].boundProperties.length;\n      while (k--) {\n        let binding = view.controllers[j].boundProperties[k].binding;\n        updateOneTimeBinding(binding);\n      }\n    }\n  }\n\n  _additionalAttribute(view, attribute) {\n    if (view && view.firstChild && view.firstChild.au && view.firstChild.au[attribute]) {\n      return view.firstChild.au[attribute].instruction.attributes[attribute];\n    }\n    // Fall back to plain string attribute\n    return this._getPlainAttribute(attribute);\n  }\n\n  _getPlainAttribute(name) {\n    // only get the string value before view rendering\n    if (this.viewFactory && this.viewFactory.viewFactory) {\n      const node = this.viewFactory.viewFactory.template.firstChild;\n      if (node && node.hasAttribute(name)) {\n        return node.getAttribute(name);\n      }\n    }\n  }\n\n  _reorderableGroup() {\n    if (this.viewFactory && this.viewFactory.viewFactory) {\n      const node = this.viewFactory.viewFactory.template.firstChild;\n      const targetId = node.getAttribute('au-target-id');\n      const instruction = this.viewFactory.viewFactory.instructions[targetId];\n      if (instruction) {\n        const bi = instruction.behaviorInstructions.find(bi => bi.attrName === 'reorderable-group');\n        if (bi) {\n          const exp = bi.attributes && bi.attributes['reorderable-group'];\n          if (exp && typeof exp.createBinding === 'function') {\n            // binding expression or interpolation binding expression.\n            return exp;\n          }\n        }\n      }\n    }\n\n    return this._getPlainAttribute('reorderable-group');\n  }\n\n  _reorderableDirection(view) {\n    let attr = this._additionalAttribute(view, 'reorderable-direction');\n    if (attr && attr.sourceExpression) {\n      attr = attr.sourceExpression.evaluate(this.scope);\n    }\n\n    if (typeof attr === 'string') {\n      return attr.toLowerCase() || 'down';\n    }\n    return 'down';\n  }\n\n  _dndHandlerSelector(view) {\n    let attr = this._additionalAttribute(view, 'reorderable-dnd-handler-selector');\n    if (attr && attr.sourceExpression) {\n      attr = attr.sourceExpression.evaluate(this.scope);\n    }\n\n    if (typeof attr === 'string') {\n      return attr;\n    }\n  }\n\n  _dndPreviewFunc(view) {\n    const func = this._additionalAttribute(view, 'reorderable-dnd-preview');\n\n    if (!func) {\n      return null;\n    } else if (typeof func === 'string') {\n      const context = getContextFor(func, this.scope);\n      let funcCall = context[func];\n\n      if (typeof funcCall === 'function') {\n        return funcCall.bind(context);\n      }\n      throw new Error(\"'reorderable-dnd-preview' must be a function or evaluate to a function\");\n    } else if (func.sourceExpression) {\n      // TODO test preview\n      return (_, scope) => {\n        return func.sourceExpression.evaluate(scope);\n      };\n    } else {\n      throw new Error(\"'reorderable-dnd-preview' must be a function or evaluate to a function\");\n    }\n  }\n\n  _reorderableAfterReorderingFunc() {\n    const func = this._additionalAttribute(this.view(0), 'reorderable-after-reordering');\n\n    if (!func) {\n      return null;\n    } else if (typeof func === 'string') {\n      const context = getContextFor(func, this.scope);\n      let funcCall = context[func];\n\n      if (typeof funcCall === 'function') {\n        return funcCall.bind(context);\n      }\n      throw new Error(\"'reorderable-after-reordering' must be a function or evaluate to a function\");\n    } else if (func.sourceExpression) {\n      return () => func.sourceExpression.evaluate(this.scope);\n    } else {\n      throw new Error(\"'reorderable-after-reordering' must be a function or evaluate to a function\");\n    }\n  }\n\n  _dndHover(location, index, direction) {\n    const repeaterId = repeaterDndType(this.repeaterId);\n    // bypass hovering on itself\n    if (this.intention &&\n        this.intention.toRepeaterId === repeaterId &&\n        this.intention.toIndex === index) {\n      return;\n    }\n\n    const {model} = this.dndService;\n\n    const {mouseEndAt, targetElementRect} = location;\n    const x = mouseEndAt.x - targetElementRect.x;\n    const y = mouseEndAt.y - targetElementRect.y;\n\n    let inLeastHalf;\n\n    if (direction === 'left') {\n      inLeastHalf = x > (targetElementRect.width / 2);\n    } else if (direction === 'right') {\n      inLeastHalf = x < (targetElementRect.width / 2);\n    } else if (direction === 'up') {\n      inLeastHalf = y > (targetElementRect.height / 2);\n    } else /* if (direction === 'down') */ {\n      inLeastHalf = y < (targetElementRect.height / 2);\n    }\n\n    // because of unknown size diff between items,\n    // check half size to avoid endless bouncing of swapping two items.\n    if (inLeastHalf ||\n        // or starting on itself\n        (!this.intention && model.repeaterId === repeaterId && index === model.index)) {\n      // hover over top half, user wants to move smth before this item.\n      this._updateIntention(index, true);\n    } else {\n      // hover over bottom half, user wants to move smth after this item.\n      this._updateIntention(index, false);\n    }\n  }\n\n  _registerDnd(view) {\n    const {local} = this;\n    const el = view.firstChild;\n    const item = view.bindingContext[local];\n    const index = view.overrideContext.$index;\n    const handlerSelector = this._dndHandlerSelector(view);\n    let handler;\n    if (handlerSelector) {\n      handler = el.querySelector(handlerSelector);\n    }\n\n    const repeaterId = repeaterDndType(this.repeaterId);\n\n    const direction = this._reorderableDirection(view);\n    const _previewFunc = this._dndPreviewFunc(view);\n\n    this.dndService.addSource({\n      dndModel: () => ({type: repeaterDndType(this.type), index, item, repeaterId}),\n      dndPreview: _previewFunc && (() => _previewFunc(item, view)),\n      dndElement: el\n    }, handler && {handler});\n\n    this.dndService.addTarget({\n      dndElement: el,\n      dndCanDrop: (model) => {\n        if (model.type !== repeaterDndType(this.type)) return false;\n\n        const {intention} = this;\n        const inSameGroup = model.repeaterId === repeaterId;\n\n        this.taskQueue.queueMicroTask(() => {\n          classes.add(el, 'reorderable-repeat-reordering');\n        });\n\n        let draggingMe;\n\n        if (intention) {\n          draggingMe = intention.toRepeaterId === repeaterId &&\n                       intention.toIndex === index;\n        } else if (inSameGroup) {\n          draggingMe = model.index === index;\n        }\n\n        if (draggingMe) {\n          // I am under dragging\n          this.taskQueue.queueMicroTask(() => {\n            classes.add(el, 'reorderable-repeat-dragging-me');\n          });\n        }\n\n        return true;\n      },\n      dndHover: (location) => {\n        this._dndHover(location, index, direction);\n      },\n      dndDrop() { /* no-op */}\n    });\n  }\n\n  _unRegisterDnd(view) {\n    classes.rm(view.firstChild, 'reorderable-repeat-reordering');\n    classes.rm(view.firstChild, 'reorderable-repeat-dragging-me');\n    this.dndService.removeSource(view.firstChild);\n    this.dndService.removeTarget(view.firstChild);\n  }\n\n  _updateIntention(targetIndex, beforeTarget) {\n    const {isProcessing, model} = this.dndService;\n    if (!isProcessing) return;\n    if (model.type !== repeaterDndType(this.type)) return;\n\n    const repeaterId = repeaterDndType(this.repeaterId);\n    const isUsingGroup = model.type !== model.repeaterId;\n    const inSameGroup = model.repeaterId === repeaterId;\n\n    if (targetIndex < 0) return;\n\n    let originalIndex;\n    let currentIndex;\n    let nextIndex;\n\n    if (inSameGroup) {\n      if (this.intention) {\n        originalIndex = this.intention.fromIndex;\n        currentIndex = this.intention.toIndex;\n      } else {\n        originalIndex = model.index;\n        if (originalIndex < 0) return;\n        currentIndex = originalIndex;\n      }\n    } else {\n      if (this.intention && this.intention.toRepeaterId === repeaterId) {\n        originalIndex = this.intention.fromIndex;\n        currentIndex = this.intention.toIndex;\n      } else {\n        originalIndex = model.index;\n        if (originalIndex < 0) return;\n        currentIndex = targetIndex;\n      }\n    }\n\n    if (currentIndex < targetIndex) {\n      // grabbed item is currently above target\n      if (beforeTarget) {\n        nextIndex = targetIndex - 1;\n      } else {\n        nextIndex = targetIndex;\n      }\n    } else /* if (currentIndex > targetIndex) or across repeaters */ {\n      // grabbed item is currently below target\n      if (beforeTarget) {\n        nextIndex = targetIndex;\n      } else {\n        nextIndex = targetIndex + 1;\n      }\n    }\n\n    if (!this.intention ||\n        this.intention.fromIndex !== originalIndex ||\n        this.intention.fromRepeaterId !== model.repeaterId ||\n        this.intention.toIndex !== nextIndex ||\n        this.intention.toRepeaterId !== repeaterId) {\n      this.intention = {\n        type: model.type,\n        item: model.item,\n        fromIndex: originalIndex,\n        fromRepeaterId: model.repeaterId,\n        toIndex: nextIndex,\n        toRepeaterId: repeaterId\n      };\n\n      if (isUsingGroup) {\n        // let other repeaters know\n        this.ea.publish('reorderable-group:intention-changed', this.intention);\n      }\n    }\n  }\n}\n"],"sourceRoot":"../src/"}