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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* `item.name` may now be a function, for labels that change per open (fixes #743)
* The trigger element is now reachable from item-level `events` handlers, including in sub-menus (fixes #729)
* Added `animation.showDuration`/`animation.hideDuration` and `animation.animateOnReopen` (fixes #739)
* `item.icon` now supports Font Awesome 6 and 7 syntax, including the separate family/style classes (`fa-solid fa-user`, `fa-sharp fa-solid fa-user`) and per-pack short prefixes (`fasds`, `fands`), alongside the existing 4 and 5 syntax (fixes #776). Font Awesome 4's full `fa fa-user` form now works too, where previously only the `fa-user` shorthand did

#### Fixed

Expand Down
44 changes: 42 additions & 2 deletions documentation/docs/font-awesome.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,46 @@ Check out the [demo](https://swisnl.github.io/jQuery-contextMenu/demo/fontawesom

It is also possible to use your own SVG icons if you like, you can [customize](customize) this by using the SASS files.

## Recent Font Awesome support
## Supported Font Awesome versions

The contextmenu supports the new Font Awesome by including the correct font-awesome CSS and adding the new classes as icon for a menu item. When using for example `fas fa-beer` it will crete the `i` tag for the icon in the menu and adjust CSS accordingly.
Include the Font Awesome CSS for the version you use, then pass its classes as
the item's [icon](items#icon). The contextmenu creates the `i` tag for the icon
inside the menu item and adjusts its own CSS accordingly. Every version from 4
to 7 works, using that version's own syntax:

```javascript
var items = {
// Font Awesome 4
a: {name: "Beer", icon: "fa-beer"}, // shorthand, `fa` is added for you
b: {name: "Beer", icon: "fa fa-beer"}, // or the full v4 syntax

// Font Awesome 5
c: {name: "Beer", icon: "fas fa-beer"},
d: {name: "GitHub", icon: "fab fa-github"},

// Font Awesome 6 and 7, which name the family and the style separately
e: {name: "Beer", icon: "fa-solid fa-beer"},
f: {name: "Beer", icon: "fa-regular fa-beer"},
g: {name: "GitHub", icon: "fa-brands fa-github"},
h: {name: "Beer", icon: "fa-sharp fa-solid fa-beer"},
i: {name: "Beer", icon: "fa-sharp-duotone fa-solid fa-beer"},

// per-pack short prefixes work too
j: {name: "Beer", icon: "fasds fa-beer"}
}
```

Anything containing a `fa-` class is treated as Font Awesome and passed through
to the `i` tag unchanged, so a family or style added in a future release needs
no change here. The one exception is the version 4 shorthand: when the class
list does not say which family or style to use, the base `fa` class is supplied
so `icon: "fa-beer"` keeps working on its own.

A single word with no `fa-` in it stays a [built-in icon](customize) name, so
`icon: "edit"` still refers to this plugin's own icon font rather than to Font
Awesome.

The menu item itself gets a `context-menu-icon--fa5` class, which the stylesheet
uses to position the child icon. The name is historical: it applies to every
Font Awesome version, not just 5, and is kept as-is so existing custom CSS keeps
matching.
110 changes: 91 additions & 19 deletions src/jquery.contextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -1954,30 +1954,37 @@
if (typeof item.icon === 'function') {
item._icon = item.icon.call(this, this, $t, key, item);
} else {
if (typeof(item.icon) === 'string' && (
item.icon.substring(0, 4) === 'fab '
|| item.icon.substring(0, 4) === 'fas '
|| item.icon.substring(0, 4) === 'fad '
|| item.icon.substring(0, 4) === 'far '
|| item.icon.substring(0, 4) === 'fal ')
) {
// to enable font awesome
if (isFontAwesomeIcon(item.icon)) {
// Font Awesome needs its own <i> tag instead of being applied
// to the menu item itself: its classes set font-family and
// font-weight and their own ::before content on whatever
// element they are put on, which used to bleed into (and
// bold) the item's label text and clash with this plugin's
// own icon styling.
//
// `--fa5` is a historical name kept for compatibility: it is
// just the hook the stylesheet uses to position a child icon
// element, and applies to every Font Awesome version rather
// than to v5 specifically.
$t.addClass(root.classNames.icon + ' ' + root.classNames.icon + '--fa5');
// built with addClass instead of interpolating the icon
// into a markup string: an icon name coming from stored
// or otherwise non-literal data could otherwise close the
// class attribute and inject arbitrary markup.
item._icon = $('<i></i>').addClass(item.icon);
} else if (typeof(item.icon) === 'string' && item.icon.substring(0, 3) === 'fa-') {
// legacy Font Awesome 4 style icon class (e.g. "fa-trash"), kept for
// backwards compatibility. Just like the fas/far/fab/fad/fal syntax
// above, this needs its own <i> tag instead of being applied to the
// menu item itself: Font Awesome's classes set font-family/font-weight
// and their own ::before content on whatever element they're put on,
// which used to bleed into (and bold) the item's label text and clash
// with this plugin's own icon styling.
$t.addClass(root.classNames.icon + ' ' + root.classNames.icon + '--fa5');
item._icon = $('<i></i>').addClass('fa').addClass(item.icon);
if (namesFontAwesomeFamily(item.icon)) {
// v5's `fas fa-user`, or v6/v7's `fa-solid fa-user` and
// `fa-sharp fa-solid fa-user`: the caller has already
// said which family and style to use, so the classes go
// through untouched. Supplying `fa` on top of these is
// what used to fight `fa-regular`/`fa-brands` for
// font-weight and font-family.
item._icon = $('<i></i>').addClass(item.icon);
} else {
// v4's `fa-user` shorthand, optionally with modifiers
// (`fa-user fa-lg`), which renders only once the base
// `fa` class is supplied here.
item._icon = $('<i></i>').addClass('fa').addClass(item.icon);
}
} else {
item._icon = root.classNames.icon + ' ' + root.classNames.icon + '-' + item.icon;
}
Expand Down Expand Up @@ -2580,6 +2587,71 @@
(selector.nodeType === 1 || (typeof selector.jquery !== 'undefined' && typeof selector.length === 'number'));
}

// Font Awesome has changed how a style is named in every major version:
//
// v4 <i class="fa fa-user">
// v5 <i class="fas fa-user">
// v6 and v7 <i class="fa-solid fa-user">
// <i class="fa-sharp fa-solid fa-user">
// <i class="fa-sharp-duotone fa-solid fa-user">
//
// and v7 keeps adding packs. Rather than tracking a version, the two
// helpers below answer the only two questions this plugin actually has:
//
// 1. is this a Font Awesome class list, or one of this plugin's own
// built-in icon names?
// 2. does that class list already say which family and style to use, or
// does v4's base `fa` still need supplying? Supplying it is what makes
// the `icon: "fa-user"` shorthand work.

// Font Awesome's style classes, plus the families that existed before v7's
// packs. This needs no entry per pack: v6 and v7 always name a style next
// to the family, so `fa-notdog-duo fa-solid fa-user` is recognised by its
// `fa-solid`. Kept as a padded string and matched with the ' x ' idiom used
// elsewhere in this file, so an icon literally called "constructor" cannot
// collide with Object.prototype.
var faFamilyAndStyleClasses = ' fa-solid fa-regular fa-light fa-thin fa-brands' +
' fa-duotone fa-classic fa-sharp fa-sharp-duotone ';

function faClassTokens(icon) {
if (typeof icon !== 'string') {
return [];
}
return trimText(icon).split(/\s+/);
}

// An `fa-` token says Font Awesome. Keying off that rather than a bare `fa`
// prefix is what keeps a short built-in icon name such as `icon: "fav"` out
// of these paths.
function isFontAwesomeIcon(icon) {
var tokens = faClassTokens(icon), i;
for (i = 0; i < tokens.length; i++) {
if (tokens[i].indexOf('fa-') === 0) {
return true;
}
}
return false;
}

function namesFontAwesomeFamily(icon) {
var tokens = faClassTokens(icon), token, i;
for (i = 0; i < tokens.length; i++) {
token = tokens[i];
// A short prefix carries family and style together and never
// contains a hyphen: `fa` in v4, `fas`/`far`/`fab` in v5, and the
// per-pack ones v7 keeps adding such as `fasds` and `fands`. Icon
// names and modifiers such as `fa-lg` always do contain one, so
// telling the two apart needs no list of prefixes at all.
if (token.indexOf('fa') === 0 && token.indexOf('-') === -1) {
return true;
}
if (faFamilyAndStyleClasses.indexOf(' ' + token + ' ') !== -1) {
return true;
}
}
return false;
}

// is the given value usable as a page coordinate? Finite numbers are, and
// so are numeric strings, which the positioning arithmetic has always
// accepted (`{x: el.dataset.x, ...}`). Note 0 is a perfectly valid
Expand Down
171 changes: 171 additions & 0 deletions test/unit/font-awesome-versions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Font Awesome names a style differently in every major version, and v7 keeps
// adding packs with their own short prefixes. These tests pin the emitted
// markup for each syntax, so the two things that matter stay true:
//
// * a class list that already names a family/style is passed through
// untouched (v5, v6, v7), and
// * one that does not still gets v4's base `fa` class supplied (the
// `icon: "fa-user"` shorthand and its modifier form).
//
// See https://github.com/swisnl/jQuery-contextMenu/pull/776

QUnit.module('Font Awesome icon syntax across versions', {
afterEach: function() {
$.contextMenu('destroy');
var $fixtureEl = $('#qunit-fixture');
if ($fixtureEl.length) {
$fixtureEl.html('');
}
}
});

function fixtureFa() {
var $fixtureEl = $('#qunit-fixture');
if ($fixtureEl.length === 0) {
$('<div id="qunit-fixture">').appendTo('body');
$fixtureEl = $('#qunit-fixture');
}
return $fixtureEl;
}

var faTriggerSeq = 0;

// Build a one-item menu with the given `icon`, open it, and report what was
// rendered: the classes on the child <i> (if any) and on the item itself.
function renderIconFa(icon) {
var cls = 'fa-trigger-' + (faTriggerSeq++);
fixtureFa().append('<div class="' + cls + '">right click me</div>');

$.contextMenu({
selector: '.' + cls,
items: {
only: {name: 'Only', icon: icon}
}
});

$('.' + cls).contextMenu();

var $item = $('.context-menu-list').filter(':visible').last().find('li.context-menu-item').first();
var $i = $item.children('i');

function sortedClasses($el) {
var raw = ($el.attr('class') || '').split(/\s+/).filter(function(one) {
return one !== '';
});
return raw.sort();
}

return {
hasChildIcon: $i.length === 1,
iconClasses: $i.length === 1 ? sortedClasses($i) : null,
itemClasses: sortedClasses($item)
};
}

function assertChildIcon(assert, icon, expectedClasses, label) {
var rendered = renderIconFa(icon);
assert.ok(rendered.hasChildIcon, label + ': renders a child <i>');
assert.deepEqual(
rendered.iconClasses,
expectedClasses.slice().sort(),
label + ': child <i> classes'
);
assert.ok(
rendered.itemClasses.indexOf('context-menu-icon--fa5') !== -1,
label + ': item keeps the positioning hook'
);
}

// ---------------------------------------------------------------- v4

QUnit.test('v4 shorthand "fa-user" gets the base fa class supplied', function(assert) {
assertChildIcon(assert, 'fa-user', ['fa', 'fa-user'], 'v4 shorthand');
});

QUnit.test('v4 shorthand with a modifier keeps the base fa class', function(assert) {
// This already worked and must keep working: nothing in the list names a
// family, so `fa` is still required for the icon to render at all.
assertChildIcon(assert, 'fa-user fa-lg', ['fa', 'fa-user', 'fa-lg'], 'v4 + modifier');
});

QUnit.test('v4 full syntax "fa fa-user" is passed through untouched', function(assert) {
// Previously fell through to the built-in icon-font branch and emitted
// "context-menu-icon-fa fa-user" onto the item, rendering nothing.
assertChildIcon(assert, 'fa fa-user', ['fa', 'fa-user'], 'v4 full');
});

// ---------------------------------------------------------------- v5

QUnit.test('v5 short prefixes are passed through untouched', function(assert) {
assertChildIcon(assert, 'fas fa-user', ['fas', 'fa-user'], 'v5 solid');
assertChildIcon(assert, 'far fa-user', ['far', 'fa-user'], 'v5 regular');
assertChildIcon(assert, 'fal fa-user', ['fal', 'fa-user'], 'v5 light');
assertChildIcon(assert, 'fad fa-user', ['fad', 'fa-user'], 'v5 duotone');
assertChildIcon(assert, 'fab fa-github', ['fab', 'fa-github'], 'v5 brands');
});

// ---------------------------------------------------------------- v6 / v7

QUnit.test('v6/v7 long-form styles no longer get a spurious fa class', function(assert) {
// The bug in #776: these hit the v4 shorthand branch and came out as
// "fa fa-solid fa-user". That stray `fa` fights fa-regular for font-weight
// and fa-brands for font-family.
assertChildIcon(assert, 'fa-solid fa-user', ['fa-solid', 'fa-user'], 'v6 solid');
assertChildIcon(assert, 'fa-regular fa-user', ['fa-regular', 'fa-user'], 'v6 regular');
assertChildIcon(assert, 'fa-light fa-user', ['fa-light', 'fa-user'], 'v6 light');
assertChildIcon(assert, 'fa-thin fa-user', ['fa-thin', 'fa-user'], 'v6 thin');
assertChildIcon(assert, 'fa-brands fa-github', ['fa-brands', 'fa-github'], 'v6 brands');
});

QUnit.test('v6/v7 family plus style combinations are passed through untouched', function(assert) {
assertChildIcon(assert, 'fa-classic fa-solid fa-user', ['fa-classic', 'fa-solid', 'fa-user'], 'classic');
assertChildIcon(assert, 'fa-sharp fa-solid fa-user', ['fa-sharp', 'fa-solid', 'fa-user'], 'sharp');
assertChildIcon(assert, 'fa-duotone fa-solid fa-user', ['fa-duotone', 'fa-solid', 'fa-user'], 'duotone');
assertChildIcon(assert, 'fa-sharp-duotone fa-solid fa-user', ['fa-sharp-duotone', 'fa-solid', 'fa-user'], 'sharp-duotone');
});

QUnit.test('v7 per-pack short prefixes are passed through untouched', function(assert) {
// Recognised by having no hyphen rather than from a list of prefixes, so a
// pack added in a later release needs no change in the plugin.
assertChildIcon(assert, 'fasds fa-user', ['fasds', 'fa-user'], 'sharp-duotone solid');
assertChildIcon(assert, 'fasr fa-user', ['fasr', 'fa-user'], 'sharp regular');
assertChildIcon(assert, 'fat fa-user', ['fat', 'fa-user'], 'thin');
assertChildIcon(assert, 'fands fa-user', ['fands', 'fa-user'], 'notdog-duo solid');
});

QUnit.test('a long-form family the plugin has never heard of still works', function(assert) {
// The point of listing only styles: a v7 pack, or one released after this
// code was written, is recognised through the style class that always sits
// beside it. No plugin change needed when Font Awesome adds a family.
assertChildIcon(assert, 'fa-notdog-duo fa-solid fa-user',
['fa-notdog-duo', 'fa-solid', 'fa-user'], 'unlisted family + known style');
assertChildIcon(assert, 'fa-whiteboard fa-regular fa-user',
['fa-whiteboard', 'fa-regular', 'fa-user'], 'another unlisted family');
});

// ------------------------------------------------- built-in icons, unchanged

QUnit.test('built-in icon names still style the item itself', function(assert) {
var rendered = renderIconFa('edit');
assert.notOk(rendered.hasChildIcon, 'no child <i> is created');
assert.ok(rendered.itemClasses.indexOf('context-menu-icon') !== -1, 'item keeps context-menu-icon');
assert.ok(rendered.itemClasses.indexOf('context-menu-icon-edit') !== -1, 'item gets context-menu-icon-edit');
assert.notOk(rendered.itemClasses.indexOf('context-menu-icon--fa5') !== -1, 'item is not treated as Font Awesome');
});

QUnit.test('a short built-in icon name beginning with fa is not mistaken for Font Awesome', function(assert) {
// Detection keys off an `fa-` token precisely so a name like this, which a
// caller may well have defined in their own CSS, keeps its old meaning.
var rendered = renderIconFa('fav');
assert.notOk(rendered.hasChildIcon, 'no child <i> is created');
assert.ok(rendered.itemClasses.indexOf('context-menu-icon-fav') !== -1, 'item gets context-menu-icon-fav');
assert.notOk(rendered.itemClasses.indexOf('context-menu-icon--fa5') !== -1, 'item is not treated as Font Awesome');
});

QUnit.test('a function icon is untouched by any of this', function(assert) {
var rendered = renderIconFa(function() {
return 'from-a-callback';
});
assert.notOk(rendered.hasChildIcon, 'no child <i> is created');
assert.ok(rendered.itemClasses.indexOf('from-a-callback') !== -1, 'the returned class is applied to the item');
});
Loading