import { isNull, isFunction, each, stripEL, keys, isArray, contains, isKey, isOdd, isWidget, isNotNull, has } from "../2.base"; import { Widget } from "../4.widget"; import { _lazyCreateWidget, Providers } from "../5.inject"; import { shortcut } from "../decorator"; import { pixFormat, Events } from "../constant"; /** * 布局容器类 * * @cfg {JSON} options 配置属性 * @cfg {Boolean} [options.scrollable=false] 子组件超出容器边界之后是否会出现滚动条 * @cfg {Boolean} [options.scrollx=false] 子组件超出容器边界之后是否会出现横向滚动条 * @cfg {Boolean} [options.scrolly=false] 子组件超出容器边界之后是否会出现纵向滚动条 */ @shortcut() export class Layout extends Widget { static xtype = "bi.layout"; props() { return { scrollable: null, // true, false, null scrollx: false, // true, false scrolly: false, // true, false items: [], innerHgap: 0, innerVgap: 0, }; } render() { const o = this.options; this._init4Margin(); this._init4Scroll(); if (isFunction(o.columnSize)) { const columnSizeFn = o.columnSize; o.columnSize = this.__watch(columnSizeFn, (context, newValue) => { o.columnSize = newValue; this.resize(); }); } if (isFunction(o.rowSize)) { const rowSizeFn = o.rowSize; o.rowSize = this.__watch(rowSizeFn, (context, newValue) => { o.rowSize = newValue; this.resize(); }); } } _init4Margin() { if (this.options.top) { this.element.css("top", pixFormat(this.options.top)); } if (this.options.left) { this.element.css("left", pixFormat(this.options.left)); } if (this.options.bottom) { this.element.css("bottom", pixFormat(this.options.bottom)); } if (this.options.right) { this.element.css("right", pixFormat(this.options.right)); } } _init4Scroll() { switch (this.options.scrollable) { case true: case "xy": this.element.css("overflow", "auto"); return; case false: this.element.css("overflow", "hidden"); return; case "x": this.element.css({ "overflow-x": "auto", "overflow-y": "hidden", }); return; case "y": this.element.css({ "overflow-x": "hidden", "overflow-y": "auto", }); return; default : break; } if (this.options.scrollx) { this.element.css({ "overflow-x": "auto", "overflow-y": "hidden", }); return; } if (this.options.scrolly) { this.element.css({ "overflow-x": "hidden", "overflow-y": "auto", }); } } appendFragment(frag) { this.element.append(frag); } _mountChildren() { const frag = Widget._renderEngine.createFragment(); let hasChild = false; for (const key in this._children) { const child = this._children[key]; if (child.element !== this.element) { frag.appendChild(child.element[0]); hasChild = true; } } if (hasChild === true) { this.appendFragment(frag); } } _getChildName(index) { return `${index}`; } _addElement(i, item, context, widget) { let w; if (widget) { return widget; } if (!this.hasWidget(this._getChildName(i))) { w = _lazyCreateWidget(item, context); w.on(Events.DESTROY, () => { each(this._children, (name, child) => { if (child === w) { delete this._children[name]; this.removeItemAt(name | 0); } }); }); this.addWidget(this._getChildName(i), w); } else { w = this.getWidgetByName(this._getChildName(i)); } return w; } _newElement(i, item, context) { const w = _lazyCreateWidget(item, context); w.on(Events.DESTROY, () => { each(this._children, (name, child) => { if (child === w) { delete this._children[name]; this.removeItemAt(name | 0); } }); }); return this._addElement(i, item, context, w); } _getOptions(item) { if (item instanceof Widget) { item = item.options; } item = stripEL(item); if (item instanceof Widget) { item = item.options; } return item; } _compare(item1, item2) { // 不比较函数 const eq = (a, b, aStack, bStack) => { if (a === b) { return a !== 0 || 1 / a === 1 / b; } if (isNull(a) || isNull(b)) { return a === b; } const className = Object.prototype.toString.call(a); switch (className) { case "[object RegExp]": case "[object String]": return `${a}` === `${b}`; case "[object Number]": if (+a !== +a) { return +b !== +b; } return +a === 0 ? 1 / +a === 1 / b : +a === +b; case "[object Date]": case "[object Boolean]": return +a === +b; default: } const areArrays = className === "[object Array]"; if (!areArrays) { if (isFunction(a) && isFunction(b)) { return true; } a = this._getOptions(a); b = this._getOptions(b); } aStack = aStack || []; bStack = bStack || []; let length = aStack.length; while (length--) { if (aStack[length] === a) { return bStack[length] === b; } } aStack.push(a); bStack.push(b); if (areArrays) { length = a.length; if (length !== b.length) { return false; } while (length--) { if (!eq(a[length], b[length], aStack, bStack)) { return false; } } } else { const aKeys = keys(a); let key; length = aKeys.length; if (keys(b).length !== length) { return false; } while (length--) { key = aKeys[length]; if (!(has(b, key) && eq(a[key], b[key], aStack, bStack))) { return false; } } } aStack.pop(); bStack.pop(); return true; }; return eq(item1, item2); } _getWrapper() { return this.element; } // 不依赖于this.options.items进行更新 _updateItemAt(oldIndex, newIndex, item) { const del = this._children[this._getChildName(oldIndex)]; const w = this._newElement(newIndex, item); // 需要有个地方临时存一下新建的组件,否则如果直接使用newIndex的话,newIndex位置的元素可能会被用到 this._children[`${this._getChildName(newIndex)}-temp`] = w; const nextSibling = del.element.next(); if (nextSibling.length > 0) { Widget._renderEngine.createElement(nextSibling).before(w.element); } else { w.element.appendTo(this._getWrapper()); } del._destroy(); w._mount(); return true; } _addItemAt(index, item) { for (let i = this.options.items.length; i > index; i--) { this._children[this._getChildName(i)] = this._children[this._getChildName(i - 1)]; } delete this._children[this._getChildName(index)]; this.options.items.splice(index, 0, item); } _removeItemAt(index) { for (let i = index; i < this.options.items.length - 1; i++) { this._children[this._getChildName(i)] = this._children[this._getChildName(i + 1)]; } delete this._children[this._getChildName(this.options.items.length - 1)]; this.options.items.splice(index, 1); } _clearGap(w) { w.element.css({ "margin-top": "", "margin-bottom": "", "margin-left": "", "margin-right": "", }); } _optimiseGap(gap) { return (gap > 0 && gap < 1) ? `${(gap * 100).toFixed(1)}%` : pixFormat(gap); } _optimiseItemLgap(item) { if (Providers.getProvider("bi.provider.system").getLayoutOptimize()) { return ((!item.type && item.el) ? ((item._lgap || 0) + (item.lgap || 0)) : item._lgap) || 0; } return (item._lgap || 0) + (item.lgap || 0); } _optimiseItemRgap(item) { if (Providers.getProvider("bi.provider.system").getLayoutOptimize()) { return ((!item.type && item.el) ? ((item._rgap || 0) + (item.rgap || 0)) : item._rgap) || 0; } return (item._rgap || 0) + (item.rgap || 0); } _optimiseItemTgap(item) { if (Providers.getProvider("bi.provider.system").getLayoutOptimize()) { return ((!item.type && item.el) ? ((item._tgap || 0) + (item.tgap || 0)) : item._tgap) || 0; } return (item._tgap || 0) + (item.tgap || 0); } _optimiseItemBgap(item) { if (Providers.getProvider("bi.provider.system").getLayoutOptimize()) { return ((!item.type && item.el) ? ((item._bgap || 0) + (item.bgap || 0)) : item._bgap) || 0; } return (item._bgap || 0) + (item.bgap || 0); } _optimiseItemHgap(item) { if (Providers.getProvider("bi.provider.system").getLayoutOptimize()) { return ((!item.type && item.el) ? ((item._hgap || 0) + (item.hgap || 0)) : item._hgap) || 0; } return (item._hgap || 0) + (item.hgap || 0); } _optimiseItemVgap(item) { if (Providers.getProvider("bi.provider.system").getLayoutOptimize()) { return ((!item.type && item.el) ? ((item._vgap || 0) + (item.vgap || 0)) : item._vgap) || 0; } return (item._vgap || 0) + (item.vgap || 0); } _handleGap(w, item, hIndex, vIndex) { const o = this.options; let innerLgap, innerRgap, innerTgap, innerBgap; if (isNull(vIndex)) { innerTgap = innerBgap = o.innerVgap; innerLgap = hIndex === 0 ? o.innerHgap : 0; innerRgap = hIndex === o.items.length - 1 ? o.innerHgap : 0; } else { innerLgap = innerRgap = o.innerHgap; innerTgap = vIndex === 0 ? o.innerVgap : 0; innerBgap = vIndex === o.items.length - 1 ? o.innerVgap : 0; } if (o.vgap + o.tgap + innerTgap + this._optimiseItemTgap(item) + this._optimiseItemVgap(item) !== 0) { const top = ((isNull(vIndex) || vIndex === 0) ? o.vgap : 0) + o.tgap + innerTgap + this._optimiseItemTgap(item) + this._optimiseItemVgap(item); w.element.css({ "margin-top": this._optimiseGap(top), }); } if (o.hgap + o.lgap + innerLgap + this._optimiseItemLgap(item) + this._optimiseItemHgap(item) !== 0) { const left = ((isNull(hIndex) || hIndex === 0) ? o.hgap : 0) + o.lgap + innerLgap + this._optimiseItemLgap(item) + this._optimiseItemHgap(item); w.element.css({ "margin-left": this._optimiseGap(left), }); } if (o.hgap + o.rgap + innerRgap + this._optimiseItemRgap(item) + this._optimiseItemHgap(item) !== 0) { const right = o.hgap + o.rgap + innerRgap + this._optimiseItemRgap(item) + this._optimiseItemHgap(item); w.element.css({ "margin-right": this._optimiseGap(right), }); } if (o.vgap + o.bgap + innerBgap + this._optimiseItemBgap(item) + this._optimiseItemVgap(item) !== 0) { const bottom = o.vgap + o.bgap + innerBgap + this._optimiseItemBgap(item) + this._optimiseItemVgap(item); w.element.css({ "margin-bottom": this._optimiseGap(bottom), }); } } // 横向换纵向 _handleReverseGap(w, item, index) { const o = this.options; const innerLgap = o.innerHgap; const innerRgap = o.innerHgap; const innerTgap = index === 0 ? o.innerVgap : 0; const innerBgap = index === o.items.length - 1 ? o.innerVgap : 0; if (o.vgap + o.tgap + innerTgap + this._optimiseItemTgap(item) + this._optimiseItemVgap(item) !== 0) { const top = (index === 0 ? o.vgap : 0) + (index === 0 ? o.tgap : 0) + innerTgap + this._optimiseItemTgap(item) + this._optimiseItemVgap(item); w.element.css({ "margin-top": this._optimiseGap(top), }); } if (o.hgap + o.lgap + innerLgap + this._optimiseItemLgap(item) + this._optimiseItemHgap(item) !== 0) { const left = o.hgap + o.lgap + innerLgap + this._optimiseItemLgap(item) + this._optimiseItemHgap(item); w.element.css({ "margin-left": this._optimiseGap(left), }); } if (o.hgap + o.rgap + innerRgap + this._optimiseItemRgap(item) + this._optimiseItemHgap(item) !== 0) { const right = o.hgap + o.rgap + innerRgap + this._optimiseItemRgap(item) + this._optimiseItemHgap(item); w.element.css({ "margin-right": this._optimiseGap(right), }); } // 这里的代码是关键 if (o.vgap + o.hgap + o.bgap + innerBgap + this._optimiseItemBgap(item) + this._optimiseItemVgap(item) !== 0) { const bottom = (index === o.items.length - 1 ? o.vgap : o.hgap) + (index === o.items.length - 1 ? o.bgap : 0) + innerBgap + this._optimiseItemBgap(item) + this._optimiseItemVgap(item); w.element.css({ "margin-bottom": this._optimiseGap(bottom), }); } } /** * 添加一个子组件到容器中 * @param {JSON/Widget} item 子组件 */ addItem(item) { return this.addItemAt(this.options.items.length, item); } prependItem(item) { return this.addItemAt(0, item); } addItemAt(index, item) { if (index < 0 || index > this.options.items.length) { return; } this._addItemAt(index, item); const w = this._addElement(index, item); // addItemAt 还是用之前的找上个兄弟节点向后插入的方式 if (index > 0) { this._children[this._getChildName(index - 1)].element.after(w.element); } else { w.element.prependTo(this._getWrapper()); } w._mount(); return w; } removeItemAt(indexes) { indexes = isArray(indexes) ? indexes : [indexes]; const deleted = []; const newItems = [], newChildren = {}; for (let i = 0, len = this.options.items.length; i < len; i++) { const child = this._children[this._getChildName(i)]; if (contains(indexes, i)) { child && deleted.push(child); } else { newChildren[this._getChildName(newItems.length)] = child; newItems.push(this.options.items[i]); } } this.options.items = newItems; this._children = newChildren; each(deleted, (i, c) => { c._destroy(); }); } shouldUpdateItem(index, item) { const child = this._children[this._getChildName(index)]; if (!child || !child.shouldUpdate) { return null; } return child.shouldUpdate(this._getOptions(item)); } addItems(items, context) { const o = this.options; const fragment = Widget._renderEngine.createFragment(); const added = []; each(items, (i, item) => { const w = this._addElement(o.items.length, item, context); this._children[this._getChildName(o.items.length)] = w; o.items.push(item); added.push(w); fragment.appendChild(w.element[0]); }); if (this._isMounted) { this._getWrapper().append(fragment); each(added, (i, w) => { w._mount(); }); } } prependItems(items, context) { items = items || []; const fragment = Widget._renderEngine.createFragment(); const added = []; for (let i = items.length - 1; i >= 0; i--) { this._addItemAt(0, items[i]); const w = this._addElement(0, items[i], context); this._children[this._getChildName(0)] = w; this.options.items.unshift(items[i]); added.push(w); fragment.appendChild(w.element[0]); } if (this._isMounted) { this._getWrapper().prepend(fragment); each(added, (i, w) => { w._mount(); }); } } getValue() { let value = [], child; each(this.options.items, i => { child = this._children[this._getChildName(i)]; if (child) { let v = child.getValue(); v = isArray(v) ? v : [v]; value = value.concat(v); } }); return value; } setValue(v) { let child; each(this.options.items, i => { child = this._children[this._getChildName(i)]; child && child.setValue(v); }); } setText(v) { let child; each(this.options.items, i => { child = this._children[this._getChildName(i)]; child && child.setText(v); }); } patchItem(oldVnode, vnode, oldIndex, newIndex) { const shouldUpdate = this.shouldUpdateItem(oldIndex, vnode); const child = this._children[this._getChildName(oldIndex)]; if (shouldUpdate) { this._children[`${this._getChildName(newIndex)}-temp`] = child; return child._update(this._getOptions(vnode), shouldUpdate); } if (shouldUpdate === null && !this._compare(oldVnode, vnode)) { // if (child.update) { // return child.update(this._getOptions(vnode)); // } return this._updateItemAt(oldIndex, newIndex, vnode); } } updateChildren(oldCh, newCh, context) { let oldStartIdx = 0, newStartIdx = 0; let oldEndIdx = oldCh.length - 1; let oldStartVnode = oldCh[0]; let oldEndVnode = oldCh[oldEndIdx]; let newEndIdx = newCh.length - 1; let newStartVnode = newCh[0]; let newEndVnode = newCh[newEndIdx]; let before; let updated; const children = {}; each(oldCh, (i, child) => { child = this._getOptions(child); const key = isNull(child.key) ? i : child.key; if (isKey(key)) { children[key] = this._children[this._getChildName(i)]; } }); while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isNull(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx]; } else if (isNull(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx]; } else if (sameVnode(oldStartVnode, newStartVnode, oldStartIdx, newStartIdx)) { const willUpdate = this.patchItem(oldStartVnode, newStartVnode, oldStartIdx, newStartIdx); updated = willUpdate || updated; children[isNull(oldStartVnode.key) ? oldStartIdx : oldStartVnode.key] = willUpdate ? this._children[`${this._getChildName(newStartIdx)}-temp`] : this._children[this._getChildName(oldStartIdx)]; oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } else if (sameVnode(oldEndVnode, newEndVnode, oldEndIdx, newEndIdx)) { const willUpdate = this.patchItem(oldEndVnode, newEndVnode, oldEndIdx, newEndIdx); updated = willUpdate || updated; children[isNull(oldEndVnode.key) ? oldEndIdx : oldEndVnode.key] = willUpdate ? this._children[`${this._getChildName(newEndIdx)}-temp`] : this._children[this._getChildName(oldEndIdx)]; oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldStartVnode, newEndVnode)) { const willUpdate = this.patchItem(oldStartVnode, newEndVnode, oldStartIdx, newStartIdx); updated = willUpdate || updated; children[isNull(oldStartVnode.key) ? oldStartIdx : oldStartVnode.key] = willUpdate ? this._children[`${this._getChildName(newStartIdx)}-temp`] : this._children[this._getChildName(oldStartIdx)]; insertBefore(oldStartVnode, oldEndVnode, true); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { const willUpdate = this.patchItem(oldEndVnode, newStartVnode, oldEndIdx, newEndIdx); updated = willUpdate || updated; children[isNull(oldEndVnode) ? oldEndIdx : oldEndVnode.key] = willUpdate ? this._children[`${this._getChildName(newEndIdx)}-temp`] : this._children[this._getChildName(oldEndIdx)]; insertBefore(oldEndVnode, oldStartVnode); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { const sameOldVnode = findOldVnode(oldCh, newStartVnode, oldStartIdx, oldEndIdx); if (isNull(sameOldVnode[0])) { // 不存在就把新的放到左边 const node = addNode(newStartVnode, newStartIdx, context); insertBefore(node, oldStartVnode); } else { // 如果新节点在旧节点区间中存在就复用一下 const sameOldIndex = sameOldVnode[1]; const willUpdate = this.patchItem(sameOldVnode[0], newStartVnode, sameOldIndex, newStartIdx); updated = willUpdate || updated; children[isNull(sameOldVnode[0].key) ? newStartIdx : sameOldVnode[0].key] = willUpdate ? this._children[`${this._getChildName(newStartIdx)}-temp`] : this._children[this._getChildName(sameOldIndex)]; oldCh[sameOldIndex] = undefined; insertBefore(sameOldVnode[0], oldStartVnode); } newStartVnode = newCh[++newStartIdx]; } } if (oldStartIdx > oldEndIdx) { before = isNull(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1]; addVnodes(before, newCh, newStartIdx, newEndIdx, context); } else if (newStartIdx > newEndIdx) { removeVnodes(oldCh, oldStartIdx, oldEndIdx); } this._children = {}; each(newCh, (i, child) => { const node = this._getOptions(child); const key = isNull(node.key) ? i : node.key; children[key]._setParent && children[key]._setParent(this); children[key]._mount(); this._children[this._getChildName(i)] = children[key]; }); const sameVnode = (vnode1, vnode2, oldIndex, newIndex) => { vnode1 = this._getOptions(vnode1); vnode2 = this._getOptions(vnode2); if (isKey(vnode1.key)) { return vnode1.key === vnode2.key; } if (oldIndex >= 0) { return oldIndex === newIndex; } }; const addNode = (vnode, index, context) => { const opt = this._getOptions(vnode); const key = isNull(opt.key) ? index : opt.key; children[key] = this._newElement(index, vnode, context); return children[key]; }; const addVnodes = (before, vnodes, startIdx, endIdx, context) => { for (; startIdx <= endIdx; ++startIdx) { const node = addNode(vnodes[startIdx], startIdx, context); insertBefore(node, before, false, startIdx); } }; const removeVnodes = (vnodes, startIdx, endIdx) => { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx]; if (isNotNull(ch)) { const node = this._getOptions(ch); const key = isNull(node.key) ? startIdx : node.key; children[key]._destroy(); } } }; const insertBefore = (insert, before, isNext, index) => { insert = this._getOptions(insert); before = before && this._getOptions(before); const insertKey = isKey(insert.key) ? insert.key : index; if (before && children[before.key]) { const beforeKey = isKey(before.key) ? before.key : index; let next; if (isNext) { next = children[beforeKey].element.next(); } else { next = children[beforeKey].element; } if (next.length > 0) { next.before(children[insertKey].element); } else { this._getWrapper().append(children[insertKey].element); } } else { this._getWrapper().append(children[insertKey].element); } }; const findOldVnode = (vnodes, vNode, beginIdx, endIdx) => { let i, found, findIndex; for (i = beginIdx; i <= endIdx; ++i) { if (vnodes[i] && sameVnode(vnodes[i], vNode)) { found = vnodes[i]; findIndex = i; } } return [found, findIndex]; }; return updated; } forceUpdate(opt) { if (this._isMounted) { each(this._children, (i, c) => { c.destroy(); }); this._children = {}; this._isMounted = false; } this.options.items = opt.items; this.stroke(opt.items); this._mount(); } update(opt) { const o = this.options; const items = opt.items || []; const context = opt.context; const oldItems = o.items; this.options.items = items; return this.updateChildren(oldItems, items, context); } stroke(items, options) { options = options || {}; each(items, (i, item) => { item && this._addElement(i, item, options.context); }); } getRowColumnCls(rowIndex, colIndex, lastRowIndex, lastColIndex) { let cls = ""; if (rowIndex === 0) { cls += " first-row"; } else if (rowIndex === lastRowIndex) { cls += " last-row"; } if (colIndex === 0) { cls += " first-col"; } else if (colIndex === lastColIndex) { cls += " last-col"; } isOdd(rowIndex + 1) ? (cls += " odd-row") : (cls += " even-row"); isOdd(colIndex + 1) ? (cls += " odd-col") : (cls += " even-col"); cls += " center-element"; return cls; } removeWidget(nameOrWidget) { let removeIndex; if (isWidget(nameOrWidget)) { each(this._children, (name, child) => { if (child === nameOrWidget) { removeIndex = name; } }); } else { removeIndex = nameOrWidget; } if (removeIndex) { this._removeItemAt(removeIndex | 0); } } empty() { super.empty(...arguments); this.options.items = []; } destroy() { super.destroy(...arguments); this.options.items = []; } populate(items, options) { items = items || []; options = options || {}; if (this._isMounted) { this.update({ items, context: options.context, }); return; } this.options.items = items; this.stroke(items, options); } resize() { this.stroke(this.options.items); } }