diff --git a/cmsplugin_cascade/bootstrap4/buttons.py b/cmsplugin_cascade/bootstrap4/buttons.py
index 02a13a8aa..97411f986 100644
--- a/cmsplugin_cascade/bootstrap4/buttons.py
+++ b/cmsplugin_cascade/bootstrap4/buttons.py
@@ -69,7 +69,7 @@ def get_instance(cls):
class BootstrapButtonMixin(IconPluginMixin):
require_parent = True
- parent_classes = ('BootstrapColumnPlugin', 'SimpleWrapperPlugin',)
+ parent_classes = ('BootstrapColumnPlugin', 'SimpleWrapperPlugin', 'NavbarNavItemsPlugin')
render_template = 'cascade/bootstrap4/button.html'
allow_children = False
default_css_class = 'btn'
@@ -188,6 +188,8 @@ def get_form(self, request, obj=None, **kwargs):
@classmethod
def get_css_classes(cls, obj):
css_classes = cls.super(BootstrapButtonPlugin, cls).get_css_classes(obj)
+ if obj.parent.plugin_type == 'NavbarNavItemsPlugin':
+ css_classes.insert(0,'nav-link text-left')
if obj.glossary.get('stretched_link'):
css_classes.append('stretched_link')
return css_classes
diff --git a/cmsplugin_cascade/bootstrap4/image.py b/cmsplugin_cascade/bootstrap4/image.py
index 8e42812df..d1231dd71 100644
--- a/cmsplugin_cascade/bootstrap4/image.py
+++ b/cmsplugin_cascade/bootstrap4/image.py
@@ -107,6 +107,7 @@ def render(self, context, instance, placeholder):
try:
tags = get_image_tags(instance)
except Exception as exc:
+ tags = None
logger.warning("Unable generate image tags. Reason: {}".format(exc))
tags = tags if tags else {}
if 'extra_styles' in tags:
diff --git a/cmsplugin_cascade/bootstrap4/mixins.py b/cmsplugin_cascade/bootstrap4/mixins.py
index 8187359b1..55df39c5a 100644
--- a/cmsplugin_cascade/bootstrap4/mixins.py
+++ b/cmsplugin_cascade/bootstrap4/mixins.py
@@ -144,6 +144,73 @@ def paddings(cls):
return glossary_fields
@property
+ def flex_directions(cls):
+ glossary_fields = []
+ choices_format = [
+ ('flex-{}row', _("horizontal")),
+ ('flex-{}row-reverse', _("horizontal reverse")),
+ ('flex-{}column', _("Vertical")),
+ ('flex-{}column-reverse', _("Vertical reverse")),
+ ]
+ for bp in Breakpoint.range(Breakpoint.xs, Breakpoint.xl):
+ if bp == Breakpoint.xs:
+ choices = [(c.format(''), l ) for c, l in choices_format]
+ choices.insert(0, ('', _("No Flex Directions")))
+ else:
+ choices = [(c.format(bp.name + '-'), l) for c, l in choices_format ]
+ choices.insert(0, ('', _("Inherit from above")))
+ glossary_fields.append(GlossaryField(
+ widgets.Select(choices=choices),
+ label=format_lazy(_("Flex Directions for {breakpoint}"), breakpoint=bp.label),
+ name='Flex_{}'.format(bp.name),
+ initial=''
+ ))
+ return glossary_fields
+
+ @property
+ def display_propertys(cls):
+ glossary_fields = []
+ choices_format = [
+ ('d-{}{}', _("horizontal")),
+ ]
+ notation = ['none', 'inline', 'inline-block', 'block', 'table', 'table-cell', 'table-row', 'flex', 'inline-flex']
+ for bp in Breakpoint.range(Breakpoint.xs, Breakpoint.xl):
+ if bp == Breakpoint.xs:
+ choices = [(c.format('', n), c.format('', n)) for c, l in choices_format for n in notation]
+ choices.insert(0, ('', _("No Flex Directions")))
+ else:
+ choices = [(c.format(bp.name + '-', n),
+ c.format('', n)) for c, l in choices_format for n in notation]
+ choices.insert(0, ('', _("Inherit from above")))
+ glossary_fields.append(GlossaryField(
+ widgets.Select(choices=choices),
+ label=format_lazy(_("Flex Directions for {breakpoint}"), breakpoint=bp.label),
+ name='Flex_{}'.format(bp.name),
+ ))
+ return glossary_fields
+
+ @property
+ def justify_content(cls):
+ glossary_fields = []
+ choices_format = [
+ ('justify-content-{}{}', _("Justify Content")),
+ ]
+ notation = [ 'start', 'end', 'center', 'between', 'around']
+ for bp in Breakpoint.range(Breakpoint.xs, Breakpoint.xl):
+ if bp == Breakpoint.xs:
+ choices = [(c.format('', n), c.format('', n)) for c, l in choices_format for n in notation]
+ choices.insert(0, ('', _("No Justify content")))
+ else:
+ choices = [(c.format(bp.name + '-', n),
+ c.format(bp.name + '-', n)) for c, l in choices_format for n in notation]
+ choices.insert(0, ('', _("Inherit from above")))
+ glossary_fields.append(GlossaryField(
+ widgets.Select(choices=choices),
+ label=format_lazy(_("Justify Content for {breakpoint}"), breakpoint=bp.label),
+ name='Justify_content_{}'.format(bp.name),
+ ))
+ return glossary_fields
+
def floats(cls):
glossary_fields = []
choices_format = [
diff --git a/cmsplugin_cascade/bootstrap4/navbar.py b/cmsplugin_cascade/bootstrap4/navbar.py
new file mode 100644
index 000000000..a658663f1
--- /dev/null
+++ b/cmsplugin_cascade/bootstrap4/navbar.py
@@ -0,0 +1,368 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+try:
+ from html.parser import HTMLParser # py3
+except ImportError:
+ from HTMLParser import HTMLParser # py2
+
+from django.forms import widgets, ModelChoiceField
+from django.utils.html import format_html
+from django.utils.translation import ungettext_lazy, ugettext_lazy as _
+from django.forms.models import ModelForm
+
+from cms.plugin_pool import plugin_pool
+from filer.models.imagemodels import Image
+
+from cmsplugin_cascade import app_settings
+from cmsplugin_cascade.fields import GlossaryField
+
+from cmsplugin_cascade.bootstrap4.plugin_base import BootstrapPluginBase
+
+from cmsplugin_cascade.image import ImageAnnotationMixin, ImagePropertyMixin, ImageFormMixin
+from cmsplugin_cascade.bootstrap4.image import get_image_tags
+from cmsplugin_cascade.bootstrap4.picture import BootstrapPicturePlugin, get_picture_elements
+from cmsplugin_cascade.bootstrap4.container import ContainerBreakpointsWidget, ContainerGridMixin, get_widget_choices
+from cmsplugin_cascade.widgets import MultipleCascadingSizeWidget, CascadingSizeWidget
+from cmsplugin_cascade.bootstrap4.grid import Breakpoint
+
+from cmsplugin_cascade.link.config import LinkPluginBase, LinkElementMixin, LinkForm
+from . import grid
+
+import logging
+logger = logging.getLogger('cascade')
+
+
+class NavbarPluginForm(ImageFormMixin, ModelForm):
+ """
+ Form class to validate the JumbotronPlugin.
+ """
+ image_file = ModelChoiceField(queryset=Image.objects.all(), required=False, label=_("Image"))
+
+ def clean_glossary(self):
+ glossary = super(NavbarPluginForm, self).clean_glossary()
+ return glossary
+
+
+class NavbarGridMixin(object):
+
+ def get_grid_instance(self):
+ fluid = self.glossary.get('fluid', False)
+ try:
+ breakpoints = [getattr(grid.Breakpoint, bp) for bp in self.glossary['breakpoints']]
+ except KeyError:
+ breakpoints = [bp for bp in grid.Breakpoint]
+ if fluid:
+ bounds = dict((bp, grid.fluid_bounds[bp]) for bp in breakpoints)
+ else:
+ bounds = dict((bp, grid.default_bounds[bp]) for bp in breakpoints)
+ return grid.Bootstrap4Container(bounds=bounds)
+
+
+class NavbarPlugin(BootstrapPluginBase):
+ name = _("Navbar")
+ model_mixins = ( NavbarGridMixin,)
+ default_css_class = 'navbar'
+ default_css_attributes = ('options',)
+ require_parent = False
+ parent_classes = None
+ render_template = 'cascade/bootstrap4/navbar.html'
+ glossary_variables = ['container_max_widths', 'media_queries']
+ ring_plugin = 'JumbotronPlugin'
+ OPTION_NAV_COLLAPSE = [(s, s) for s in [ "inherit", "navbar-expand","navbar-expand-sm", "navbar-expand-md","navbar-expand-lg", "navbar-expand-xl"] ]
+ OPTION_NAV_COLOR = [(s, s) for s in [ "navbar-light", "navbar-dark"]]
+ OPTION_NAV_BG_COLOR = [ "bg-primary", "bg-secondary","bg-success", "bg-danger", "bg-warning", "bg-info" ,"bg-light", "bg-dark" , "bg-white", "bg-transparent"]
+ OPTION_NAV_BG_GRADIENT = [ "bg-gradient-primary", "bg-gradient-secondary", "bg-gradient-success", "bg-gradient-danger", "bg-gradient-warning", "bg-gradient-info", "bg-gradient-light", "bg-gradient-dark"]
+ OPTION_NAV_BG_MIX = OPTION_NAV_BG_COLOR + OPTION_NAV_BG_GRADIENT
+ OPTION_NAV_PLACEMENTS=["inherit", "fixed-top" , "fixed-bottom" , "sticky-top"]
+
+ container_glossary_fields = (
+ GlossaryField(
+ ContainerBreakpointsWidget(choices=get_widget_choices()),
+ label=_("Available Breakpoints"),
+ name='breakpoints',
+ initial=app_settings.CMSPLUGIN_CASCADE['bootstrap4']['default_bounds'].keys(),
+ help_text=_("Supported display widths for Bootstrap's grid system.")
+ ),
+ GlossaryField(
+ MultipleCascadingSizeWidget([bp.name for bp in Breakpoint], allowed_units=['px', '%'], required=False),
+ label=_("Adapt Picture Heights"),
+ name='container_max_heights',
+ initial={'xs': '100%', 'sm': '100%', 'md': '100%', 'lg': '100%', 'xl': '100%'},
+ help_text=_("Heights of picture in percent or pixels for distinct Bootstrap's breakpoints.")
+ ),
+ GlossaryField(
+ widgets.CheckboxSelectMultiple(choices=BootstrapPicturePlugin.RESIZE_OPTIONS),
+ label=_("Resize Options"),
+ name='resize_options',
+ initial=['crop', 'subject_location', 'high_resolution'],
+ help_text=_("Options to use when resizing the image.")
+ ),
+ )
+
+ navbar_collapse = GlossaryField(
+ widgets.Select(choices=OPTION_NAV_COLLAPSE),
+ label=_('navbar collapse'),
+ name='navbar_classes collapse',
+ )
+
+ navbar_color = GlossaryField(
+ widgets.Select(choices=OPTION_NAV_COLOR),
+ label=_('navbar color'),
+ name='navbar_color',
+ )
+
+ navbar_bg_color= GlossaryField(
+ widgets.Select(choices=[(s, s) for s in OPTION_NAV_BG_MIX ]),
+ label=_('navbar-bg'),
+ name='navbar_navbg',
+ )
+
+ navbar_placement= GlossaryField(
+ widgets.Select(choices= [(s, s) for s in OPTION_NAV_PLACEMENTS]),
+ label=_('navbar-place'),
+ name='navbar_place',
+ )
+
+ @classmethod
+ def get_css_classes(cls, obj):
+ css_classes = super(NavbarPlugin, cls).get_css_classes(obj)
+ navbar_collapse = obj.glossary.get('navbar_collapse', '')
+ navbar_color = obj.glossary.get('navbar_color', '')
+ navbar_bg_color = obj.glossary.get('navbar_bg_color', '')
+ navbar_placement = obj.glossary.get('placement', '')
+ if navbar_collapse != 'inherit':
+ css_classes.append(navbar_collapse)
+ if navbar_color != 'inherit':
+ css_classes.append(navbar_color)
+ if navbar_bg_color != 'inherit':
+ css_classes.append(navbar_bg_color)
+ if navbar_placement != 'inherit':
+ css_classes.append(navbar_placement )
+ return css_classes
+
+ def get_form(self, request, obj=None, **kwargs):
+ if self.get_parent_instance(request, obj) is None:
+ # we only ask for breakpoints, if the jumbotron is the root of the placeholder
+ kwargs.update(glossary_fields=list(self.container_glossary_fields))
+ kwargs['glossary_fields'].extend(self.glossary_fields)
+ form = super(NavbarPlugin, self).get_form(request, obj, **kwargs)
+ return form
+
+
+ @classmethod
+ def get_identifier(cls, obj):
+ identifier = super(NavbarPlugin, cls).get_identifier(obj)
+ glossary = obj.get_complete_glossary()
+ css_classes_without_default = obj.css_classes.replace( cls.default_css_class , '' , 1)
+ return format_html('
{0}{1}
',
+ identifier, css_classes_without_default)
+
+plugin_pool.register_plugin(NavbarPlugin)
+
+
+class NavbarLinksItemsPlugin(BootstrapPluginBase):
+ name = _("Nav Links Items")
+ chojust=[ "inherit", "justify-content-start","justify-content-end", "justify-content-center", "justify-content-between", "justify-content-around" ]
+ chomrml=[ "inherit", "mr-auto", "ml-auto" ]
+ choflex=[ "flex-row", "flex-wrap"]
+ default_css_class = ''
+ parent_classes = ['NavbarPlugin']
+ render_template = 'cascade/bootstrap4/navbar_links.html'
+
+ jus= GlossaryField(
+ widgets.Select(choices= [(s, s) for s in chojust]),
+ label=_('navbar-place'),
+ name='navbar_place',
+ help_text=_("Adjust interval place s."),
+ )
+
+
+ navflex= GlossaryField(
+ widgets.Select(choices= [(s, s) for s in choflex]),
+ label=_('navbar-place'),
+ name='navbar_place',
+ help_text=_("Adjust interval place s."),
+ )
+
+ mrml= GlossaryField(
+ widgets.Select(choices= [(s, s) for s in chomrml]),
+ label=_('navbar-place'),
+ name='navbar_place',
+ help_text=_("Adjust interval place UL"),
+ )
+
+plugin_pool.register_plugin(NavbarLinksItemsPlugin)
+
+
+class NavbarBrandPlugin(BootstrapPluginBase, LinkPluginBase,):
+ name = _("Nav brand")
+ parent_classes = ['NavbarPlugin']
+ model_mixins = (LinkElementMixin,)
+ render_template = 'cascade/bootstrap4/navbar_brand.html'
+ fields = list(LinkPluginBase.fields)
+
+plugin_pool.register_plugin(NavbarBrandPlugin)
+
+
+class NavbarBrandImagePluginForm(ImageFormMixin, ModelForm):
+ """
+ Form class to validate the NavbarBrandImage.
+ """
+ image_file = ModelChoiceField(queryset=Image.objects.all(), required=False, label=_("Image"))
+
+ def clean_glossary(self):
+ glossary = super(NavbarBrandImagePluginForm , self).clean_glossary()
+ return glossary
+
+
+class NavbarBrandImagePlugin(ImageAnnotationMixin, BootstrapPluginBase, ):
+ name = _("Nav brand Image")
+ model_mixins = (ImagePropertyMixin,)
+ form = NavbarBrandImagePluginForm
+ parent_classes = ['NavbarBrandPlugin']
+ allow_children = True
+ alien_child_classes = True
+ html_tag_attributes = {'image_title': 'title', 'alt_tag': 'tag'}
+ raw_id_fields = ['image_file']
+ fields = [ 'glossary','image_file', ]
+ SIZE_CHOICES = ('auto', 'width/height', 'cover', 'contain')
+
+ RESIZE_OPTIONS = [
+ ('upscale', _("Upscale image")),
+ ('crop', _("Crop image")),
+ ('subject_location', _("With subject location")),
+ ('high_resolution', _("Optimized for Retina")),
+]
+ render_template = 'cascade/bootstrap4/navbar_brand_image.html'
+
+ image_width_fixed = GlossaryField(
+ CascadingSizeWidget(allowed_units=['px'], required=False),
+ label=_("Fixed Image Width"),
+ help_text=_("Set a fixed image width in pixels."),
+ )
+
+ image_height = GlossaryField(
+ CascadingSizeWidget(allowed_units=['px', '%'], required=False),
+ label=_("Adapt Image Height"),
+ help_text=_("Set a fixed height in pixels, or percent relative to the image width."),
+ )
+
+ resize_options = GlossaryField(
+ widgets.CheckboxSelectMultiple(choices=RESIZE_OPTIONS),
+ label=_("Resize Options"),
+ help_text=_("Options to use when resizing the image."),
+ initial=['subject_location', 'high_resolution'],
+ )
+
+ def get_form(self, request, obj=None, **kwargs):
+ if self.get_parent_instance(request, obj) is None:
+ # we only ask for breakpoints, if the jumbotron is the root of the placeholder
+ kwargs.update(glossary_fields=list(self.container_glossary_fields))
+ kwargs['glossary_fields'].extend(self.glossary_fields)
+ form = super(NavbarBrandImagePlugin, self).get_form(request, obj, **kwargs)
+ return form
+
+ def render(self, context, instance, placeholder):
+ tags = get_image_tags(instance)
+ try:
+ tags = get_image_tags(instance)
+ except Exception as exc:
+ logger.warning("Unable generate image tags. Reason: {}".format(exc))
+ tags = tags if tags else {}
+ if 'extra_styles' in tags:
+ extra_styles = tags.pop('extra_styles')
+ inline_styles = instance.glossary.get('inline_styles', {})
+ inline_styles.update(extra_styles)
+ instance.glossary['inline_styles'] = inline_styles
+ context.update(dict(instance=instance, placeholder=placeholder, **tags))
+ return context
+
+plugin_pool.register_plugin(NavbarBrandImagePlugin)
+
+
+class NavbarCollapsePlugin(BootstrapPluginBase):
+ name = _("Nav Collapse")
+ parent_classes = ['NavbarPlugin']
+ alien_child_classes = True
+ render_template = 'cascade/bootstrap4/navbar_collapse.html'
+ default_css_class = 'collapse navbar-collapse'
+
+ @classmethod
+ def get_css_classes(cls, obj):
+ css_classes = super(NavbarCollapsePlugin, cls).get_css_classes(obj)
+ return css_classes
+
+ @classmethod
+ def get_identifier(cls, obj):
+ identifier = super(NavbarCollapsePlugin, cls).get_identifier(obj)
+ glossary = obj.get_complete_glossary()
+ css_classes_without_default = obj.css_classes.replace( cls.default_css_class , '' , 1)
+ return format_html('{0}{1}
',
+ identifier, css_classes_without_default)
+
+plugin_pool.register_plugin(NavbarCollapsePlugin)
+
+
+class NavbarNavListPlugin(BootstrapPluginBase):
+ name = _("Nav list")
+ parent_classes = ['NavbarPlugin', 'NavbarCollapsePlugin' ]
+ alien_child_classes = True
+ render_template = 'cascade/bootstrap4/navbar_nav_list.html'
+ default_css_class = 'navbar-nav'
+
+ @classmethod
+ def get_css_classes(cls, obj):
+ css_classes = super(NavbarNavListPlugin, cls).get_css_classes(obj)
+ return css_classes
+
+ @classmethod
+ def get_identifier(cls, obj):
+ identifier = super(NavbarNavListPlugin, cls).get_identifier(obj)
+ glossary = obj.get_complete_glossary()
+ if hasattr(cls,'default_css_class'):
+ css_classes_without_default = obj.css_classes.replace( cls.default_css_class , '' , 1)
+ else:
+ css_classes_without_default = obj.css_classes
+ return format_html('{0}{1}
',
+ identifier, css_classes_without_default )
+
+plugin_pool.register_plugin(NavbarNavListPlugin)
+
+
+class NavbarNavItemsMainMemuPlugin(BootstrapPluginBase):
+ name = _("Nav items main menu")
+ parent_classes = ['NavbarNavListPlugin']
+ alien_child_classes = True
+ render_template = 'cascade/bootstrap4/navbar_nav_items_links.html'
+
+plugin_pool.register_plugin(NavbarNavItemsMainMemuPlugin)
+
+
+class NavbarNavItemsPlugin(BootstrapPluginBase):
+ default_css_class = 'nav-item'
+ name = _("Nav item")
+ parent_classes = ['NavbarNavListPlugin']
+ alien_child_classes = True
+ render_template = 'cascade/bootstrap4/navbar_nav_item.html'
+
+plugin_pool.register_plugin(NavbarNavItemsPlugin)
+
+
+class NavbarNavLinkPlugin(BootstrapPluginBase):
+
+ name = _("Nav Link")
+ parent_classes = ['NavbarNavItemsPlugin']
+ alien_child_classes = True
+ render_template = 'cascade/bootstrap4/navbar_nav_link.html'
+
+plugin_pool.register_plugin(NavbarNavLinkPlugin)
+
+
+class NavbarToogler(BootstrapPluginBase):
+ name = _("Nav toogler")
+ default_css_class = ''
+ parent_classes = ['NavbarPlugin']
+ render_template = 'cascade/bootstrap4/navbar_toogler.html'
+
+plugin_pool.register_plugin(NavbarToogler)
diff --git a/cmsplugin_cascade/bootstrap4/settings.py b/cmsplugin_cascade/bootstrap4/settings.py
index 713b8c743..ee3e972e4 100644
--- a/cmsplugin_cascade/bootstrap4/settings.py
+++ b/cmsplugin_cascade/bootstrap4/settings.py
@@ -12,7 +12,7 @@
CASCADE_PLUGINS = ['accordion', 'buttons', 'card', 'carousel', 'container', 'embeds', 'image', 'jumbotron',
- 'picture', 'tabs']
+ 'picture', 'tabs', 'navbar']
if 'cms_bootstrap' in settings.INSTALLED_APPS:
CASCADE_PLUGINS.append('secondary_menu')
@@ -58,7 +58,12 @@ def set_defaults(config):
config['plugins_with_extra_mixins'].setdefault('HorizontalRulePlugin', BootstrapUtilities(
BootstrapUtilities.margins,
))
-
+ config['plugins_with_extra_mixins'].setdefault('NavbarNavListPlugin', BootstrapUtilities(
+ BootstrapUtilities.flex_directions, BootstrapUtilities.margins, BootstrapUtilities.display_propertys
+ ))
+ config['plugins_with_extra_mixins'].setdefault('NavbarCollapsePlugin', BootstrapUtilities(
+ BootstrapUtilities.justify_content
+ ))
config['plugins_with_extra_fields'].setdefault('BootstrapJumbotronPlugin', PluginExtraFieldsConfig(
inline_styles={
'extra_fields:Paddings': ['margin-top', 'margin-bottom', 'padding-top', 'padding-bottom'],
diff --git a/cmsplugin_cascade/icon/cms_plugins.py b/cmsplugin_cascade/icon/cms_plugins.py
index 642ca991a..7376a6474 100644
--- a/cmsplugin_cascade/icon/cms_plugins.py
+++ b/cmsplugin_cascade/icon/cms_plugins.py
@@ -30,6 +30,7 @@ class SimpleIconPlugin(IconPluginMixin, LinkPluginBase):
model_mixins = (LinkElementMixin,)
fields = list(LinkPluginBase.fields)
ring_plugin = 'IconPlugin'
+ raw_id_fields = LinkPluginBase.raw_id_fields
icon_font = GlossaryField(
widgets.Select(),
@@ -51,6 +52,13 @@ def get_form(self, request, obj=None, **kwargs):
kwargs.update(form=VoluntaryLinkForm.get_form_class())
return super(SimpleIconPlugin, self).get_form(request, obj, **kwargs)
+ @classmethod
+ def get_css_classes(cls, obj):
+ css_classes = cls.super(SimpleIconPlugin, cls).get_css_classes(obj)
+ if obj.parent.plugin_type == 'NavbarNavItemsPlugin':
+ css_classes.insert(0,'nav-link navbar-text')
+ return css_classes
+
def render(self, context, instance, placeholder):
context = super(SimpleIconPlugin, self).render(context, instance, placeholder)
icon_font = self.get_icon_font(instance)
@@ -58,6 +66,10 @@ def render(self, context, instance, placeholder):
if icon_font and symbol:
font_attr = 'class="{}{}"'.format(icon_font.config_data.get('css_prefix_text', 'icon-'), symbol)
context['icon_font_attrs'] = mark_safe(font_attr)
+ link_attributes = LinkPluginBase.get_html_tag_attributes(instance)
+ link_html_tag_attributes = format_html_join(' ', '{0}="{1}"',
+ [(attr, val) for attr, val in link_attributes.items() if val])
+ context['link_html_tag_attributes'] = link_html_tag_attributes
return context
plugin_pool.register_plugin(SimpleIconPlugin)
@@ -179,6 +191,10 @@ def render(self, context, instance, placeholder):
format_html_join('', '{0}:{1};',
[(k, v) for k, v in styles.items()])))
context['icon_font_attrs'] = mark_safe(' '.join(attrs))
+ link_attributes = LinkPluginBase.get_html_tag_attributes(instance)
+ link_html_tag_attributes = format_html_join(' ', '{0}="{1}"',
+ [(attr, val) for attr, val in link_attributes.items() if val])
+ context['link_html_tag_attributes'] = link_html_tag_attributes
return context
plugin_pool.register_plugin(FramedIconPlugin)
diff --git a/cmsplugin_cascade/link/cms_plugins.py b/cmsplugin_cascade/link/cms_plugins.py
index 7c5128297..f96f9b933 100644
--- a/cmsplugin_cascade/link/cms_plugins.py
+++ b/cmsplugin_cascade/link/cms_plugins.py
@@ -26,6 +26,13 @@ class Media:
def get_identifier(cls, obj):
return mark_safe(obj.glossary.get('link_content', ''))
+ @classmethod
+ def get_css_classes(cls, obj):
+ css_classes = cls.super(TextLinkPlugin, cls).get_css_classes(obj)
+ if obj.parent.plugin_type == 'TextPlugin' and obj.parent.parent.plugin_type == 'NavbarNavItemsPlugin' :
+ css_classes.insert(0,'nav-link navbar-text')
+ return css_classes
+
def get_form(self, request, obj=None, **kwargs):
link_content = CharField(required=True, label=_("Link Content"),
# replace auto-generated id so that CKEditor automatically transfers the text into this input field
diff --git a/cmsplugin_cascade/static/cascade/clipboards_catalog/navbars/bootstrap_navbar_example_1.json b/cmsplugin_cascade/static/cascade/clipboards_catalog/navbars/bootstrap_navbar_example_1.json
new file mode 100644
index 000000000..e2ac07ad0
--- /dev/null
+++ b/cmsplugin_cascade/static/cascade/clipboards_catalog/navbars/bootstrap_navbar_example_1.json
@@ -0,0 +1,205 @@
+{
+ "plugins":[
+ [
+ "NavbarPlugin",
+ {
+ "glossary":{
+ "breakpoints":[
+ "xs",
+ "sm",
+ "md",
+ "lg",
+ "xl"
+ ],
+ "container_max_heights":{
+ "xs":"100%",
+ "sm":"100%",
+ "md":"100%",
+ "lg":"100%",
+ "xl":"100%"
+ },
+ "resize_options":[
+ "crop",
+ "subject_location",
+ "high_resolution"
+ ],
+ "navbar_collapse":"navbar-expand-lg",
+ "navbar_color":"navbar-dark",
+ "navbar_bg_color":"bg-dark",
+ "navbar_placement":"fixed-top",
+ "hide_plugin":""
+ },
+ "pk":79
+ },
+ [
+ [
+ "NavbarBrandPlugin",
+ {
+ "glossary":{
+ "target":"",
+ "title":"",
+ "hide_plugin":"on",
+ "link":{
+ "type":"cmspage",
+ "model":"cms.Page",
+ "pk":2,
+ "section":""
+ }
+ },
+ "pk":90
+ },
+ [
+ [
+ "NavbarBrandImagePlugin",
+ {
+ "glossary":{
+ "image_title":"dd",
+ "alt_tag":"d",
+ "image_width_fixed":"20px",
+ "image_height":"",
+ "resize_options":[
+ "crop",
+ "subject_location",
+ "high_resolution"
+ ],
+ "hide_plugin":"",
+ "image":{
+ "pk":1,
+ "model":"filer.Image",
+ "width":250,
+ "height":305,
+ "exif_orientation":1
+ }
+ },
+ "pk":91
+ },
+ []
+ ]
+ ]
+ ],
+ [
+ "NavbarToogler",
+ {
+ "glossary":{
+ "hide_plugin":""
+ },
+ "pk":80
+ },
+ []
+ ],
+ [
+ "NavbarCollapsePlugin",
+ {
+ "glossary":{
+ "Justify_content_xs":"",
+ "Justify_content_sm":"",
+ "Justify_content_md":"justify-content-md-center",
+ "Justify_content_lg":"",
+ "hide_plugin":""
+ },
+ "pk":81
+ },
+ [
+ [
+ "NavbarNavListPlugin",
+ {
+ "glossary":{
+ "Flex_xs":"",
+ "Flex_sm":"",
+ "Flex_md":"",
+ "Flex_lg":"",
+ "margins_xs":"",
+ "margins_sm":"",
+ "margins_md":"",
+ "margins_lg":"",
+ "hide_plugin":""
+ },
+ "pk":82
+ },
+ [
+ [
+ "NavbarNavItemsMainMemuPlugin",
+ {
+ "glossary":{
+ "hide_plugin":""
+ },
+ "pk":83
+ },
+ []
+ ],
+ [
+ "NavbarNavItemsPlugin",
+ {
+ "glossary":{
+ "hide_plugin":""
+ },
+ "pk":84
+ },
+ [
+ [
+ "SimpleIconPlugin",
+ {
+ "glossary":{
+ "icon_font":"1",
+ "symbol":"smile",
+ "target":"_blank",
+ "title":"smile",
+ "hide_plugin":"",
+ "link":{
+ "type":"cmspage",
+ "model":"cms.Page",
+ "pk":2,
+ "section":""
+ }
+ },
+ "pk":85
+ },
+ []
+ ]
+ ]
+ ],
+ [
+ "NavbarNavItemsPlugin",
+ {
+ "glossary":{
+ "hide_plugin":""
+ },
+ "pk":88
+ },
+ [
+ [
+ "BootstrapButtonPlugin",
+ {
+ "glossary":{
+ "button_type":"btn-link",
+ "button_size":"",
+ "button_options":[],
+ "target":"",
+ "title":"Link extern",
+ "stretched_link":"",
+ "icon_align":"icon-left",
+ "icon_font":"1",
+ "symbol":"firefox",
+ "hide_plugin":"",
+ "link":{
+ "type":"cmspage",
+ "model":"cms.Page",
+ "pk":2,
+ "section":""
+ },
+ "link_content":"Link extern"
+ },
+ "pk":89
+ },
+ []
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+}
diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar.html
new file mode 100644
index 000000000..f8041a9d6
--- /dev/null
+++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar.html
@@ -0,0 +1,14 @@
+{% load bootstrap_tags cms_tags %}
+{% block extra-styles %}{% endblock %}
+{% block extra-scripts %}{% endblock %}
+{% with instance_css_classes=instance.css_classes instance_inline_styles=instance.inline_styles %}
+
diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_brand.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_brand.html
new file mode 100644
index 000000000..6e241b5f4
--- /dev/null
+++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_brand.html
@@ -0,0 +1,11 @@
+{% extends "cascade/link/link-base.html" %}
+{% load bootstrap_tags cms_tags %}
+{% block link_link %}{% with instance_link=instance|default:"#" %}
+{% block navbar-brand %}{% for child in instance.child_plugin_instances %}
+{{ block.super }}
+{% render_plugin child %}
+{% endfor %}
+{% endblock %}
+{% endwith %}
+{% endblock %}
+{% with instance_css_classes=instance.css_classes instance_inline_styles=instance.inline_styles %}{% block link_content %}{{ instance.content }}{% endblock %}{% endwith %}
diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_brand_image.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_brand_image.html
new file mode 100644
index 000000000..a1d526c58
--- /dev/null
+++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_brand_image.html
@@ -0,0 +1,21 @@
+{% load l10n thumbnail %}
+{% localize off %}{% spaceless %}
+{% with css_classes=instance.css_classes inline_styles=instance.inline_styles %}
+{% if instance.image %} {% if not instance.image %}{% endif %}
+ {% else %}
+{% thumbnail instance.image srcset.size crop=srcset.crop upscale=srcset.upscale subject_location=instance.image.subject_location as thumb %}
+ {% if not sizes %} width="{{ thumb.width }}" height="{{ thumb.height }}"{% endif %} src="{{ instance.image.url }}"/>{% if not instance.image %}{% endif %}
+ {% endif %}
+ {% endwith %}
+{% endspaceless %}{% endlocalize %}
+
diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_collapse.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_collapse.html
new file mode 100644
index 000000000..7e149103c
--- /dev/null
+++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_collapse.html
@@ -0,0 +1,7 @@
+{% load bootstrap_tags cms_tags %}
+{% with instance_css_classes=instance.css_classes instance_inline_styles=instance.inline_styles %}
+
+{% block navbar-collapse %}{% for child in instance.child_plugin_instances %}{% render_plugin child %}{% endfor %}
+{% endblock %}
+
+{% endwith %}
diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_links.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_links.html
new file mode 100644
index 000000000..dd5416f12
--- /dev/null
+++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_links.html
@@ -0,0 +1,4 @@
+{% load bootstrap_tags cms_tags %}
+
+{% block navbar-nav %}
{% main_menu "bootstrap4/menu/navbar.html" %}
{% endblock %}
+
diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_nav_item.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_nav_item.html
new file mode 100644
index 000000000..e7507beb5
--- /dev/null
+++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_nav_item.html
@@ -0,0 +1,17 @@
+{% load static bootstrap_tags cms_tags sekizai_tags sass_tags %}
+{% with css_classes=instance.css_classes inline_styles=instance.inline_styles %}
+{% block navbar-nav-li %}
+{% for child in instance.child_plugin_instances %}
+{% render_plugin child %}
+{% endfor %}
+
+{% endblock %}
+{% endwith %}
+{% addtoblock "css" %}
+
+{% endaddtoblock %}
+
diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_nav_items_links.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_nav_items_links.html
new file mode 100644
index 000000000..cc26b1819
--- /dev/null
+++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_nav_items_links.html
@@ -0,0 +1,4 @@
+{% load bootstrap_tags cms_tags %}
+{% block navbar-nav %}
+{% main_menu "bootstrap4/menu/navbar.html" %}
+{% endblock %}
diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_nav_link.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_nav_link.html
new file mode 100644
index 000000000..42c1b3fd2
--- /dev/null
+++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_nav_link.html
@@ -0,0 +1,3 @@
+{% load cms_tags %}
+{% block navbar-nav-li %}{% for child in instance.child_plugin_instances %}{% render_plugin child %}{% endfor %}
+{% endblock %}
diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_nav_list.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_nav_list.html
new file mode 100644
index 000000000..419acd817
--- /dev/null
+++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_nav_list.html
@@ -0,0 +1,5 @@
+{% load cms_tags %}
+{% with instance_css_classes=instance.css_classes instance_inline_styles=instance.inline_styles %}
+{% block navbar-nav-list %}{% for child in instance.child_plugin_instances %}{% render_plugin child %}{% endfor %}
+{% endblock %}
+{% endwith %}
diff --git a/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_toogler.html b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_toogler.html
new file mode 100644
index 000000000..fc75a17d7
--- /dev/null
+++ b/cmsplugin_cascade/templates/cascade/bootstrap4/navbar_toogler.html
@@ -0,0 +1,3 @@
+
diff --git a/cmsplugin_cascade/templates/cascade/plugins/simpleicon.html b/cmsplugin_cascade/templates/cascade/plugins/simpleicon.html
index 07970e230..1db3cf1ab 100644
--- a/cmsplugin_cascade/templates/cascade/plugins/simpleicon.html
+++ b/cmsplugin_cascade/templates/cascade/plugins/simpleicon.html
@@ -10,4 +10,4 @@
{% if instance_link %}{% elif css_classes or inline_styles %}{% endif %}
{% endwith %}
-{% endspaceless %}
\ No newline at end of file
+{% endspaceless %}
diff --git a/cmsplugin_cascade/utils.py b/cmsplugin_cascade/utils.py
index bcf5b98b6..daa1e28b3 100644
--- a/cmsplugin_cascade/utils.py
+++ b/cmsplugin_cascade/utils.py
@@ -41,9 +41,12 @@ def validate_link(link_data):
def compute_aspect_ratio(image):
- if image.exif.get('Orientation', 1) > 4:
- # image is rotated by 90 degrees, while keeping width and height
- return float(image.width) / float(image.height)
+ if image.width != 0 or image.height:
+ if image.exif.get('Orientation', 1) > 4:
+ # image is rotated by 90 degrees, while keeping width and height
+ return float(image.width) / float(image.height)
+ else:
+ return float(image.height) / float(image.width)
else:
return float(image.height) / float(image.width)
diff --git a/examples/bs4demo/settings.py b/examples/bs4demo/settings.py
index 675ee358d..11b5942e6 100644
--- a/examples/bs4demo/settings.py
+++ b/examples/bs4demo/settings.py
@@ -237,9 +237,16 @@
CMS_PLACEHOLDER_CONF = {
# this placeholder is used in templates/main.html, it shows how to
# scaffold a djangoCMS page starting with an empty placeholder
+ 'Header Content': {
+ 'plugins': ['BootstrapContainerPlugin', 'BootstrapJumbotronPlugin','NavbarPlugin'],
+ 'parent_classes': {'NavbarPlugin': None, 'BootstrapContainerPlugin': None, 'BootstrapJumbotronPlugin': None},
+ 'glossary': CASCADE_WORKAREA_GLOSSARY,
+ },
+
+
'Main Content': {
'plugins': ['BootstrapContainerPlugin', 'BootstrapJumbotronPlugin'],
- 'parent_classes': {'BootstrapContainerPlugin': None, 'BootstrapJumbotronPlugin': None},
+ 'parent_classes': {'NavbarPlugin': None, 'BootstrapContainerPlugin': None, 'BootstrapJumbotronPlugin': None},
'glossary': CASCADE_WORKAREA_GLOSSARY,
},
# this placeholder is used in templates/wrapped.html, it shows how to
@@ -256,7 +263,7 @@
'language': '{{ language }}',
'skin': 'moono-lisa',
'toolbar': 'CMS',
- 'stylesSet': format_lazy('default:{}', reverse_lazy('admin:cascade_texticon_wysiwig_config')),
+ 'stylesSet': format_lazy('default:{}', reverse_lazy('admin:cascade_texteditor_config')),
}
SELECT2_CSS = 'node_modules/select2/dist/css/select2.min.css'
diff --git a/examples/bs4demo/templates/bs4demo/main.html b/examples/bs4demo/templates/bs4demo/main.html
index 51ae5935b..96cee7941 100644
--- a/examples/bs4demo/templates/bs4demo/main.html
+++ b/examples/bs4demo/templates/bs4demo/main.html
@@ -6,15 +6,29 @@
{% endblock head %}
{% block header %}
- {% if DJANGO_CLIENT_FRAMEWORK == 'angular-ui' %}
- {% include "bootstrap4/includes/ng-nav-navbar.html" with navbar_classes="navbar-expand-lg navbar-light bg-light fixed-top" %}
- {% else %}
- {% include "bootstrap4/includes/nav-navbar.html" with navbar_classes="navbar-expand-lg navbar-light bg-light fixed-top" role="navigation" %}
+
+{% static_placeholder 'Header Content' %}
+{% if request.toolbar and request.toolbar.edit_mode_active %}
+{% addtoblock "js" %}
+
+{% endaddtoblock %}
{% endif %}
{% if cms_version >= "3.5.0" and request.toolbar %}
{% endif %}
diff --git a/requirements/base.txt b/requirements/base.txt
index a12c39af0..e081d774f 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -10,7 +10,7 @@ django-sass-processor==0.6
django-sekizai==0.10.0
Django-Select2==5.11.1
django-treebeard==4.3
-djangocms-bootstrap==1.0.1
+djangocms-bootstrap==1.1.0
djangocms-text-ckeditor==3.5.3
easy-thumbnails==2.5.0
html5lib==0.9999999