refined js
This commit is contained in:
parent
8537c86963
commit
2082617721
@ -1,265 +1,6 @@
|
|||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js"
|
|
||||||
integrity="sha512-FHsFVKQ/T1KWJDGSbrUhTJyS1ph3eRrxI228ND0EGaEp6v4a/vGwPWd3Dtd/+9cI7ccofZvl/wulICEurHN1pg=="
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
|
|
||||||
<div id="zoom"></div>
|
<div id="zoom"></div>
|
||||||
<div id="graph-wrapper">
|
<div id="graph-wrapper">
|
||||||
<script>
|
<script>
|
||||||
const MINIMAL_NODE_SIZE = 8;
|
|
||||||
const MAX_NODE_SIZE = 12;
|
|
||||||
const ACTIVE_RADIUS_FACTOR = 1.5;
|
|
||||||
const STROKE = 1;
|
|
||||||
const FONT_SIZE = 16;
|
|
||||||
const TICKS = 200;
|
|
||||||
const FONT_BASELINE = 40;
|
|
||||||
const MAX_LABEL_LENGTH = 50;
|
|
||||||
|
|
||||||
const graphData = {% include notes_graph.json %}
|
const graphData = {% include notes_graph.json %}
|
||||||
let nodesData = graphData.nodes;
|
|
||||||
let linksData = graphData.edges;
|
|
||||||
|
|
||||||
const nodeSize = {};
|
|
||||||
|
|
||||||
const updateNodeSize = () => {
|
|
||||||
nodesData.forEach((el) => {
|
|
||||||
let weight =
|
|
||||||
3 *
|
|
||||||
Math.sqrt(
|
|
||||||
linksData.filter((l) => l.source === el.id || l.target === el.id)
|
|
||||||
.length + 1
|
|
||||||
);
|
|
||||||
if (weight < MINIMAL_NODE_SIZE) {
|
|
||||||
weight = MINIMAL_NODE_SIZE;
|
|
||||||
} else if (weight > MAX_NODE_SIZE) {
|
|
||||||
weight = MAX_NODE_SIZE;
|
|
||||||
}
|
|
||||||
nodeSize[el.id] = weight;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClick = (d) => {
|
|
||||||
window.location = d.path
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseover = function (d) {
|
|
||||||
const relatedNodesSet = new Set();
|
|
||||||
linksData
|
|
||||||
.filter((n) => n.target.id == d.id || n.source.id == d.id)
|
|
||||||
.forEach((n) => {
|
|
||||||
relatedNodesSet.add(n.target.id);
|
|
||||||
relatedNodesSet.add(n.source.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
node.attr("class", (node_d) => {
|
|
||||||
if (node_d.id !== d.id && !relatedNodesSet.has(node_d.id)) {
|
|
||||||
return "inactive";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
|
|
||||||
link.attr("class", (link_d) => {
|
|
||||||
if (link_d.source.id !== d.id && link_d.target.id !== d.id) {
|
|
||||||
return "inactive";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
|
|
||||||
link.attr("stroke-width", (link_d) => {
|
|
||||||
if (link_d.source.id === d.id || link_d.target.id === d.id) {
|
|
||||||
return STROKE * 4;
|
|
||||||
}
|
|
||||||
return STROKE;
|
|
||||||
});
|
|
||||||
text.attr("class", (text_d) => {
|
|
||||||
if (text_d.id !== d.id && !relatedNodesSet.has(text_d.id)) {
|
|
||||||
return "inactive";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseout = function (d) {
|
|
||||||
node.attr("class", "");
|
|
||||||
link.attr("class", "");
|
|
||||||
text.attr("class", "");
|
|
||||||
link.attr("stroke-width", STROKE);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sameNodes = (previous, next) => {
|
|
||||||
if (next.length !== previous.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const map = new Map();
|
|
||||||
for (const node of previous) {
|
|
||||||
map.set(node.id, node.label);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const node of next) {
|
|
||||||
const found = map.get(node.id);
|
|
||||||
if (!found || found !== node.title) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sameEdges = (previous, next) => {
|
|
||||||
if (next.length !== previous.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const set = new Set();
|
|
||||||
for (const edge of previous) {
|
|
||||||
set.add(`${edge.source.id}-${edge.target.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const edge of next) {
|
|
||||||
if (!set.has(`${edge.source}-${edge.target}`)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const graphWrapper = document.getElementById('graph-wrapper')
|
|
||||||
const element = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
||||||
element.setAttribute("width", graphWrapper.getBoundingClientRect().width);
|
|
||||||
element.setAttribute("height", window.innerHeight * 0.8);
|
|
||||||
graphWrapper.appendChild(element);
|
|
||||||
|
|
||||||
const reportWindowSize = () => {
|
|
||||||
element.setAttribute("width", window.innerWidth);
|
|
||||||
element.setAttribute("height", window.innerHeight);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.onresize = reportWindowSize;
|
|
||||||
|
|
||||||
const svg = d3.select("svg");
|
|
||||||
const width = Number(svg.attr("width"));
|
|
||||||
const height = Number(svg.attr("height"));
|
|
||||||
let zoomLevel = 1;
|
|
||||||
|
|
||||||
const simulation = d3
|
|
||||||
.forceSimulation(nodesData)
|
|
||||||
.force("forceX", d3.forceX().x(width / 2))
|
|
||||||
.force("forceY", d3.forceY().y(height / 2))
|
|
||||||
.force("charge", d3.forceManyBody())
|
|
||||||
.force(
|
|
||||||
"link",
|
|
||||||
d3
|
|
||||||
.forceLink(linksData)
|
|
||||||
.id((d) => d.id)
|
|
||||||
.distance(70)
|
|
||||||
)
|
|
||||||
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
||||||
.force("collision", d3.forceCollide().radius(80))
|
|
||||||
.stop();
|
|
||||||
|
|
||||||
const g = svg.append("g");
|
|
||||||
let link = g.append("g").attr("class", "links").selectAll(".link");
|
|
||||||
let node = g.append("g").attr("class", "nodes").selectAll(".node");
|
|
||||||
let text = g.append("g").attr("class", "text").selectAll(".text");
|
|
||||||
|
|
||||||
const resize = () => {
|
|
||||||
if (d3.event) {
|
|
||||||
const scale = d3.event.transform;
|
|
||||||
zoomLevel = scale.k;
|
|
||||||
g.attr("transform", scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
const zoomOrKeep = (value) => (zoomLevel >= 1 ? value / zoomLevel : value);
|
|
||||||
|
|
||||||
const font = Math.max(Math.round(zoomOrKeep(FONT_SIZE)), 1);
|
|
||||||
|
|
||||||
text.attr("font-size", (d) => font);
|
|
||||||
text.attr("y", (d) => d.y - zoomOrKeep(FONT_BASELINE) + 8);
|
|
||||||
link.attr("stroke-width", zoomOrKeep(STROKE));
|
|
||||||
node.attr("r", (d) => {
|
|
||||||
return zoomOrKeep(nodeSize[d.id]);
|
|
||||||
});
|
|
||||||
svg
|
|
||||||
.selectAll("circle")
|
|
||||||
.filter((_d, i, nodes) => d3.select(nodes[i]).attr("active"))
|
|
||||||
.attr("r", (d) => zoomOrKeep(ACTIVE_RADIUS_FACTOR * nodeSize[d.id]));
|
|
||||||
|
|
||||||
document.getElementById("zoom").innerHTML = zoomLevel.toFixed(2);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ticked = () => {
|
|
||||||
node.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
|
|
||||||
text
|
|
||||||
.attr("x", (d) => d.x)
|
|
||||||
.attr("y", (d) => d.y - (FONT_BASELINE - nodeSize[d.id]) / zoomLevel);
|
|
||||||
link
|
|
||||||
.attr("x1", (d) => d.source.x)
|
|
||||||
.attr("y1", (d) => d.source.y)
|
|
||||||
.attr("x2", (d) => d.target.x)
|
|
||||||
.attr("y2", (d) => d.target.y);
|
|
||||||
};
|
|
||||||
|
|
||||||
const restart = () => {
|
|
||||||
updateNodeSize();
|
|
||||||
node = node.data(nodesData, (d) => d.id);
|
|
||||||
node.exit().remove();
|
|
||||||
node = node
|
|
||||||
.enter()
|
|
||||||
.append("circle")
|
|
||||||
.attr("r", (d) => {
|
|
||||||
return nodeSize[d.id];
|
|
||||||
})
|
|
||||||
.on("click", onClick)
|
|
||||||
.on("mouseover", onMouseover)
|
|
||||||
.on("mouseout", onMouseout)
|
|
||||||
.merge(node);
|
|
||||||
|
|
||||||
link = link.data(linksData, (d) => `${d.source.id}-${d.target.id}`);
|
|
||||||
link.exit().remove();
|
|
||||||
link = link.enter().append("line").attr("stroke-width", STROKE).merge(link);
|
|
||||||
|
|
||||||
text = text.data(nodesData, (d) => d.label);
|
|
||||||
text.exit().remove();
|
|
||||||
text = text
|
|
||||||
.enter()
|
|
||||||
.append("text")
|
|
||||||
.text((d) => shorten(d.label.replace(/_*/g, ""), MAX_LABEL_LENGTH))
|
|
||||||
.attr("font-size", `${FONT_SIZE}px`)
|
|
||||||
.attr("text-anchor", "middle")
|
|
||||||
.attr("alignment-baseline", "central")
|
|
||||||
.on("click", onClick)
|
|
||||||
.on("mouseover", onMouseover)
|
|
||||||
.on("mouseout", onMouseout)
|
|
||||||
.merge(text);
|
|
||||||
|
|
||||||
node.attr("active", (d) => isCurrentPath(d.path) ? true : null);
|
|
||||||
text.attr("active", (d) => isCurrentPath(d.path) ? true : null);
|
|
||||||
|
|
||||||
simulation.nodes(nodesData);
|
|
||||||
simulation.force("link").links(linksData);
|
|
||||||
simulation.alpha(1).restart();
|
|
||||||
simulation.stop();
|
|
||||||
|
|
||||||
for (let i = 0; i < TICKS; i++) {
|
|
||||||
simulation.tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
ticked();
|
|
||||||
};
|
|
||||||
|
|
||||||
const zoomHandler = d3.zoom().scaleExtent([0.2, 3]).on("zoom", resize);
|
|
||||||
|
|
||||||
zoomHandler(svg);
|
|
||||||
restart();
|
|
||||||
|
|
||||||
function isCurrentPath(notePath) {
|
|
||||||
return window.location.pathname.includes(notePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
function shorten(str, maxLen, separator = ' ') {
|
|
||||||
if (str.length <= maxLen) return str;
|
|
||||||
return str.substr(0, str.lastIndexOf(separator, maxLen)) + '...';
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
@ -8,6 +8,7 @@
|
|||||||
</main>
|
</main>
|
||||||
{%- include footer.html -%}
|
{%- include footer.html -%}
|
||||||
{% include link-previews.html %}
|
{% include link-previews.html %}
|
||||||
<script src="{{ "/assets/js/scripts.js" | relative_url }}" async></script>
|
<script src="{{ "/assets/js/d3.v6.min.js" | relative_url }}" defer></script>
|
||||||
|
<script src="{{ "/assets/js/scripts.js" | relative_url }}" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -16,7 +16,8 @@
|
|||||||
|
|
||||||
.nodes circle {
|
.nodes circle {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
fill: $accent-color;
|
stroke: $accent-color;
|
||||||
|
fill: transparent;
|
||||||
transition: all 0.15s ease-out;
|
transition: all 0.15s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2
assets/js/d3.v6.min.js
vendored
Normal file
2
assets/js/d3.v6.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -75,3 +75,228 @@ function setupListeners (linkElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.querySelectorAll('.note-contents a').forEach(setupListeners)
|
document.querySelectorAll('.note-contents a').forEach(setupListeners)
|
||||||
|
|
||||||
|
// Notes graph
|
||||||
|
|
||||||
|
const d3 = window.d3
|
||||||
|
|
||||||
|
if (typeof graphData !== 'undefined') {
|
||||||
|
const MINIMAL_NODE_SIZE = 10
|
||||||
|
const MAX_NODE_SIZE = 12
|
||||||
|
const ACTIVE_RADIUS_FACTOR = 1.5
|
||||||
|
const STROKE = 1
|
||||||
|
const FONT_SIZE = 16
|
||||||
|
const TICKS = 200
|
||||||
|
const FONT_BASELINE = 40
|
||||||
|
const MAX_LABEL_LENGTH = 50
|
||||||
|
|
||||||
|
/* global graphData */
|
||||||
|
|
||||||
|
const nodesData = graphData.nodes
|
||||||
|
const linksData = graphData.edges
|
||||||
|
|
||||||
|
const nodeSize = {}
|
||||||
|
|
||||||
|
const updateNodeSize = () => {
|
||||||
|
nodesData.forEach((el) => {
|
||||||
|
let weight =
|
||||||
|
3 *
|
||||||
|
Math.sqrt(
|
||||||
|
linksData.filter((l) => l.source === el.id || l.target === el.id)
|
||||||
|
.length + 1
|
||||||
|
)
|
||||||
|
if (weight < MINIMAL_NODE_SIZE) {
|
||||||
|
weight = MINIMAL_NODE_SIZE
|
||||||
|
} else if (weight > MAX_NODE_SIZE) {
|
||||||
|
weight = MAX_NODE_SIZE
|
||||||
|
}
|
||||||
|
nodeSize[el.id] = weight
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = (d) => {
|
||||||
|
window.location = d3.select(d.target).data()[0].path
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseover = function (d) {
|
||||||
|
const relatedNodesSet = new Set()
|
||||||
|
linksData
|
||||||
|
.filter((n) => n.target.id === d.id || n.source.id === d.id)
|
||||||
|
.forEach((n) => {
|
||||||
|
relatedNodesSet.add(n.target.id)
|
||||||
|
relatedNodesSet.add(n.source.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
node.attr('class', (nodeD) => {
|
||||||
|
if (nodeD.id !== d.id && !relatedNodesSet.has(nodeD.id)) {
|
||||||
|
return 'inactive'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
link.attr('class', (linkD) => {
|
||||||
|
if (linkD.source.id !== d.id && linkD.target.id !== d.id) {
|
||||||
|
return 'inactive'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
link.attr('stroke-width', (linkD) => {
|
||||||
|
if (linkD.source.id === d.id || linkD.target.id === d.id) {
|
||||||
|
return STROKE * 4
|
||||||
|
}
|
||||||
|
return STROKE
|
||||||
|
})
|
||||||
|
text.attr('class', (textD) => {
|
||||||
|
if (textD.id !== d.id && !relatedNodesSet.has(textD.id)) {
|
||||||
|
return 'inactive'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseout = function (d) {
|
||||||
|
node.attr('class', '')
|
||||||
|
link.attr('class', '')
|
||||||
|
text.attr('class', '')
|
||||||
|
link.attr('stroke-width', STROKE)
|
||||||
|
}
|
||||||
|
|
||||||
|
const graphWrapper = document.getElementById('graph-wrapper')
|
||||||
|
const element = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||||
|
element.setAttribute('width', graphWrapper.getBoundingClientRect().width)
|
||||||
|
element.setAttribute('height', window.innerHeight * 0.8)
|
||||||
|
graphWrapper.appendChild(element)
|
||||||
|
|
||||||
|
const reportWindowSize = () => {
|
||||||
|
element.setAttribute('width', window.innerWidth)
|
||||||
|
element.setAttribute('height', window.innerHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onresize = reportWindowSize
|
||||||
|
|
||||||
|
const svg = d3.select('svg')
|
||||||
|
const width = Number(svg.attr('width'))
|
||||||
|
const height = Number(svg.attr('height'))
|
||||||
|
let zoomLevel = 1
|
||||||
|
|
||||||
|
const simulation = d3
|
||||||
|
.forceSimulation(nodesData)
|
||||||
|
.force('forceX', d3.forceX().x(width / 2))
|
||||||
|
.force('forceY', d3.forceY().y(height / 2))
|
||||||
|
.force('charge', d3.forceManyBody())
|
||||||
|
.force(
|
||||||
|
'link',
|
||||||
|
d3
|
||||||
|
.forceLink(linksData)
|
||||||
|
.id((d) => d.id)
|
||||||
|
.distance(70)
|
||||||
|
)
|
||||||
|
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||||
|
.force('collision', d3.forceCollide().radius(80))
|
||||||
|
.stop()
|
||||||
|
|
||||||
|
const g = svg.append('g')
|
||||||
|
let link = g.append('g').attr('class', 'links').selectAll('.link')
|
||||||
|
let node = g.append('g').attr('class', 'nodes').selectAll('.node')
|
||||||
|
let text = g.append('g').attr('class', 'text').selectAll('.text')
|
||||||
|
|
||||||
|
const resize = (event) => {
|
||||||
|
if (event) {
|
||||||
|
const scale = event.transform
|
||||||
|
zoomLevel = scale.k
|
||||||
|
g.attr('transform', scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoomOrKeep = (value) => (zoomLevel >= 1 ? value / zoomLevel : value)
|
||||||
|
|
||||||
|
const font = Math.max(Math.round(zoomOrKeep(FONT_SIZE)), 1)
|
||||||
|
|
||||||
|
text.attr('font-size', (d) => font)
|
||||||
|
text.attr('y', (d) => d.y - zoomOrKeep(FONT_BASELINE) + 8)
|
||||||
|
link.attr('stroke-width', zoomOrKeep(STROKE))
|
||||||
|
node.attr('r', (d) => {
|
||||||
|
return zoomOrKeep(nodeSize[d.id])
|
||||||
|
})
|
||||||
|
svg
|
||||||
|
.selectAll('circle')
|
||||||
|
.filter((_d, i, nodes) => d3.select(nodes[i]).attr('active'))
|
||||||
|
.attr('r', (d) => zoomOrKeep(ACTIVE_RADIUS_FACTOR * nodeSize[d.id]))
|
||||||
|
|
||||||
|
document.getElementById('zoom').innerHTML = zoomLevel.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticked = () => {
|
||||||
|
node.attr('cx', (d) => d.x).attr('cy', (d) => d.y)
|
||||||
|
text
|
||||||
|
.attr('x', (d) => d.x)
|
||||||
|
.attr('y', (d) => d.y - (FONT_BASELINE - nodeSize[d.id]) / zoomLevel)
|
||||||
|
link
|
||||||
|
.attr('x1', (d) => d.source.x)
|
||||||
|
.attr('y1', (d) => d.source.y)
|
||||||
|
.attr('x2', (d) => d.target.x)
|
||||||
|
.attr('y2', (d) => d.target.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
const restart = () => {
|
||||||
|
updateNodeSize()
|
||||||
|
node = node.data(nodesData, (d) => d.id)
|
||||||
|
node.exit().remove()
|
||||||
|
node = node
|
||||||
|
.enter()
|
||||||
|
.append('circle')
|
||||||
|
.attr('r', (d) => {
|
||||||
|
return nodeSize[d.id]
|
||||||
|
})
|
||||||
|
.on('click', onClick)
|
||||||
|
.on('mouseover', onMouseover)
|
||||||
|
.on('mouseout', onMouseout)
|
||||||
|
.merge(node)
|
||||||
|
|
||||||
|
link = link.data(linksData, (d) => `${d.source.id}-${d.target.id}`)
|
||||||
|
link.exit().remove()
|
||||||
|
link = link.enter().append('line').attr('stroke-width', STROKE).merge(link)
|
||||||
|
|
||||||
|
text = text.data(nodesData, (d) => d.label)
|
||||||
|
text.exit().remove()
|
||||||
|
text = text
|
||||||
|
.enter()
|
||||||
|
.append('text')
|
||||||
|
.text((d) => shorten(d.label.replace(/_*/g, ''), MAX_LABEL_LENGTH))
|
||||||
|
.attr('font-size', `${FONT_SIZE}px`)
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('alignment-baseline', 'central')
|
||||||
|
.on('click', onClick)
|
||||||
|
.on('mouseover', onMouseover)
|
||||||
|
.on('mouseout', onMouseout)
|
||||||
|
.merge(text)
|
||||||
|
|
||||||
|
node.attr('active', (d) => isCurrentPath(d.path) ? true : null)
|
||||||
|
text.attr('active', (d) => isCurrentPath(d.path) ? true : null)
|
||||||
|
|
||||||
|
simulation.nodes(nodesData)
|
||||||
|
simulation.force('link').links(linksData)
|
||||||
|
simulation.alpha(1).restart()
|
||||||
|
simulation.stop()
|
||||||
|
|
||||||
|
for (let i = 0; i < TICKS; i++) {
|
||||||
|
simulation.tick()
|
||||||
|
}
|
||||||
|
|
||||||
|
ticked()
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoomHandler = d3.zoom().scaleExtent([0.2, 3]).on('zoom', resize)
|
||||||
|
|
||||||
|
zoomHandler(svg)
|
||||||
|
restart()
|
||||||
|
|
||||||
|
function isCurrentPath (notePath) {
|
||||||
|
return window.location.pathname.includes(notePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
function shorten (str, maxLen, separator = ' ') {
|
||||||
|
if (str.length <= maxLen) return str
|
||||||
|
return str.substr(0, str.lastIndexOf(separator, maxLen)) + '...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user