Skip to content

Commit b2e62f8

Browse files
authored
Merge pull request #494 from UUDigitalHumanitieslab/feature/tooltip-fix
Bring tooltips back
2 parents 3026925 + 2588f89 commit b2e62f8

9 files changed

+211
-54
lines changed

frontend/src/forms/ontology-class-picker-children-view.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import FilteredCollection from '../common-adapters/filtered-collection';
33
import FlatItemCollection from '../common-adapters/flat-item-collection';
44
import FlatItem from '../common-adapters/flat-item-model';
55
import { CollectionView } from '../core/view';
6+
import attachTooltip from '../tooltip/tooltip-view';
67
import { animatedScroll, getScrollTop } from '../utilities/scrolling-utilities';
78
import OntologyClassPickerItemView from './ontology-class-picker-item-view';
89

@@ -22,9 +23,11 @@ export default class OntologyClassPickerChildrenView extends CollectionView<
2223
}
2324

2425
makeItem(model: FlatItem): OntologyClassPickerItemView {
25-
return new OntologyClassPickerItemView({ model }).on({
26+
const item = new OntologyClassPickerItemView({ model }).on({
2627
click: this.onItemClicked,
2728
}, this);
29+
attachTooltip(item, { model });
30+
return item;
2831
}
2932

3033
remove(): this {

frontend/src/forms/ontology-class-picker-item-view.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default class OntologyClassPickerItemView extends CompositeView<FlatItem>
1010
initialize(): this {
1111
this.labelView = new LabelView({
1212
model: this.model,
13-
toolTipSetting: false
13+
toolTipSetting: false,
1414
});
1515
this.listenTo(this.model, { 'focus': this.onFocus, 'blur': this.onBlur });
1616
return this.render();

frontend/src/forms/ontology-class-picker-view.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Node from '../common-rdf/node';
55
import { skos } from '../common-rdf/ns';
66
import { CollectionView } from '../core/view';
77
import LabelView from '../label/label-view';
8+
import attachTooltip from '../tooltip/tooltip-view';
89
import OntologyClassPickerChildrenView from './ontology-class-picker-children-view';
910
import OntologyClassPickerItemView from './ontology-class-picker-item-view';
1011
import ontologyClassPickerTemplate from './ontology-class-picker-template';
@@ -29,10 +30,12 @@ export default class OntologyClassPickerView extends CollectionView<
2930
}
3031

3132
makeItem(model: FlatItem): OntologyClassPickerItemView {
32-
return new OntologyClassPickerItemView({ model }).on({
33+
const item = new OntologyClassPickerItemView({ model }).on({
3334
click: this.onItemClicked,
3435
hover: this.isNonLeaf(model) ? this.onSuperclassHovered : undefined,
3536
}, this);
37+
attachTooltip(item, { model, direction: 'left' });
38+
return item;
3639
}
3740

3841
isLeaf(node: FlatItem) {

frontend/src/label/label-view-test.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { enableI18n, event } from '../test-util';
33
import { readit, rdfs, skos } from './../common-rdf/ns';
44
import { FlatLdObject } from '../common-rdf/json';
55
import Node from '../common-rdf/node';
6-
import LabelView from './label-view';
76
import FlatItem from '../common-adapters/flat-item-model';
87

8+
import LabelView from './label-view';
9+
910
function getDefaultItem(): FlatItem {
1011
return new FlatItem(new Node(getDefaultAttributes()));
1112
}
@@ -22,8 +23,8 @@ function getDefaultAttributes(): FlatLdObject {
2223
],
2324
[skos.definition]: [
2425
{ '@value': 'This is a test definition'}
25-
]
26-
}
26+
],
27+
};
2728
}
2829

2930
describe('LabelView', function () {
@@ -34,26 +35,15 @@ describe('LabelView', function () {
3435

3536
});
3637

37-
it('includes a tooltip if a definition exists', async function () {
38+
it('can be constructed in isolation', async function () {
3839
let view = new LabelView({ model: this.item });
3940
await event(this.item, 'complete');
4041
expect(view.el.className).toContain('is-readit-content');
41-
expect(view.$el.attr('data-tooltip')).toEqual('This is a test definition');
42-
});
43-
44-
it('does not include a tooltip if a definition does not exist', async function () {
45-
let attributes = getDefaultAttributes();
46-
delete attributes[skos.definition];
47-
let view = new LabelView({ model: new FlatItem(new Node(attributes))});
48-
await event(view.model, 'complete');
49-
expect(view.el.className).toContain('is-readit-content');
50-
expect(view.$el.attr('data-tooltip')).toBeUndefined();
5142
});
5243

5344
it('excludes a tooltip if told so', async function () {
5445
let view = new LabelView({ model: this.item, toolTipSetting: false });
5546
await event(this.item, 'complete');
5647
expect(view.el.className).toContain('is-readit-content');
57-
expect(view.$el.attr('data-tooltip')).toBeUndefined();
5848
});
59-
})
49+
});

frontend/src/label/label-view.ts

Lines changed: 15 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { ViewOptions as BaseOpt } from 'backbone';
22
import { extend } from 'lodash';
3-
import View from '../core/view';
43

5-
import { skos } from '../common-rdf/ns';
4+
import View from '../core/view';
5+
import { rdfs, skos } from '../common-rdf/ns';
66
import FlatItem from '../common-adapters/flat-item-model';
7+
import attachTooltip from '../tooltip/tooltip-view';
78

89
type TooltipSetting = false | 'top' | 'bottom' | 'left' | 'right';
910

@@ -12,60 +13,39 @@ export interface ViewOptions extends BaseOpt<FlatItem> {
1213
}
1314

1415
export default class LabelView extends View<FlatItem> {
15-
label: string;
16-
cssClassName: string;
1716
toolTipSetting: TooltipSetting;
1817

1918
constructor(options?: ViewOptions) {
2019
super(options);
2120

22-
this.toolTipSetting = 'top';
21+
this.toolTipSetting = 'right';
2322
if (options && options.toolTipSetting !== undefined) {
2423
this.toolTipSetting = options.toolTipSetting;
2524
}
26-
this.model.when('class', this.processClass, this);
25+
this.model.when('classLabel', this.processClass, this);
2726
}
2827

2928
processClass() {
30-
this.label = this.model.get('classLabel');
31-
this.cssClassName = this.model.get('cssClass');
32-
this.addDefinition();
29+
this.addTooltip();
3330
this.render();
3431
}
3532

3633
render(): this {
37-
this.$el.html();
38-
this.$el.text(this.label);
39-
this.$el.addClass(this.cssClassName);
34+
this.$el.text(this.model.get('classLabel'));
35+
this.$el.addClass(this.model.get('cssClass'));
4036
return this;
4137
}
4238

43-
addDefinition(): void {
44-
if (this.hasTooltip() && this.model.get('class').has(skos.definition)) {
45-
this.$el.addClass("tooltip");
46-
this.$el.addClass("is-tooltip");
47-
this.setTooltipOrientation();
48-
49-
let definition = this.model.get('class').get(skos.definition)[0] as string;
50-
this.$el.attr("data-tooltip", definition);
51-
52-
if (definition.length > 65) {
53-
this.$el.addClass("is-tooltip-multiline");
54-
}
39+
addTooltip(): void {
40+
if (typeof this.toolTipSetting === 'string') {
41+
attachTooltip(this, {
42+
direction: this.toolTipSetting,
43+
model: this.model,
44+
});
5545
}
5646
}
57-
58-
hasTooltip(): boolean {
59-
return typeof this.toolTipSetting === 'string';
60-
}
61-
62-
setTooltipOrientation(): this {
63-
let orientation = `-${this.toolTipSetting}`;
64-
this.$el.addClass(`is-tooltip${orientation}`);
65-
return this;
66-
}
67-
6847
}
48+
6949
extend(LabelView.prototype, {
7050
tagName: 'span',
7151
className: 'tag',

frontend/src/style/main.sass

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
@import page
1818
@import metadata
1919
@import suggestions
20+
@import tooltip
2021

2122
html, body
2223
height: 100%

frontend/src/style/tooltip.sass

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.rit-tooltip
2+
background-opacity: 0
3+
pointer-events: none
4+
position: absolute
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { enableI18n, event } from '../test-util';
2+
3+
import { readit, rdfs, skos } from './../common-rdf/ns';
4+
import { FlatLdObject } from '../common-rdf/json';
5+
import Node from '../common-rdf/node';
6+
import FlatItem from '../common-adapters/flat-item-model';
7+
8+
import { Tooltip } from './tooltip-view';
9+
10+
function getDefaultItem(): FlatItem {
11+
return new FlatItem(new Node(getDefaultAttributes()));
12+
}
13+
14+
function getDefaultAttributes(): FlatLdObject {
15+
return {
16+
'@id': readit('test'),
17+
"@type": [rdfs.Class],
18+
[skos.prefLabel]: [
19+
{ '@value': 'Content' },
20+
],
21+
[skos.altLabel]: [
22+
{ '@value': 'alternativeLabel'}
23+
],
24+
[skos.definition]: [
25+
{ '@value': 'This is a test definition'}
26+
]
27+
}
28+
}
29+
30+
describe('Tooltip', function () {
31+
beforeAll(enableI18n);
32+
33+
beforeEach( async function() {
34+
this.item = getDefaultItem();
35+
36+
});
37+
38+
it('includes the definition if it exists', async function () {
39+
let view = new Tooltip({ model: this.item });
40+
await event(this.item, 'complete');
41+
expect(view.$el.data('tooltip')).toEqual('This is a test definition');
42+
});
43+
});

frontend/src/tooltip/tooltip-view.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { ViewOptions as BaseOpt, View as BView } from 'backbone';
2+
import { extend } from 'lodash';
3+
import * as i18next from 'i18next';
4+
5+
import View from '../core/view';
6+
import FlatItem from '../common-adapters/flat-item-model';
7+
import { rdfs, skos } from '../common-rdf/ns';
8+
9+
type Direction = 'top' | 'bottom' | 'left' | 'right';
10+
11+
export interface ViewOptions extends BaseOpt<FlatItem> {
12+
direction?: Direction;
13+
}
14+
15+
const oppositeDirection = {
16+
top: 'bottom',
17+
bottom: 'top',
18+
left: 'right',
19+
right: 'left',
20+
};
21+
22+
const cssPropsToCopy = [
23+
'border-bottom-width',
24+
'border-left-width',
25+
'border-right-width',
26+
'border-top-width',
27+
'box-sizing',
28+
'margin-bottom',
29+
'margin-left',
30+
'margin-right',
31+
'margin-top',
32+
'padding-bottom',
33+
'padding-left',
34+
'padding-right',
35+
'padding-top',
36+
];
37+
38+
/**
39+
* A simple, empty, transparent view with the sole purpose of having a Bulma
40+
* tooltip associated. It is not really meant to be used directly; rather, you
41+
* should layer it over another view using the `attachTooltip` function below.
42+
*/
43+
export class Tooltip extends View<FlatItem> {
44+
preferredDirection: string;
45+
direction: string;
46+
47+
constructor(options?: ViewOptions) {
48+
super(options);
49+
this.preferredDirection = options && options.direction || 'right';
50+
this.model.when('classLabel', this.render, this);
51+
}
52+
53+
render(): this {
54+
const cls = this.model.get('class');
55+
const languageOption = { '@language': i18next.language };
56+
const definition = cls.get(skos.definition, languageOption);
57+
const comment = definition || cls.get(rdfs.comment, languageOption);
58+
const text = definition && definition[0] || comment && comment[0];
59+
if (text) {
60+
this.$el.attr('data-tooltip', text);
61+
} else {
62+
this.$el.removeClass('tooltip');
63+
}
64+
return this;
65+
}
66+
67+
show(): this {
68+
this.$el.addClass(`is-tooltip-active is-tooltip-${this.direction}`);
69+
return this;
70+
}
71+
72+
hide(): this {
73+
this.$el.removeClass(`is-tooltip-active is-tooltip-${this.direction}`);
74+
return this;
75+
}
76+
77+
positionTo<V extends BView<any>>(view: V): this {
78+
const other = view.$el;
79+
const offset = other.offset();
80+
const width = other.width();
81+
const height = other.height();
82+
this.$el.css(other.css(cssPropsToCopy))
83+
.width(width).height(height).offset(offset);
84+
const direction = this.direction = this.preferredDirection;
85+
const viewport = window['visualViewport'];
86+
if (viewport) {
87+
const distance = (
88+
direction === 'top' ? offset.top - viewport.offsetTop :
89+
direction === 'left' ? offset.left - viewport.offsetLeft :
90+
direction === 'right' ? (viewport.offsetLeft + viewport.width) - (offset.left + width) :
91+
(viewport.offsetTop + viewport.height) - (offset.top + height)
92+
);
93+
if (distance < 400) this.direction = oppositeDirection[direction];
94+
}
95+
return this;
96+
}
97+
}
98+
99+
extend(Tooltip.prototype, {
100+
className: 'rit-tooltip tooltip is-tooltip-multiline',
101+
});
102+
103+
/**
104+
* Attach a `Tooltip` to the given `view`. The tooltip view will be a direct
105+
* child of the `<body>` element in order to ensure that the tooltip balloon is
106+
* never obscured by the overflow edges of containing elements. Events are
107+
* taken care of and the tooltip is `.remove`d automatically when `view` is.
108+
*/
109+
export default function attachTooltip<V extends BView<any>>(
110+
view: V, options: ViewOptions
111+
): Tooltip {
112+
const tooltip = new Tooltip(options);
113+
tooltip.$el.appendTo(document.body);
114+
const openTooltip = () => tooltip.positionTo(view).show();
115+
function attachEvents() {
116+
view.delegate('mouseenter', '', openTooltip);
117+
view.delegate('mouseleave', '', tooltip.hide.bind(tooltip));
118+
}
119+
attachEvents();
120+
const { remove, setElement } = view;
121+
extend(view, {
122+
remove() {
123+
tooltip.remove();
124+
return remove.call(view);
125+
},
126+
setElement(element) {
127+
const result = setElement.call(view, element);
128+
attachEvents();
129+
return result;
130+
},
131+
});
132+
return tooltip;
133+
}

0 commit comments

Comments
 (0)