diff --git a/src/tooltip-manager.coffee b/src/tooltip-manager.coffee deleted file mode 100644 index 1a9b6fe44..000000000 --- a/src/tooltip-manager.coffee +++ /dev/null @@ -1,176 +0,0 @@ -_ = require 'underscore-plus' -{Disposable, CompositeDisposable} = require 'event-kit' -Tooltip = null - -# Essential: Associates tooltips with HTML elements. -# -# You can get the `TooltipManager` via `atom.tooltips`. -# -# ## Examples -# -# The essence of displaying a tooltip -# -# ```coffee -# # display it -# disposable = atom.tooltips.add(div, {title: 'This is a tooltip'}) -# -# # remove it -# disposable.dispose() -# ``` -# -# In practice there are usually multiple tooltips. So we add them to a -# CompositeDisposable -# -# ```coffee -# {CompositeDisposable} = require 'atom' -# subscriptions = new CompositeDisposable -# -# div1 = document.createElement('div') -# div2 = document.createElement('div') -# subscriptions.add atom.tooltips.add(div1, {title: 'This is a tooltip'}) -# subscriptions.add atom.tooltips.add(div2, {title: 'Another tooltip'}) -# -# # remove them all -# subscriptions.dispose() -# ``` -# -# You can display a key binding in the tooltip as well with the -# `keyBindingCommand` option. -# -# ```coffee -# disposable = atom.tooltips.add @caseOptionButton, -# title: "Match Case" -# keyBindingCommand: 'find-and-replace:toggle-case-option' -# keyBindingTarget: @findEditor.element -# ``` -module.exports = -class TooltipManager - defaults: - trigger: 'hover' - container: 'body' - html: true - placement: 'auto top' - viewportPadding: 2 - - hoverDefaults: - {delay: {show: 1000, hide: 100}} - - constructor: ({@keymapManager, @viewRegistry}) -> - @tooltips = new Map() - - # Essential: Add a tooltip to the given element. - # - # * `target` An `HTMLElement` - # * `options` An object with one or more of the following options: - # * `title` A {String} or {Function} to use for the text in the tip. If - # a function is passed, `this` will be set to the `target` element. This - # option is mutually exclusive with the `item` option. - # * `html` A {Boolean} affecting the interpretation of the `title` option. - # If `true` (the default), the `title` string will be interpreted as HTML. - # Otherwise it will be interpreted as plain text. - # * `item` A view (object with an `.element` property) or a DOM element - # containing custom content for the tooltip. This option is mutually - # exclusive with the `title` option. - # * `class` A {String} with a class to apply to the tooltip element to - # enable custom styling. - # * `placement` A {String} or {Function} returning a string to indicate - # the position of the tooltip relative to `element`. Can be `'top'`, - # `'bottom'`, `'left'`, `'right'`, or `'auto'`. When `'auto'` is - # specified, it will dynamically reorient the tooltip. For example, if - # placement is `'auto left'`, the tooltip will display to the left when - # possible, otherwise it will display right. - # When a function is used to determine the placement, it is called with - # the tooltip DOM node as its first argument and the triggering element - # DOM node as its second. The `this` context is set to the tooltip - # instance. - # * `trigger` A {String} indicating how the tooltip should be displayed. - # Choose from one of the following options: - # * `'hover'` Show the tooltip when the mouse hovers over the element. - # This is the default. - # * `'click'` Show the tooltip when the element is clicked. The tooltip - # will be hidden after clicking the element again or anywhere else - # outside of the tooltip itself. - # * `'focus'` Show the tooltip when the element is focused. - # * `'manual'` Show the tooltip immediately and only hide it when the - # returned disposable is disposed. - # * `delay` An object specifying the show and hide delay in milliseconds. - # Defaults to `{show: 1000, hide: 100}` if the `trigger` is `hover` and - # otherwise defaults to `0` for both values. - # * `keyBindingCommand` A {String} containing a command name. If you specify - # this option and a key binding exists that matches the command, it will - # be appended to the title or rendered alone if no title is specified. - # * `keyBindingTarget` An `HTMLElement` on which to look up the key binding. - # If this option is not supplied, the first of all matching key bindings - # for the given command will be rendered. - # - # Returns a {Disposable} on which `.dispose()` can be called to remove the - # tooltip. - add: (target, options) -> - if target.jquery - disposable = new CompositeDisposable - disposable.add @add(element, options) for element in target - return disposable - - Tooltip ?= require './tooltip' - - {keyBindingCommand, keyBindingTarget} = options - - if keyBindingCommand? - bindings = @keymapManager.findKeyBindings(command: keyBindingCommand, target: keyBindingTarget) - keystroke = getKeystroke(bindings) - if options.title? and keystroke? - options.title += " " + getKeystroke(bindings) - else if keystroke? - options.title = getKeystroke(bindings) - - delete options.selector - options = _.defaults(options, @defaults) - if options.trigger is 'hover' - options = _.defaults(options, @hoverDefaults) - - tooltip = new Tooltip(target, options, @viewRegistry) - - if not @tooltips.has(target) - @tooltips.set(target, []) - @tooltips.get(target).push(tooltip) - - hideTooltip = -> - tooltip.leave(currentTarget: target) - tooltip.hide() - - window.addEventListener('resize', hideTooltip) - - disposable = new Disposable => - window.removeEventListener('resize', hideTooltip) - hideTooltip() - tooltip.destroy() - - if @tooltips.has(target) - tooltipsForTarget = @tooltips.get(target) - index = tooltipsForTarget.indexOf(tooltip) - if index isnt -1 - tooltipsForTarget.splice(index, 1) - if tooltipsForTarget.length is 0 - @tooltips.delete(target) - - disposable - - # Extended: Find the tooltips that have been applied to the given element. - # - # * `target` The `HTMLElement` to find tooltips on. - # - # Returns an {Array} of `Tooltip` objects that match the `target`. - findTooltips: (target) -> - if @tooltips.has(target) - @tooltips.get(target).slice() - else - [] - -humanizeKeystrokes = (keystroke) -> - keystrokes = keystroke.split(' ') - keystrokes = (_.humanizeKeystroke(stroke) for stroke in keystrokes) - keystrokes.join(' ') - -getKeystroke = (bindings) -> - if bindings?.length - "#{humanizeKeystrokes(bindings[0].keystrokes)}" diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js new file mode 100644 index 000000000..937f831d1 --- /dev/null +++ b/src/tooltip-manager.js @@ -0,0 +1,199 @@ +const _ = require('underscore-plus') +const {Disposable, CompositeDisposable} = require('event-kit') +let Tooltip = null + +// Essential: Associates tooltips with HTML elements. +// +// You can get the `TooltipManager` via `atom.tooltips`. +// +// ## Examples +// +// The essence of displaying a tooltip +// +// ```javascript +// // display it +// const disposable = atom.tooltips.add(div, {title: 'This is a tooltip'}) +// +// // remove it +// disposable.dispose() +// ``` +// +// In practice there are usually multiple tooltips. So we add them to a +// CompositeDisposable +// +// ```javascript +// const {CompositeDisposable} = require('atom') +// const subscriptions = new CompositeDisposable() +// +// const div1 = document.createElement('div') +// const div2 = document.createElement('div') +// subscriptions.add(atom.tooltips.add(div1, {title: 'This is a tooltip'})) +// subscriptions.add(atom.tooltips.add(div2, {title: 'Another tooltip'})) +// +// // remove them all +// subscriptions.dispose() +// ``` +// +// You can display a key binding in the tooltip as well with the +// `keyBindingCommand` option. +// +// ```javascript +// disposable = atom.tooltips.add(this.caseOptionButton, { +// title: 'Match Case', +// keyBindingCommand: 'find-and-replace:toggle-case-option', +// keyBindingTarget: this.findEditor.element +// }) +// ``` +module.exports = +class TooltipManager { + constructor ({keymapManager, viewRegistry}) { + this.defaults = { + trigger: 'hover', + container: 'body', + html: true, + placement: 'auto top', + viewportPadding: 2 + } + + this.hoverDefaults = { + delay: {show: 1000, hide: 100} + } + + this.keymapManager = keymapManager + this.viewRegistry = viewRegistry + this.tooltips = new Map() + } + + // Essential: Add a tooltip to the given element. + // + // * `target` An `HTMLElement` + // * `options` An object with one or more of the following options: + // * `title` A {String} or {Function} to use for the text in the tip. If + // a function is passed, `this` will be set to the `target` element. This + // option is mutually exclusive with the `item` option. + // * `html` A {Boolean} affecting the interpretation of the `title` option. + // If `true` (the default), the `title` string will be interpreted as HTML. + // Otherwise it will be interpreted as plain text. + // * `item` A view (object with an `.element` property) or a DOM element + // containing custom content for the tooltip. This option is mutually + // exclusive with the `title` option. + // * `class` A {String} with a class to apply to the tooltip element to + // enable custom styling. + // * `placement` A {String} or {Function} returning a string to indicate + // the position of the tooltip relative to `element`. Can be `'top'`, + // `'bottom'`, `'left'`, `'right'`, or `'auto'`. When `'auto'` is + // specified, it will dynamically reorient the tooltip. For example, if + // placement is `'auto left'`, the tooltip will display to the left when + // possible, otherwise it will display right. + // When a function is used to determine the placement, it is called with + // the tooltip DOM node as its first argument and the triggering element + // DOM node as its second. The `this` context is set to the tooltip + // instance. + // * `trigger` A {String} indicating how the tooltip should be displayed. + // Choose from one of the following options: + // * `'hover'` Show the tooltip when the mouse hovers over the element. + // This is the default. + // * `'click'` Show the tooltip when the element is clicked. The tooltip + // will be hidden after clicking the element again or anywhere else + // outside of the tooltip itself. + // * `'focus'` Show the tooltip when the element is focused. + // * `'manual'` Show the tooltip immediately and only hide it when the + // returned disposable is disposed. + // * `delay` An object specifying the show and hide delay in milliseconds. + // Defaults to `{show: 1000, hide: 100}` if the `trigger` is `hover` and + // otherwise defaults to `0` for both values. + // * `keyBindingCommand` A {String} containing a command name. If you specify + // this option and a key binding exists that matches the command, it will + // be appended to the title or rendered alone if no title is specified. + // * `keyBindingTarget` An `HTMLElement` on which to look up the key binding. + // If this option is not supplied, the first of all matching key bindings + // for the given command will be rendered. + // + // Returns a {Disposable} on which `.dispose()` can be called to remove the + // tooltip. + add (target, options) { + if (target.jquery) { + const disposable = new CompositeDisposable() + for (const element of target) { disposable.add(this.add(element, options)) } + return disposable + } + + if (Tooltip == null) { Tooltip = require('./tooltip') } + + const {keyBindingCommand, keyBindingTarget} = options + + if (keyBindingCommand != null) { + const bindings = this.keymapManager.findKeyBindings({command: keyBindingCommand, target: keyBindingTarget}) + const keystroke = getKeystroke(bindings) + if ((options.title != null) && (keystroke != null)) { + options.title += ` ${getKeystroke(bindings)}` + } else if (keystroke != null) { + options.title = getKeystroke(bindings) + } + } + + delete options.selector + options = _.defaults(options, this.defaults) + if (options.trigger === 'hover') { + options = _.defaults(options, this.hoverDefaults) + } + + const tooltip = new Tooltip(target, options, this.viewRegistry) + + if (!this.tooltips.has(target)) { + this.tooltips.set(target, []) + } + this.tooltips.get(target).push(tooltip) + + const hideTooltip = function () { + tooltip.leave({currentTarget: target}) + tooltip.hide() + } + + window.addEventListener('resize', hideTooltip) + + const disposable = new Disposable(() => { + window.removeEventListener('resize', hideTooltip) + hideTooltip() + tooltip.destroy() + + if (this.tooltips.has(target)) { + const tooltipsForTarget = this.tooltips.get(target) + const index = tooltipsForTarget.indexOf(tooltip) + if (index !== -1) { + tooltipsForTarget.splice(index, 1) + } + if (tooltipsForTarget.length === 0) { + this.tooltips.delete(target) + } + } + }) + + return disposable + } + + // Extended: Find the tooltips that have been applied to the given element. + // + // * `target` The `HTMLElement` to find tooltips on. + // + // Returns an {Array} of `Tooltip` objects that match the `target`. + findTooltips (target) { + if (this.tooltips.has(target)) { + return this.tooltips.get(target).slice() + } else { + return [] + } + } +} + +function humanizeKeystrokes (keystroke) { + let keystrokes = keystroke.split(' ') + keystrokes = (keystrokes.map((stroke) => _.humanizeKeystroke(stroke))) + return keystrokes.join(' ') +} + +function getKeystroke (bindings) { + if (bindings && bindings.length) { + return `${humanizeKeystrokes(bindings[0].keystrokes)}` + } +}