mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-10-24 08:24:14 +02:00
448 lines
18 KiB
JavaScript
448 lines
18 KiB
JavaScript
const d3 = require('d3')
|
||
|
||
module.exports = d3;
|
||
|
||
/*
|
||
D3 Text Wrap
|
||
By Vijith Assar
|
||
http://www.vijithassar.com
|
||
http://www.github.com/vijithassar
|
||
@vijithassar
|
||
|
||
Detailed instructions at http://www.github.com/vijithassar/d3textwrap
|
||
*/
|
||
|
||
(function () {
|
||
// set this variable to a string value to always force a particular
|
||
// wrap method for development purposes, for example to check tspan
|
||
// rendering using a foreignobject-enabled browser. set to 'tspan' to
|
||
// use tspans and 'foreignobject' to use foreignobject
|
||
var forceWrapMethod = false // by default no wrap method is forced
|
||
forceWrapMethod = 'tspans' // uncomment this statement to force tspans
|
||
// force_wrap_method = 'foreignobjects'; // uncomment this statement to force foreignobjects
|
||
|
||
// exit immediately if something in this location
|
||
// has already been defined; the plugin will defer to whatever
|
||
// else you're doing in your code
|
||
if (d3.selection.prototype.textwrap) {
|
||
return false
|
||
}
|
||
|
||
// double check the force_wrap_method flag
|
||
// and reset if someone screwed up the above
|
||
// settings
|
||
if (typeof forceWrapMethod === 'undefined') {
|
||
forceWrapMethod = false
|
||
}
|
||
|
||
// create the plugin method twice, both for regular use
|
||
// and again for use inside the enter() selection
|
||
d3.selection.prototype.textwrap = d3.selection.enter.prototype.textwrap = function (bounds, padding) {
|
||
// default value of padding is zero if it's undefined
|
||
padding = parseInt(padding) || 0
|
||
|
||
// save callee into a variable so we can continue to refer to it
|
||
// as the function scope changes
|
||
var selection = this
|
||
|
||
// create a variable to store desired return values in
|
||
var returnValue
|
||
|
||
// extract wrap boundaries from any d3-selected rect and return them
|
||
// in a format that matches the simpler object argument option
|
||
var extractBounds = function (bounds) {
|
||
// discard the nested array wrappers added by d3
|
||
var boundingRect = bounds[0][0]
|
||
// sanitize the svg element name so we can test against it
|
||
var elementType = boundingRect.tagName.toString()
|
||
// if it's not a rect, exit
|
||
if (elementType !== 'rect') {
|
||
return false
|
||
// if it's a rect, proceed to extracting the position attributes
|
||
} else {
|
||
var boundsExtracted = {}
|
||
boundsExtracted.x = d3.select(boundingRect).attr('x') || 0
|
||
boundsExtracted.y = d3.select(boundingRect).attr('y') || 0
|
||
boundsExtracted.width = d3.select(boundingRect).attr('width') || 0
|
||
boundsExtracted.height = d3.select(boundingRect).attr('height') || 0
|
||
// also pass along the getter function
|
||
boundsExtracted.attr = bounds.attr
|
||
}
|
||
return boundsExtracted
|
||
}
|
||
|
||
// double check the input argument for the wrapping
|
||
// boundaries to make sure it actually contains all
|
||
// the information we'll need in order to wrap successfully
|
||
var verifyBounds = function (bounds) {
|
||
// quickly add a simple getter method so you can use either
|
||
// bounds.x or bounds.attr('x') as your notation,
|
||
// the latter being a common convention among D3
|
||
// developers
|
||
if (!bounds.attr) {
|
||
bounds.attr = function (property) {
|
||
if (this[property]) {
|
||
return this[property]
|
||
}
|
||
}
|
||
}
|
||
// if it's an associative array, make sure it has all the
|
||
// necessary properties represented directly
|
||
if (
|
||
(typeof bounds === 'object') &&
|
||
(typeof bounds.x !== 'undefined') &&
|
||
(typeof bounds.y !== 'undefined') &&
|
||
(typeof bounds.width !== 'undefined') &&
|
||
(typeof bounds.height !== 'undefined')
|
||
// if that's the case, then the bounds are fine
|
||
) {
|
||
// return the lightly modified bounds
|
||
return bounds
|
||
// if it's a numerically indexed array, assume it's a
|
||
// d3-selected rect and try to extract the positions
|
||
} else if (
|
||
// first try to make sure it's an array using Array.isArray
|
||
(
|
||
(typeof Array.isArray === 'function') &&
|
||
(Array.isArray(bounds))
|
||
) ||
|
||
// but since Array.isArray isn't always supported, fall
|
||
// back to casting to the object to string when it's not
|
||
(Object.prototype.toString.call(bounds) === '[object Array]')
|
||
) {
|
||
// once you're sure it's an array, extract the boundaries
|
||
// from the rect
|
||
var extractedBounds = extractBounds(bounds)
|
||
return extractedBounds
|
||
} else {
|
||
// but if the bounds are neither an object nor a numerical
|
||
// array, then the bounds argument is invalid and you'll
|
||
// need to fix it
|
||
return false
|
||
}
|
||
}
|
||
|
||
var applyPadding = function (bounds, padding) {
|
||
var paddedBounds = bounds
|
||
if (padding !== 0) {
|
||
paddedBounds.x = parseInt(paddedBounds.x) + padding
|
||
paddedBounds.y = parseInt(paddedBounds.y) + padding
|
||
paddedBounds.width -= padding * 2
|
||
paddedBounds.height -= padding * 2
|
||
}
|
||
return paddedBounds
|
||
}
|
||
|
||
// verify bounds
|
||
var verifiedBounds = verifyBounds(bounds)
|
||
|
||
// modify bounds if a padding value is provided
|
||
if (padding) {
|
||
verifiedBounds = applyPadding(verifiedBounds, padding)
|
||
}
|
||
|
||
// check that we have the necessary conditions for this function to operate properly
|
||
if (
|
||
// selection it's operating on cannot be not empty
|
||
(selection.length === 0) ||
|
||
// d3 must be available
|
||
(!d3) ||
|
||
// desired wrapping bounds must be provided as an input argument
|
||
(!bounds) ||
|
||
// input bounds must validate
|
||
(!verifiedBounds)
|
||
) {
|
||
// try to return the calling selection if possible
|
||
// so as not to interfere with methods downstream in the
|
||
// chain
|
||
if (selection) {
|
||
return selection
|
||
// if all else fails, just return false. if you hit this point then you're
|
||
// almost certainly trying to call the textwrap() method on something that
|
||
// doesn't make sense!
|
||
} else {
|
||
return false
|
||
}
|
||
// if we've validated everything then we can finally proceed
|
||
// to the meat of this operation
|
||
} else {
|
||
// reassign the verified bounds as the set we want
|
||
// to work with from here on; this ensures that we're
|
||
// using the same data structure for our bounds regardless
|
||
// of whether the input argument was a simple object or
|
||
// a d3 selection
|
||
bounds = verifiedBounds
|
||
|
||
// wrap using html and foreignObjects if they are supported
|
||
var wrapWithForeignobjects = function (item) {
|
||
// establish variables to quickly reference target nodes later
|
||
var parent = d3.select(item[0].parentNode)
|
||
var textNode = parent.select('text')
|
||
var styledLineHeight = textNode.style('line-height')
|
||
// extract our desired content from the single text element
|
||
var textToWrap = textNode.text()
|
||
// remove the text node and replace with a foreign object
|
||
textNode.remove()
|
||
var foreignObject = parent.append('foreignObject')
|
||
// add foreign object and set dimensions, position, etc
|
||
foreignObject
|
||
.attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility')
|
||
.attr('x', bounds.x)
|
||
.attr('y', bounds.y)
|
||
.attr('width', bounds.width)
|
||
.attr('height', bounds.height)
|
||
// insert an HTML div
|
||
var wrapDiv = foreignObject
|
||
.append('xhtml:div')
|
||
// this class is currently hardcoded
|
||
// probably not necessary but easy to
|
||
// override using .classed() and for now
|
||
// it's nice to avoid a litany of input
|
||
// arguments
|
||
.attr('class', 'wrapped')
|
||
// set div to same dimensions as foreign object
|
||
wrapDiv
|
||
.style('height', bounds.height)
|
||
.style('width', bounds.width)
|
||
// insert text content
|
||
.html(textToWrap)
|
||
if (styledLineHeight) {
|
||
wrapDiv.style('line-height', styledLineHeight)
|
||
}
|
||
returnValue = parent.select('foreignObject')
|
||
}
|
||
|
||
// wrap with tspans if foreignObject is undefined
|
||
var wrapWithTspans = function (item) {
|
||
// operate on the first text item in the selection
|
||
var textNode = item[0]
|
||
var parent = textNode.parentNode
|
||
var textNodeSelected = d3.select(textNode)
|
||
// measure initial size of the text node as rendered
|
||
var textNodeHeight = textNode.getBBox().height
|
||
var textNodeWidth = textNode.getBBox().width
|
||
// figure out the line height, either from rendered height
|
||
// of the font or attached styling
|
||
var lineHeight
|
||
var renderedLineHeight = textNodeHeight
|
||
var styledLineHeight = textNodeSelected.style('line-height')
|
||
if (
|
||
(styledLineHeight) &&
|
||
(parseInt(styledLineHeight))
|
||
) {
|
||
lineHeight = parseInt(styledLineHeight.replace('px', ''))
|
||
} else {
|
||
lineHeight = renderedLineHeight
|
||
}
|
||
// only fire the rest of this if the text content
|
||
// overflows the desired dimensions
|
||
if (textNodeWidth > bounds.width) {
|
||
// store whatever is inside the text node
|
||
// in a variable and then zero out the
|
||
// initial content; we'll reinsert in a moment
|
||
// using tspan elements.
|
||
var textToWrap = textNodeSelected.text()
|
||
textNodeSelected.text('')
|
||
if (textToWrap) {
|
||
// keep track of whether we are splitting by spaces
|
||
// so we know whether to reinsert those spaces later
|
||
var breakDelimiter
|
||
// split at spaces to create an array of individual words
|
||
var textToWrapArray
|
||
if (textToWrap.indexOf(' ') !== -1) {
|
||
breakDelimiter = ' '
|
||
textToWrapArray = textToWrap.split(' ')
|
||
} else {
|
||
// if there are no spaces, figure out the split
|
||
// points by comparing rendered text width against
|
||
// bounds and translating that into character position
|
||
// cuts
|
||
breakDelimiter = ''
|
||
var stringLength = textToWrap.length
|
||
var numberOfSubstrings = Math.ceil(textNodeWidth / bounds.width)
|
||
var spliceInterval = Math.floor(stringLength / numberOfSubstrings)
|
||
if (
|
||
!(spliceInterval * numberOfSubstrings >= stringLength)
|
||
) {
|
||
numberOfSubstrings++
|
||
}
|
||
textToWrapArray = []
|
||
var substring
|
||
var startPosition
|
||
for (var i = 0; i < numberOfSubstrings; i++) {
|
||
startPosition = i * spliceInterval
|
||
substring = textToWrap.substr(startPosition, spliceInterval)
|
||
textToWrapArray.push(substring)
|
||
}
|
||
}
|
||
|
||
// new array where we'll store the words re-assembled into
|
||
// substrings that have been tested against the desired
|
||
// maximum wrapping width
|
||
var substrings = []
|
||
// computed text length is arguably incorrectly reported for
|
||
// all tspans after the first one, in that they will include
|
||
// the width of previous separate tspans. to compensate we need
|
||
// to manually track the computed text length of all those
|
||
// previous tspans and substrings, and then use that to offset
|
||
// the miscalculation. this then gives us the actual correct
|
||
// position we want to use in rendering the text in the SVG.
|
||
var totalOffset = 0
|
||
// object for storing the results of text length computations later
|
||
var temp = {}
|
||
// loop through the words and test the computed text length
|
||
// of the string against the maximum desired wrapping width
|
||
for (i = 0; i < textToWrapArray.length; i++) {
|
||
var word = textToWrapArray[i]
|
||
var previousString = textNodeSelected.text()
|
||
var previousWidth = textNode.getComputedTextLength()
|
||
// initialize the current word as the first word
|
||
// or append to the previous string if one exists
|
||
var newstring
|
||
if (previousString) {
|
||
newstring = previousString + breakDelimiter + word
|
||
} else {
|
||
newstring = word
|
||
}
|
||
// add the newest substring back to the text node and
|
||
// measure the length
|
||
textNodeSelected.text(newstring)
|
||
var newWidth = textNode.getComputedTextLength()
|
||
// adjust the length by the offset we've tracked
|
||
// due to the misreported length discussed above
|
||
|
||
// if our latest version of the string is too
|
||
// big for the bounds, use the previous
|
||
// version of the string (without the newest word
|
||
// added) and use the latest word to restart the
|
||
// process with a new tspan
|
||
if (newWidth > bounds.width) {
|
||
if (
|
||
(previousString) &&
|
||
(previousString !== '')
|
||
) {
|
||
totalOffset = totalOffset + previousWidth
|
||
temp = { string: previousString, width: previousWidth, offset: totalOffset }
|
||
substrings.push(temp)
|
||
textNodeSelected.text('')
|
||
textNodeSelected.text(word)
|
||
// Handle case where there is just one more word to be wrapped
|
||
if (i === textToWrapArray.length - 1) {
|
||
newstring = word
|
||
textNodeSelected.text(newstring)
|
||
newWidth = textNode.getComputedTextLength()
|
||
}
|
||
}
|
||
}
|
||
// if we're up to the last word in the array,
|
||
// get the computed length as is without
|
||
// appending anything further to it
|
||
if (i === textToWrapArray.length - 1) {
|
||
textNodeSelected.text('')
|
||
var finalString = newstring
|
||
if (
|
||
(finalString) &&
|
||
(finalString !== '')
|
||
) {
|
||
if ((newWidth - totalOffset) > 0) { newWidth = newWidth - totalOffset }
|
||
temp = { string: finalString, width: newWidth, offset: totalOffset }
|
||
substrings.push(temp)
|
||
}
|
||
}
|
||
}
|
||
|
||
// append each substring as a tspan
|
||
var currentTspan
|
||
// var tspanCount
|
||
// double check that the text content has been removed
|
||
// before we start appending tspans
|
||
textNodeSelected.text('')
|
||
for (i = 0; i < substrings.length; i++) {
|
||
substring = substrings[i].string
|
||
// only append if we're sure it won't make the tspans
|
||
// overflow the bounds.
|
||
if ((i) * lineHeight < bounds.height - (lineHeight * 1.5)) {
|
||
currentTspan = textNodeSelected.append('tspan')
|
||
.text(substring)
|
||
// vertical shift to all tspans after the first one
|
||
currentTspan
|
||
.attr('dy', function (d) {
|
||
if (i > 0) {
|
||
return lineHeight
|
||
}
|
||
})
|
||
// shift left from default position, which
|
||
// is probably based on the full length of the
|
||
// text string until we make this adjustment
|
||
currentTspan
|
||
.attr('x', function () {
|
||
var xOffset = bounds.x
|
||
if (padding) { xOffset += padding }
|
||
return xOffset
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// position the overall text node, whether wrapped or not
|
||
textNodeSelected.attr('y', function () {
|
||
var yOffset = bounds.y
|
||
// shift by line-height to move the baseline into
|
||
// the bounds – otherwise the text baseline would be
|
||
// at the top of the bounds
|
||
if (lineHeight) { yOffset += lineHeight }
|
||
// shift by padding, if it's there
|
||
if (padding) { yOffset += padding }
|
||
return yOffset
|
||
})
|
||
// shift to the right by the padding value
|
||
textNodeSelected.attr('x', function () {
|
||
var xOffset = bounds.x
|
||
if (padding) { xOffset += padding }
|
||
return xOffset
|
||
})
|
||
|
||
// assign our modified text node with tspans
|
||
// to the return value
|
||
returnValue = d3.select(parent).selectAll('text')
|
||
}
|
||
|
||
// variable used to hold the functions that let us
|
||
// switch between the wrap methods
|
||
var wrapMethod
|
||
|
||
// if a wrap method if being forced, assign that
|
||
// function
|
||
if (forceWrapMethod) {
|
||
if (forceWrapMethod === 'foreignobjects') {
|
||
wrapMethod = wrapWithForeignobjects
|
||
} else if (forceWrapMethod === 'tspans') {
|
||
wrapMethod = wrapWithTspans
|
||
}
|
||
}
|
||
|
||
// if no wrap method is being forced, then instead
|
||
// test for browser support of foreignobject and
|
||
// use whichever wrap method makes sense accordingly
|
||
if (!forceWrapMethod) {
|
||
if (typeof SVGForeignObjectElement !== 'undefined') {
|
||
wrapMethod = wrapWithForeignobjects
|
||
} else {
|
||
wrapMethod = wrapWithTspans
|
||
}
|
||
}
|
||
|
||
// run the desired wrap function for each item
|
||
// in the d3 selection that called .textwrap()
|
||
for (var i = 0; i < selection.length; i++) {
|
||
var item = selection[i]
|
||
wrapMethod(item)
|
||
}
|
||
|
||
// return the modified nodes so we can chain other
|
||
// methods to them.
|
||
return returnValue
|
||
}
|
||
}
|
||
})()
|