Skip to content

Link style #1681

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/components/annotations/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,18 @@ function drawOne(gd, index) {
}

function drawGraphicalElements() {
// if the text has *only* a link, make the whole box into a link
var anchor = annText.selectAll('a');
if(anchor.size() === 1 && anchor.text() === annText.text()) {
var wholeLink = annTextGroupInner.insert('a', ':first-child').attr({
'xlink:xlink:href': anchor.attr('xlink:href'),
'xlink:xlink:show': anchor.attr('xlink:show')
})
.style({cursor: 'pointer'});

wholeLink.node().appendChild(annTextBG.node());
}


// make sure lines are aligned the way they will be
// at the end, even if their position changes
Expand Down
147 changes: 87 additions & 60 deletions src/lib/svg_text_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,19 +221,25 @@ function texToSVG(_texString, _config, _callback) {
}

var TAG_STYLES = {
// would like to use baseline-shift but FF doesn't support it yet
// would like to use baseline-shift for sub/sup but FF doesn't support it
// so we need to use dy along with the uber hacky shift-back-to
// baseline below
sup: 'font-size:70%" dy="-0.6em',
sub: 'font-size:70%" dy="0.3em',
b: 'font-weight:bold',
i: 'font-style:italic',
a: '',
a: 'cursor:pointer',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

span: '',
br: '',
em: 'font-style:italic;font-weight:bold'
};

// sub/sup: extra tspan with zero-width space to get back to the right baseline
var TAG_CLOSE = {
sup: '<tspan dy="0.42em">&#x200b;</tspan>',
sub: '<tspan dy="-0.21em">&#x200b;</tspan>'
};

var PROTOCOLS = ['http:', 'https:', 'mailto:'];

var STRIP_TAGS = new RegExp('</?(' + Object.keys(TAG_STYLES).join('|') + ')( [^>]*)?/?>', 'g');
Expand All @@ -254,6 +260,18 @@ var UNICODE_TO_ENTITY = Object.keys(stringMappings.unicodeToEntity).map(function

var NEWLINES = /(\r\n?|\n)/g;

var SPLIT_TAGS = /(<[^<>]*>)/;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐎


var ONE_TAG = /<(\/?)([^ >]*)(\s+(.*))?>/i;

// Style and href: pull them out of either single or double quotes.
// Because we hack in other attributes with style (sub & sup), drop any trailing
// semicolon in user-supplied styles so we can consistently append the tag-dependent style
var STYLEMATCH = /(^|[\s"'])style\s*=\s*("([^"]*);?"|'([^']*);?')/i;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

learn something new every day.

Copy link
Contributor

@rreusser rreusser May 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(PSA: have to be careful with g! it adds a dependency on internal state of a regexp, which is super confusing if you're not expecting it)

var HREFMATCH = /(^|[\s"'])href\s*=\s*("([^"]*)"|'([^']*)')/i;

var COLORMATCH = /(^|;)\s*color:/;

exports.plainText = function(_str) {
// strip out our pseudo-html so we have a readable
// version to put into text fields
Expand All @@ -280,84 +298,93 @@ function encodeForHTML(_str) {
}

function convertToSVG(_str) {
_str = convertEntities(_str);

// normalize behavior between IE and others wrt newlines and whitespace:pre
// this combination makes IE barf https://github.com/plotly/plotly.js/issues/746
// Chrome and FF display \n, \r, or \r\n as a space in this mode.
// I feel like at some point we turned these into <br> but currently we don't so
// I'm just going to cement what we do now in Chrome and FF
_str = _str.replace(NEWLINES, ' ');
_str = convertEntities(_str)
/*
* Normalize behavior between IE and others wrt newlines and whitespace:pre
* this combination makes IE barf https://github.com/plotly/plotly.js/issues/746
* Chrome and FF display \n, \r, or \r\n as a space in this mode.
* I feel like at some point we turned these into <br> but currently we don't so
* I'm just going to cement what we do now in Chrome and FF
*/
.replace(NEWLINES, ' ');

var result = _str
.split(/(<[^<>]*>)/).map(function(d) {
var match = d.match(/<(\/?)([^ >]*)\s*(.*)>/i),
tag = match && match[2].toLowerCase(),
style = TAG_STYLES[tag];

if(style !== undefined) {
var close = match[1],
extra = match[3],
/**
* extraStyle: any random extra css (that's supported by svg)
* use this like <span style="font-family:Arial"> to change font in the middle
*
* at one point we supported <font family="..." size="..."> but as this isn't even
* valid HTML anymore and we dropped it accidentally for many months, we will not
* resurrect it.
*/
extraStyle = extra.match(/^style\s*=\s*"([^"]+)"\s*/i);

// anchor and br are the only ones that don't turn into a tspan
.split(SPLIT_TAGS).map(function(d) {
var match = d.match(ONE_TAG);
var tag = match && match[2].toLowerCase();
var tagStyle = TAG_STYLES[tag];

if(tagStyle !== undefined) {
var isClose = match[1];
if(isClose) return (tag === 'a' ? '</a>' : '</tspan>') + (TAG_CLOSE[tag] || '');

// break: later we'll turn these into newline <tspan>s
// but we need to know about all the other tags first
if(tag === 'br') return '<br>';

/**
* extra includes href and any random extra css (that's supported by svg)
* use this like <span style="font-family:Arial"> to change font in the middle
*
* at one point we supported <font family="..." size="..."> but as this isn't even
* valid HTML anymore and we dropped it accidentally for many months, we will not
* resurrect it.
*/
var extra = match[4];

var out;

// anchor is the only tag that doesn't turn into a tspan
if(tag === 'a') {
if(close) return '</a>';
else if(extra.substr(0, 4).toLowerCase() !== 'href') return '<a>';
else {
// remove quotes, leading '=', replace '&' with '&amp;'
var href = extra.substr(4)
.replace(/["']/g, '')
.replace(/=/, '');

// check protocol
var hrefMatch = extra && extra.match(HREFMATCH);
var href = hrefMatch && (hrefMatch[3] || hrefMatch[4]);

out = '<a';

if(href) {
// check safe protocols
var dummyAnchor = document.createElement('a');
dummyAnchor.href = href;
if(PROTOCOLS.indexOf(dummyAnchor.protocol) === -1) return '<a>';

return '<a xlink:show="new" xlink:href="' + encodeForHTML(href) + '">';
if(PROTOCOLS.indexOf(dummyAnchor.protocol) !== -1) {
out += ' xlink:show="new" xlink:href="' + encodeForHTML(href) + '"';
}
}
}
else if(tag === 'br') return '<br>';
else if(close) {
// closing tag

// sub/sup: extra tspan with zero-width space to get back to the right baseline
if(tag === 'sup') return '</tspan><tspan dy="0.42em">&#x200b;</tspan>';
if(tag === 'sub') return '</tspan><tspan dy="-0.21em">&#x200b;</tspan>';
else return '</tspan>';
}
else {
var tspanStart = '<tspan';
out = '<tspan';

if(tag === 'sup' || tag === 'sub') {
// sub/sup: extra zero-width space, fixes problem if new line starts with sub/sup
tspanStart = '&#x200b;' + tspanStart;
}

if(extraStyle) {
// most of the svg css users will care about is just like html,
// but font color is different. Let our users ignore this.
extraStyle = extraStyle[1].replace(/(^|;)\s*color:/, '$1 fill:');
style = encodeForHTML(extraStyle) + (style ? ';' + style : '');
out = '&#x200b;' + out;
}
}

return tspanStart + (style ? ' style="' + style + '"' : '') + '>';
// now add style, from both the tag name and any extra css
// Most of the svg css that users will care about is just like html,
// but font color is different (uses fill). Let our users ignore this.
var cssMatch = extra && extra.match(STYLEMATCH);
var css = cssMatch && (cssMatch[3] || cssMatch[4]);
if(css) {
css = encodeForHTML(css.replace(COLORMATCH, '$1 fill:'));
if(tagStyle) css += ';' + tagStyle;
}
else if(tagStyle) css = tagStyle;

if(css) return out + ' style="' + css + '">';

return out + '>';
}
else {
return exports.xml_entity_encode(d).replace(/</g, '&lt;');
}
});

// now deal with line breaks
// TODO: this next section attempts to close and reopen tags that
// span a line break. But
// a) it only closes and reopens one tag, and
// b) all tags are treated like equivalent tspans (even <a> which isn't a tspan even now!)
// we should really do this in a type-aware way *before* converting to tspans.
var indices = [];
for(var index = result.indexOf('<br>'); index > 0; index = result.indexOf('<br>', index + 1)) {
indices.push(index);
Expand Down
34 changes: 34 additions & 0 deletions test/jasmine/tests/annotations_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1251,4 +1251,38 @@ describe('annotation effects', function() {
.catch(failTest)
.then(done);
});

it('makes the whole text box a link if the link is the whole text', function(done) {
makePlot([
{x: 20, y: 20, text: '<a href="https://plot.ly">Plot</a>', showarrow: false},
{x: 50, y: 50, text: '<a href="https://plot.ly">or</a> not', showarrow: false},
{x: 80, y: 80, text: '<a href="https://plot.ly">arrow</a>'},
{x: 20, y: 80, text: 'nor <a href="https://plot.ly">this</a>'}
])
.then(function() {
function checkBoxLink(index, isLink) {
var boxLink = d3.selectAll('.annotation[data-index="' + index + '"] g>a');
expect(boxLink.size()).toBe(isLink ? 1 : 0);

var textLink = d3.selectAll('.annotation[data-index="' + index + '"] text a');
expect(textLink.size()).toBe(1);
checkLink(textLink);

if(isLink) checkLink(boxLink);
}

function checkLink(link) {
expect(link.style('cursor')).toBe('pointer');
expect(link.attr('xlink:href')).toBe('https://plot.ly');
expect(link.attr('xlink:show')).toBe('new');
}

checkBoxLink(0, true);
checkBoxLink(1, false);
checkBoxLink(2, true);
checkBoxLink(3, false);
})
.catch(failTest)
.then(done);
});
});
Loading