Home Reference Source

src/Types/YXml/utils.js

import { YXmlText, YXmlHook } from './YXml.js'

export function defaultDomFilter (node, attributes) {
  return attributes
}

export function getAnchorViewPosition (scrollElement) {
  if (scrollElement == null) {
    return null
  }
  let anchor = document.getSelection().anchorNode
  if (anchor != null) {
    let top = getBoundingClientRect(anchor).top
    if (top >= 0 && top <= document.documentElement.clientHeight) {
      return {
        anchor: anchor,
        top: top
      }
    }
  }
  return {
    anchor: null,
    scrollTop: scrollElement.scrollTop,
    scrollHeight: scrollElement.scrollHeight
  }
}

// get BoundingClientRect that works on text nodes
export function getBoundingClientRect (element) {
  if (element.getBoundingClientRect != null) {
    // is element node
    return element.getBoundingClientRect()
  } else {
    // is text node
    if (element.parentNode == null) {
      // range requires that text nodes have a parent
      let span = document.createElement('span')
      span.appendChild(element)
    }
    let range = document.createRange()
    range.selectNode(element)
    return range.getBoundingClientRect()
  }
}

export function fixScrollPosition (scrollElement, fix) {
  if (scrollElement !== null && fix !== null) {
    if (fix.anchor === null) {
      if (scrollElement.scrollTop === fix.scrollTop) {
        scrollElement.scrollTop = scrollElement.scrollHeight - fix.scrollHeight
      }
    } else {
      scrollElement.scrollTop = getBoundingClientRect(fix.anchor).top - fix.top
    }
  }
}


export function reflectChangesOnDom (events, _document) {
  // Make sure that no filtered attributes are applied to the structure
  // if they were, delete them
  /*
  events.forEach(event => {
    const target = event.target
    if (event.attributesChanged === undefined) {
      // event.target is Y.XmlText
      return
    }
    const keys = this._domFilter(target.nodeName, Array.from(event.attributesChanged))
    if (keys === null) {
      target._delete()
    } else {
      const removeKeys = new Set() // is a copy of event.attributesChanged
      event.attributesChanged.forEach(key => { removeKeys.add(key) })
      keys.forEach(key => {
        // remove all accepted keys from removeKeys
        removeKeys.delete(key)
      })
      // remove the filtered attribute
      removeKeys.forEach(key => {
        target.removeAttribute(key)
      })
    }
  })
  */
  this._mutualExclude(() => {
    events.forEach(event => {
      const yxml = event.target
      const dom = yxml._dom
      if (dom != null) {
        // TODO: do this once before applying stuff
        // let anchorViewPosition = getAnchorViewPosition(yxml._scrollElement)
        if (yxml.constructor === YXmlText) {
          yxml._dom.nodeValue = yxml.toString()
        } else if (event.attributesChanged !== undefined) {
          // update attributes
          event.attributesChanged.forEach(attributeName => {
            const value = yxml.getAttribute(attributeName)
            if (value === undefined) {
              dom.removeAttribute(attributeName)
            } else {
              dom.setAttribute(attributeName, value)
            }
          })
          /*
           * TODO: instead of hard-checking the types, it would be best to
           *       specify the type's features. E.g.
           *         - _yxmlHasAttributes
           *         - _yxmlHasChildren
           *       Furthermore, the features shouldn't be encoded in the types,
           *       only in the attributes (above)
           */
          if (event.childListChanged && yxml.constructor !== YXmlHook) {
            let currentChild = dom.firstChild
            yxml.forEach(function (t) {
              let expectedChild = t.getDom(_document)
              if (expectedChild.parentNode === dom) {
                // is already attached to the dom. Look for it
                while (currentChild !== expectedChild) {
                  let del = currentChild
                  currentChild = currentChild.nextSibling
                  dom.removeChild(del)
                }
                currentChild = currentChild.nextSibling
              } else {
                // this dom is not yet attached to dom
                dom.insertBefore(expectedChild, currentChild)
              }
            })
            while (currentChild !== null) {
              let tmp = currentChild.nextSibling
              dom.removeChild(currentChild)
              currentChild = tmp
            }
          }
        }
        /* TODO: smartscrolling
        .. else if (event.type === 'childInserted' || event.type === 'insert') {
          let nodes = event.values
          for (let i = nodes.length - 1; i >= 0; i--) {
            let node = nodes[i]
            node.setDomFilter(yxml._domFilter)
            node.enableSmartScrolling(yxml._scrollElement)
            let dom = node.getDom()
            let fixPosition = null
            let nextDom = null
            if (yxml._content.length > event.index + i + 1) {
              nextDom = yxml.get(event.index + i + 1).getDom()
            }
            yxml._dom.insertBefore(dom, nextDom)
            if (anchorViewPosition === null) {
              // nop
            } else if (anchorViewPosition.anchor !== null) {
              // no scrolling when current selection
              if (!dom.contains(anchorViewPosition.anchor) && !anchorViewPosition.anchor.contains(dom)) {
                fixPosition = anchorViewPosition
              }
            } else if (getBoundingClientRect(dom).top <= 0) {
              // adjust scrolling if modified element is out of view,
              // there is no anchor element, and the browser did not adjust scrollTop (this is checked later)
              fixPosition = anchorViewPosition
            }
            fixScrollPosition(yxml._scrollElement, fixPosition)
          }
        } else if (event.type === 'childRemoved' || event.type === 'delete') {
          for (let i = event.values.length - 1; i >= 0; i--) {
            let dom = event.values[i]._dom
            let fixPosition = null
            if (anchorViewPosition === null) {
              // nop
            } else if (anchorViewPosition.anchor !== null) {
              // no scrolling when current selection
              if (!dom.contains(anchorViewPosition.anchor) && !anchorViewPosition.anchor.contains(dom)) {
                fixPosition = anchorViewPosition
              }
            } else if (getBoundingClientRect(dom).top <= 0) {
              // adjust scrolling if modified element is out of view,
              // there is no anchor element, and the browser did not adjust scrollTop (this is checked later)
              fixPosition = anchorViewPosition
            }
            dom.remove()
            fixScrollPosition(yxml._scrollElement, fixPosition)
          }
        }
        */
      }
    })
  })
}