const Util = require('../util');
const Category = require('./category');
const {
FONT_FAMILY
} = require('../const');
const DomUtil = Util.DomUtil;
const Group = Util.Group;
const CONTAINER_CLASS = 'g2-legend';
const TITLE_CLASS = 'g2-legend-title';
const LIST_CLASS = 'g2-legend-list';
const ITEM_CLASS = 'g2-legend-list-item';
const TEXT_CLASS = 'g2-legend-text';
const MARKER_CLASS = 'g2-legend-marker';
// find a dom node from the chidren of the node with className.
function findNodeByClass(node, className) {
return node.getElementsByClassName(className)[0];
}
function getParentNode(node, className) {
let nodeClass = node.className;
if (Util.isNil(nodeClass)) {
return node;
}
nodeClass = nodeClass.split(' ');
if (nodeClass.indexOf(className) > -1) {
return node;
}
if (node.parentNode) {
if (node.parentNode.className === CONTAINER_CLASS) {
return node.parentNode;
}
return getParentNode(node.parentNode, className);
}
return null;
}
function findItem(items, refer) {
let rst = null;
const value = (refer instanceof Group) ? refer.get('value') : refer;
Util.each(items, item => {
if (item.value === value) {
rst = item;
return false;
}
});
return rst;
}
class CatHtml extends Category {
getDefaultCfg() {
const cfg = super.getDefaultCfg();
return Util.mix({}, cfg, {
/**
* type 标识
* @type {String}
*/
type: 'category-legend',
/**
* html 容器
* @type {DOM}
*/
container: null,
/**
* 使用html时的外层模板
* @type {String}
*/
containerTpl: '
',
/**
* html 模板
* @type {String|Function}
*/
itemTpl: '' +
'' +
'{value}',
/**
* html style
* @type {Attrs}
*/
legendStyle: {},
/**
* 图例文字样式
* @type {ATTRS}
*/
textStyle: {
fill: '#333',
fontSize: 12,
textAlign: 'middle',
textBaseline: 'top',
fontFamily: FONT_FAMILY
},
/**
* 当文本太长时是否进行缩略
* @type {Boolean}
*/
abridgeText: false,
/**
* abridgeText 为 true 时,鼠标放置在 item 上时显示全称的悬浮 div 的 html 模板
* @type {String}
*/
tipTpl: '',
/**
* abridgeText 为 true 时,鼠标放置在 item 上时显示全称的悬浮 div 的样式
* @type {Attrs}
*/
tipStyle: {
display: 'none',
fontSize: '12px',
backgroundColor: '#fff',
position: 'absolute',
width: 'auto',
height: 'auto',
padding: '3px',
boxShadow: '2px 2px 5px #888'
},
/**
* useHtml 为 true 时生效,用于自动定位
* @type {[type]}
*/
autoPosition: true
});
}
_init() {
return;
}
beforeRender() {
return;
}
render() {
this._renderHTML();
}
// user interaction
_bindEvents() {
const legendWrapper = this.get('legendWrapper');
const itemListDom = findNodeByClass(legendWrapper, LIST_CLASS);
if (this.get('hoverable')) {
itemListDom.onmousemove = ev => this._onMousemove(ev);
itemListDom.onmouseout = ev => this._onMouseleave(ev);
}
if (this.get('clickable')) {
itemListDom.onclick = ev => this._onClick(ev);
}
}
// mouse move listener of an item
// when mouse over an item, reduce the opacity of the other items.
_onMousemove(ev) {
const items = this.get('items');
const target = ev.target;
let targetClass = target.className;
targetClass = targetClass.split(' ');
if (targetClass.indexOf(CONTAINER_CLASS) > -1 || targetClass.indexOf(LIST_CLASS) > -1) {
return;
}
const parentDom = getParentNode(target, ITEM_CLASS);
const hoveredItem = findItem(items, parentDom.getAttribute('data-value'));
if (hoveredItem) {
// change the opacity of other items
this.deactivate();
this.activate(parentDom.getAttribute('data-value'));
this.emit('itemhover', {
item: hoveredItem,
currentTarget: parentDom,
checked: hoveredItem.checked
});
} else if (!hoveredItem) {
// restore the opacity of all the items
this.deactivate();
this.emit('itemunhover', ev);
}
return;
}
// mouse leave listener of an item
_onMouseleave(ev) {
// restore the opacity of all the items when mouse leave
this.deactivate();
this.emit('itemunhover', ev);
return;
}
// the click listener of an item
_onClick(ev) {
const legendWrapper = this.get('legendWrapper');
const itemListDom = findNodeByClass(legendWrapper, LIST_CLASS);
const unCheckedColor = this.get('unCheckColor');
const items = this.get('items');
const mode = this.get('selectedMode');
const childNodes = itemListDom.childNodes;
const target = ev.target;
let targetClass = target.className;
targetClass = targetClass.split(' ');
if (targetClass.indexOf(CONTAINER_CLASS) > -1 || targetClass.indexOf(LIST_CLASS) > -1) {
return;
}
const parentDom = getParentNode(target, ITEM_CLASS);
const textDom = findNodeByClass(parentDom, TEXT_CLASS);
const markerDom = findNodeByClass(parentDom, MARKER_CLASS);
const clickedItem = findItem(items, parentDom.getAttribute('data-value'));
if (!clickedItem) {
return;
}
const domClass = parentDom.className;
const originColor = parentDom.getAttribute('data-color');
if (mode === 'single') { // 单选模式
// update checked status
clickedItem.checked = true;
// 其他图例项全部置灰
Util.each(childNodes, child => {
if (child !== parentDom) {
const childMarkerDom = findNodeByClass(child, MARKER_CLASS);
childMarkerDom.style.backgroundColor = unCheckedColor;
child.className = child.className.replace('checked', 'unChecked');
child.style.color = unCheckedColor;
const childItem = findItem(items, child.getAttribute('data-value'));
childItem.checked = false;
} else {
if (textDom) {
textDom.style.color = this.get('textStyle').fill;
}
if (markerDom) {
markerDom.style.backgroundColor = originColor;
}
parentDom.className = domClass.replace('unChecked', 'checked');
}
});
} else { // 混合模式
const clickedItemChecked = (domClass.indexOf('checked') !== -1);// domClass.includes('checked');
let count = 0;
Util.each(childNodes, child => {
if (child.className.indexOf('checked') !== -1) { // .includes('checked')
count++;
}
});
if (!this.get('allowAllCanceled') && clickedItemChecked && count === 1) {
this.emit('clicklastitem', {
item: clickedItem,
currentTarget: parentDom,
checked: (mode === 'single') ? true : clickedItem.checked
});
return;
}
// 在判断最后一个图例后再更新checked状态,防止点击最后一个图例item时图例样式没有变化但是checked状态改变了 fix #422
clickedItem.checked = !clickedItem.checked;
if (clickedItemChecked) {
if (markerDom) {
markerDom.style.backgroundColor = unCheckedColor;
}
parentDom.className = domClass.replace('checked', 'unChecked');
parentDom.style.color = unCheckedColor;
} else {
if (markerDom) {
markerDom.style.backgroundColor = originColor;
}
parentDom.className = domClass.replace('unChecked', 'checked');
parentDom.style.color = this.get('textStyle').fill;
}
}
this.emit('itemclick', {
item: clickedItem,
currentTarget: parentDom,
checked: (mode === 'single') ? true : clickedItem.checked
});
return;
}
// activate an item by reduce the opacity of other items.
// it is reserved for bi-direction interaction between charts / graph and legend
activate(value) {
const self = this;
const items = self.get('items');
const item = findItem(items, value);
const legendWrapper = self.get('legendWrapper');
const itemListDom = findNodeByClass(legendWrapper, LIST_CLASS);
const childNodes = itemListDom.childNodes;
childNodes.forEach(child => {
const childMarkerDom = findNodeByClass(child, MARKER_CLASS);
const childItem = findItem(items, child.getAttribute('data-value'));
if (this.get('highlight')) {
if (childItem === item && childItem.checked) {
childMarkerDom.style.border = '1px solid #333';
return;
}
} else {
if (childItem === item) {
childMarkerDom.style.opacity = self.get('activeOpacity');
} else {
if (childItem.checked) childMarkerDom.style.opacity = self.get('inactiveOpacity');
}
}
// if (childItem !== item && childItem.checked) {
// if (this.get('highlight')) {
// childMarkerDom.style.border = '1px solid #fff';
// } else childMarkerDom.style.opacity = 0.5;
// } else {
// if (this.get('highlight')) {
// childMarkerDom.style.border = '1px solid #333';
// } else childMarkerDom.style.opacity = 1;
// }
});
return;
}
// restore the opacity of items
// it is reserved for bi-direction interaction between charts / graph and legend
deactivate() {
const self = this;
const legendWrapper = self.get('legendWrapper');
const itemListDom = findNodeByClass(legendWrapper, LIST_CLASS);
const childNodes = itemListDom.childNodes;
childNodes.forEach(child => {
const childMarkerDom = findNodeByClass(child, MARKER_CLASS);
if (this.get('highlight')) {
childMarkerDom.style.border = '1px solid #fff';
} else {
childMarkerDom.style.opacity = self.get('inactiveOpacity');
}
});
return;
}
_renderHTML() {
// const canvas = this.get('canvas');
let container = this.get('container');
// const outterNode = container.parentNode;
const title = this.get('title');
const containerTpl = this.get('containerTpl');
const legendWrapper = DomUtil.createDom(containerTpl);
const titleDom = findNodeByClass(legendWrapper, TITLE_CLASS);
const itemListDom = findNodeByClass(legendWrapper, LIST_CLASS); // ul
const unCheckedColor = this.get('unCheckColor');
const LEGEND_STYLE = Util.deepMix({}, {
CONTAINER_CLASS: {
height: 'auto',
width: 'auto',
position: 'absolute',
overflowY: 'auto',
fontSize: '12px',
fontFamily: FONT_FAMILY,
lineHeight: '20px',
color: '#8C8C8C'
},
TITLE_CLASS: {
marginBottom: this.get('titleGap') + 'px',
fontSize: '12px',
color: '#333', // 默认样式
textBaseline: 'middle',
fontFamily: FONT_FAMILY
},
LIST_CLASS: {
listStyleType: 'none',
margin: 0,
padding: 0,
textAlign: 'center'
},
LIST_ITEM_CLASS: {
cursor: 'pointer',
marginBottom: '5px',
marginRight: '24px'
},
MARKER_CLASS: {
width: '9px',
height: '9px',
borderRadius: '50%',
display: 'inline-block',
marginRight: '4px',
verticalAlign: 'middle'
}
}, this.get('legendStyle'));
// fix:IE 9 兼容问题,先加入 legendWrapper
// let container = this.get('container');
if ((/^\#/.test(container)) || ((typeof container === 'string') && container.constructor === String)) { // 如果传入 dom 节点的 id
const id = container.replace('#', '');
container = document.getElementById(id);
container.appendChild(legendWrapper);
} else {
const position = this.get('position');
let rangeStyle = {};
if (position === 'left' || position === 'right') {
rangeStyle = {
maxHeight: (this.get('maxLength') || container.offsetHeight) + 'px'
};
} else {
rangeStyle = {
maxWidth: (this.get('maxLength') || container.offsetWidth) + 'px'
};
}
DomUtil.modifyCSS(legendWrapper, Util.mix({}, LEGEND_STYLE.CONTAINER_CLASS, rangeStyle, this.get(CONTAINER_CLASS)));
container.appendChild(legendWrapper);
}
DomUtil.modifyCSS(itemListDom, Util.mix({}, LEGEND_STYLE.LIST_CLASS, this.get(LIST_CLASS)));
// render title
if (titleDom) {
if (title && title.text) {
titleDom.innerHTML = title.text;
DomUtil.modifyCSS(titleDom, Util.mix({}, LEGEND_STYLE.TITLE_CLASS, this.get(TITLE_CLASS), title));
} else {
legendWrapper.removeChild(titleDom);
}
}
// 开始渲染图例项
const items = this.get('items');
const itemTpl = this.get('itemTpl');
const position = this.get('position');
const layout = this.get('layout');
const itemDisplay = ((position === 'right' || position === 'left') || layout === 'vertical') ? 'block' : 'inline-block';
const itemStyle = Util.mix({}, LEGEND_STYLE.LIST_ITEM_CLASS, {
display: itemDisplay
}, this.get(ITEM_CLASS));
const markerStyle = Util.mix({}, LEGEND_STYLE.MARKER_CLASS, this.get(MARKER_CLASS));
Util.each(items, (item, index) => {
const checked = item.checked;
const value = this._formatItemValue(item.value);
const markerColor = item.marker.fill || item.marker.stroke;
const color = checked ? markerColor : unCheckedColor;
let domStr;
if (Util.isFunction(itemTpl)) {
domStr = itemTpl(value, color, checked, index);
} else {
domStr = itemTpl;
}
const itemDiv = Util.substitute(domStr, Util.mix({}, item, {
index,
checked: checked ? 'checked' : 'unChecked',
value,
color,
originColor: markerColor,
// @2018-07-09 by blue.lb 修复如果legend值中存在双引号"时, 导致的无法点击触发legend正常操作bug
originValue: item.value.replace(/\"/g, '"')
}));
// li
const itemDom = DomUtil.createDom(itemDiv);
itemDom.style.color = this.get('textStyle').fill;
const markerDom = findNodeByClass(itemDom, MARKER_CLASS);
const textDom = findNodeByClass(itemDom, TEXT_CLASS);
DomUtil.modifyCSS(itemDom, itemStyle);
markerDom && DomUtil.modifyCSS(markerDom, markerStyle);
// textDom && DomUtil.modifyCSS(textDom, this.get('textStyle'));
if (!checked) {
itemDom.style.color = unCheckedColor;
if (markerDom) {
markerDom.style.backgroundColor = unCheckedColor;
}
}
itemListDom.appendChild(itemDom);
// abridge the text if the width of the text exceeds the width of the item
if (this.get('abridgeText')) {
let text = value;
// const itemWidth = parseFloat(this.get(ITEM_CLASS).width.substr(0, this.get(ITEM_CLASS).width.length - 2));
const itemWidth = itemDom.offsetWidth;
let fs = this.get('textStyle').fontSize;
if (isNaN(fs)) {
// 6.5pt = 6.5 * 1/72 * 96 = 8.6px
if (fs.indexOf('pt') !== -1) fs = parseFloat(fs.substr(0, fs.length - 2)) * 1 / 72 * 96;
else if (fs.indexOf('px') !== -1) fs = parseFloat(fs.substr(0, fs.length - 2));
}
const textWidth = fs * text.length;
const letterNum = Math.floor(itemWidth / fs);
if (itemWidth < 2 * fs) { // unable to contain '...'
text = '';
} else if (itemWidth < textWidth) { // replace the tail as '...
if (letterNum > 1) text = text.substr(0, letterNum - 1) + '...';
}
textDom.innerText = text;
// show the text tip while mouse hovering an item
itemDom.addEventListener('mouseover', () => {
const tipDom = findNodeByClass(legendWrapper.parentNode, 'textTip');
tipDom.style.display = 'block';
tipDom.style.left = itemDom.offsetLeft + itemDom.offsetWidth + 'px';
tipDom.style.top = itemDom.offsetTop + 15 + 'px';
tipDom.innerText = value;
});
// hide the text tip while mouse leave the item
itemDom.addEventListener('mouseout', () => {
const tipDom = findNodeByClass(legendWrapper.parentNode, 'textTip');
tipDom.style.display = 'none';
});
}
});
// append the tip div as a brother node of legend dom
if (this.get('abridgeText')) {
const tipTpl = this.get('tipTpl');
const tipDom = DomUtil.createDom(tipTpl);
const tipDomStyle = this.get('tipStyle');
DomUtil.modifyCSS(tipDom, tipDomStyle);
legendWrapper.parentNode.appendChild(tipDom);
// hide the tip while mouse entering the tip dom
tipDom.addEventListener('mouseover', () => {
tipDom.style.display = 'none';
});
}
this.set('legendWrapper', legendWrapper);
}
_adjustPositionOffset() {
const autoPosition = this.get('autoPosition');
// @2018-12-29 by maplor. if autoPosition is false, don't set inline-style
if (autoPosition === false) {
return;
}
const position = this.get('position');
const offset = this.get('offset');
const offsetX = this.get('offsetX');
const offsetY = this.get('offsetY');
if (offsetX) offset[0] = offsetX;
if (offsetY) offset[1] = offsetY;
const legendWrapper = this.get('legendWrapper');
legendWrapper.style.left = position[0] + 'px';
legendWrapper.style.top = position[1] + 'px';
legendWrapper.style.marginLeft = offset[0] + 'px';
legendWrapper.style.marginTop = offset[1] + 'px';
}
getWidth() {
return DomUtil.getOuterWidth(this.get('legendWrapper'));
}
getHeight() {
return DomUtil.getOuterHeight(this.get('legendWrapper'));
}
move(x, y) {
if (!(/^\#/.test(this.get('container')))) {
DomUtil.modifyCSS(this.get('legendWrapper'), {
left: x + 'px',
top: y + 'px'
});
this.set('x', x);
this.set('y', y);
} else {
super.move(x, y);
}
}
destroy() {
const legendWrapper = this.get('legendWrapper');
if (legendWrapper && legendWrapper.parentNode) {
legendWrapper.parentNode.removeChild(legendWrapper);
}
super.destroy();
}
}
module.exports = CatHtml;