/** * @module bindings/dom */ /* eslint-env browser */ /* global getSelection */ import { YXmlText } from '../../types/YXmlText.mjs' import { YXmlHook } from '../../types/YXmlHook.mjs' import { removeDomChildrenUntilElementFound } from './util.mjs' const findScrollReference = scrollingElement => { if (scrollingElement !== null) { let anchor = getSelection().anchorNode if (anchor == null) { let children = scrollingElement.children // only iterate through non-text nodes for (let i = 0; i < children.length; i++) { const elem = children[i] const rect = elem.getBoundingClientRect() if (rect.top >= 0) { return { elem, top: rect.top } } } } else { /** * @type {Element} */ let elem = anchor.parentElement if (anchor instanceof Element) { elem = anchor } return { elem, top: elem.getBoundingClientRect().top } } } return null } const fixScroll = (scrollingElement, ref) => { if (ref !== null) { const { elem, top } = ref const currentTop = elem.getBoundingClientRect().top const newScroll = scrollingElement.scrollTop + currentTop - top if (newScroll >= 0) { scrollingElement.scrollTop = newScroll } } } /** * @private */ export const typeObserver = function (events) { this._mutualExclude(() => { const scrollRef = findScrollReference(this.scrollingElement) events.forEach(event => { const yxml = event.target const dom = this.typeToDom.get(yxml) if (dom !== undefined && dom !== false) { if (yxml.constructor === YXmlText) { 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(childType => { const childNode = this.typeToDom.get(childType) switch (childNode) { case undefined: // Does not exist. Create it. const node = childType.toDom(this.opts.document, this.opts.hooks, this) dom.insertBefore(node, currentChild) break case false: // nop break default: // Is already attached to the dom. // Find it and remove all dom nodes in-between. removeDomChildrenUntilElementFound(dom, currentChild, childNode) currentChild = childNode.nextSibling break } }) removeDomChildrenUntilElementFound(dom, currentChild, null) } } } }) fixScroll(this.scrollingElement, scrollRef) }) }