| Current Path : /home/rtorresani/www/vendor/magento/module-ui/view/base/web/js/lib/knockout/bindings/ |
| Current File : //home/rtorresani/www/vendor/magento/module-ui/view/base/web/js/lib/knockout/bindings/tooltip.js |
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
define([
'jquery',
'ko',
'underscore',
'mage/template',
'text!ui/template/tooltip/tooltip.html',
'../template/renderer'
], function ($, ko, _, template, tooltipTmpl, renderer) {
'use strict';
var tooltip,
defaults,
positions,
transformProp,
checkedPositions = {},
iterator = 0,
previousTooltip,
tooltipData,
positionData = {},
tooltipsCollection = {},
isTouchDevice = (function () {
return 'ontouchstart' in document.documentElement;
})(),
CLICK_EVENT = (function () {
return isTouchDevice ? 'touchstart' : 'click';
})();
defaults = {
tooltipWrapper: '[data-tooltip=tooltip-wrapper]',
tooltipContentBlock: 'data-tooltip-content',
closeButtonClass: 'action-close',
tailClass: 'data-tooltip-tail',
action: 'hover',
delay: 300,
track: false,
step: 20,
position: 'top',
closeButton: false,
showed: false,
strict: true,
center: false,
closeOnScroll: true
};
tooltipData = {
tooltipClasses: '',
trigger: false,
timeout: 0,
element: false,
event: false,
targetElement: {},
showed: false,
currentID: 0
};
/**
* Polyfill for css transform
*/
transformProp = (function () {
var style = document.createElement('div').style,
base = 'Transform',
vendors = ['webkit', 'moz', 'ms', 'o'],
vi = vendors.length,
property;
if (typeof style.transform !== 'undefined') {
return 'transform';
}
while (vi--) {
property = vendors[vi] + base;
if (typeof style[property] !== 'undefined') {
return property;
}
}
})();
positions = {
/*eslint max-depth: [0, 0]*/
map: {
horizontal: {
s: 'w',
p: 'left'
},
vertical: {
s: 'h',
p: 'top'
}
},
/**
* Wrapper function to get tooltip data (position, className, etc)
*
* @param {Object} s - object with sizes and positions elements
* @returns {Object} tooltip data (position, className, etc)
*/
top: function (s) {
return positions._topLeftChecker(s, positions.map, 'vertical', '_bottom', 'top', 'right');
},
/**
* Wrapper function to get tooltip data (position, className, etc)
*
* @param {Object} s - object with sizes and positions elements
* @returns {Object} tooltip data (position, className, etc)
*/
left: function (s) {
return positions._topLeftChecker(s, positions.map, 'horizontal', '_right', 'left', 'top');
},
/**
* Wrapper function to get tooltip data (position, className, etc)
*
* @param {Object} s - object with sizes and positions elements
* @returns {Object} tooltip data (position, className, etc)
*/
bottom: function (s) {
return positions._bottomRightChecker(s, positions.map, 'vertical', '_top', 'bottom', 'left');
},
/**
* Wrapper function to get tooltip data (position, className, etc)
*
* @param {Object} s - object with sizes and positions elements
* @returns {Object} tooltip data (position, className, etc)
*/
right: function (s) {
return positions._bottomRightChecker(s, positions.map, 'horizontal', '_left', 'right', 'bottom');
},
/**
* Check can tooltip setted on current position or not. If can't setted - delegate call.
*
* @param {Object} s - object with sizes and positions elements
* @param {Object} map - mapping for get direction positions
* @param {String} direction - vertical or horizontal
* @param {String} className - class whats should be setted to tooltip
* @param {String} side - parent method name
* @param {String} delegate - method name if tooltip can't be setted in current position
* @returns {Object} tooltip data (position, className, etc)
*/
_topLeftChecker: function (s, map, direction, className, side, delegate) {
var result = {
position: {}
},
config = tooltip.getTooltip(tooltipData.currentID),
startPosition = !config.strict ? s.eventPosition : s.elementPosition,
changedDirection;
checkedPositions[side] = true;
if (
startPosition[map[direction].p] - s.tooltipSize[map[direction].s] - config.step >
s.scrollPosition[map[direction].p]
) {
result.position[map[direction].p] = startPosition[map[direction].p] - s.tooltipSize[map[direction].s] -
config.step;
result.className = className;
result.side = side;
changedDirection = direction === 'vertical' ? 'horizontal' : 'vertical';
result = positions._normalize(s, result, config, delegate, map, changedDirection);
} else if (!checkedPositions[delegate]) {
result = positions[delegate].apply(null, arguments);
} else {
result = positions.positionCenter(s, result);
}
return result;
},
/**
* Check can tooltip setted on current position or not. If can't setted - delegate call.
*
* @param {Object} s - object with sizes and positions elements
* @param {Object} map - mapping for get direction positions
* @param {String} direction - vertical or horizontal
* @param {String} className - class whats should be setted to tooltip
* @param {String} side - parent method name
* @param {String} delegate - method name if tooltip can't be setted in current position
* @returns {Object} tooltip data (position, className, etc)
*/
_bottomRightChecker: function (s, map, direction, className, side, delegate) {
var result = {
position: {}
},
config = tooltip.getTooltip(tooltipData.currentID),
startPosition = !config.strict ? s.eventPosition : {
top: s.elementPosition.top + s.elementSize.h,
left: s.elementPosition.left + s.elementSize.w
},
changedDirection;
checkedPositions[side] = true;
if (
startPosition[map[direction].p] + s.tooltipSize[map[direction].s] + config.step <
s.scrollPosition[map[direction].p] + s.windowSize[map[direction].s]
) {
result.position[map[direction].p] = startPosition[map[direction].p] + config.step;
result.className = className;
result.side = side;
changedDirection = direction === 'vertical' ? 'horizontal' : 'vertical';
result = positions._normalize(s, result, config, delegate, map, changedDirection);
} else if (!checkedPositions[delegate]) {
result = positions[delegate].apply(null, arguments);
} else {
result = positions.positionCenter(s, result);
}
return result;
},
/**
* Centered tooltip if tooltip does not fit in window
*
* @param {Object} s - object with sizes and positions elements
* @param {Object} data - current data (position, className, etc)
* @returns {Object} tooltip data (position, className, etc)
*/
positionCenter: function (s, data) {
data = positions._positionCenter(s, data, 'horizontal', positions.map);
data = positions._positionCenter(s, data, 'vertical', positions.map);
return data;
},
/**
* Centered tooltip side
*
* @param {Object} s - object with sizes and positions elements
* @param {Object} data - current data (position, className, etc)
* @param {String} direction - vertical or horizontal
* @param {Object} map - mapping for get direction positions
* @returns {Object} tooltip data (position, className, etc)
*/
_positionCenter: function (s, data, direction, map) {
if (s.tooltipSize[map[direction].s] < s.windowSize[map[direction].s]) {
data.position[map[direction].p] = (s.windowSize[map[direction].s] -
s.tooltipSize[map[direction].s]) / 2 + s.scrollPosition[map[direction].p];
} else {
data.position[map[direction].p] = s.scrollPosition[map[direction].p];
data.tooltipSize = {};
data.tooltipSize[map[direction].s] = s.windowSize[map[direction].s];
}
return data;
},
/**
* Normalize horizontal or vertical position.
*
* @param {Object} s - object with sizes and positions elements
* @param {Object} data - current data (position, className, etc)
* @param {Object} config - tooltip config
* @param {String} delegate - method name if tooltip can't be setted in current position
* @param {Object} map - mapping for get direction positions
* @param {String} direction - vertical or horizontal
* @returns {Object} tooltip data (position, className, etc)
*/
_normalize: function (s, data, config, delegate, map, direction) {
var startPosition = !config.center ? s.eventPosition : {
left: s.elementPosition.left + s.elementSize.w / 2,
top: s.elementPosition.top + s.elementSize.h / 2
},
depResult;
if (startPosition[map[direction].p] - s.tooltipSize[map[direction].s] / 2 >
s.scrollPosition[map[direction].p] && startPosition[map[direction].p] +
s.tooltipSize[map[direction].s] / 2 <
s.scrollPosition[map[direction].p] + s.windowSize[map[direction].s]
) {
data.position[map[direction].p] = startPosition[map[direction].p] - s.tooltipSize[map[direction].s] / 2;
} else {
/*eslint-disable no-lonely-if*/
if (!checkedPositions[delegate]) {
depResult = positions[delegate].apply(null, arguments);
if (depResult.hasOwnProperty('className')) {
data = depResult;
} else {
data = positions._normalizeTail(s, data, config, delegate, map, direction, startPosition);
}
} else {
data = positions._normalizeTail(s, data, config, delegate, map, direction, startPosition);
}
}
return data;
},
/**
* Calc tail position.
*
* @param {Object} s - object with sizes and positions elements
* @param {Object} data - current data (position, className, etc)
* @param {Object} config - tooltip config
* @param {String} delegate - method name if tooltip can't be setted in current position
* @param {Object} map - mapping for get direction positions
* @param {String} direction - vertical or horizontal
* @param {Object} startPosition - start position
* @returns {Object} tooltip data (position, className, etc)
*/
_normalizeTail: function (s, data, config, delegate, map, direction, startPosition) {
data.tail = {};
if (s.tooltipSize[map[direction].s] < s.windowSize[map[direction].s]) {
if (
startPosition[map[direction].p] >
s.windowSize[map[direction].s] / 2 + s.scrollPosition[map[direction].p]
) {
data.position[map[direction].p] = s.windowSize[map[direction].s] +
s.scrollPosition[map[direction].p] - s.tooltipSize[map[direction].s];
data.tail[map[direction].p] = startPosition[map[direction].p] -
s.tooltipSize[map[direction].s] / 2 - data.position[map[direction].p];
} else {
data.position[map[direction].p] = s.scrollPosition[map[direction].p];
data.tail[map[direction].p] = startPosition[map[direction].p] -
s.tooltipSize[map[direction].s] / 2 - data.position[map[direction].p];
}
} else {
data.position[map[direction].p] = s.scrollPosition[map[direction].p];
data.tail[map[direction].p] = s.eventPosition[map[direction].p] - s.windowSize[map[direction].s] / 2;
data.tooltipSize = {};
data.tooltipSize[map[direction].s] = s.windowSize[map[direction].s];
}
return data;
}
};
tooltip = {
/**
* Set new tooltip to tooltipCollection, save config, and add unic id
*
* @param {Object} config - tooltip config
* @returns {String} tooltip id
*/
setTooltip: function (config) {
var property = 'id-' + iterator;
tooltipsCollection[property] = config;
iterator++;
return property;
},
/**
* Get tooltip config by id
*
* @param {String} id - tooltip id
* @returns {Object} tooltip config
*/
getTooltip: function (id) {
return tooltipsCollection[id];
},
/**
* Set content to current tooltip
*
* @param {Object} tooltipElement - tooltip element
* @param {Object} viewModel - tooltip view model
* @param {String} id - tooltip id
* @param {Object} bindingCtx - tooltip context
* @param {Object} event - action event
*/
setContent: function (tooltipElement, viewModel, id, bindingCtx, event) {
var html = $(tooltipElement).html(),
config = tooltip.getTooltip(id),
body = $('body');
tooltipData.currentID = id;
tooltipData.trigger = $(event.currentTarget);
tooltip.setTargetData(event);
body.on('mousemove.setTargetData', tooltip.setTargetData);
tooltip.clearTimeout(id);
tooltipData.timeout = _.delay(function () {
body.off('mousemove.setTargetData', tooltip.setTargetData);
if (tooltipData.trigger[0] === tooltipData.targetElement) {
tooltip.destroy(id);
event.stopPropagation();
tooltipElement = tooltip.createTooltip(id);
tooltipElement.find('.' + defaults.tooltipContentBlock).append(html);
tooltipElement.applyBindings(bindingCtx);
tooltip.setHandlers(id);
tooltip.setPosition(tooltipElement, id);
previousTooltip = id;
}
}, config.delay);
},
/**
* Set position to current tooltip
*
* @param {Object} tooltipElement - tooltip element
* @param {String} id - tooltip id
*/
setPosition: function (tooltipElement, id) {
var config = tooltip.getTooltip(id);
tooltip.sizeData = {
windowSize: {
h: $(window).outerHeight(),
w: $(window).outerWidth()
},
scrollPosition: {
top: $(window).scrollTop(),
left: $(window).scrollLeft()
},
tooltipSize: {
h: tooltipElement.outerHeight(),
w: tooltipElement.outerWidth()
},
elementSize: {
h: tooltipData.trigger.outerHeight(),
w: tooltipData.trigger.outerWidth()
},
elementPosition: tooltipData.trigger.offset(),
eventPosition: this.getEventPosition(tooltipData.event)
};
_.extend(positionData, positions[config.position](tooltip.sizeData));
tooltipElement.css(positionData.position);
tooltipElement.addClass(positionData.className);
tooltip._setTooltipSize(positionData, tooltipElement);
tooltip._setTailPosition(positionData, tooltipElement);
checkedPositions = {};
},
/**
* Check position data and change tooltip size if needs
*
* @param {Object} data - position data
* @param {Object} tooltipElement - tooltip element
*/
_setTooltipSize: function (data, tooltipElement) {
if (data.tooltipSize) {
data.tooltipSize.w ?
tooltipElement.css('width', data.tooltipSize.w) :
tooltipElement.css('height', data.tooltipSize.h);
}
},
/**
* Check position data and set position to tail
*
* @param {Object} data - position data
* @param {Object} tooltipElement - tooltip element
*/
_setTailPosition: function (data, tooltipElement) {
var tail,
tailMargin;
if (data.tail) {
tail = tooltipElement.find('.' + defaults.tailClass);
if (data.tail.left) {
tailMargin = parseInt(tail.css('margin-left'), 10);
tail.css('margin-left', tailMargin + data.tail.left);
} else {
tailMargin = parseInt(tail.css('margin-top'), 10);
tail.css('margin-top', tailMargin + data.tail.top);
}
}
},
/**
* Resolves position for tooltip
*
* @param {Object} event
* @returns {Object}
*/
getEventPosition: function (event) {
var position = {
left: event.originalEvent && event.originalEvent.pageX || 0,
top: event.originalEvent && event.originalEvent.pageY || 0
};
if (position.left === 0 && position.top === 0) {
_.extend(position, event.target.getBoundingClientRect());
}
return position;
},
/**
* Close tooltip if action happened outside handler and tooltip element
*
* @param {String} id - tooltip id
* @param {Object} event - action event
*/
outerClick: function (id, event) {
var tooltipElement = $(event.target).parents(defaults.tooltipWrapper)[0],
isTrigger = event.target === tooltipData.trigger[0] || $.contains(tooltipData.trigger[0], event.target);
if (tooltipData.showed && tooltipElement !== tooltipData.element[0] && !isTrigger) {
tooltip.destroy(id);
}
},
/**
* Parse keydown event and if event trigger is escape key - close tooltip
*
* @param {Object} event - action event
*/
keydownHandler: function (event) {
if (tooltipData.showed && event.keyCode === 27) {
tooltip.destroy(tooltipData.currentID);
}
},
/**
* Change tooltip position when track is enabled
*
* @param {Object} event - current event
*/
track: function (event) {
var inequality = {},
map = positions.map,
translate = {
left: 'translateX',
top: 'translateY'
},
eventPosition = {
left: event.pageX,
top: event.pageY
},
tooltipSize = {
w: tooltipData.element.outerWidth(),
h: tooltipData.element.outerHeight()
},
direction = positionData.side === 'bottom' || positionData.side === 'top' ? 'horizontal' : 'vertical';
inequality[map[direction].p] = eventPosition[map[direction].p] - (positionData.position[map[direction].p] +
tooltipSize[map[direction].s] / 2);
if (positionData.position[map[direction].p] + inequality[map[direction].p] +
tooltip.sizeData.tooltipSize[map[direction].s] >
tooltip.sizeData.windowSize[map[direction].s] + tooltip.sizeData.scrollPosition[map[direction].p] ||
inequality[map[direction].p] + positionData.position[map[direction].p] <
tooltip.sizeData.scrollPosition[map[direction].p]) {
return false;
}
tooltipData.element[0].style[transformProp] = translate[map[direction].p] +
'(' + inequality[map[direction].p] + 'px)';
},
/**
* Set handlers to tooltip
*
* @param {String} id - tooltip id
*/
setHandlers: function (id) {
var config = tooltip.getTooltip(id);
if (config.track) {
tooltipData.trigger.on('mousemove.track', tooltip.track);
}
if (config.action === 'click') {
$(window).on(CLICK_EVENT + '.outerClick', tooltip.outerClick.bind(null, id));
}
if (config.closeButton) {
$('.' + config.closeButtonClass).on('click.closeButton', tooltip.destroy.bind(null, id));
}
if (config.closeOnScroll) {
document.addEventListener('scroll', tooltip.destroy, true);
$(window).on('scroll.tooltip', tooltip.outerClick.bind(null, id));
}
$(window).on('keydown.tooltip', tooltip.keydownHandler);
$(window).on('resize.outerClick', tooltip.outerClick.bind(null, id));
},
/**
* Toggle tooltip
*
* @param {Object} tooltipElement - tooltip element
* @param {Object} viewModel - tooltip view model
* @param {String} id - tooltip id
*/
toggleTooltip: function (tooltipElement, viewModel, id) {
if (previousTooltip === id && tooltipData.showed) {
tooltip.destroy(id);
return false;
}
tooltip.setContent.apply(null, arguments);
return false;
},
/**
* Create tooltip and append to DOM
*
* @param {String} id - tooltip id
* @returns {Object} tooltip element
*/
createTooltip: function (id) {
var body = $('body'),
config = tooltip.getTooltip(id);
$(template(tooltipTmpl, {
data: config
})).appendTo(body);
tooltipData.showed = true;
tooltipData.element = $(config.tooltipWrapper);
return tooltipData.element;
},
/**
* Check action and clean timeout
*
* @param {String} id - tooltip id
*/
clearTimeout: function (id) {
var config = tooltip.getTooltip(id);
if (config.action === 'hover') {
clearTimeout(tooltipData.timeout);
}
},
/**
* Check previous tooltip
*/
checkPreviousTooltip: function () {
if (!tooltipData.timeout) {
tooltip.destroy();
}
},
/**
* Destroy tooltip instance
*/
destroy: function () {
if (tooltipData.element) {
tooltipData.element.remove();
tooltipData.showed = false;
}
positionData = {};
tooltipData.timeout = false;
tooltip.removeHandlers();
},
/**
* Remove tooltip handlers
*/
removeHandlers: function () {
$('.' + defaults.closeButtonClass).off('click.closeButton');
tooltipData.trigger.off('mousemove.track');
document.removeEventListener('scroll', tooltip.destroy, true);
$(window).off('scroll.tooltip');
$(window).off(CLICK_EVENT + '.outerClick');
$(window).off('keydown.tooltip');
$(window).off('resize.outerClick');
},
/**
* Set target element
*
* @param {Object} event - current event
*/
setTargetData: function (event) {
tooltipData.event = event;
//TODO: bug chrome v.49; Link to issue https://bugs.chromium.org/p/chromium/issues/detail?id=161464
if (event.timeStamp - (tooltipData.timestamp || 0) < 1) {
return;
}
if (event.type === 'mousemove') {
tooltipData.targetElement = event.target;
} else {
tooltipData.targetElement = event.currentTarget;
tooltipData.timestamp = event.timeStamp;
}
},
/**
* Merged user config with defaults configuration
*
* @param {Object} config - user config
* @returns {Object} merged config
*/
processingConfig: function (config) {
return _.extend({}, defaults, config);
}
};
ko.bindingHandlers.tooltip = {
/**
* Initialize tooltip
*
* @param {Object} elem - tooltip DOM element
* @param {Function} valueAccessor - ko observable property, tooltip data
* @param {Object} allBindings - all bindings on current element
* @param {Object} viewModel - current element viewModel
* @param {Object} bindingCtx - current element binding context
*/
init: function (elem, valueAccessor, allBindings, viewModel, bindingCtx) {
var config = tooltip.processingConfig(valueAccessor()),
$parentScope = config.parentScope ? $(config.parentScope) : $(elem).parent(),
tooltipId;
$(elem).addClass('hidden');
if (isTouchDevice) {
config.action = 'click';
}
tooltipId = tooltip.setTooltip(config);
if (config.action === 'hover') {
$parentScope.on(
'mouseenter',
config.trigger,
tooltip.setContent.bind(null, elem, viewModel, tooltipId, bindingCtx)
);
$parentScope.on(
'mouseleave',
config.trigger,
tooltip.checkPreviousTooltip.bind(null, tooltipId)
);
} else if (config.action === 'click') {
$parentScope.on(
'click',
config.trigger,
tooltip.toggleTooltip.bind(null, elem, viewModel, tooltipId, bindingCtx)
);
}
return {
controlsDescendantBindings: true
};
}
};
renderer.addAttribute('tooltip');
});