diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ff70d42..3e8a7b48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,12 +28,14 @@ * A `select`'s phantom mousedown is no longer treated as an outside click (fixes #744) * A re-dispatched layer click now targets the element actually clicked (fixes #771) * Guard against undefined `e.data` in the contextmenu handler (fixes #777) +* A raw `Element` passed as `context` now scopes the registration instead of being silently ignored (fixes #809). An empty `
`/`` element with no controls or options in it, because such an element reports a length of `0`, +* a selector string matching no element, which registers nothing at all. + +#### Example +```javascript +// scope the menu to a container, selected with a selector string +$.contextMenu({ + selector: '.context-menu', + context: 'div#panel' +}); + +// scope the menu to a container, selected with a dom element +var element = document.getElementById('panel'); +$.contextMenu({ + selector: '.context-menu', + context: element +}); + +// scope the menu to a container, selected with a jQuery object. +// $(container).contextMenu(options) is shorthand for this. +$.contextMenu({ + selector: '.context-menu', + context: $('#panel') +}); +``` + ### items Object with [items](docs/items.html) to be listed in contextMenu. See [items](docs/items.html) for a full documentation on how to build your menu items. diff --git a/src/jquery.contextMenu.js b/src/jquery.contextMenu.js index 6dfa7f87..ceace1d5 100644 --- a/src/jquery.contextMenu.js +++ b/src/jquery.contextMenu.js @@ -2747,6 +2747,11 @@ var $document = $(document); var $context = $document; var _hasContext = false; + // was `context` given as a raw Element? Those used to be ignored, which + // means they were also tracked in `namespaces` - see the 'create' + // operation, which keeps doing that so destroy-by-selector keeps + // working for them. + var _contextFromElement = false; // an Element / jQuery object can't be used as a delegated-event // selector string, so it's normalized here once and handled @@ -2754,7 +2759,39 @@ var useElementSelector = isElementSelector(o.selector); var $elements = useElementSelector ? (o.selector.jquery ? o.selector : $(o.selector)) : null; - if (!o.context || !o.context.length) { + // `context` may be given as a selector string, an Element, a jQuery + // object or `document`, and is normalized here to a single DOM node: + // every consumer of `o.context` treats it as a raw node + // (`o.context !== el`, `$.contains(o.context, el)`, ...). + // + // A raw Element used to be dropped by the `!o.context.length` test + // below - an Element has no `length` at all - which silently left the + // menu registered for the whole document instead of scoped to the + // element the caller passed. That is what is fixed here. + // See https://github.com/swisnl/jQuery-contextMenu/issues/809 + // + // Everything else is left exactly as it was, on purpose. Newly + // honouring a context that is ignored today un-scopes menus that work + // today, silently and without an error, so it is a breaking change and + // belongs in a major release: + // - values carrying a numeric `length` keep the legacy length test. + // That covers and {{trigger}}
'); + assert.ok(form.length > 0, 'sanity check: the form reports a non-zero length'); + + var counter = registerScopedMenu(form); + + $('#outside').trigger($.Event('contextmenu')); + assert.equal(counter.shown, 0, 'a trigger outside the form does not open the menu'); + + $('#inside').trigger($.Event('contextmenu')); + assert.equal(counter.shown, 1, 'a trigger inside the form opens the menu'); + }); + + // --- legacy behaviour, deliberately unchanged ----------------------------- + // + // Everything below pins down a context that is *ignored* today. Honouring it + // would scope menus that are registered page-wide today, which stops them + // firing outside the context element with no error to explain why. That is a + // breaking change for working integrations, so it is out of scope for a + // patch release. See the pull request for #809. + + QUnit.test('an empty
passed as context is still ignored (legacy)', function(assert) { + // An empty reports `length === 0`, which the length test reads as + // "no context given". + var form = setupScopedFixture('{{trigger}}
'); + assert.equal(form.length, 0, 'sanity check: the form reports length 0'); + + var counter = registerScopedMenu(form); + + $('#inside').trigger($.Event('contextmenu')); + assert.equal(counter.shown, 1, 'a trigger inside the empty form opens the menu'); + + counter.shown = 0; + $('#outside').trigger($.Event('contextmenu')); + assert.equal(counter.shown, 1, 'the registration is still document-wide, as it is today'); + }); + + QUnit.test('an empty ' + + 'outside' + ); + + var counter = registerScopedMenu(document.getElementById('in-context')); + + $('#outside').trigger($.Event('contextmenu')); + assert.equal(counter.shown, 1, 'the registration is still document-wide, as it is today'); + }); + + QUnit.test('a context selector matching nothing still registers nothing (legacy)', function(assert) { + fixture().html('anywhere'); + + var counter = registerScopedMenu('#does-not-exist'); + + $('#anywhere').trigger($.Event('contextmenu')); + assert.equal(counter.shown, 0, 'the menu is bound to nothing at all, as it is today'); + }); + + QUnit.test('destroy with an Element context still tears everything down (legacy)', function(assert) { + // For 'destroy', `context` means the trigger element rather than a + // container - that is what $.fn.contextMenu('destroy') passes - and a raw + // Element has never been accepted there: it falls through to the "no + // context, no selector" branch, which destroys every registered menu. + // Scoping it would silently turn this call into a no-op. + fixture().html( + '
inside
' + + 'other' + ); + + var counter = registerScopedMenu(document.getElementById('in-context')); + var otherShown = 0; + $.contextMenu({ + selector: '.other-item', + events: {show: function() { otherShown++; }}, + items: {copy: {name: 'Copy'}} + }); + + $.contextMenu('destroy', {context: document.getElementById('in-context')}); + + $('#inside').trigger($.Event('contextmenu')); + assert.equal(counter.shown, 0, 'the Element-scoped menu was destroyed'); + + $('#other').trigger($.Event('contextmenu')); + assert.equal(otherShown, 0, 'the unrelated menu was destroyed as well, as it is today'); + }); +})();