Skip to content
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

Bring tooltips back #494

Merged
merged 10 commits into from
Sep 8, 2021
5 changes: 4 additions & 1 deletion frontend/src/forms/ontology-class-picker-children-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import FilteredCollection from '../common-adapters/filtered-collection';
import FlatItemCollection from '../common-adapters/flat-item-collection';
import FlatItem from '../common-adapters/flat-item-model';
import { CollectionView } from '../core/view';
import attachTooltip from '../tooltip/tooltip-view';
import { animatedScroll, getScrollTop } from '../utilities/scrolling-utilities';
import OntologyClassPickerItemView from './ontology-class-picker-item-view';

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

makeItem(model: FlatItem): OntologyClassPickerItemView {
return new OntologyClassPickerItemView({ model }).on({
const item = new OntologyClassPickerItemView({ model }).on({
click: this.onItemClicked,
}, this);
attachTooltip(item, { model });
return item;
}

remove(): this {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/forms/ontology-class-picker-item-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default class OntologyClassPickerItemView extends CompositeView<FlatItem>
initialize(): this {
this.labelView = new LabelView({
model: this.model,
toolTipSetting: false
toolTipSetting: false,
});
this.listenTo(this.model, { 'focus': this.onFocus, 'blur': this.onBlur });
return this.render();
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/forms/ontology-class-picker-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Node from '../common-rdf/node';
import { skos } from '../common-rdf/ns';
import { CollectionView } from '../core/view';
import LabelView from '../label/label-view';
import attachTooltip from '../tooltip/tooltip-view';
import OntologyClassPickerChildrenView from './ontology-class-picker-children-view';
import OntologyClassPickerItemView from './ontology-class-picker-item-view';
import ontologyClassPickerTemplate from './ontology-class-picker-template';
Expand All @@ -29,10 +30,12 @@ export default class OntologyClassPickerView extends CollectionView<
}

makeItem(model: FlatItem): OntologyClassPickerItemView {
return new OntologyClassPickerItemView({ model }).on({
const item = new OntologyClassPickerItemView({ model }).on({
click: this.onItemClicked,
hover: this.isNonLeaf(model) ? this.onSuperclassHovered : undefined,
}, this);
attachTooltip(item, { model, direction: 'left' });
return item;
}

isLeaf(node: FlatItem) {
Expand Down
22 changes: 6 additions & 16 deletions frontend/src/label/label-view-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { enableI18n, event } from '../test-util';
import { readit, rdfs, skos } from './../common-rdf/ns';
import { FlatLdObject } from '../common-rdf/json';
import Node from '../common-rdf/node';
import LabelView from './label-view';
import FlatItem from '../common-adapters/flat-item-model';

import LabelView from './label-view';

function getDefaultItem(): FlatItem {
return new FlatItem(new Node(getDefaultAttributes()));
}
Expand All @@ -22,8 +23,8 @@ function getDefaultAttributes(): FlatLdObject {
],
[skos.definition]: [
{ '@value': 'This is a test definition'}
]
}
],
};
}

describe('LabelView', function () {
Expand All @@ -34,26 +35,15 @@ describe('LabelView', function () {

});

it('includes a tooltip if a definition exists', async function () {
it('can be constructed in isolation', async function () {
let view = new LabelView({ model: this.item });
await event(this.item, 'complete');
expect(view.el.className).toContain('is-readit-content');
expect(view.$el.attr('data-tooltip')).toEqual('This is a test definition');
});

it('does not include a tooltip if a definition does not exist', async function () {
let attributes = getDefaultAttributes();
delete attributes[skos.definition];
let view = new LabelView({ model: new FlatItem(new Node(attributes))});
await event(view.model, 'complete');
expect(view.el.className).toContain('is-readit-content');
expect(view.$el.attr('data-tooltip')).toBeUndefined();
});

it('excludes a tooltip if told so', async function () {
let view = new LabelView({ model: this.item, toolTipSetting: false });
await event(this.item, 'complete');
expect(view.el.className).toContain('is-readit-content');
expect(view.$el.attr('data-tooltip')).toBeUndefined();
});
})
});
50 changes: 15 additions & 35 deletions frontend/src/label/label-view.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ViewOptions as BaseOpt } from 'backbone';
import { extend } from 'lodash';
import View from '../core/view';

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

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

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

export default class LabelView extends View<FlatItem> {
label: string;
cssClassName: string;
toolTipSetting: TooltipSetting;

constructor(options?: ViewOptions) {
super(options);

this.toolTipSetting = 'top';
this.toolTipSetting = 'right';
if (options && options.toolTipSetting !== undefined) {
this.toolTipSetting = options.toolTipSetting;
}
this.model.when('class', this.processClass, this);
this.model.when('classLabel', this.processClass, this);
}

processClass() {
this.label = this.model.get('classLabel');
this.cssClassName = this.model.get('cssClass');
this.addDefinition();
this.addTooltip();
this.render();
}

render(): this {
this.$el.html();
this.$el.text(this.label);
this.$el.addClass(this.cssClassName);
this.$el.text(this.model.get('classLabel'));
this.$el.addClass(this.model.get('cssClass'));
return this;
}

addDefinition(): void {
if (this.hasTooltip() && this.model.get('class').has(skos.definition)) {
this.$el.addClass("tooltip");
this.$el.addClass("is-tooltip");
this.setTooltipOrientation();

let definition = this.model.get('class').get(skos.definition)[0] as string;
this.$el.attr("data-tooltip", definition);

if (definition.length > 65) {
this.$el.addClass("is-tooltip-multiline");
}
addTooltip(): void {
if (typeof this.toolTipSetting === 'string') {
attachTooltip(this, {
direction: this.toolTipSetting,
model: this.model,
});
}
}

hasTooltip(): boolean {
return typeof this.toolTipSetting === 'string';
}

setTooltipOrientation(): this {
let orientation = `-${this.toolTipSetting}`;
this.$el.addClass(`is-tooltip${orientation}`);
return this;
}

}

extend(LabelView.prototype, {
tagName: 'span',
className: 'tag',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/style/main.sass
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
@import page
@import metadata
@import suggestions
@import tooltip

html, body
height: 100%
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/style/tooltip.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.rit-tooltip
background-opacity: 0
pointer-events: none
position: absolute
43 changes: 43 additions & 0 deletions frontend/src/tooltip/tooltip-view-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { enableI18n, event } from '../test-util';

import { readit, rdfs, skos } from './../common-rdf/ns';
import { FlatLdObject } from '../common-rdf/json';
import Node from '../common-rdf/node';
import FlatItem from '../common-adapters/flat-item-model';

import { Tooltip } from './tooltip-view';

function getDefaultItem(): FlatItem {
return new FlatItem(new Node(getDefaultAttributes()));
}

function getDefaultAttributes(): FlatLdObject {
return {
'@id': readit('test'),
"@type": [rdfs.Class],
[skos.prefLabel]: [
{ '@value': 'Content' },
],
[skos.altLabel]: [
{ '@value': 'alternativeLabel'}
],
[skos.definition]: [
{ '@value': 'This is a test definition'}
]
}
}

describe('Tooltip', function () {
beforeAll(enableI18n);

beforeEach( async function() {
this.item = getDefaultItem();

});

it('includes the definition if it exists', async function () {
let view = new Tooltip({ model: this.item });
await event(this.item, 'complete');
expect(view.$el.data('tooltip')).toEqual('This is a test definition');
});
});
133 changes: 133 additions & 0 deletions frontend/src/tooltip/tooltip-view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { ViewOptions as BaseOpt, View as BView } from 'backbone';
import { extend } from 'lodash';
import * as i18next from 'i18next';

import View from '../core/view';
import FlatItem from '../common-adapters/flat-item-model';
import { rdfs, skos } from '../common-rdf/ns';

type Direction = 'top' | 'bottom' | 'left' | 'right';

export interface ViewOptions extends BaseOpt<FlatItem> {
direction?: Direction;
}

const oppositeDirection = {
top: 'bottom',
bottom: 'top',
left: 'right',
right: 'left',
};

const cssPropsToCopy = [
'border-bottom-width',
'border-left-width',
'border-right-width',
'border-top-width',
'box-sizing',
'margin-bottom',
'margin-left',
'margin-right',
'margin-top',
'padding-bottom',
'padding-left',
'padding-right',
'padding-top',
];

/**
* A simple, empty, transparent view with the sole purpose of having a Bulma
* tooltip associated. It is not really meant to be used directly; rather, you
* should layer it over another view using the `attachTooltip` function below.
*/
export class Tooltip extends View<FlatItem> {
preferredDirection: string;
direction: string;

constructor(options?: ViewOptions) {
super(options);
this.preferredDirection = options && options.direction || 'right';
this.model.when('classLabel', this.render, this);
}

render(): this {
const cls = this.model.get('class');
const languageOption = { '@language': i18next.language };
const definition = cls.get(skos.definition, languageOption);
const comment = definition || cls.get(rdfs.comment, languageOption);
const text = definition && definition[0] || comment && comment[0];
if (text) {
this.$el.attr('data-tooltip', text);
} else {
this.$el.removeClass('tooltip');
}
return this;
}

show(): this {
this.$el.addClass(`is-tooltip-active is-tooltip-${this.direction}`);
return this;
}

hide(): this {
this.$el.removeClass(`is-tooltip-active is-tooltip-${this.direction}`);
return this;
}

positionTo<V extends BView<any>>(view: V): this {
const other = view.$el;
const offset = other.offset();
const width = other.width();
const height = other.height();
this.$el.css(other.css(cssPropsToCopy))
.width(width).height(height).offset(offset);
const direction = this.direction = this.preferredDirection;
const viewport = window['visualViewport'];
if (viewport) {
const distance = (
direction === 'top' ? offset.top - viewport.offsetTop :
direction === 'left' ? offset.left - viewport.offsetLeft :
direction === 'right' ? (viewport.offsetLeft + viewport.width) - (offset.left + width) :
(viewport.offsetTop + viewport.height) - (offset.top + height)
);
if (distance < 400) this.direction = oppositeDirection[direction];
}
return this;
}
}

extend(Tooltip.prototype, {
className: 'rit-tooltip tooltip is-tooltip-multiline',
});

/**
* Attach a `Tooltip` to the given `view`. The tooltip view will be a direct
* child of the `<body>` element in order to ensure that the tooltip balloon is
* never obscured by the overflow edges of containing elements. Events are
* taken care of and the tooltip is `.remove`d automatically when `view` is.
*/
export default function attachTooltip<V extends BView<any>>(
view: V, options: ViewOptions
): Tooltip {
const tooltip = new Tooltip(options);
tooltip.$el.appendTo(document.body);
const openTooltip = () => tooltip.positionTo(view).show();
function attachEvents() {
view.delegate('mouseenter', '', openTooltip);
view.delegate('mouseleave', '', tooltip.hide.bind(tooltip));
}
attachEvents();
const { remove, setElement } = view;
extend(view, {
remove() {
tooltip.remove();
return remove.call(view);
},
setElement(element) {
const result = setElement.call(view, element);
attachEvents();
return result;
},
});
return tooltip;
}