123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- import {
- isObject,
- assign,
- forEach,
- reduce
- } from 'min-dash';
- import {
- append as svgAppend,
- attr as svgAttr,
- create as svgCreate,
- remove as svgRemove
- } from 'tiny-svg';
- var DEFAULT_BOX_PADDING = 0;
- var DEFAULT_LABEL_SIZE = {
- width: 150,
- height: 50
- };
- function parseAlign(align) {
- var parts = align.split('-');
- return {
- horizontal: parts[0] || 'center',
- vertical: parts[1] || 'top'
- };
- }
- function parsePadding(padding) {
- if (isObject(padding)) {
- return assign({ top: 0, left: 0, right: 0, bottom: 0 }, padding);
- } else {
- return {
- top: padding,
- left: padding,
- right: padding,
- bottom: padding
- };
- }
- }
- function getTextBBox(text, fakeText) {
- fakeText.textContent = text;
- var textBBox;
- try {
- var bbox,
- emptyLine = text === '';
- // add dummy text, when line is empty to
- // determine correct height
- fakeText.textContent = emptyLine ? 'dummy' : text;
- textBBox = fakeText.getBBox();
- // take text rendering related horizontal
- // padding into account
- bbox = {
- width: textBBox.width + textBBox.x * 2,
- height: textBBox.height
- };
- if (emptyLine) {
- // correct width
- bbox.width = 0;
- }
- return bbox;
- } catch (e) {
- return { width: 0, height: 0 };
- }
- }
- /**
- * Layout the next line and return the layouted element.
- *
- * Alters the lines passed.
- *
- * @param {Array<String>} lines
- * @return {Object} the line descriptor, an object { width, height, text }
- */
- function layoutNext(lines, maxWidth, fakeText) {
- var originalLine = lines.shift(),
- fitLine = originalLine;
- var textBBox;
- for (;;) {
- textBBox = getTextBBox(fitLine, fakeText);
- textBBox.width = fitLine ? textBBox.width : 0;
- // try to fit
- if (fitLine === ' ' || fitLine === '' || textBBox.width < Math.round(maxWidth) || fitLine.length < 2) {
- return fit(lines, fitLine, originalLine, textBBox);
- }
- fitLine = shortenLine(fitLine, textBBox.width, maxWidth);
- }
- }
- function fit(lines, fitLine, originalLine, textBBox) {
- if (fitLine.length < originalLine.length) {
- var remainder = originalLine.slice(fitLine.length).trim();
- lines.unshift(remainder);
- }
- return {
- width: textBBox.width,
- height: textBBox.height,
- text: fitLine
- };
- }
- /**
- * Shortens a line based on spacing and hyphens.
- * Returns the shortened result on success.
- *
- * @param {String} line
- * @param {Number} maxLength the maximum characters of the string
- * @return {String} the shortened string
- */
- function semanticShorten(line, maxLength) {
- var parts = line.split(/(\s|-)/g),
- part,
- shortenedParts = [],
- length = 0;
- // try to shorten via spaces + hyphens
- if (parts.length > 1) {
- while ((part = parts.shift())) {
- if (part.length + length < maxLength) {
- shortenedParts.push(part);
- length += part.length;
- } else {
- // remove previous part, too if hyphen does not fit anymore
- if (part === '-') {
- shortenedParts.pop();
- }
- break;
- }
- }
- }
- return shortenedParts.join('');
- }
- function shortenLine(line, width, maxWidth) {
- var length = Math.max(line.length * (maxWidth / width), 1);
- // try to shorten semantically (i.e. based on spaces and hyphens)
- var shortenedLine = semanticShorten(line, length);
- if (!shortenedLine) {
- // force shorten by cutting the long word
- shortenedLine = line.slice(0, Math.max(Math.round(length - 1), 1));
- }
- return shortenedLine;
- }
- function getHelperSvg() {
- var helperSvg = document.getElementById('helper-svg');
- if (!helperSvg) {
- helperSvg = svgCreate('svg');
- svgAttr(helperSvg, {
- id: 'helper-svg',
- width: 0,
- height: 0,
- style: 'visibility: hidden; position: fixed'
- });
- document.body.appendChild(helperSvg);
- }
- return helperSvg;
- }
- /**
- * Creates a new label utility
- *
- * @param {Object} config
- * @param {Dimensions} config.size
- * @param {Number} config.padding
- * @param {Object} config.style
- * @param {String} config.align
- */
- export default function Text(config) {
- this._config = assign({}, {
- size: DEFAULT_LABEL_SIZE,
- padding: DEFAULT_BOX_PADDING,
- style: {},
- align: 'center-top'
- }, config || {});
- }
- /**
- * Returns the layouted text as an SVG element.
- *
- * @param {String} text
- * @param {Object} options
- *
- * @return {SVGElement}
- */
- Text.prototype.createText = function(text, options) {
- return this.layoutText(text, options).element;
- };
- /**
- * Returns a labels layouted dimensions.
- *
- * @param {String} text to layout
- * @param {Object} options
- *
- * @return {Dimensions}
- */
- Text.prototype.getDimensions = function(text, options) {
- return this.layoutText(text, options).dimensions;
- };
- /**
- * Creates and returns a label and its bounding box.
- *
- * @method Text#createText
- *
- * @param {String} text the text to render on the label
- * @param {Object} options
- * @param {String} options.align how to align in the bounding box.
- * Any of { 'center-middle', 'center-top' },
- * defaults to 'center-top'.
- * @param {String} options.style style to be applied to the text
- * @param {boolean} options.fitBox indicates if box will be recalculated to
- * fit text
- *
- * @return {Object} { element, dimensions }
- */
- Text.prototype.layoutText = function(text, options) {
- var box = assign({}, this._config.size, options.box),
- style = assign({}, this._config.style, options.style),
- align = parseAlign(options.align || this._config.align),
- padding = parsePadding(options.padding !== undefined ? options.padding : this._config.padding),
- fitBox = options.fitBox || false;
- var lineHeight = getLineHeight(style);
- var lines = text.split(/\r?\n/g),
- layouted = [];
- var maxWidth = box.width - padding.left - padding.right;
- // ensure correct rendering by attaching helper text node to invisible SVG
- var helperText = svgCreate('text');
- svgAttr(helperText, { x: 0, y: 0 });
- svgAttr(helperText, style);
- var helperSvg = getHelperSvg();
- svgAppend(helperSvg, helperText);
- while (lines.length) {
- layouted.push(layoutNext(lines, maxWidth, helperText));
- }
- if (align.vertical === 'middle') {
- padding.top = padding.bottom = 0;
- }
- var totalHeight = reduce(layouted, function(sum, line, idx) {
- return sum + (lineHeight || line.height);
- }, 0) + padding.top + padding.bottom;
- var maxLineWidth = reduce(layouted, function(sum, line, idx) {
- return line.width > sum ? line.width : sum;
- }, 0);
- // the y position of the next line
- var y = padding.top;
- if (align.vertical === 'middle') {
- y += (box.height - totalHeight) / 2;
- }
- // magic number initial offset
- y -= (lineHeight || layouted[0].height) / 4;
- var textElement = svgCreate('text');
- svgAttr(textElement, style);
- // layout each line taking into account that parent
- // shape might resize to fit text size
- forEach(layouted, function(line) {
- var x;
- y += (lineHeight || line.height);
- switch (align.horizontal) {
- case 'left':
- x = padding.left;
- break;
- case 'right':
- x = ((fitBox ? maxLineWidth : maxWidth)
- - padding.right - line.width);
- break;
- default:
- // aka center
- x = Math.max((((fitBox ? maxLineWidth : maxWidth)
- - line.width) / 2 + padding.left), 0);
- }
- var tspan = svgCreate('tspan');
- svgAttr(tspan, { x: x, y: y });
- tspan.textContent = line.text;
- svgAppend(textElement, tspan);
- });
- svgRemove(helperText);
- var dimensions = {
- width: maxLineWidth,
- height: totalHeight
- };
- return {
- dimensions: dimensions,
- element: textElement
- };
- };
- function getLineHeight(style) {
- if ('fontSize' in style && 'lineHeight' in style) {
- return style.lineHeight * parseInt(style.fontSize, 10);
- }
- }
|