Skip to content

Commit ad65c23

Browse files
authored
Merge pull request #1681 from plotly/link-style
Link style
2 parents 6653e71 + 1019d7f commit ad65c23

File tree

5 files changed

+223
-78
lines changed

5 files changed

+223
-78
lines changed

src/components/annotations/draw.js

+12
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,18 @@ function drawOne(gd, index) {
188188
}
189189

190190
function drawGraphicalElements() {
191+
// if the text has *only* a link, make the whole box into a link
192+
var anchor = annText.selectAll('a');
193+
if(anchor.size() === 1 && anchor.text() === annText.text()) {
194+
var wholeLink = annTextGroupInner.insert('a', ':first-child').attr({
195+
'xlink:xlink:href': anchor.attr('xlink:href'),
196+
'xlink:xlink:show': anchor.attr('xlink:show')
197+
})
198+
.style({cursor: 'pointer'});
199+
200+
wholeLink.node().appendChild(annTextBG.node());
201+
}
202+
191203

192204
// make sure lines are aligned the way they will be
193205
// at the end, even if their position changes

src/lib/svg_text_utils.js

+87-60
Original file line numberDiff line numberDiff line change
@@ -221,19 +221,25 @@ function texToSVG(_texString, _config, _callback) {
221221
}
222222

223223
var TAG_STYLES = {
224-
// would like to use baseline-shift but FF doesn't support it yet
224+
// would like to use baseline-shift for sub/sup but FF doesn't support it
225225
// so we need to use dy along with the uber hacky shift-back-to
226226
// baseline below
227227
sup: 'font-size:70%" dy="-0.6em',
228228
sub: 'font-size:70%" dy="0.3em',
229229
b: 'font-weight:bold',
230230
i: 'font-style:italic',
231-
a: '',
231+
a: 'cursor:pointer',
232232
span: '',
233233
br: '',
234234
em: 'font-style:italic;font-weight:bold'
235235
};
236236

237+
// sub/sup: extra tspan with zero-width space to get back to the right baseline
238+
var TAG_CLOSE = {
239+
sup: '<tspan dy="0.42em">&#x200b;</tspan>',
240+
sub: '<tspan dy="-0.21em">&#x200b;</tspan>'
241+
};
242+
237243
var PROTOCOLS = ['http:', 'https:', 'mailto:'];
238244

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

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

263+
var SPLIT_TAGS = /(<[^<>]*>)/;
264+
265+
var ONE_TAG = /<(\/?)([^ >]*)(\s+(.*))?>/i;
266+
267+
// Style and href: pull them out of either single or double quotes.
268+
// Because we hack in other attributes with style (sub & sup), drop any trailing
269+
// semicolon in user-supplied styles so we can consistently append the tag-dependent style
270+
var STYLEMATCH = /(^|[\s"'])style\s*=\s*("([^"]*);?"|'([^']*);?')/i;
271+
var HREFMATCH = /(^|[\s"'])href\s*=\s*("([^"]*)"|'([^']*)')/i;
272+
273+
var COLORMATCH = /(^|;)\s*color:/;
274+
257275
exports.plainText = function(_str) {
258276
// strip out our pseudo-html so we have a readable
259277
// version to put into text fields
@@ -280,84 +298,93 @@ function encodeForHTML(_str) {
280298
}
281299

282300
function convertToSVG(_str) {
283-
_str = convertEntities(_str);
284-
285-
// normalize behavior between IE and others wrt newlines and whitespace:pre
286-
// this combination makes IE barf https://github.com/plotly/plotly.js/issues/746
287-
// Chrome and FF display \n, \r, or \r\n as a space in this mode.
288-
// I feel like at some point we turned these into <br> but currently we don't so
289-
// I'm just going to cement what we do now in Chrome and FF
290-
_str = _str.replace(NEWLINES, ' ');
301+
_str = convertEntities(_str)
302+
/*
303+
* Normalize behavior between IE and others wrt newlines and whitespace:pre
304+
* this combination makes IE barf https://github.com/plotly/plotly.js/issues/746
305+
* Chrome and FF display \n, \r, or \r\n as a space in this mode.
306+
* I feel like at some point we turned these into <br> but currently we don't so
307+
* I'm just going to cement what we do now in Chrome and FF
308+
*/
309+
.replace(NEWLINES, ' ');
291310

292311
var result = _str
293-
.split(/(<[^<>]*>)/).map(function(d) {
294-
var match = d.match(/<(\/?)([^ >]*)\s*(.*)>/i),
295-
tag = match && match[2].toLowerCase(),
296-
style = TAG_STYLES[tag];
297-
298-
if(style !== undefined) {
299-
var close = match[1],
300-
extra = match[3],
301-
/**
302-
* extraStyle: any random extra css (that's supported by svg)
303-
* use this like <span style="font-family:Arial"> to change font in the middle
304-
*
305-
* at one point we supported <font family="..." size="..."> but as this isn't even
306-
* valid HTML anymore and we dropped it accidentally for many months, we will not
307-
* resurrect it.
308-
*/
309-
extraStyle = extra.match(/^style\s*=\s*"([^"]+)"\s*/i);
310-
311-
// anchor and br are the only ones that don't turn into a tspan
312+
.split(SPLIT_TAGS).map(function(d) {
313+
var match = d.match(ONE_TAG);
314+
var tag = match && match[2].toLowerCase();
315+
var tagStyle = TAG_STYLES[tag];
316+
317+
if(tagStyle !== undefined) {
318+
var isClose = match[1];
319+
if(isClose) return (tag === 'a' ? '</a>' : '</tspan>') + (TAG_CLOSE[tag] || '');
320+
321+
// break: later we'll turn these into newline <tspan>s
322+
// but we need to know about all the other tags first
323+
if(tag === 'br') return '<br>';
324+
325+
/**
326+
* extra includes href and any random extra css (that's supported by svg)
327+
* use this like <span style="font-family:Arial"> to change font in the middle
328+
*
329+
* at one point we supported <font family="..." size="..."> but as this isn't even
330+
* valid HTML anymore and we dropped it accidentally for many months, we will not
331+
* resurrect it.
332+
*/
333+
var extra = match[4];
334+
335+
var out;
336+
337+
// anchor is the only tag that doesn't turn into a tspan
312338
if(tag === 'a') {
313-
if(close) return '</a>';
314-
else if(extra.substr(0, 4).toLowerCase() !== 'href') return '<a>';
315-
else {
316-
// remove quotes, leading '=', replace '&' with '&amp;'
317-
var href = extra.substr(4)
318-
.replace(/["']/g, '')
319-
.replace(/=/, '');
320-
321-
// check protocol
339+
var hrefMatch = extra && extra.match(HREFMATCH);
340+
var href = hrefMatch && (hrefMatch[3] || hrefMatch[4]);
341+
342+
out = '<a';
343+
344+
if(href) {
345+
// check safe protocols
322346
var dummyAnchor = document.createElement('a');
323347
dummyAnchor.href = href;
324-
if(PROTOCOLS.indexOf(dummyAnchor.protocol) === -1) return '<a>';
325-
326-
return '<a xlink:show="new" xlink:href="' + encodeForHTML(href) + '">';
348+
if(PROTOCOLS.indexOf(dummyAnchor.protocol) !== -1) {
349+
out += ' xlink:show="new" xlink:href="' + encodeForHTML(href) + '"';
350+
}
327351
}
328352
}
329-
else if(tag === 'br') return '<br>';
330-
else if(close) {
331-
// closing tag
332-
333-
// sub/sup: extra tspan with zero-width space to get back to the right baseline
334-
if(tag === 'sup') return '</tspan><tspan dy="0.42em">&#x200b;</tspan>';
335-
if(tag === 'sub') return '</tspan><tspan dy="-0.21em">&#x200b;</tspan>';
336-
else return '</tspan>';
337-
}
338353
else {
339-
var tspanStart = '<tspan';
354+
out = '<tspan';
340355

341356
if(tag === 'sup' || tag === 'sub') {
342357
// sub/sup: extra zero-width space, fixes problem if new line starts with sub/sup
343-
tspanStart = '&#x200b;' + tspanStart;
344-
}
345-
346-
if(extraStyle) {
347-
// most of the svg css users will care about is just like html,
348-
// but font color is different. Let our users ignore this.
349-
extraStyle = extraStyle[1].replace(/(^|;)\s*color:/, '$1 fill:');
350-
style = encodeForHTML(extraStyle) + (style ? ';' + style : '');
358+
out = '&#x200b;' + out;
351359
}
360+
}
352361

353-
return tspanStart + (style ? ' style="' + style + '"' : '') + '>';
362+
// now add style, from both the tag name and any extra css
363+
// Most of the svg css that users will care about is just like html,
364+
// but font color is different (uses fill). Let our users ignore this.
365+
var cssMatch = extra && extra.match(STYLEMATCH);
366+
var css = cssMatch && (cssMatch[3] || cssMatch[4]);
367+
if(css) {
368+
css = encodeForHTML(css.replace(COLORMATCH, '$1 fill:'));
369+
if(tagStyle) css += ';' + tagStyle;
354370
}
371+
else if(tagStyle) css = tagStyle;
372+
373+
if(css) return out + ' style="' + css + '">';
374+
375+
return out + '>';
355376
}
356377
else {
357378
return exports.xml_entity_encode(d).replace(/</g, '&lt;');
358379
}
359380
});
360381

382+
// now deal with line breaks
383+
// TODO: this next section attempts to close and reopen tags that
384+
// span a line break. But
385+
// a) it only closes and reopens one tag, and
386+
// b) all tags are treated like equivalent tspans (even <a> which isn't a tspan even now!)
387+
// we should really do this in a type-aware way *before* converting to tspans.
361388
var indices = [];
362389
for(var index = result.indexOf('<br>'); index > 0; index = result.indexOf('<br>', index + 1)) {
363390
indices.push(index);

test/jasmine/tests/annotations_test.js

+34
Original file line numberDiff line numberDiff line change
@@ -1251,4 +1251,38 @@ describe('annotation effects', function() {
12511251
.catch(failTest)
12521252
.then(done);
12531253
});
1254+
1255+
it('makes the whole text box a link if the link is the whole text', function(done) {
1256+
makePlot([
1257+
{x: 20, y: 20, text: '<a href="https://plot.ly">Plot</a>', showarrow: false},
1258+
{x: 50, y: 50, text: '<a href="https://plot.ly">or</a> not', showarrow: false},
1259+
{x: 80, y: 80, text: '<a href="https://plot.ly">arrow</a>'},
1260+
{x: 20, y: 80, text: 'nor <a href="https://plot.ly">this</a>'}
1261+
])
1262+
.then(function() {
1263+
function checkBoxLink(index, isLink) {
1264+
var boxLink = d3.selectAll('.annotation[data-index="' + index + '"] g>a');
1265+
expect(boxLink.size()).toBe(isLink ? 1 : 0);
1266+
1267+
var textLink = d3.selectAll('.annotation[data-index="' + index + '"] text a');
1268+
expect(textLink.size()).toBe(1);
1269+
checkLink(textLink);
1270+
1271+
if(isLink) checkLink(boxLink);
1272+
}
1273+
1274+
function checkLink(link) {
1275+
expect(link.style('cursor')).toBe('pointer');
1276+
expect(link.attr('xlink:href')).toBe('https://plot.ly');
1277+
expect(link.attr('xlink:show')).toBe('new');
1278+
}
1279+
1280+
checkBoxLink(0, true);
1281+
checkBoxLink(1, false);
1282+
checkBoxLink(2, true);
1283+
checkBoxLink(3, false);
1284+
})
1285+
.catch(failTest)
1286+
.then(done);
1287+
});
12541288
});

0 commit comments

Comments
 (0)