/** @module align */ kit.PluginManager.register('align',function(module, exports, require) { var support = require('support'); /** * @class Rect * @param node */ var Rect = function (node) { this.node = node; this.get(); }; Rect.prototype.get = function () { function getBoundingClientRect(node) { var rect = node.getBoundingClientRect(); return { top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left, width: rect.width, height: rect.height, x: rect.x, y: rect.y }; } var metrics = {}; if(this.node instanceof HTMLElement) { var pr = getBoundingClientRect(this.node.offsetParent); var nr = getBoundingClientRect(this.node); metrics = { offsetHeight: this.node.offsetHeight, offsetWidth: this.node.offsetWidth, clientHeight: this.node.clientHeight, clientWidth: this.node.clientWidth, offsetLeft: this.node.offsetLeft, offsetTop: this.node.offsetTop, clientLeft: this.node.clientLeft, clientTop: this.node.clientTop, clientRight: this.node.offsetWidth - this.node.clientWidth - this.node.clientLeft, clientBottom: this.node.offsetHeight - this.node.clientHeight - this.node.clientTop, offsetRight: pr.right - nr.right, offsetBottom: pr.bottom - nr.bottom, }; metrics = Object.assign({}, nr, metrics); } if(this.node instanceof Window) { metrics = { offsetHeight: window.innerHeight, offsetWidth: window.innerWidth, clientHeight: document.body.offsetHeight, clientWidth: document.body.offsetWidth, offsetLeft: window.pageXOffset, offsetTop: window.pageYOffset, clientLeft: 0, clientTop: 0, clientRight: window.innerWidth - document.body.offsetWidth, clientBottom: window.innerHeight - document.body.offsetHeight, offsetRight: document.body.scrollWidth - document.body.offsetWidth - window.pageXOffset, offsetBottom: document.body.scrollHeight - document.body.offsetHeight - window.pageYOffset, }; metrics = Object.assign({ left: 0, top: 0, right: document.body.offsetWidth, bottom: document.body.offsetHeight, x: 0, y: 0, width: document.body.offsetWidth, height: document.body.offsetHeight, }, metrics) } Object.assign(this, metrics); return this; }; Rect.prototype.getLine = function (axis = 'horizontal') { var line = new Line(null, axis); line.node = this.node; line.rect = this; line.setAxis(axis); return line; }; var viewLineDriver = { horizontal: { left: 'start', right: 'end', x: 'a', width: 'length', offsetWidth: 'offset', clientWidth: 'client', offsetLeft: 'offsetStart', clientLeft: 'clientStart', offsetRight: 'offsetEnd', clientRight: 'clientEnd', }, vertical: { top: 'start', bottom: 'end', y: 'a', height: 'length', offsetHeight: 'offset', clientHeight: 'client', offsetTop: 'offsetStart', clientTop: 'clientStart', offsetBottom: 'offsetEnd', clientBottom: 'clientEnd', }, }; /** * @param node {HTMLElement} * @param axis * @property start * @property end * @property a * @property length * @property offset * @property client * @property offsetStart * @property clientStart * @property offsetEnd * @property clientEnd * */ var Line = function (node, axis = 'horizontal') { this.axis = axis; this.node = node; this.update(); }; Line.prototype.setAxis = function(axis){ this.axis = axis; for (var key in viewLineDriver[this.axis]) { this[viewLineDriver[this.axis][key]] = this.rect[key]; } }; Line.prototype.update = function () { if(!this.node) { return this; } this.rect = new Rect(this.node); this.setAxis(this.axis); return this; }; var metric = { Rect: Rect, Line: Line, }; /** * * @class {AlignCalc} * @param limiterNode {HTMLElement} * @param parentNode {HTMLElement} * @param node {HTMLElement} */ var AlignCalc = function (node, parentNode, limiterNode = window) { var _this = this; this.axis = 'horizontal'; this.direction = 'bottom'; this.align = 'bottom'; this.offset = [0, 0]; this.setLimiterNode(limiterNode); this.setParentNode(parentNode); this.setNode(node); this.style = { start: function (extra) { this.nodeMetricsLine.node.style[this.map.style.start()] = '' + (extra?this.shiftExtra:this.shift) + 'px'; }.bind(this), end: function (extra) { this.nodeMetricsLine.node.style[this.map.style.end()] = 'unset'; }.bind(this), }; this.map = { style: { start: function(){ switch (_this.axis) { case 'vertical': return 'top'; case 'horizontal': return 'left'; } }, end: function(){ switch (_this.axis) { case 'vertical': return 'bottom'; case 'horizontal': return 'right'; } }, }, direction: { get: function () { switch (_this.direction) { case 'left': case 'top': return 'start'; case 'right': case 'bottom': return 'end'; default: return _this.direction; } } }, align: { get: function () { switch (_this.align) { case 'left': case 'top': return 'start'; case 'right': case 'bottom': return 'end'; default: return _this.align; } } }, axisNumber: function () { return _this.axis === 'horizontal' ? 0 : 1; } }; var getShiftWhenHidingNodeUnderLimiterStart = function(shift) { var d = this.parentMetricsLine.start + shift - Math.min(this.limiterMetricsLine.start, this.parentMetricsLine.end); if(d < 0) { shift = shift - d; } return shift; }.bind(this); this.core = { direction: { start: { shift: function () { return - this.nodeMetricsLine.offset - this.offset[this.map.axisNumber()]; }.bind(this), applicability: function () { return (this.parentMetricsLine.start + this.shift) - this.limiterMetricsLine.start; }.bind(this), valid: function () { return (this.parentMetricsLine.start + this.shift) >= this.limiterMetricsLine.start; }.bind(this), shiftExtra: function () { var shift = getShiftWhenHidingNodeUnderLimiterStart(this.shift); var applicability = 0; if(!this.valid) { applicability = (this.parentMetricsLine.start + shift) - this.limiterMetricsLine.start; shift = getShiftWhenHidingNodeUnderLimiterStart(shift - applicability); } return shift; }.bind(this), }, end: { shift: function () { return this.offset[this.map.axisNumber()]; }.bind(this), applicability: function () { return this.limiterMetricsLine.end - (this.parentMetricsLine.start + this.nodeMetricsLine.length + this.shift); }.bind(this), valid: function () { return this.limiterMetricsLine.end >= (this.parentMetricsLine.start + this.nodeMetricsLine.length + this.shift); }.bind(this), shiftExtra: function () { var shift = getShiftWhenHidingNodeUnderLimiterStart(this.shift); var applicability = 0; if(!this.valid) { applicability = this.limiterMetricsLine.end - (this.parentMetricsLine.start + this.nodeMetricsLine.length + shift); shift = getShiftWhenHidingNodeUnderLimiterStart(shift + applicability); } return shift; }.bind(this), }, /** * todo: applicability, valid, shiftExtra */ center: { shift: function () { return - this.nodeMetricsLine.offset / 2 + this.offset[this.map.axisNumber()]; }.bind(this), applicability: function () { return this.limiterMetricsLine.end - (this.parentMetricsLine.start + this.nodeMetricsLine.length + this.shift); }.bind(this), valid: function () { return this.limiterMetricsLine.end >= (this.parentMetricsLine.start + this.nodeMetricsLine.length + this.shift); }.bind(this), shiftExtra: function () { var shift = getShiftWhenHidingNodeUnderLimiterStart(this.shift); var applicability = 0; if(!this.valid) { applicability = this.limiterMetricsLine.end - (this.parentMetricsLine.start + this.nodeMetricsLine.length + shift); shift = getShiftWhenHidingNodeUnderLimiterStart(shift + applicability); } return shift; }.bind(this), }, }, align: { start: { shift: function () { return this.nodeMetricsLine.clientStart; }.bind(this), }, center: { shift: function () { return this.parentMetricsLine.offset / 2 - this.parentMetricsLine.clientStart; }.bind(this), }, end: { shift: function () { return this.parentMetricsLine.offset - this.parentMetricsLine.clientStart; }.bind(this), }, }, }; return this; }; /** * @param node * @return {AlignCalc} */ AlignCalc.prototype.setLimiterNode = function (node) { if(!this.limiterMetricsLine || (this.limiterMetricsLine.node !== node)) { this.limiterMetricsLine = new Line(node, this.axis); } return this; }; /** * * @param node * @return {AlignCalc} */ AlignCalc.prototype.setParentNode = function (node) { if(!this.parentMetricsLine || (this.parentMetricsLine.node !== node)) { this.parentMetricsLine = new Line(node, this.axis); } return this; }; /** * * @param node * @return {AlignCalc} */ AlignCalc.prototype.setNode = function (node) { if(!this.nodeMetricsLine || (this.nodeMetricsLine.node !== node)) { this.nodeMetricsLine = new Line(node, this.axis); } return this; }; /** * * @param extra * @return {AlignCalc} */ AlignCalc.prototype.render = function (extra = false) { for (var key in this.style) { if(this.style.hasOwnProperty(key)) { this.style[key](extra); } } return this; }; /** * * @return {AlignCalc} */ AlignCalc.prototype.updateMetrics = function(){ var node = this.nodeMetricsLine.node; var o = node.style.overflow, w = node.style.width, h = node.style.height; node.style.overflow = 'hidden'; node.style.width = ''; node.style.height = ''; this.limiterMetricsLine.update(); node.style.overflow = o; node.style.width = w; node.style.height = h; this.parentMetricsLine.update(); this.nodeMetricsLine.update(); return this; }; /** * * @param direction * @return {AlignCalc} */ AlignCalc.prototype.setDirection = function(direction){ this.direction = direction; return this; }; /** * * @param align * @return {AlignCalc} */ AlignCalc.prototype.setAlign = function(align){ this.align = align; return this; }; /** * * @param axis * @return {AlignCalc} */ AlignCalc.prototype.setAxis = function(axis){ this.axis = axis; this.limiterMetricsLine.setAxis(axis); this.parentMetricsLine.setAxis(axis); this.nodeMetricsLine.setAxis(axis); return this; }; /** * * @param align * @param direction * @return {AlignCalc} */ AlignCalc.prototype.set = function(align, direction){ this.align = align; this.direction = direction; return this; }; /** * * @return {AlignCalc} */ AlignCalc.prototype.calc = function () { this.shift = this.core.direction[this.map.direction.get()].shift() + this.core.align[this.map.align.get()].shift(); this.applicability = this.core.direction[this.map.direction.get()].applicability(); this.valid = this.core.direction[this.map.direction.get()].valid(); this.shiftExtra = this.core.direction[this.map.direction.get()].shiftExtra(); return this; }; /** * * @return {{valid: *, shiftExtra: *, shift: *, applicability: *}} */ AlignCalc.prototype.getData = function () { return { shift: this.shift, applicability: this.applicability, valid: this.valid, shiftExtra: this.shiftExtra, }; }; /** * @param node {HTMLElement|String} * @param parentNode {HTMLElement|String} * @param limiterNode {HTMLElement|String} * @constructor */ var AlignClass = function(node, parentNode, limiterNode = window) { this.changeOfDirection = true; this.withinTheScreen = true; this.offset = [0, 0]; this.condition = {vertical: {}, horizontal: {},}; this.setNode(node); this.setParentNode(parentNode); this.setLimiterNode(limiterNode); }; /** * * @param node * @return {AlignClass} */ AlignClass.prototype.setNode = function(node){ this.unset(); this.node = support.getNode(node); this.reset(); if(this.node instanceof HTMLElement) { this.node.kit = this.node.kit || {}; this.node.kit.align = this; } if(this.__calc) { this.__calc.setNode(this.node); } return this; }; /** * @param node * @return {AlignClass} */ AlignClass.prototype.setParentNode = function(node){ this.parentNode = support.getNode(node); if(this.__calc) { this.__calc.setParentNode(this.parentNode); } return this; }; AlignClass.prototype.setLimiterNode = function(node){ this.limiterNode = support.getNode(node) || window; if(this.__calc) { this.__calc.setLimiterNode(this.limiterNode); } return this; }; AlignClass.prototype.unset = function(){ if(this.node instanceof HTMLElement) { if(this.node.kit && this.node.kit.align){ delete this.node.kit.align; } this.node.removeAttribute('data-kit-align-horizontal-direction'); this.node.removeAttribute('data-kit-align-horizontal-align'); this.node.removeAttribute('data-kit-align-vertical-direction'); this.node.removeAttribute('data-kit-align-vertical-align'); } }; AlignClass.prototype.reset = function(bHorizontal = true, bVertical = true){ if(bHorizontal) { this.condition.horizontal = {}; if(this.node instanceof HTMLElement) { this.node.style.left = ''; this.node.style.right = ''; this.node.removeAttribute('data-kit-align-horizontal-direction'); this.node.removeAttribute('data-kit-align-horizontal-align'); } } if(bVertical) { this.condition.vertical = {}; if(this.node instanceof HTMLElement) { this.node.style.top = ''; this.node.style.bottom = ''; this.node.removeAttribute('data-kit-align-vertical-direction'); this.node.removeAttribute('data-kit-align-vertical-align'); } } this.__nodeComputedStyle = undefined; return this; }; /** * * @param hAlign * @param vAlign * @param hDirection * @param vDirection * @return {AlignClass} */ AlignClass.prototype.set = function(hAlign, vAlign, hDirection, vDirection){ this.setEx('vertical', vAlign, vDirection); this.setEx('horizontal', hAlign, hDirection); return this; }; AlignClass.prototype.setVertical = function(align, direction){ this.setEx('vertical', align, direction); return this; }; AlignClass.prototype.setHorizontal = function(align, direction){ this.setEx('horizontal', align, direction); return this; }; AlignClass.prototype.__setAdditionalStyles = function(){ if(!this.__nodeComputedStyle) { this.__nodeComputedStyle = getComputedStyle(this.node); } if(!['absolute', 'relative'].includes(this.__nodeComputedStyle.position)) { this.node.style.position = 'absolute'; } if(!this.__parentNodeComputedStyle) { this.__parentNodeComputedStyle = getComputedStyle(this.parentNode); } if(!['absolute', 'relative', 'fixed'].includes(this.__parentNodeComputedStyle.position)) { this.parentNode.style.position = 'relative'; } }; AlignClass.prototype.setEx = function(axis, align, direction){ this.reset(axis === 'horizontal', axis === 'vertical'); this.__setAdditionalStyles(); //todo this.condition[axis].direction = direction; this.condition[axis].align = align; if(!this.__calc) { this.__calc = new AlignCalc(this.node, this.parentNode, this.limiterNode); } this.__calc.offset = this.offset; this.node.setAttribute('data-kit-align-'+axis+'-direction', direction); this.node.setAttribute('data-kit-align-'+axis+'-align', align); var defAlignResult = this.__calc.updateMetrics().setAxis(axis).setDirection(direction).setAlign(align).calc().getData(); // console.log('defAlignResult', axis, align, direction, defAlignResult); if(this.changeOfDirection) { if(!defAlignResult.valid) { this.node.setAttribute('data-kit-align-'+axis+'-direction', this.__getInverse(direction)); this.node.setAttribute('data-kit-align-'+axis+'-align', this.__getInverse(align)); var inverseAlignResult = this.__calc.updateMetrics().setDirection(this.__getInverse(direction)).setAlign(this.__getInverse(align)).calc().getData(); // console.log('inverseAlignResult', axis, this.__getInverse(align), this.__getInverse(direction), inverseAlignResult); this.condition[axis].direction = this.__getInverse(direction); this.condition[axis].align = this.__getInverse(align); if(!inverseAlignResult.valid && (defAlignResult.applicability > inverseAlignResult.applicability) ) { this.node.setAttribute('data-kit-align-'+axis+'-direction', direction); this.node.setAttribute('data-kit-align-'+axis+'-align', align); this.condition[axis].direction = direction; this.condition[axis].align = align; this.__calc.updateMetrics().setDirection(direction).setAlign(align).calc(); } } } this.__calc.render(this.withinTheScreen); return this; }; AlignClass.prototype.__getInverse = function(value){ var inverseConfig = { start: 'end', left: 'right', top: 'bottom', end: 'start', right: 'left', bottom: 'top', }; if(inverseConfig.hasOwnProperty(value)) { return inverseConfig[value]; } return value; }; /** * @memberOf kit */ var align = { AlignCalc: AlignCalc, Class: AlignClass, getInstance: function (node, parentNode) { if(node instanceof HTMLElement && node.kit && node.kit.align) { return node.kit.align; } return new AlignClass(node, parentNode); }, metric: metric, }; module.exports = align; return module.exports; });