Skip to content

Commit 84b3f66

Browse files
authored
support font icons (#380)
* support font icons * global font icon config
1 parent 3ff8f64 commit 84b3f66

File tree

18 files changed

+169
-35
lines changed

18 files changed

+169
-35
lines changed

docs/basic.rst

+4
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ If you want to apply a strict Content Security Policy (CSP), you can pass ``nonc
6767
E.g. if using `Talisman
6868
<https://github.com/wntrblm/flask-talisman>`_ it can be called with ``bootstrap.load_js(nonce=csp_nonce())``.
6969

70+
In order to use icon font, there is an additional helper called ``bootstrap.load_icon_font_css()``.
71+
This is used only by ``render_icon(..., font=True)`` or can be globally controlled via ``BOOTSTRAP_ICON_USE_FONT``.
72+
See its also the documentation for that marco.
73+
7074
Starter template
7175
----------------
7276

docs/macros.rst

+7-4
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,9 @@ By default, it will enable the CSRF token check for all the POST requests, read
607607
render_icon()
608608
-------------
609609

610-
Render a Bootstrap icon.
610+
Render a Bootstrap icon. This is either an SVG with a ``use`` element which refers to a locally hosted SVG sprite with an fragment identifier.
611+
Note that serving the SVG sprite across a domain has an `issue with Chrome <https://issues.chromium.org/issues/41164645>`_.
612+
Or it is possible to have a font icon rendered. This does support``BOOTSTRAP_SERVE_LOCAL`` but requires ``bootstrap.load_icon_font_css()`` in the template header.
611613

612614
Example
613615
~~~~~~~
@@ -621,14 +623,15 @@ Example
621623
API
622624
~~~~
623625

624-
.. py:function:: render_icon(name, size=config.BOOTSTRAP_ICON_SIZE, color=config.BOOTSTRAP_ICON_COLOR, title=None, desc=None, classes=None)
626+
.. py:function:: render_icon(name, size=config.BOOTSTRAP_ICON_SIZE, color=config.BOOTSTRAP_ICON_COLOR, title=None, desc=None, classes=None, font=False)
625627
626628
:param name: The name of icon, you can find all available names at `Bootstrap Icon <https://icons.getbootstrap.com/>`_.
627629
:param size: The size of icon, you can pass any vaild size value (e.g. ``32``/``'32px'``, ``1.5em``, etc.), default to
628630
use configuration ``BOOTSTRAP_ICON_SIZE`` (default value is `'1em'`).
629631
:param color: The color of icon, follow the context with ``currentColor`` if not set. Accept values are Bootstrap style name
630632
(one of ``['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'muted']``) or any valid color
631633
string (e.g. ``'red'``, ``'#ddd'`` or ``'(250, 250, 250)'``), default to use configuration ``BOOTSTRAP_ICON_COLOR`` (default value is ``None``).
632-
:param title: The title of the icon for accessibility support.
633-
:param desc: The description of the icon for accessibility support.
634+
:param title: The title of the icon for accessibility support. This is not supported for ``font=True``.
635+
:param desc: The description of the icon for accessibility support. This is not supported for ``font=True``.
634636
:param classes: The classes to use for styling (e.g. ``'text-success bg-body-secondary p-2 rounded-3'``).
637+
:param font: Generate ``<svg></svg>`` if set to ``False`` and generate ``<i></i>`` to use the icon font if set to ``True``, default to use configuration ``BOOTSTRAP_ICON_USE_FONT`` (default value is ``False``).

examples/bootstrap4/templates/base.html

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<title>Bootstrap-Flask Demo Application</title>
99
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
1010
{{ bootstrap.load_css() }}
11+
{{ bootstrap.load_icon_font_css() }}
1112
<style>
1213
pre {
1314
background: #ddd;

examples/bootstrap4/templates/icon.html

+23-11
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,43 @@
22
{% from 'bootstrap4/utils.html' import render_icon %}
33

44
{% block content %}
5-
<h2>Icon</h2>
5+
<h2>SVG icon</h2>
66
<pre>{% raw %}{{ render_icon('heart') }}{% endraw %}</pre>
77
Output: {{ render_icon('heart') }}
88

9-
<h2>Icon with custom size</h2>
9+
<h2>SVG icon with custom size</h2>
1010
<pre>{% raw %}{{ render_icon('heart', 32) }}{% endraw %}</pre>
1111
Output: {{ render_icon('heart', 32) }}
1212

13-
<h2>Icon with custom size and Bootstrap color</h2>
13+
<h2>SVG icon with custom size and Bootstrap color</h2>
1414
<pre>{% raw %}{{ render_icon('heart', 25, 'primary') }}{% endraw %}</pre>
1515
Output: {{ render_icon('heart', 25, 'primary') }}
1616

17-
<h2>Icon with custom size and custom color</h2>
17+
<h2>SVG icon with custom size and custom color</h2>
1818
<pre>{% raw %}{{ render_icon('heart', '2em', 'red') }}{% endraw %}</pre>
1919
Output: {{ render_icon('heart', '2em', 'red') }}
2020

21-
<h2>Icon with additional classes for styling</h2>
22-
<pre>{% raw %}{{ render_icon('heart', '2em', classes='text-success bg-light p-2 rounded-lg') }}{% endraw %}</pre>
23-
Output: {{ render_icon('heart', '4em', classes='text-success bg-light p-2 rounded-lg') }}
24-
25-
<h2>Icon with title and descr</h2>
21+
<h2>SVG icon with title and descr</h2>
2622
<pre>{% raw %}{{ render_icon('heart', title='Heart', desc='A heart.') }}{% endraw %}</pre>
2723
Output: {{ render_icon('heart', title='Heart', desc='A heart.') }}
2824

29-
<h2>Button example</h2>
25+
<h2>SVG icon with additional classes for styling</h2>
26+
<pre>{% raw %}{{ render_icon('heart', '2em', classes='text-success bg-light p-2 rounded-lg') }}{% endraw %}</pre>
27+
Output: {{ render_icon('heart', '4em', classes='text-success bg-light p-2 rounded-lg') }}
28+
29+
<h2>Buttons with SVG icon</h2>
3030
<a class="btn btn-primary text-white">Download {{ render_icon('arrow-down-circle') }}</a>
3131
<a class="btn btn-success text-white">Bookmark {{ render_icon('bookmark-star') }}</a>
32-
{% endblock %}
32+
33+
<h2>Font icon with custom size and Bootstrap color</h2>
34+
<pre>{% raw %}{{ render_icon('heart', '25px', 'primary', font=True) }}{% endraw %}</pre>
35+
Output: {{ render_icon('heart', '25px', 'primary', font=True) }}
36+
37+
<h2>Font icon with custom size and custom color</h2>
38+
<pre>{% raw %}{{ render_icon('heart', '2em', 'red', font=True) }}{% endraw %}</pre>
39+
Output: {{ render_icon('heart', '2em', 'red', font=True) }}
40+
41+
<h2>Buttons with font icon</h2>
42+
<a class="btn btn-primary text-white">Download {{ render_icon('arrow-down-circle', font=True) }}</a>
43+
<a class="btn btn-success text-white">Bookmark {{ render_icon('bookmark-star', font=True) }}</a>
44+
{% endblock %}

examples/bootstrap4/templates/icons.html

+1
Original file line numberDiff line numberDiff line change
@@ -16409,3 +16409,4 @@ <h2>Icons</h2>
1640916409
</ul>
1641016410
<p>This is a total of 2050 icons.</p>
1641116411
{% endblock %}
16412+

examples/bootstrap5/templates/base.html

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<title>Bootstrap-Flask Demo Application</title>
99
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
1010
{{ bootstrap.load_css() }}
11+
{{ bootstrap.load_icon_font_css() }}
1112
<style>
1213
pre {
1314
background: #ddd;

examples/bootstrap5/templates/icon.html

+23-11
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,43 @@
22
{% from 'bootstrap5/utils.html' import render_icon %}
33

44
{% block content %}
5-
<h2>Icon</h2>
5+
<h2>SVG icon</h2>
66
<pre>{% raw %}{{ render_icon('heart') }}{% endraw %}</pre>
77
Output: {{ render_icon('heart') }}
88

9-
<h2>Icon with custom size</h2>
9+
<h2>SVG icon with custom size</h2>
1010
<pre>{% raw %}{{ render_icon('heart', 32) }}{% endraw %}</pre>
1111
Output: {{ render_icon('heart', 32) }}
1212

13-
<h2>Icon with custom size and Bootstrap color</h2>
13+
<h2>SVG icon with custom size and Bootstrap color</h2>
1414
<pre>{% raw %}{{ render_icon('heart', 25, 'primary') }}{% endraw %}</pre>
1515
Output: {{ render_icon('heart', 25, 'primary') }}
1616

17-
<h2>Icon with custom size and custom color</h2>
17+
<h2>SVG icon with custom size and custom color</h2>
1818
<pre>{% raw %}{{ render_icon('heart', '2em', 'red') }}{% endraw %}</pre>
1919
Output: {{ render_icon('heart', '2em', 'red') }}
2020

21-
<h2>Icon with additional classes for styling</h2>
22-
<pre>{% raw %}{{ render_icon('heart', '2em', classes='text-success bg-body-secondary p-2 rounded-3') }}{% endraw %}</pre>
23-
Output: {{ render_icon('heart', '4em', classes='text-success bg-body-secondary p-2 rounded-3') }}
24-
25-
<h2>Icon with title and descr</h2>
21+
<h2>SVG icon with title and descr</h2>
2622
<pre>{% raw %}{{ render_icon('heart', title='Heart', desc='A heart.') }}{% endraw %}</pre>
2723
Output: {{ render_icon('heart', title='Heart', desc='A heart.') }}
2824

29-
<h2>Button example</h2>
25+
<h2>SVG icon with additional classes for styling</h2>
26+
<pre>{% raw %}{{ render_icon('heart', '2em', classes='text-success bg-body-secondary p-2 rounded-3') }}{% endraw %}</pre>
27+
Output: {{ render_icon('heart', '4em', classes='text-success bg-body-secondary p-2 rounded-3') }}
28+
29+
<h2>Buttons with SVG icon</h2>
3030
<a class="btn btn-primary text-white">Download {{ render_icon('arrow-down-circle') }}</a>
3131
<a class="btn btn-success text-white">Bookmark {{ render_icon('bookmark-star') }}</a>
32-
{% endblock %}
32+
33+
<h2>Font icon with custom size and Bootstrap color</h2>
34+
<pre>{% raw %}{{ render_icon('heart', '25px', 'primary', font=True) }}{% endraw %}</pre>
35+
Output: {{ render_icon('heart', '25px', 'primary', font=True) }}
36+
37+
<h2>Font icon with custom size and custom color</h2>
38+
<pre>{% raw %}{{ render_icon('heart', '2em', 'red', font=True) }}{% endraw %}</pre>
39+
Output: {{ render_icon('heart', '2em', 'red', font=True) }}
40+
41+
<h2>Buttons with font icon</h2>
42+
<a class="btn btn-primary text-white">Download {{ render_icon('arrow-down-circle', font=True) }}</a>
43+
<a class="btn btn-success text-white">Bookmark {{ render_icon('bookmark-star', font=True) }}</a>
44+
{% endblock %}

examples/bootstrap5/templates/icons.html

+1
Original file line numberDiff line numberDiff line change
@@ -16409,3 +16409,4 @@ <h2>Icons</h2>
1640916409
</ul>
1641016410
<p>This is a total of 2050 icons.</p>
1641116411
{% endblock %}
16412+

examples/update-icons.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def generate(version):
4040
number += 1
4141
file.write('</ul>\n')
4242
file.write(f'<p>This is a total of {number} icons.</p>\n')
43-
file.write('{% endblock %}\n')
43+
file.write('{% endblock %}\n\n')
4444
print(f'For Bootstrap{version}, a total of {number} icons are supported.')
4545

4646
for value in (4, 5):

flask_bootstrap/__init__.py

+17
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class _Bootstrap:
4040
bootstrap_version = None
4141
jquery_version = None
4242
popper_version = None
43+
icons_version = None
4344
bootstrap_css_integrity = None
4445
bootstrap_js_integrity = None
4546
jquery_integrity = None
@@ -78,6 +79,7 @@ def init_app(self, app):
7879
app.config.setdefault('BOOTSTRAP_BOOTSWATCH_THEME', None)
7980
app.config.setdefault('BOOTSTRAP_ICON_SIZE', '1em')
8081
app.config.setdefault('BOOTSTRAP_ICON_COLOR', None)
82+
app.config.setdefault('BOOTSTRAP_ICON_USE_FONT', False)
8183
app.config.setdefault('BOOTSTRAP_MSG_CATEGORY', 'primary')
8284
app.config.setdefault('BOOTSTRAP_TABLE_VIEW_TITLE', 'View')
8385
app.config.setdefault('BOOTSTRAP_TABLE_EDIT_TITLE', 'Edit')
@@ -122,6 +124,19 @@ def load_css(self, version=None, bootstrap_sri=None, bootswatch_theme=None):
122124
css = f'<link rel="stylesheet" href="{bootstrap_url}">'
123125
return Markup(css)
124126

127+
def load_icon_font_css(self):
128+
"""Load Bootstrap's css icon font resource.
129+
130+
.. versionadded:: 2.4.2
131+
"""
132+
serve_local = current_app.config['BOOTSTRAP_SERVE_LOCAL']
133+
if serve_local:
134+
icons_url = url_for('bootstrap.static', filename='font/bootstrap-icons.min.css')
135+
else:
136+
icons_url = f'{CDN_BASE}/bootstrap-icons@{self.icons_version}/font/bootstrap-icons.min.css'
137+
css = f'<link rel="stylesheet" href="{icons_url}">'
138+
return Markup(css)
139+
125140
def _get_js_script(self, version, name, sri, nonce):
126141
"""Get <script> tag for JavaScript resources."""
127142
serve_local = current_app.config['BOOTSTRAP_SERVE_LOCAL']
@@ -227,6 +242,7 @@ def create_app():
227242
bootstrap_version = '4.6.1'
228243
jquery_version = '3.5.1'
229244
popper_version = '1.16.1'
245+
icons_version = '1.11.3'
230246
bootstrap_css_integrity = 'sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn'
231247
bootstrap_js_integrity = 'sha384-VHvPCCyXqtD5DqJeNxl2dtTyhF78xXNXdkwX1CZeRusQfRKp+tA7hAShOK/B/fQ2'
232248
jquery_integrity = 'sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0='
@@ -262,6 +278,7 @@ def create_app():
262278
"""
263279
bootstrap_version = '5.3.2'
264280
popper_version = '2.11.8'
281+
icons_version = '1.11.3'
265282
bootstrap_css_integrity = 'sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN'
266283
bootstrap_js_integrity = 'sha384-BBtl+eGJRgqQAUMxJ7pMwbEyER4l1g+O15P+16Ep7Q9Q+zqX6gSbd85u4mG4QzX+'
267284
popper_integrity = 'sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r'

flask_bootstrap/static/bootstrap4/css/font/bootstrap-icons.min.css

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Binary file not shown.
Binary file not shown.

flask_bootstrap/static/bootstrap5/css/font/bootstrap-icons.min.css

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Binary file not shown.
Binary file not shown.

flask_bootstrap/templates/base/utils.html

+11-6
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,20 @@
1010
{% endmacro %}
1111

1212

13-
{% macro render_icon(name, size=config.BOOTSTRAP_ICON_SIZE, color=config.BOOTSTRAP_ICON_COLOR, title=None, desc=None, classes=None) %}
14-
{%- set bootstrap_colors = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'muted'] -%}
13+
{% macro render_icon(name, size=config.BOOTSTRAP_ICON_SIZE, color=config.BOOTSTRAP_ICON_COLOR, title=None, desc=None, classes=None, font=config.BOOTSTRAP_ICON_USE_FONT) %}
14+
{% set bootstrap_colors = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'muted'] %}
15+
{%- if font == true -%}
16+
<i class="bi-{{ name }}{% if color in bootstrap_colors %} text-{{ color }}{% endif %}" style="{% if color and color not in bootstrap_colors %}color: {{ color }}; {% endif %}font-size: {{ size }};"></i>
17+
{%- else -%}
1518
<svg class="bi{% if not color %}{% if classes %} {{ classes }}{% endif %}"
1619
{%- elif color in bootstrap_colors %} text-{{ color }}"{% else %}" style="color: {{ color }}"{% endif -%}
1720
{%- if size %} width="{{ size }}"{% endif %}{% if size %} height="{{ size }}"{% endif %} fill="currentColor">
18-
{%- if title %}<title>{{ title }}</title>{% endif -%}
19-
{%- if desc %}<desc>{{ desc }}</desc>{% endif -%}
20-
<use xlink:href="{{ url_for('bootstrap.static', filename='icons/bootstrap-icons.svg') }}#{{ name }}"/></svg>
21-
{%- endmacro %}
21+
{% if title is not none %}<title>{{ title }}</title>{% endif %}
22+
{% if desc is not none %}<desc>{{ desc }}</desc>{% endif %}
23+
<use xlink:href="{{ url_for('bootstrap.static', filename='icons/bootstrap-icons.svg') }}#{{ name }}"/>
24+
</svg>
25+
{%- endif -%}
26+
{% endmacro %}
2227

2328

2429
{% macro arg_url_for(endpoint, base) %}

tests/test_bootstrap4/test_render_icon.py

+69-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from flask import render_template_string
22

33

4-
def test_render_icon(app, client):
4+
def test_render_icon_svg(app, client):
55
@app.route('/icon')
66
def icon():
77
return render_template_string('''
@@ -78,7 +78,7 @@ def icon_desc_classes():
7878
assert 'class="bi text-success bg-light"' in data
7979

8080

81-
def test_render_icon_config(app, client):
81+
def test_render_icon_svg_config(app, client):
8282
app.config['BOOTSTRAP_ICON_SIZE'] = 100
8383
app.config['BOOTSTRAP_ICON_COLOR'] = 'success'
8484

@@ -94,3 +94,70 @@ def icon():
9494
assert 'width="100"' in data
9595
assert 'height="100"' in data
9696
assert 'text-success' in data
97+
98+
99+
def test_render_icon_font(app, client):
100+
@app.route('/icon')
101+
def icon():
102+
return render_template_string('''
103+
{% from 'bootstrap4/utils.html' import render_icon %}
104+
{{ render_icon('heart', font=True) }}
105+
''')
106+
107+
@app.route('/icon-size')
108+
def icon_size():
109+
return render_template_string('''
110+
{% from 'bootstrap4/utils.html' import render_icon %}
111+
{{ render_icon('heart', 32, font=True) }}
112+
''')
113+
114+
@app.route('/icon-style')
115+
def icon_style():
116+
return render_template_string('''
117+
{% from 'bootstrap4/utils.html' import render_icon %}
118+
{{ render_icon('heart', color='primary', font=True) }}
119+
''')
120+
121+
@app.route('/icon-color')
122+
def icon_color():
123+
return render_template_string('''
124+
{% from 'bootstrap4/utils.html' import render_icon %}
125+
{{ render_icon('heart', color='green', font=True) }}
126+
''')
127+
128+
response = client.get('/icon')
129+
data = response.get_data(as_text=True)
130+
assert '<i class="bi-heart' in data
131+
assert 'size: 1em;' in data
132+
133+
response = client.get('/icon-size')
134+
data = response.get_data(as_text=True)
135+
assert '<i class="bi-heart' in data
136+
assert 'size: 32;' in data
137+
138+
response = client.get('/icon-style')
139+
data = response.get_data(as_text=True)
140+
assert '<i class="bi-heart' in data
141+
assert ' text-primary' in data
142+
143+
response = client.get('/icon-color')
144+
data = response.get_data(as_text=True)
145+
assert '<i class="bi-heart' in data
146+
assert 'color: green;' in data
147+
148+
149+
def test_render_icon_font_config(app, client):
150+
app.config['BOOTSTRAP_ICON_SIZE'] = 100
151+
app.config['BOOTSTRAP_ICON_COLOR'] = 'success'
152+
153+
@app.route('/icon')
154+
def icon():
155+
return render_template_string('''
156+
{% from 'bootstrap4/utils.html' import render_icon %}
157+
{{ render_icon('heart', font=True) }}
158+
''')
159+
160+
response = client.get('/icon')
161+
data = response.get_data(as_text=True)
162+
assert 'size: 100;' in data
163+
assert 'text-success' in data

0 commit comments

Comments
 (0)