@codemirror/tooltip
Advanced tools
Comparing version 0.18.4 to 0.19.0
@@ -0,1 +1,13 @@ | ||
## 0.19.0 (2021-08-11) | ||
### Bug fixes | ||
Move tooltips to avoid overlapping between them, when necessary. | ||
Make sure tooltips don't stay visible when the editor goes out of view. | ||
### New features | ||
Hover tooltips are now grouped together in a single DOM element when multiple such tooltips are active. | ||
## 0.18.4 (2021-03-15) | ||
@@ -2,0 +14,0 @@ |
@@ -26,3 +26,5 @@ import { EditorView, ViewUpdate } from '@codemirror/view'; | ||
Whether the tooltip should be shown above or below the target | ||
position. Defaults to false. | ||
position. Not guaranteed for hover tooltips since all hover | ||
tooltips for the same range are always positioned together. | ||
Defaults to false. | ||
*/ | ||
@@ -33,3 +35,3 @@ above?: boolean; | ||
enough space on that side to show the tooltip inside the | ||
viewport. Defaults to false. | ||
viewport. Not guaranteed for hover tooltips. Defaults to false. | ||
*/ | ||
@@ -71,2 +73,6 @@ strictSide?: boolean; | ||
pointer is before the position, 1 if after the position. | ||
Note that all hover tooltips are hosted within a single tooltip | ||
container element. This allows multiple tooltips over the same | ||
range to be "merged" together without overlapping. | ||
*/ | ||
@@ -73,0 +79,0 @@ declare function hoverTooltip(source: (view: EditorView, pos: number, side: -1 | 1) => Tooltip | null | Promise<Tooltip | null>, options?: { |
@@ -5,57 +5,65 @@ import { ViewPlugin, Direction, EditorView, logException } from '@codemirror/view'; | ||
const ios = typeof navigator != "undefined" && | ||
!/Edge\/(\d+)/.exec(navigator.userAgent) && /Apple Computer/.test(navigator.vendor) && | ||
(/Mobile\/\w+/.test(navigator.userAgent) || navigator.maxTouchPoints > 2); | ||
!/*@__PURE__*//Edge\/(\d+)/.exec(navigator.userAgent) && /*@__PURE__*//Apple Computer/.test(navigator.vendor) && | ||
(/*@__PURE__*//Mobile\/\w+/.test(navigator.userAgent) || navigator.maxTouchPoints > 2); | ||
const Outside = "-10000px"; | ||
const tooltipPlugin = ViewPlugin.fromClass(class { | ||
constructor(view) { | ||
this.view = view; | ||
this.inView = true; | ||
this.measureReq = { read: this.readMeasure.bind(this), write: this.writeMeasure.bind(this), key: this }; | ||
this.input = view.state.facet(showTooltip); | ||
class TooltipViewManager { | ||
constructor(view, facet, createTooltipView) { | ||
this.facet = facet; | ||
this.createTooltipView = createTooltipView; | ||
this.input = view.state.facet(facet); | ||
this.tooltips = this.input.filter(t => t); | ||
this.tooltipViews = this.tooltips.map(tp => this.createTooltip(tp)); | ||
this.tooltipViews = this.tooltips.map(createTooltipView); | ||
} | ||
update(update) { | ||
let input = update.state.facet(showTooltip); | ||
if (input == this.input) { | ||
const input = update.state.facet(this.facet); | ||
const tooltips = input.filter(x => x); | ||
if (input === this.input) { | ||
for (let t of this.tooltipViews) | ||
if (t.update) | ||
t.update(update); | ||
return { shouldMeasure: false }; | ||
} | ||
else { | ||
let tooltips = input.filter(x => x); | ||
let views = []; | ||
for (let i = 0; i < tooltips.length; i++) { | ||
let tip = tooltips[i], known = -1; | ||
if (!tip) | ||
continue; | ||
for (let i = 0; i < this.tooltips.length; i++) { | ||
let other = this.tooltips[i]; | ||
if (other && other.create == tip.create) | ||
known = i; | ||
} | ||
if (known < 0) { | ||
views[i] = this.createTooltip(tip); | ||
} | ||
else { | ||
let tooltipView = views[i] = this.tooltipViews[known]; | ||
if (tooltipView.update) | ||
tooltipView.update(update); | ||
} | ||
let tooltipViews = []; | ||
for (let i = 0; i < tooltips.length; i++) { | ||
let tip = tooltips[i], known = -1; | ||
if (!tip) | ||
continue; | ||
for (let i = 0; i < this.tooltips.length; i++) { | ||
let other = this.tooltips[i]; | ||
if (other && other.create == tip.create) | ||
known = i; | ||
} | ||
for (let t of this.tooltipViews) | ||
if (views.indexOf(t) < 0) | ||
t.dom.remove(); | ||
this.input = input; | ||
this.tooltips = tooltips; | ||
this.tooltipViews = views; | ||
this.maybeMeasure(); | ||
if (known < 0) { | ||
tooltipViews[i] = this.createTooltipView(tip); | ||
} | ||
else { | ||
let tooltipView = tooltipViews[i] = this.tooltipViews[known]; | ||
if (tooltipView.update) | ||
tooltipView.update(update); | ||
} | ||
} | ||
for (let t of this.tooltipViews) | ||
if (tooltipViews.indexOf(t) < 0) | ||
t.dom.remove(); | ||
this.input = input; | ||
this.tooltips = tooltips; | ||
this.tooltipViews = tooltipViews; | ||
return { shouldMeasure: true }; | ||
} | ||
} | ||
const tooltipPlugin = /*@__PURE__*/ViewPlugin.fromClass(class { | ||
constructor(view) { | ||
this.view = view; | ||
this.inView = true; | ||
this.measureReq = { read: this.readMeasure.bind(this), write: this.writeMeasure.bind(this), key: this }; | ||
this.manager = new TooltipViewManager(view, showTooltip, t => this.createTooltip(t)); | ||
} | ||
update(update) { | ||
const { shouldMeasure } = this.manager.update(update); | ||
if (shouldMeasure) | ||
this.maybeMeasure(); | ||
} | ||
createTooltip(tooltip) { | ||
let tooltipView = tooltip.create(this.view); | ||
tooltipView.dom.classList.add("cm-tooltip"); | ||
// FIXME drop this on the next breaking release | ||
if (tooltip.class) | ||
tooltipView.dom.classList.add(tooltip.class); | ||
tooltipView.dom.style.top = Outside; | ||
@@ -68,3 +76,3 @@ this.view.dom.appendChild(tooltipView.dom); | ||
destroy() { | ||
for (let { dom } of this.tooltipViews) | ||
for (let { dom } of this.manager.tooltipViews) | ||
dom.remove(); | ||
@@ -75,4 +83,4 @@ } | ||
editor: this.view.dom.getBoundingClientRect(), | ||
pos: this.tooltips.map(t => this.view.coordsAtPos(t.pos)), | ||
size: this.tooltipViews.map(({ dom }) => dom.getBoundingClientRect()), | ||
pos: this.manager.tooltips.map(t => this.view.coordsAtPos(t.pos)), | ||
size: this.manager.tooltipViews.map(({ dom }) => dom.getBoundingClientRect()), | ||
innerWidth: window.innerWidth, | ||
@@ -84,4 +92,5 @@ innerHeight: window.innerHeight | ||
let { editor } = measured; | ||
for (let i = 0; i < this.tooltipViews.length; i++) { | ||
let tooltip = this.tooltips[i], tView = this.tooltipViews[i], { dom } = tView; | ||
let others = []; | ||
for (let i = 0; i < this.manager.tooltips.length; i++) { | ||
let tooltip = this.manager.tooltips[i], tView = this.manager.tooltipViews[i], { dom } = tView; | ||
let pos = measured.pos[i], size = measured.size[i]; | ||
@@ -100,4 +109,8 @@ // Hide tooltips that are outside of the editor. | ||
above = !above; | ||
let top = above ? pos.top - height : pos.bottom, right = left + width; | ||
for (let r of others) | ||
if (r.left < right && r.right > left && r.top < top + height && r.bottom > top) | ||
top = above ? r.top - height : r.bottom; | ||
if (ios) { | ||
dom.style.top = ((above ? pos.top - height : pos.bottom) - editor.top) + "px"; | ||
dom.style.top = (top - editor.top) + "px"; | ||
dom.style.left = (left - editor.left) + "px"; | ||
@@ -107,5 +120,6 @@ dom.style.position = "absolute"; | ||
else { | ||
dom.style.top = (above ? pos.top - height : pos.bottom) + "px"; | ||
dom.style.top = top + "px"; | ||
dom.style.left = left + "px"; | ||
} | ||
others.push({ left, top, right, bottom: top + height }); | ||
dom.classList.toggle("cm-tooltip-above", above); | ||
@@ -118,6 +132,11 @@ dom.classList.toggle("cm-tooltip-below", !above); | ||
maybeMeasure() { | ||
if (this.tooltips.length) { | ||
if (this.view.inView || this.inView) | ||
if (this.manager.tooltips.length) { | ||
if (this.view.inView) | ||
this.view.requestMeasure(this.measureReq); | ||
this.inView = this.view.inView; | ||
if (this.inView != this.view.inView) { | ||
this.inView = this.view.inView; | ||
if (!this.inView) | ||
for (let tv of this.manager.tooltipViews) | ||
tv.dom.style.top = Outside; | ||
} | ||
} | ||
@@ -130,3 +149,3 @@ } | ||
}); | ||
const baseTheme = EditorView.baseTheme({ | ||
const baseTheme = /*@__PURE__*/EditorView.baseTheme({ | ||
".cm-tooltip": { | ||
@@ -140,2 +159,5 @@ position: "fixed", | ||
}, | ||
"&light .cm-tooltip-section:not(:first-child)": { | ||
borderTop: "1px solid #ddd", | ||
}, | ||
"&dark .cm-tooltip": { | ||
@@ -146,18 +168,59 @@ backgroundColor: "#333338", | ||
}); | ||
// FIXME backward-compat shim. Delete on next major version. | ||
/** | ||
@internal | ||
*/ | ||
function tooltips() { | ||
return []; | ||
} | ||
/** | ||
Behavior by which an extension can provide a tooltip to be shown. | ||
*/ | ||
const showTooltip = Facet.define({ | ||
const showTooltip = /*@__PURE__*/Facet.define({ | ||
enables: [tooltipPlugin, baseTheme] | ||
}); | ||
const HoverTime = 750, HoverMaxDist = 6; | ||
const showHoverTooltip = /*@__PURE__*/Facet.define(); | ||
class HoverTooltipHost { | ||
constructor(view) { | ||
this.view = view; | ||
this.mounted = false; | ||
this.dom = document.createElement("div"); | ||
this.dom.classList.add("cm-tooltip-hover"); | ||
this.manager = new TooltipViewManager(view, showHoverTooltip, t => this.createHostedView(t)); | ||
} | ||
// Needs to be static so that host tooltip instances always match | ||
static create(view) { | ||
return new HoverTooltipHost(view); | ||
} | ||
createHostedView(tooltip) { | ||
const hostedView = tooltip.create(this.view); | ||
hostedView.dom.classList.add("cm-tooltip-section"); | ||
this.dom.appendChild(hostedView.dom); | ||
if (this.mounted && hostedView.mount) | ||
hostedView.mount(this.view); | ||
return hostedView; | ||
} | ||
mount(view) { | ||
for (const hostedView of this.manager.tooltipViews) { | ||
if (hostedView.mount) | ||
hostedView.mount(view); | ||
} | ||
this.mounted = true; | ||
} | ||
positioned() { | ||
for (const hostedView of this.manager.tooltipViews) { | ||
if (hostedView.positioned) | ||
hostedView.positioned(); | ||
} | ||
} | ||
update(update) { | ||
this.manager.update(update); | ||
} | ||
} | ||
const showHoverTooltipHost = /*@__PURE__*/showTooltip.compute([showHoverTooltip], state => { | ||
const tooltips = state.facet(showHoverTooltip).filter(t => t); | ||
if (tooltips.length === 0) | ||
return null; | ||
return { | ||
pos: Math.min(...tooltips.map(t => t.pos)), | ||
end: Math.max(...tooltips.filter(t => t.end != null).map(t => t.end)), | ||
create: HoverTooltipHost.create, | ||
above: tooltips[0].above | ||
}; | ||
}); | ||
class HoverPlugin { | ||
constructor(view, source, field, setHover) { | ||
constructor(view, source, field, setHover, hoverTime) { | ||
this.view = view; | ||
@@ -167,3 +230,5 @@ this.source = source; | ||
this.setHover = setHover; | ||
this.hoverTime = hoverTime; | ||
this.lastMouseMove = null; | ||
this.lastMoveTime = 0; | ||
this.hoverTimeout = -1; | ||
@@ -190,5 +255,5 @@ this.restartTimeout = -1; | ||
return; | ||
let now = Date.now(), lastMove = this.lastMouseMove; | ||
if (now - lastMove.timeStamp < HoverTime) | ||
this.hoverTimeout = setTimeout(this.checkHover, HoverTime - (now - lastMove.timeStamp)); | ||
let hovered = Date.now() - this.lastMoveTime; | ||
if (hovered < this.hoverTime) | ||
this.hoverTimeout = setTimeout(this.checkHover, this.hoverTime - hovered); | ||
else | ||
@@ -231,4 +296,5 @@ this.startHover(); | ||
this.lastMouseMove = event; | ||
this.lastMoveTime = Date.now(); | ||
if (this.hoverTimeout < 0) | ||
this.hoverTimeout = setTimeout(this.checkHover, HoverTime); | ||
this.hoverTimeout = setTimeout(this.checkHover, this.hoverTime); | ||
let tooltip = this.active; | ||
@@ -238,3 +304,3 @@ if (tooltip && !isInTooltip(event.target) || this.pending) { | ||
if ((pos == end ? this.view.posAtCoords({ x: event.clientX, y: event.clientY }) != pos | ||
: !isOverRange(this.view, pos, end, event.clientX, event.clientY, HoverMaxDist))) { | ||
: !isOverRange(this.view, pos, end, event.clientX, event.clientY, 6 /* MaxDist */))) { | ||
this.view.dispatch({ effects: this.setHover.of(null) }); | ||
@@ -286,2 +352,6 @@ this.pending = null; | ||
pointer is before the position, 1 if after the position. | ||
Note that all hover tooltips are hosted within a single tooltip | ||
container element. This allows multiple tooltips over the same | ||
range to be "merged" together without overlapping. | ||
*/ | ||
@@ -310,10 +380,12 @@ function hoverTooltip(source, options = {}) { | ||
}, | ||
provide: f => showTooltip.from(f) | ||
provide: f => showHoverTooltip.from(f) | ||
}); | ||
let hoverTime = options.hoverTime || 750 /* Time */; | ||
return [ | ||
hoverState, | ||
ViewPlugin.define(view => new HoverPlugin(view, source, hoverState, setHover)) | ||
ViewPlugin.define(view => new HoverPlugin(view, source, hoverState, setHover, hoverTime)), | ||
showHoverTooltipHost | ||
]; | ||
} | ||
export { hoverTooltip, showTooltip, tooltips }; | ||
export { hoverTooltip, showTooltip }; |
{ | ||
"name": "@codemirror/tooltip", | ||
"version": "0.18.4", | ||
"version": "0.19.0", | ||
"description": "Tooltip support for the CodeMirror code editor", | ||
"scripts": { | ||
"test": "echo 'No tests'", | ||
"test": "cm-runtests", | ||
"prepare": "cm-buildhelper src/tooltip.ts" | ||
@@ -29,7 +29,7 @@ }, | ||
"dependencies": { | ||
"@codemirror/state": "^0.18.0", | ||
"@codemirror/view": "^0.18.0" | ||
"@codemirror/state": "^0.19.0", | ||
"@codemirror/view": "^0.19.0" | ||
}, | ||
"devDependencies": { | ||
"@codemirror/buildhelper": "^0.1.0" | ||
"@codemirror/buildhelper": "^0.1.5" | ||
}, | ||
@@ -36,0 +36,0 @@ "repository": { |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
37349
824
2
+ Added@codemirror/rangeset@0.19.9(transitive)
+ Added@codemirror/state@0.19.9(transitive)
+ Added@codemirror/text@0.19.6(transitive)
+ Added@codemirror/view@0.19.48(transitive)
- Removed@codemirror/rangeset@0.18.5(transitive)
- Removed@codemirror/state@0.18.7(transitive)
- Removed@codemirror/text@0.18.1(transitive)
- Removed@codemirror/view@0.18.19(transitive)
Updated@codemirror/state@^0.19.0
Updated@codemirror/view@^0.19.0