mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2025-01-07 23:59:22 +03:00
Make arguments atom.contextMenu.add consistent with atom.menu.add
This commit is contained in:
parent
504c4c7af6
commit
f8225a6441
@ -3,161 +3,97 @@
|
||||
ContextMenuManager = require '../src/context-menu-manager'
|
||||
|
||||
describe "ContextMenuManager", ->
|
||||
[contextMenu] = []
|
||||
[contextMenu, parent, child, grandchild] = []
|
||||
|
||||
beforeEach ->
|
||||
{resourcePath} = atom.getLoadSettings()
|
||||
contextMenu = new ContextMenuManager({resourcePath})
|
||||
|
||||
describe "adding definitions", ->
|
||||
it 'loads', ->
|
||||
contextMenu.add 'file-path',
|
||||
'.selector':
|
||||
'label': 'command'
|
||||
parent = document.createElement("div")
|
||||
child = document.createElement("div")
|
||||
grandchild = document.createElement("div")
|
||||
parent.classList.add('parent')
|
||||
child.classList.add('child')
|
||||
grandchild.classList.add('grandchild')
|
||||
child.appendChild(grandchild)
|
||||
parent.appendChild(child)
|
||||
|
||||
expect(contextMenu.definitions['.selector'][0].label).toEqual 'label'
|
||||
expect(contextMenu.definitions['.selector'][0].command).toEqual 'command'
|
||||
describe "::add(itemsBySelector)", ->
|
||||
it "can add top-level menu items that can be removed with the returned disposable", ->
|
||||
disposable = contextMenu.add
|
||||
'.parent': [{label: 'A', command: 'a'}]
|
||||
'.child': [{label: 'B', command: 'b'}]
|
||||
'.grandchild': [{label: 'C', command: 'c'}]
|
||||
|
||||
it 'does not add duplicate menu items', ->
|
||||
contextMenu.add 'file-path',
|
||||
'.selector':
|
||||
'label': 'command'
|
||||
expect(contextMenu.templateForElement(grandchild)).toEqual [
|
||||
{label: 'C', command: 'c'}
|
||||
{label: 'B', command: 'b'}
|
||||
{label: 'A', command: 'a'}
|
||||
]
|
||||
|
||||
contextMenu.add 'file-path',
|
||||
'.selector':
|
||||
'label': 'command'
|
||||
disposable.dispose()
|
||||
expect(contextMenu.templateForElement(grandchild)).toEqual []
|
||||
|
||||
expect(contextMenu.definitions['.selector'][0].label).toEqual 'label'
|
||||
expect(contextMenu.definitions['.selector'][0].command).toEqual 'command'
|
||||
expect(contextMenu.definitions['.selector'].length).toBe 1
|
||||
it "can add submenu items to existing menus that can be removed with the returned disposable", ->
|
||||
disposable1 = contextMenu.add
|
||||
'.grandchild': [{label: 'A', submenu: [{label: 'B', command: 'b'}]}]
|
||||
disposable2 = contextMenu.add
|
||||
'.grandchild': [{label: 'A', submenu: [{label: 'C', command: 'c'}]}]
|
||||
|
||||
it 'allows multiple separators', ->
|
||||
contextMenu.add 'file-path',
|
||||
'.selector':
|
||||
'separator1': '-'
|
||||
'separator2': '-'
|
||||
|
||||
expect(contextMenu.definitions['.selector'].length).toBe 2
|
||||
expect(contextMenu.definitions['.selector'][0].type).toEqual 'separator'
|
||||
expect(contextMenu.definitions['.selector'][1].type).toEqual 'separator'
|
||||
|
||||
it 'allows duplicate commands with different labels', ->
|
||||
contextMenu.add 'file-path',
|
||||
'.selector':
|
||||
'label': 'command'
|
||||
|
||||
contextMenu.add 'file-path',
|
||||
'.selector':
|
||||
'another label': 'command'
|
||||
|
||||
expect(contextMenu.definitions['.selector'][0].label).toEqual 'label'
|
||||
expect(contextMenu.definitions['.selector'][0].command).toEqual 'command'
|
||||
expect(contextMenu.definitions['.selector'][1].label).toEqual 'another label'
|
||||
expect(contextMenu.definitions['.selector'][1].command).toEqual 'command'
|
||||
|
||||
it "loads submenus", ->
|
||||
contextMenu.add 'file-path',
|
||||
'.selector':
|
||||
'parent':
|
||||
'child-1': 'child-1:trigger'
|
||||
'child-2': 'child-2:trigger'
|
||||
'parent-2': 'parent-2:trigger'
|
||||
|
||||
expect(contextMenu.definitions['.selector'].length).toBe 2
|
||||
expect(contextMenu.definitions['.selector'][0].label).toEqual 'parent'
|
||||
expect(contextMenu.definitions['.selector'][0].submenu.length).toBe 2
|
||||
expect(contextMenu.definitions['.selector'][0].submenu[0].label).toBe 'child-1'
|
||||
expect(contextMenu.definitions['.selector'][0].submenu[0].command).toBe 'child-1:trigger'
|
||||
expect(contextMenu.definitions['.selector'][0].submenu[1].label).toBe 'child-2'
|
||||
expect(contextMenu.definitions['.selector'][0].submenu[1].command).toBe 'child-2:trigger'
|
||||
|
||||
describe 'dev mode', ->
|
||||
it 'loads', ->
|
||||
contextMenu.add 'file-path',
|
||||
'.selector':
|
||||
'label': 'command'
|
||||
, devMode: true
|
||||
|
||||
expect(contextMenu.devModeDefinitions['.selector'][0].label).toEqual 'label'
|
||||
expect(contextMenu.devModeDefinitions['.selector'][0].command).toEqual 'command'
|
||||
|
||||
describe "building a menu template", ->
|
||||
beforeEach ->
|
||||
contextMenu.definitions = {
|
||||
'.parent':[
|
||||
label: 'parent'
|
||||
command: 'command-p'
|
||||
]
|
||||
'.child': [
|
||||
label: 'child'
|
||||
command: 'command-c'
|
||||
expect(contextMenu.templateForElement(grandchild)).toEqual [{
|
||||
label: 'A',
|
||||
submenu: [
|
||||
{label: 'C', command: 'c'}
|
||||
{label: 'B', command: 'b'}
|
||||
]
|
||||
}
|
||||
}]
|
||||
|
||||
contextMenu.devModeDefinitions =
|
||||
'.parent': [
|
||||
label: 'dev-label'
|
||||
command: 'dev-command'
|
||||
disposable2.dispose()
|
||||
expect(contextMenu.templateForElement(grandchild)).toEqual [{
|
||||
label: 'A',
|
||||
submenu: [
|
||||
{label: 'B', command: 'b'}
|
||||
]
|
||||
}]
|
||||
|
||||
disposable1.dispose()
|
||||
expect(contextMenu.templateForElement(grandchild)).toEqual []
|
||||
|
||||
it "favors the most specific / recently added item in the case of a duplicate label", ->
|
||||
grandchild.classList.add('foo')
|
||||
|
||||
disposable1 = contextMenu.add
|
||||
'.grandchild': [{label: 'A', command: 'a'}]
|
||||
disposable2 = contextMenu.add
|
||||
'.grandchild.foo': [{label: 'A', command: 'b'}]
|
||||
disposable3 = contextMenu.add
|
||||
'.grandchild': [{label: 'A', command: 'c'}]
|
||||
|
||||
expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'b'}]
|
||||
|
||||
disposable2.dispose()
|
||||
expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'c'}]
|
||||
|
||||
disposable3.dispose()
|
||||
expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'a'}]
|
||||
|
||||
it "allows multiple separators", ->
|
||||
contextMenu.add
|
||||
'.grandchild': [
|
||||
{label: 'A', command: 'a'},
|
||||
{type: 'separator'},
|
||||
{label: 'B', command: 'b'},
|
||||
{type: 'separator'},
|
||||
{label: 'C', command: 'c'}
|
||||
]
|
||||
|
||||
describe "on a single element", ->
|
||||
[element] = []
|
||||
|
||||
beforeEach ->
|
||||
element = ($$ -> @div class: 'parent')[0]
|
||||
|
||||
it "creates a menu with a single item", ->
|
||||
menu = contextMenu.combinedMenuTemplateForElement(element)
|
||||
|
||||
expect(menu[0].label).toEqual 'parent'
|
||||
expect(menu[0].command).toEqual 'command-p'
|
||||
expect(menu[1]).toBeUndefined()
|
||||
|
||||
describe "in devMode", ->
|
||||
beforeEach -> contextMenu.devMode = true
|
||||
|
||||
it "creates a menu with development items", ->
|
||||
menu = contextMenu.combinedMenuTemplateForElement(element)
|
||||
|
||||
expect(menu[0].label).toEqual 'parent'
|
||||
expect(menu[0].command).toEqual 'command-p'
|
||||
expect(menu[1].type).toEqual 'separator'
|
||||
expect(menu[2].label).toEqual 'dev-label'
|
||||
expect(menu[2].command).toEqual 'dev-command'
|
||||
|
||||
|
||||
describe "on multiple elements", ->
|
||||
[element] = []
|
||||
|
||||
beforeEach ->
|
||||
element = $$ ->
|
||||
@div class: 'parent', =>
|
||||
@div class: 'child'
|
||||
|
||||
element = element.find('.child')[0]
|
||||
|
||||
it "creates a menu with a two items", ->
|
||||
menu = contextMenu.combinedMenuTemplateForElement(element)
|
||||
|
||||
expect(menu[0].label).toEqual 'child'
|
||||
expect(menu[0].command).toEqual 'command-c'
|
||||
expect(menu[1].label).toEqual 'parent'
|
||||
expect(menu[1].command).toEqual 'command-p'
|
||||
expect(menu[2]).toBeUndefined()
|
||||
|
||||
describe "in devMode", ->
|
||||
beforeEach -> contextMenu.devMode = true
|
||||
|
||||
xit "creates a menu with development items", ->
|
||||
menu = contextMenu.combinedMenuTemplateForElement(element)
|
||||
|
||||
expect(menu[0].label).toEqual 'child'
|
||||
expect(menu[0].command).toEqual 'command-c'
|
||||
expect(menu[1].label).toEqual 'parent'
|
||||
expect(menu[1].command).toEqual 'command-p'
|
||||
expect(menu[2].label).toEqual 'dev-label'
|
||||
expect(menu[2].command).toEqual 'dev-command'
|
||||
expect(menu[3]).toBeUndefined()
|
||||
expect(contextMenu.templateForElement(grandchild)).toEqual [
|
||||
{label: 'A', command: 'a'},
|
||||
{type: 'separator'},
|
||||
{label: 'B', command: 'b'},
|
||||
{type: 'separator'},
|
||||
{label: 'C', command: 'c'}
|
||||
]
|
||||
|
||||
describe "executeBuildHandlers", ->
|
||||
menuTemplate = [
|
||||
|
@ -4,6 +4,12 @@ remote = require 'remote'
|
||||
path = require 'path'
|
||||
CSON = require 'season'
|
||||
fs = require 'fs-plus'
|
||||
{specificity} = require 'clear-cut'
|
||||
{Disposable} = require 'event-kit'
|
||||
MenuHelpers = require './menu-helpers'
|
||||
|
||||
SpecificityCache = {}
|
||||
SequenceCount = 0
|
||||
|
||||
# Extended: Provides a registry for commands that you'd like to appear in the
|
||||
# context menu.
|
||||
@ -13,16 +19,17 @@ fs = require 'fs-plus'
|
||||
module.exports =
|
||||
class ContextMenuManager
|
||||
constructor: ({@resourcePath, @devMode}) ->
|
||||
@definitions = {}
|
||||
@devModeDefinitions = {}
|
||||
@definitions = {'.overlayer': []} # TODO: Remove once color picker package stops touching private data
|
||||
@activeElement = null
|
||||
|
||||
@devModeDefinitions['.workspace'] = [
|
||||
label: 'Inspect Element'
|
||||
command: 'application:inspect'
|
||||
executeAtBuild: (e) ->
|
||||
@commandOptions = x: e.pageX, y: e.pageY
|
||||
]
|
||||
@itemSets = []
|
||||
|
||||
# @devModeDefinitions['.workspace'] = [
|
||||
# label: 'Inspect Element'
|
||||
# command: 'application:inspect'
|
||||
# executeAtBuild: (e) ->
|
||||
# @commandOptions = x: e.pageX, y: e.pageY
|
||||
# ]
|
||||
|
||||
atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems()
|
||||
|
||||
@ -41,91 +48,72 @@ class ContextMenuManager
|
||||
# * `devMode` Determines whether the entries should only be shown when
|
||||
# the window is in dev mode.
|
||||
add: (name, object, {devMode}={}) ->
|
||||
for selector, items of object
|
||||
for label, commandOrSubmenu of items
|
||||
unless typeof arguments[0] is 'object'
|
||||
return @add(@convertLegacyItems(object), {devMode})
|
||||
|
||||
itemsBySelector = _.deepClone(arguments[0])
|
||||
devMode = arguments[1]?.devMode ? false
|
||||
addedItemSets = []
|
||||
|
||||
for selector, items of itemsBySelector
|
||||
itemSet = new ContextMenuItemSet(selector, items)
|
||||
addedItemSets.push(itemSet)
|
||||
@itemSets.push(itemSet)
|
||||
|
||||
new Disposable =>
|
||||
for itemSet in addedItemSets
|
||||
@itemSets.splice(@itemSets.indexOf(itemSet), 1)
|
||||
|
||||
templateForElement: (element) ->
|
||||
template = []
|
||||
currentTarget = element
|
||||
|
||||
while currentTarget?
|
||||
matchingItemSets =
|
||||
@itemSets
|
||||
.filter (itemSet) -> currentTarget.webkitMatchesSelector(itemSet.selector)
|
||||
.sort (a, b) -> a.compare(b)
|
||||
|
||||
for {items} in matchingItemSets
|
||||
MenuHelpers.merge(template, item) for item in items
|
||||
|
||||
currentTarget = currentTarget.parentElement
|
||||
|
||||
template
|
||||
|
||||
convertLegacyItems: (legacyItems) ->
|
||||
itemsBySelector = {}
|
||||
|
||||
for selector, commandsByLabel of legacyItems
|
||||
itemsBySelector[selector] = items = []
|
||||
|
||||
for label, commandOrSubmenu of commandsByLabel
|
||||
if typeof commandOrSubmenu is 'object'
|
||||
submenu = []
|
||||
for submenuLabel, command of commandOrSubmenu
|
||||
submenu.push(@buildMenuItem(submenuLabel, command))
|
||||
@addBySelector(selector, {label: label, submenu: submenu}, {devMode})
|
||||
items.push({label, submenu: @convertLegacyItems(commandOrSubmenu)})
|
||||
else if commandOrSubmenu is '-'
|
||||
items.push({type: 'separator'})
|
||||
else
|
||||
menuItem = @buildMenuItem(label, commandOrSubmenu)
|
||||
@addBySelector(selector, menuItem, {devMode})
|
||||
items.push({label, command: commandOrSubmenu})
|
||||
|
||||
undefined
|
||||
|
||||
buildMenuItem: (label, command) ->
|
||||
if command is '-'
|
||||
{type: 'separator'}
|
||||
else
|
||||
{label, command}
|
||||
|
||||
# Registers a command to be displayed when the relevant item is right
|
||||
# clicked.
|
||||
#
|
||||
# * `selector` The css selector for the active element which should include
|
||||
# the given command in its context menu.
|
||||
# * `definition` The object containing keys which match the menu template API.
|
||||
# * `options` An optional {Object} with the following keys:
|
||||
# * `devMode` Indicates whether this command should only appear while the
|
||||
# editor is in dev mode.
|
||||
addBySelector: (selector, definition, {devMode}={}) ->
|
||||
definitions = if devMode then @devModeDefinitions else @definitions
|
||||
if not _.findWhere(definitions[selector], definition) or _.isEqual(definition, {type: 'separator'})
|
||||
(definitions[selector] ?= []).push(definition)
|
||||
|
||||
# Returns definitions which match the element and devMode.
|
||||
definitionsForElement: (element, {devMode}={}) ->
|
||||
definitions = if devMode then @devModeDefinitions else @definitions
|
||||
matchedDefinitions = []
|
||||
for selector, items of definitions when element.webkitMatchesSelector(selector)
|
||||
matchedDefinitions.push(_.clone(item)) for item in items
|
||||
|
||||
matchedDefinitions
|
||||
|
||||
# Used to generate the context menu for a specific element and it's
|
||||
# parents.
|
||||
#
|
||||
# The menu items are sorted such that menu items that match closest to the
|
||||
# active element are listed first. The further down the list you go, the higher
|
||||
# up the ancestor hierarchy they match.
|
||||
#
|
||||
# * `element` The DOM element to generate the menu template for.
|
||||
menuTemplateForMostSpecificElement: (element, {devMode}={}) ->
|
||||
menuTemplate = @definitionsForElement(element, {devMode})
|
||||
if element.parentElement
|
||||
menuTemplate.concat(@menuTemplateForMostSpecificElement(element.parentElement, {devMode}))
|
||||
else
|
||||
menuTemplate
|
||||
|
||||
# Returns a menu template for both normal entries as well as
|
||||
# development mode entries.
|
||||
combinedMenuTemplateForElement: (element) ->
|
||||
normalItems = @menuTemplateForMostSpecificElement(element)
|
||||
devItems = if @devMode then @menuTemplateForMostSpecificElement(element, devMode: true) else []
|
||||
|
||||
menuTemplate = normalItems
|
||||
menuTemplate.push({ type: 'separator' }) if normalItems.length > 0 and devItems.length > 0
|
||||
menuTemplate.concat(devItems)
|
||||
|
||||
# Executes `executeAtBuild` if defined for each menu item with
|
||||
# the provided event and then removes the `executeAtBuild` property from
|
||||
# the menu item.
|
||||
#
|
||||
# This is useful for commands that need to provide data about the event
|
||||
# to the command.
|
||||
executeBuildHandlers: (event, menuTemplate) ->
|
||||
for template in menuTemplate
|
||||
template?.executeAtBuild?.call(template, event)
|
||||
delete template.executeAtBuild
|
||||
itemsBySelector
|
||||
|
||||
# Public: Request a context menu to be displayed.
|
||||
#
|
||||
# * `event` A DOM event.
|
||||
showForEvent: (event) ->
|
||||
@activeElement = event.target
|
||||
menuTemplate = @combinedMenuTemplateForElement(event.target)
|
||||
menuTemplate = @templateForElement(@activeElement)
|
||||
|
||||
return unless menuTemplate?.length > 0
|
||||
@executeBuildHandlers(event, menuTemplate)
|
||||
# @executeBuildHandlers(event, menuTemplate)
|
||||
remote.getCurrentWindow().emit('context-menu', menuTemplate)
|
||||
undefined
|
||||
return
|
||||
|
||||
class ContextMenuItemSet
|
||||
constructor: (@selector, @items) ->
|
||||
@specificity = (SpecificityCache[@selector] ?= specificity(@selector))
|
||||
@sequenceNumber = SequenceCount++
|
||||
|
||||
compare: (other) ->
|
||||
other.specificity - @specificity or
|
||||
other.sequenceNumber - @sequenceNumber
|
||||
|
@ -15,11 +15,12 @@ unmerge = (menu, item) ->
|
||||
unless matchingItem.submenu?.length > 0
|
||||
menu.splice(menu.indexOf(matchingItem), 1)
|
||||
|
||||
findMatchingItem = (menu, {label, submenu}) ->
|
||||
findMatchingItem = (menu, {type, label, submenu}) ->
|
||||
return if type is 'separator'
|
||||
for item in menu
|
||||
if normalizeLabel(item.label) is normalizeLabel(label) and item.submenu? is submenu?
|
||||
return item
|
||||
null
|
||||
return
|
||||
|
||||
normalizeLabel = (label) ->
|
||||
return undefined unless label?
|
||||
|
Loading…
Reference in New Issue
Block a user