Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/_data/nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ module.exports = [
{ id: 'html5-polyfill', text: 'HTML5 polyfill', url: '/demo/html5-polyfill.html' },
{ id: 'html5-polyfill-firefox8', text: 'HTML5 polyfill (Firefox)', url: '/demo/html5-polyfill-firefox8.html' },
{ id: 'input', text: 'Input Commands', url: '/demo/input.html' },
{ id: 'input-rename-trigger', text: 'Input Commands (accessing the trigger)', url: '/demo/input-rename-trigger.html' },
{ id: 'keeping-contextmenu-open', text: 'Keeping the context menu open', url: '/demo/keeping-contextmenu-open.html' },
{ id: 'menu-title', text: 'Menus with titles', url: '/demo/menu-title.html' },
{ id: 'menu-promise', text: 'Menu with promise', url: '/demo/menu-promise.html' },
Expand Down
63 changes: 63 additions & 0 deletions documentation/demo/input-rename-trigger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
currentMenu: input-rename-trigger
---

# Demo: Renaming the trigger from an input command

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->


- [Example code](#example-code)
- [Example HTML](#example-html)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Item level [`events`](../docs/items.html#events) handlers are bound with the menu's
options object as jQuery event data, so `e.data.$trigger` is the element that opened
the menu. Inside the handler `this` is the `<input>` itself, exactly like any other
jQuery event handler.

Both buttons below share a single menu definition. Left-click one, type a new label
and click somewhere else so the input loses focus. The button you clicked is renamed,
the other one is not.

<button class="context-menu-rename btn btn-neutral">Button one</button>
<button class="context-menu-rename btn btn-neutral">Button two</button>

## Example code

<script type="text/javascript" class="showcase">
$(function(){
$.contextMenu({
selector: '.context-menu-rename',
trigger: 'left',
items: {
label: {
name: "Label",
type: 'text',
events: {
focus: function(e) {
// `e.data` is the menu's options object, `$trigger` is the
// element the menu was opened on
$(this).val(e.data.$trigger.text());
},
focusout: function(e) {
// `this` is the <input>, so write its value back onto the
// button that opened this menu
e.data.$trigger.text($(this).val());
}
}
},
sep1: "---------",
close: {
name: "Close",
callback: $.noop
}
}
});
});
</script>

## Example HTML
<div style="display:none;" class="showcase" data-showcase-import=".context-menu-rename"></div>
32 changes: 31 additions & 1 deletion documentation/docs/items.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,12 +316,20 @@ __Only used with [types](#type) `text`, `textarea`, `radio`, `checkbox` and `sel

`events`: `object`

Inside a handler:

* `this` is the `<input>`, `<textarea>` or `<select>` element, just like in any other jQuery event handler.
* `e.data` is the options object of the menu the item belongs to, so `e.data.$trigger` is the
element the menu was opened on. This also works for items in a sub-menu, and it always points
at the element that opened the currently visible menu, so a single menu definition shared by
many triggers still resolves to the right one.
* `e.data.$menu` is the `<ul>` of the menu the item belongs to.

#### Example
```javascript
$.contextMenu({
selector: 'span.context-menu',
events: {
items: {
command1: {
name: "Foobar",
type: "text",
Expand All @@ -336,6 +344,28 @@ $.contextMenu({
});
```

#### Example: writing a value back to the trigger

```javascript
$.contextMenu({
selector: 'button.context-menu',
trigger: 'left',
items: {
label: {
name: "Label",
type: "text",
events: {
focusout: function(e){
// `this` is the <input>, `e.data.$trigger` is the button
e.data.$trigger.text($(this).val());
}
}
}
}
});
```

See the [renaming the trigger from an input command](../demo/input-rename-trigger.html) demo.

### value

Expand Down
31 changes: 30 additions & 1 deletion src/jquery.contextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,28 @@
},
// operations
op = {
// Stamp the element that opened the menu onto the menu's options object
// and onto the options object of every sub-menu that already exists.
// Item-level `events` handlers are bound with their own menu's options
// object as jQuery event data, so without this a handler on an input in
// a sub-menu has no way to reach the trigger.
// See https://github.com/swisnl/jQuery-contextMenu/issues/729
setTrigger: function (opt, $trigger) {
opt.$trigger = $trigger;

if (!opt.items) {
return;
}

$.each(opt.items, function (key, item) {
// only descend into sub-menus that have actually been created;
// ones still waiting on a promise pick the trigger up from the
// root in op.create()
if (item && item.$menu) {
op.setTrigger(item, $trigger);
}
});
},
show: function (opt, x, y) {
var $trigger = $(this),
css = {};
Expand All @@ -1291,7 +1313,7 @@
$(document).trigger('contextmenu:hide');

// backreference for callbacks
opt.$trigger = $trigger;
op.setTrigger(opt, $trigger);

// show event
if (opt.events.show.call($trigger, opt) === false) {
Expand Down Expand Up @@ -1448,6 +1470,13 @@
root = opt;
}

// sub-menus created after the menu was shown (a promise resolving,
// for instance) missed op.setTrigger(), so inherit the trigger from
// the root here. See op.setTrigger().
if (opt !== root && root.$trigger) {
opt.$trigger = root.$trigger;
}

// define handler for fast input clicks
var handleFastInputClick = function(e) {
var $inputClicked = $(this);
Expand Down
154 changes: 154 additions & 0 deletions test/unit/issue-729-trigger-in-item-events.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
QUnit.module('issue 729 - trigger reachable from item events handlers', {
beforeEach: function() {
var $fixture = $('#qunit-fixture');
if ($fixture.length === 0) {
$fixture = $('<div id="qunit-fixture"></div>').appendTo('body');
}
$fixture.html(
'<button class="trigger729" id="trigger-a">A</button>' +
'<button class="trigger729" id="trigger-b">B</button>'
);
},
afterEach: function() {
$.contextMenu('destroy');
$(document).trigger('contextmenu:hide');
$('#qunit-fixture').html('');
}
});

// Regression test for https://github.com/swisnl/jQuery-contextMenu/issues/729
// An item-level `events` handler is bound with its menu's options object as
// jQuery event data, so `e.data.$trigger` must point at the element that opened
// the menu - also when several elements share one menu definition.
QUnit.test('e.data.$trigger identifies the element that opened the menu', function(assert) {
var seen = [];

$.contextMenu({
selector: '.trigger729',
items: {
label: {
name: 'Label',
type: 'text',
events: {
focusout: function(e) {
seen.push(e.data.$trigger);
}
}
}
}
});

$('#trigger-a').trigger($.Event('contextmenu', {pageX: 10, pageY: 10}));
$('.context-menu-root').find('input[type=text]').trigger('focusout');

assert.equal(seen.length, 1, 'the focusout handler ran');
assert.ok(seen[0] instanceof $, 'e.data.$trigger is a jQuery object');
assert.equal(seen[0][0], $('#trigger-a')[0], 'e.data.$trigger is the first trigger');

$(document).trigger('contextmenu:hide');
$('#trigger-b').trigger($.Event('contextmenu', {pageX: 10, pageY: 10}));
$('.context-menu-root').find('input[type=text]').trigger('focusout');

assert.equal(seen.length, 2, 'the focusout handler ran again');
assert.equal(seen[1][0], $('#trigger-b')[0], 'e.data.$trigger is the second trigger');
});

QUnit.test('e.data.$trigger is available on sub-menu items too', function(assert) {
var seen = [];

$.contextMenu({
selector: '.trigger729',
items: {
sub: {
name: 'Sub',
items: {
label: {
name: 'Nested label',
type: 'text',
events: {
focusout: function(e) {
seen.push(e.data.$trigger);
}
}
}
}
}
}
});

$('#trigger-a').trigger($.Event('contextmenu', {pageX: 10, pageY: 10}));
$('input[type=text]').trigger('focusout');

assert.equal(seen.length, 1, 'the nested focusout handler ran');
assert.ok(seen[0] && seen[0].length, 'e.data.$trigger is set for a sub-menu item');
assert.equal(seen[0][0], $('#trigger-a')[0], 'e.data.$trigger is the trigger, not the sub-menu opener');

$(document).trigger('contextmenu:hide');
$('#trigger-b').trigger($.Event('contextmenu', {pageX: 10, pageY: 10}));
$('input[type=text]').trigger('focusout');

assert.equal(seen.length, 2, 'the nested focusout handler ran again');
assert.equal(seen[1][0], $('#trigger-b')[0], 'e.data.$trigger follows the second trigger');
});

QUnit.test('e.data.$trigger is available on a promise-built sub-menu', function(assert) {
var done = assert.async();
var deferred = $.Deferred();
var seen = null;

$.contextMenu({
selector: '.trigger729',
items: {
sub: {
name: 'Sub',
items: deferred.promise()
}
}
});

$('#trigger-a').trigger($.Event('contextmenu', {pageX: 10, pageY: 10}));

deferred.resolve({
label: {
name: 'Nested label',
type: 'text',
events: {
focusout: function(e) {
seen = e.data.$trigger;
}
}
}
});

setTimeout(function() {
$('input[type=text]').trigger('focusout');
assert.ok(seen && seen.length, 'e.data.$trigger is set on a lazily created sub-menu');
assert.equal(seen[0], $('#trigger-a')[0], 'e.data.$trigger is the trigger');
done();
}, 0);
});

// The reporter's scenario: a text input in the menu writes its value back onto
// the button that opened the menu when the input loses focus.
QUnit.test('reporter scenario - focusout writes the input value back to the trigger', function(assert) {
$.contextMenu({
selector: '.trigger729',
items: {
label: {
name: 'Label',
type: 'text',
events: {
focusout: function(e) {
e.data.$trigger.text($(this).val());
}
}
}
}
});

$('#trigger-b').trigger($.Event('contextmenu', {pageX: 10, pageY: 10}));
$('.context-menu-root').find('input[type=text]').val('Renamed').trigger('focusout');

assert.equal($('#trigger-b').text(), 'Renamed', 'the trigger label was updated');
assert.equal($('#trigger-a').text(), 'A', 'the other trigger was left alone');
});
Loading